kcp协议与tcp协议类似,是一种ARQ协议。他的优点在于比tcp的延迟更小30%-40%,但相应的会牺牲一部分的带宽,大该比tcp多浪费10%~20%。tcp的设计目标是增大网络利用率,而kcp的设计目标是增大网络传输速率。因此kcp与tcp对比,kcp有如下机制可以提高传输速度:

1.kcp的RTO每次是增加为1.5倍,相比tcp的2倍,具有更短的超时重传时间
2.无延迟ACK,通过配置让ack立即发送,而tcp为了增加网络利用率会尽量让ack与用户数据一起发送
3.快速重传门限可控制,可以适当缩小fastack门限,提高重传响应速度
4.earlyRetrans机制,无用户数据时立刻发送ack
5.拥塞控制可取消,取消拥塞控制后,用户的发包速率不受网络拥塞的影响

golang版本kcp源码下载地址:https://github.com/skywind3000/kcp

一个发送数据接收数据的基本流程如下

//用户有一段数据buf需要发送,于是调用send函数

kcp1.Send(buf.Bytes())//send函数将buffer分片成kcp的数据包格式,存在待发送队列中

kcp1.flush()//将发送队列中的数据通过下层协议(UDP)进行发送

//kcp2接收到下层协议(UDP)传进来的数据底层数据buffer

kcp2.Input(buffer[:hr], true, false)//调用Input函数将UDP接收的数据buffer转换成kcp的数据包格式

hr = int32(kcp2.Recv(buffer[:]))//kcp2将接收的kcp数据包还原成kcp1发送的buffer数据

在Send的时候用户数据长度不作限制,但在Recv的时候不一定能够一次性接收完Send的所有数据,用户在Recv后应该做校验。

 
func (kcp *KCP) Recv(buffer []byte) (n int)
    // merge fragment
    count := 0
    for k := range kcp.rcv_queue {
        seg := &kcp.rcv_queue[k]
        copy(buffer, seg.data)
        buffer = buffer[len(seg.data):]
        n += len(seg.data)
        count++
        kcp.delSegment(*seg)
        if seg.frg == 0 {
            break
        }
    }
    if count > 0 {
        kcp.rcv_queue = kcp.remove_front(kcp.rcv_queue, count)
    }
count = 0
    for k := range kcp.rcv_buf {
        seg := &kcp.rcv_buf[k]
        if seg.sn == kcp.rcv_nxt && len(kcp.rcv_queue) < int(kcp.rcv_wnd) {
            kcp.rcv_nxt++
            count++
        } else {
            break
        }
    }

    if count > 0 {
        kcp.rcv_queue = append(kcp.rcv_queue, kcp.rcv_buf[:count]...)
        kcp.rcv_buf = kcp.remove_front(kcp.rcv_buf, count)
    }
// fast recover
    if len(kcp.rcv_queue) < int(kcp.rcv_wnd) && fast_recover {
        // ready to send back IKCP_CMD_WINS in ikcp_flush
        // tell remote my window size
        kcp.probe |= IKCP_ASK_TELL
    }

recv函数将接收消息队列中的数据包还原成原来的消息格式,通过buffer返回给调用者

还会把rcv_buf中的与接收序号相匹配的数据拷贝到rcv_queue中。这里注意到在Input->parse_data函数中有同样的处理,这里之所以需要重复处理是因为kcp.rcv_queue的大小可能会发生改变,len(kcp.rcv_queue) < int(kcp.rcv_wnd)条件有可能重新成立。

fast_recover标识的意思是快速告知对端我又有窗口大小空出来了,因为在Input函数中有可能窗口会满了,此时发送给对端的是窗口满消息,而在recv过后,因为取走了消息,可用接收窗口又变大了,此时需要快速告知对端可以继续发消息了。

