kcp源码走读
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源码走读的更多相关文章
- Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现
欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使 ...
- Apache Spark源码走读之16 -- spark repl实现详解
欢迎转载,转载请注明出处,徽沪一郎. 概要 之所以对spark shell的内部实现产生兴趣全部缘于好奇代码的编译加载过程,scala是需要编译才能执行的语言,但提供的scala repl可以实现代码 ...
- Apache Spark源码走读之13 -- hiveql on spark实现详解
欢迎转载,转载请注明出处,徽沪一郎 概要 在新近发布的spark 1.0中新加了sql的模块,更为引人注意的是对hive中的hiveql也提供了良好的支持,作为一个源码分析控,了解一下spark是如何 ...
- Apache Spark源码走读之7 -- Standalone部署方式分析
欢迎转载,转载请注明出处,徽沪一郎. 楔子 在Spark源码走读系列之2中曾经提到Spark能以Standalone的方式来运行cluster,但没有对Application的提交与具体运行流程做详细 ...
- twitter storm 源码走读之5 -- worker进程内部消息传递处理和数据结构分析
欢迎转载,转载请注明出处,徽沪一郎. 本文从外部消息在worker进程内部的转化,传递及处理过程入手,一步步分析在worker-data中的数据项存在的原因和意义.试图从代码实现的角度来回答,如果是从 ...
- storm-kafka源码走读之KafkaSpout
from: http://blog.csdn.net/wzhg0508/article/details/40903919 (五)storm-kafka源码走读之KafkaSpout 原创 2014年1 ...
- spring-data-redis-cache 使用及源码走读
预期读者 准备使用 spring 的 data-redis-cache 的同学 了解 @CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的使 ...
- ConcurrentHashMap源码走读
目录 ConcurrentHashMap源码走读 简介 放入数据 容器元素总数更新 容器扩容 协助扩容 遍历 ConcurrentHashMap源码走读 简介 在从JDK8开始,为了提高并发度,Con ...
- underscorejs 源码走读笔记
Underscore 简介 Underscore 是一个JavaScript实用库,提供了类似Prototype.js的一些功能,但是没有继承任何JavaScript内置对象.它弥补了部分jQuery ...
随机推荐
- hadoop Partiton中的字符串Hash函数改进
最近的MapReduce端的Partition根据map生成的Key来进行哈希,导致哈希出来的Reduce端处理任务数量非常不均匀,有些Reduce端处理的数据量非常小(几分钟就执行完成,而最后的pa ...
- Linux下干净卸载mysql详解
转自:http://blog.csdn.net/tjcyjd/article/details/52189182 1.使用以下命令查看当前安装mysql情况 rpm -qa|grep -i mysql ...
- 用CSS绘制最常见的40种形状和图形
今天在国外的网站上看到了很多看似简单却又非常强大的纯CSS绘制的图形,里面有最简单的矩形.圆形和三角形,也有各种常见的多边形,甚至是阴阳太极和网站小图标,真的非常强大,分享给大家. Square(正方 ...
- buffer cache 深度解析
本文首先详细介绍了oracle中buffer cache的概念以及所包含的内存结构.然后结合各个后台进程(包括DBWRn.CKPT.LGWR等)深入介绍了oracle对于buffer cache的管理 ...
- 自己个儿写的循环分配txt中的内容
<?php header("Content-type:text/html;charset=utf8"); //当前日期 $now = time(); //作为程序是否执行的依 ...
- 谷歌强制厂商升级KitKat 仍无法改善安卓碎片化
据一份泄露的内部文档显示,谷歌计划推出新的Android版本及设备审批条例,限制硬件制造商推出Android 4.4 KitKat以下的旧版本硬件,来改变平台碎片化现象.如果厂商一意孤行,继续推出搭载 ...
- 01——微信小程序官方demo讲解——文件结构
1.环境概览 首先环境配置的部分略过,打开小程序开发工具.选择一个空目录,即可开始一个demo项目. 其中新建成功后的目录如图所示: 2.文件结构描述 如图所示,左边是界面展示,右边是目录结构. 目录 ...
- 第五章:Reminders实验:第一部分[Learn Android Studio 汉化教程]
Learn Android Studio 汉化教程 By now you are familiar with the basics of creating a new project, program ...
- Linux系统SVN安装指导配置说明
Linux 系统 SVN 安装指导配置说明 1 安装 SVN 前检查: 检查是否安装 SVN : rpm -qa subversion 查看安装 SVN 版本信息: svnserve --vers ...
- TCP/IP协议详解之广播和多播
广播和多播仅应用于 U D P,它们对需将报文同时传往多个接收者的应用来说十分重要.T C P是一个面向连接的协议,它意味着分别运行于两主机(由 I P地址确定)内的两进程(由端口号确定)间存在一条连 ...