func (kcp *KCP) Send(buffer []byte) int {
var count int
if len(buffer) == 0 {
return -1
} // append to previous segment in streaming mode (if possible)
if kcp.stream != 0 {
n := len(kcp.snd_queue)
if n > 0 {
seg := &kcp.snd_queue[n-1]
if len(seg.data) < int(kcp.mss) {
capacity := int(kcp.mss) - len(seg.data)
extend := capacity
if len(buffer) < capacity {
extend = len(buffer)
} // grow slice, the underlying cap is guaranteed to
// be larger than kcp.mss
oldlen := len(seg.data)
seg.data = seg.data[:oldlen+extend]
copy(seg.data[oldlen:], buffer)
buffer = buffer[extend:]
}
} if len(buffer) == 0 {
return 0
}
} if len(buffer) <= int(kcp.mss) {
count = 1
} else {
count = (len(buffer) + int(kcp.mss) - 1) / int(kcp.mss)
} if count > 255 {
return -2
} if count == 0 {
count = 1
} for i := 0; i < count; i++ {
var size int
if len(buffer) > int(kcp.mss) {
size = int(kcp.mss)
} else {
size = len(buffer)
}
seg := kcp.newSegment(size)
copy(seg.data, buffer[:size])
if kcp.stream == 0 { // message mode
seg.frg = uint8(count - i - 1)
} else { // stream mode
seg.frg = 0
}
kcp.snd_queue = append(kcp.snd_queue, seg)
buffer = buffer[size:]
}
return 0
}

send函数主要功能是把用户需要发送的字符数组转化成kcp的数据包。如果用户的数据超过一个MSS,还会对数据进行分片。这里有两种分片的方式,消息方式和流方式。消息方式把用户数据分片,为每个分片设置ID,将分片后的数据一个一个地存入发送队列种,接收方通过id解析回原来的包,消息方式一个分片的数据量可能不能达到MSS(最大分片大小)。流方式则是会检测每个发送队列里的分片是否达到最大mss,如果没有达到就会用新的数据填充分片。流方式的网络速度优于消息方式,但是流方式接收方接收时是一个分片一个分片地接收,而消息方式kcp接收函数会自己把原本属于一个数据的分片重组回来。

func (kcp *KCP) flush(ackOnly bool)

flush函数大致功能如下:

发送ack

发送探测窗口消息

将发送队列中的消息存入缓存队列(缓存队列实际上就是发送窗口)

检查缓存队列中当前需要发送的数据(包括新传数据与重传数据)

根据重传数据更新发送窗口大小

if change >  {
inflight := kcp.snd_nxt - kcp.snd_una
kcp.ssthresh = inflight /
if kcp.ssthresh < IKCP_THRESH_MIN {
kcp.ssthresh = IKCP_THRESH_MIN
}
kcp.cwnd = kcp.ssthresh + resent
kcp.incr = kcp.cwnd * kcp.mss
}

在发生快速重传的时候,会将慢启动阈值调整为当前发送窗口的一半,并把拥塞窗口大小调整为kcp.ssthresh + resent,resent是触发快速重传的丢包的次数,resent的值代表的意思在被弄丢的包后面收到了resent个数的包的ack。这样调整后kcp就进入了拥塞控制状态。

    if lost >  {
kcp.ssthresh = cwnd /
if kcp.ssthresh < IKCP_THRESH_MIN {
kcp.ssthresh = IKCP_THRESH_MIN
}
kcp.cwnd =
kcp.incr = kcp.mss
}

如果发生的超时重传,那么就重新进入慢启动状态。

func (kcp *KCP) Input(data []byte, regular, ackNoDelay bool) int {
una := kcp.snd_una
if len(data) < IKCP_OVERHEAD {
return -
} var maxack uint32
var lastackts uint32
var flag int
var inSegs uint64 for {
var ts, sn, length, una, conv uint32
var wnd uint16
var cmd, frg uint8 if len(data) < int(IKCP_OVERHEAD) {
break
} data = ikcp_decode32u(data, &conv)
if conv != kcp.conv {
return -
} data = ikcp_decode8u(data, &cmd)
data = ikcp_decode8u(data, &frg)
data = ikcp_decode16u(data, &wnd)
data = ikcp_decode32u(data, &ts)
data = ikcp_decode32u(data, &sn)
data = ikcp_decode32u(data, &una)
data = ikcp_decode32u(data, &length)
if len(data) < int(length) {
return -
} if cmd != IKCP_CMD_PUSH && cmd != IKCP_CMD_ACK &&
cmd != IKCP_CMD_WASK && cmd != IKCP_CMD_WINS {
return -
} // only trust window updates from regular packets. i.e: latest update
if regular { kcp.rmt_wnd = uint32(wnd) }
kcp.parse_una(una)
kcp.shrink_buf() if cmd == IKCP_CMD_ACK {
kcp.parse_ack(sn)
kcp.shrink_buf()
if flag == {
flag =
maxack = sn
} else if _itimediff(sn, maxack) > {
maxack = sn
}
lastackts = ts
} else if cmd == IKCP_CMD_PUSH {
if _itimediff(sn, kcp.rcv_nxt+kcp.rcv_wnd) < {
kcp.ack_push(sn, ts)
if _itimediff(sn, kcp.rcv_nxt) >= {
seg := kcp.newSegment(int(length))
seg.conv = conv
seg.cmd = cmd
seg.frg = frg
seg.wnd = wnd
seg.ts = ts
seg.sn = sn
seg.una = una
copy(seg.data, data[:length])
kcp.parse_data(seg)
} else {
atomic.AddUint64(&DefaultSnmp.RepeatSegs, )
}
} else {
atomic.AddUint64(&DefaultSnmp.RepeatSegs, )
}
} else if cmd == IKCP_CMD_WASK {
// ready to send back IKCP_CMD_WINS in Ikcp_flush
// tell remote my window size
kcp.probe |= IKCP_ASK_TELL
} else if cmd == IKCP_CMD_WINS {
// do nothing
} else {
return -
} inSegs++
data = data[length:]
}
atomic.AddUint64(&DefaultSnmp.InSegs, inSegs) if flag != && regular {
kcp.parse_fastack(maxack)
current := currentMs()
if _itimediff(current, lastackts) >= {
kcp.update_ack(_itimediff(current, lastackts))
}
} if _itimediff(kcp.snd_una, una) > {
if kcp.cwnd < kcp.rmt_wnd {
mss := kcp.mss
if kcp.cwnd < kcp.ssthresh {
kcp.cwnd++
kcp.incr += mss
} else {
if kcp.incr < mss {
kcp.incr = mss
}
kcp.incr += (mss*mss)/kcp.incr + (mss / )
if (kcp.cwnd+)*mss <= kcp.incr {
kcp.cwnd++
}
}
if kcp.cwnd > kcp.rmt_wnd {
kcp.cwnd = kcp.rmt_wnd
kcp.incr = kcp.rmt_wnd * mss
}
}
} if ackNoDelay && len(kcp.acklist) > { // ack immediately
kcp.flush(true)
} else if kcp.rmt_wnd == && len(kcp.acklist) > { // window zero
kcp.flush(true)
}
return
}

input函数接收udp协议传过来的报文,把udp报文解码成kcp报文进行缓存。

kcp报文分为ack报文,数据报文,探测窗口报文,响应窗口报文四种。

kcp报文的una字段表示对端希望接收的下一个kcp包序号,也就是说明接收端已经收到了所有小于una序号的kcp包。解析una字段后需要把发送缓冲区里面包序号小于una的包全部丢弃掉。 ack报文则包含了对端收到的kcp包的序号,接到ack包后需要删除发送缓冲区中与ack包中的发送包序号(sn)相同的kcp包。上述una和ack处理完后,需要更新kcp.snd_una(发送端第一个未被确认的包),如果snd_una增加了那么就说明对端正常收到且回应了发送方发送缓冲区第一个待确认的包,此时需要更新cwnd(拥塞窗口)

收到数据报文时,需要判断数据报文是否在接收窗口内,如果是则保存ack,如果数据报文的sn正好是待接收的第一个报文rcv_nxt,那么就更新rcv_nxt(加1)。如果配置了ackNodelay模式(无延迟ack)或者远端窗口为0(代表暂时不能发送用户数据),那么这里会立刻fulsh()发送ack。

kcp源码走读的更多相关文章

  1. Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现

    欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使 ...

  2. Apache Spark源码走读之16 -- spark repl实现详解

    欢迎转载,转载请注明出处,徽沪一郎. 概要 之所以对spark shell的内部实现产生兴趣全部缘于好奇代码的编译加载过程,scala是需要编译才能执行的语言,但提供的scala repl可以实现代码 ...

  3. Apache Spark源码走读之13 -- hiveql on spark实现详解

    欢迎转载,转载请注明出处,徽沪一郎 概要 在新近发布的spark 1.0中新加了sql的模块,更为引人注意的是对hive中的hiveql也提供了良好的支持,作为一个源码分析控,了解一下spark是如何 ...

  4. Apache Spark源码走读之7 -- Standalone部署方式分析

    欢迎转载,转载请注明出处,徽沪一郎. 楔子 在Spark源码走读系列之2中曾经提到Spark能以Standalone的方式来运行cluster,但没有对Application的提交与具体运行流程做详细 ...

  5. twitter storm 源码走读之5 -- worker进程内部消息传递处理和数据结构分析

    欢迎转载,转载请注明出处,徽沪一郎. 本文从外部消息在worker进程内部的转化,传递及处理过程入手,一步步分析在worker-data中的数据项存在的原因和意义.试图从代码实现的角度来回答,如果是从 ...

  6. storm-kafka源码走读之KafkaSpout

    from: http://blog.csdn.net/wzhg0508/article/details/40903919 (五)storm-kafka源码走读之KafkaSpout 原创 2014年1 ...

  7. spring-data-redis-cache 使用及源码走读

    预期读者 准备使用 spring 的 data-redis-cache 的同学 了解 @CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的使 ...

  8. ConcurrentHashMap源码走读

    目录 ConcurrentHashMap源码走读 简介 放入数据 容器元素总数更新 容器扩容 协助扩容 遍历 ConcurrentHashMap源码走读 简介 在从JDK8开始,为了提高并发度,Con ...

  9. underscorejs 源码走读笔记

    Underscore 简介 Underscore 是一个JavaScript实用库,提供了类似Prototype.js的一些功能,但是没有继承任何JavaScript内置对象.它弥补了部分jQuery ...

随机推荐

  1. 第三章:Hadoop简介及配置Hadoop-1.2.1,hbase-0.94.13集群

    前面给大家讲了怎么安装Hadoop,肯定会有人还是很迷茫,装完以后原来就是这个样子,但是怎么用,下面,先给大家讲下Hadoop简介:大致理解下就OK了 hadoop是一个平台,提供了庞大的存储和并行计 ...

  2. kafka常用命令(cdh5.10.0+kafka)

    参考资料:http://kafka.apache.org/quickstart 进入kafka安装目录(CDH安装路径为:/opt/cloudera/parcels/KAFKA):进入bin目录: c ...

  3. java后台获取URL带参demo

    URL:http://aos.wall.youmi.net/v2/check_fb_sig?order=YM130402cygr_UTb42&app=30996ced018a2a5e& ...

  4. 关于操作 ASP.NET Web API的实例

    WCF的野心造成了它的庞大复杂,HTTP的单纯造就了它的简单优美.为了实现分布式Web应用,我们不得不将两者凑合在一起 —— WCF服务以HTTP绑定宿主于IIS. 于是有了让人晕头转向的配置.让人郁 ...

  5. Flask之视图(一)

    2.关于Flask 知识点 从Hello World开始 给路由传递参数 返回状态码 重定向 正则URL 设置cookie和获取cookie 扩展 上下文 请求钩子 Flask装饰器路由的实现 Fla ...

  6. 【Oracle】安装Oracle 10gR2 For CentOS

    Oracle10gR2安装安装环境项目     版本信息     备注操作系统     CentOS5.364bit    Oracle数据库     Oracle10.2.0.4 64bit 硬件信 ...

  7. Spring与Redis的实现

    前言 Redis作为缓存还是相当不错的,一定程度上缓解了数据库的IO操作,具体不多说,具体网上查找资料. 实战 不多说,直接上代码. 第一步:所需要的依赖 <!-- redis --> & ...

  8. mysql 挑选列导入

    insert into boleht_development.`htprojects`(id,pname,`general`,imgsrc,whatwedo,howwedo,bp) select ci ...

  9. Core1.1环境下,自己实现的一个简单的CRUD框架(反射实现)

    我实现了一个简单的EF框架,主要用于操纵数据库.实现了对数据库的基本操纵--CRUD 这是项目结构 这是一个 core 下的 DLL 写了一个数据库工厂,用于执行sql语句.调用sql语句工厂 写了一 ...

  10. MongoDB动态条件之分页查询

    一.使用QueryByExampleExecutor 1. 继承MongoRepository public interface StudentRepository extends MongoRepo ...