背景

先介绍下写这篇博客时的背景。无意间就看到了某网站需要懂KCP、UDT的RD,随即投了份简历。因此,这篇博客算是一份对过往知识的梳理,同时也算是一次面试的准备过程。至于我为什么会接触RUDP这块,应该也算是研究生阶段的研究方向。当时刚上研究生的时候,VR、AR还是热门话题,而导师是研究ARQ、QoS出身的,便让我试着用RaptorQ去开发一套视频传输工具去加速大容量视频数据的传输。之后,就顺便学习了市面上一些主流RUDP的技术方案。
这里先罗列下以前学习过的技术方案,以后有机会的我们再慢慢一个一个来梳理;
KCP https://github.com/skywind3000/kcp
UDT http://udt.sourceforge.net/
QUIC Multipath Extension https://github.com/qdeconinck/mp-quic

KCP

相比UDT,KCP我最喜欢的一点就是简洁:一方面体现在使用的语言上;另一方面体现在代码行数上。抛去边边角角的无关紧要的函数,KCP核心代码就集中在4个函数中,分别是ikcp_send(), ikcp_flush(), ikcp_input(), ikcp_recv()四部分。接下来,我们以“一发->一收”这种场景来对代码进行梳理。
值得提的一点是,KCP源码当中的辅助函数名字真的有点“迷”,你不ctrl b跳转进去,真的一下子搞不灵清那个函数到底想干嘛;此外,KCP中代码中充斥着宏链表,有时候还是挺耽搁理解的。因此,在后面的分析梳理过程中,我们能用伪代码的尽量用伪代码吧。
在开始进入具体的实现细节之前,铺垫一下KCP相关的“宏观”知识会更有助于理解。

  1. 在TCP连接建立完成后发出的每个数据包中,都携带了有效的ACK序号,表示到该序号为止的所有数据包均已经成功接收;对此,KCP采取了类似的思路,在KCP中则是以“una”字段表示;

    1. 所不同的点在于,KCP中以数据包为基本单位进行编号;而TCP则是以字节byte为基本单位进行编号;
    2. UNA这种累积确认的方式,在出现“hole”这种情形时并不是十分有效;举例来说,当接收端收到1,3,4,5之后,UNA只能一直ACK 2,而毫无办法(这里其实又牵涉到了"快重传”,这个我们稍后再提);
  2. 对于“hole”这种情况,TCP后来引入了SACK选择确认,其中每个hole用一个[begin, end)序号对表示,序号对被存放在TCP首部选项中;TCP首部选项的最大长度仅为40字节,因此可以选择确认的"Hole" 个数有限;而在KCP中,有一种专门的数据包,其数据内容是一个数组,数组中的每个元素是要确认数据包的序号;因此,KCP中选择确认采用的单位是“接收到的数据包”,而不是TCP中缺失的“hole”;
  3. 既然我们刚刚在1中提到了“快重传”,那我们顺便来理一下。在TCP中,当接收端收到失序的数据包时,将会立即发送ACK,其目的是尽早通知发送方填补上这个缺口;当发送方连续收到3个重复的ACK时,将会触发“快重传”逻辑,立即重传(本质上,快重传应同SACK结合使用,只有借助SACK中的信息,我们才能准确地、有针对性地进行对缺失部分进行填补);如第2点所述的,KCP中有一种专门的SACK包,收到该SACK包的发送方首先可以根据包中的UNA字段以及SN字段清理掉一部分接收方成功接收的数据包;然后UNA字段以及所有SN值中的最大值正好就构成了一个区间[UNA, MaxSN);到此为止,发送窗口中所有位于上述区间的数据包,其实就是“失序”的数据包,毕竟人家序号为MaxSN的包都已经被接收端成功接收了;因此,位于上述区间内的所有发送缓存数据包,均会被记录“失序”次数;当失序次数达到上限值时,将会触发重传逻辑,而不是等待超时重传;当然,相比TCP中的3,KCP中的值是可以人为设定的;
  4. 目前,新的TCP中其实引入了Early Retransmit机制,其本质上是为了解决Duplicate ACK个数不足无法触发快重传的问题(参考:http://perthcharles.github.io/2015/10/31/wiki-network-tcp-early-retrans/);因此,我可以思考下KCP SACK数据包发送的时机?
  5. KCP同时支持字节流stream和消息message这两种传输形式;简便期间,我们仅分析消息message这种情形。
  6. KCP以“宏链表”为核心数据结构,串起整个work flow;按照数据流动的方向,依次是snd_queue(缓存待发送数据)、snd_buf(发送滑动窗口)、rcv_buf()、rcv_queue();
  7. 从用户使用的角度来看,在发送用户数据时只需要调用KCP的ikcp_send()函数即可;在接收用户数据时只需要调用KCP的ikcp_recv()函数即可;
  8. 从设计上来说,KCP并未指定底层所使用的传输层协议,而是通过ikcp_flush()、ikcp_input()与底层传输协议(多是UDP)交互;ikcp_flush()将需要发送的KCP分段交给UDP;而ikcp_input()从网络上获取UDP数据包,处理后进而上交给KCP(ikcp_feed()这名字会不会更贴切点,把数据喂给KCP);
  9. KCP多用在手机视频、手游等弱网环境,在这种网络环境下使用FEC是种常规做法;而鉴于第4点,我们实际上可以很轻松地在KCP和UDP之间再插入一层FEC编码层;这也是“分层模型”架构上带来的好处;

在有了上述的这些铺垫之后,让我们从代码层面着手开始梳理,首当其冲的便是ikcp_send()。ikcp_send()的功能十分简单,只需要将用户待发送的数据根据MSS值切分成段即可,切好的KCP段被插入到snd_queue中等待进一步处理;

int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
{// 根据MSS(Maximum Segment Size)计算Segment的个数;int count = 0;if (len <= (int)kcp->mss) count = 1;else count = (len + kcp->mss - 1) / kcp->mss;// 在KCP中,一个KCP分段由一个IKCPSEG结构体表示;// 这部分代码,对用户数据进行分段,分段对应的结构体被插入到snd_queue当中;for (int i = 0; i < count; i++) {int size = len > (int)kcp->mss ? (int)kcp->mss : len;       // mss, mss, ..., lenIKCPSEG *seg = ikcp_segment_new(kcp, size);                   // mallocmemcpy(seg->data, buffer, size);                            // 数据从用户buffer复制到KCP内部的IKCPSEG结构中;seg->len = size;                                           // 之后,每片用户数据都以IKCPSEG结构的形式出现;seg->frg = (kcp->stream == 0)? (count - i - 1) : 0;         // 该分段之后的分段数,类似TCP的设计;iqueue_init(&seg->node);                                 // 插入到snd_queue中iqueue_add_tail(&seg->node, &kcp->snd_queue);kcp->nsnd_que++;buffer += size;   len -= size;}return 0;
}

在讲完了ikcp_send()之后,我们接着来讲讲ikcp_flush()。相比ikcp_send(),ikcp_flush()会显得老长老长的;当然,这也是不可避免的,毕竟KCP可靠性相关的处理逻辑都被放在了这个函数中。为此,我们一段一段地去看,化整为零。值得注意的是,在ikcp_flush()函数的前部,有ACK、窗口探测相关的逻辑;针对这部分,我们暂且跳过,因为这部分其实牵涉接收过程(一个主机既可以是发送方,也可以是接收方,)

void ikcp_flush(ikcpcb *kcp) {/*flush acknowledges...*//*probe window size (if remote window size equals zero)...*//*flush window probing commands...*//*calculate window size...*/// snd_queue是用户待发送分段的队列,snd_buf是KCP的滑动窗口(  窗口内序号范围为: [snd_una, snd_una + cwnd) )// 这部分的逻辑就是,只要发送窗口允许,就尽可能地从snd_queue搬运待发送分段;while (_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0) {IKCPSEG *newseg;if (iqueue_is_empty(&kcp->snd_queue)) break;newseg = iqueue_entry(kcp->snd_queue.next, IKCPSEG, node);// 简单地从snd_queue搬运分段segiqueue_del(&newseg->node);iqueue_add_tail(&newseg->node, &kcp->snd_buf);kcp->nsnd_que--;kcp->nsnd_buf++;// 前面的ikcp_send()函数,只是为每个分段套了一个IKCPSEG结构体外壳;newseg->conv = kcp->conv;      // 用conv字段区分每次KCP会话newseg->cmd = IKCP_CMD_PUSH; // IKCP_CMD_PUSH表明该KCP包携带用户数据;newseg->wnd = seg.wnd;         // 发送端当前的窗口值,供接收端发送数据时做流控;newseg->ts = current;           // 发送时间戳newseg->sn = kcp->snd_nxt++;   // 分配sn序列号newseg->una = kcp->rcv_nxt;        // 累积确认,una编号之前的所有数据包均已收到,也就是发送端期望收到的数据包;newseg->resendts = current;   newseg->rto = kcp->rx_rto;       // 超时重传时间(Retransmission TimeOut, RTO)newseg->fastack = 0;          // 用于快重传,被ack略过的次数;newseg->xmit = 0;              // 已发送次数}// calculate resentresent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;// 遍历滑动窗口中的每一个分段for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) {IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node);int needsend = 0;if (segment->xmit == 0) {                                       // Case 1: 全新的分段,上面代码段中刚插入进来的; needsend = 1;                                             // 将该分段标记为“需要发送”segment->xmit++;                                           // 发送次数+1 segment->rto = kcp->rx_rto;                           segment->resendts = current + segment->rto + rtomin;       // 超时重传时刻 ??}else if (_itimediff(current, segment->resendts) >= 0) {         // Case 2: 超时,触发重传逻辑;needsend = 1;segment->xmit++;kcp->xmit++;if (kcp->nodelay == 0) {segment->rto += kcp->rx_rto;                            // 超时发生时,RTO 2倍退避(TCP style)}  else {segment->rto += kcp->rx_rto / 2;                      // 超时发生时,RTO 1.5倍退避(KCP style)}segment->resendts = current + segment->rto;                 // 计算超时重传时间lost = 1;}else if (segment->fastack >= resent) {                         // Case 3: 该分段被多次乱序ACK,触发快重传逻辑;needsend = 1;segment->xmit++;segment->fastack = 0;segment->resendts = current + segment->rto;            change++;}if (needsend) {                             // 如果满足上述任一Case,发送该分段;int size, need;segment->ts = current;segment->wnd = seg.wnd;segment->una = kcp->rcv_nxt;size = (int)(ptr - buffer);need = IKCP_OVERHEAD + segment->len;if (size + need > (int)kcp->mtu) {ikcp_output(kcp, buffer, size);ptr = buffer;}ptr = ikcp_encode_seg(ptr, segment);if (segment->len > 0) {memcpy(ptr, segment->data, segment->len);ptr += segment->len;}if (segment->xmit >= kcp->dead_link) {kcp->state = -1;}}}// flash remain segmentssize = (int)(ptr - buffer);if (size > 0) {ikcp_output(kcp, buffer, size);}// update ssthresh
}

参考文献:
https://wetest.qq.com/lab/view/391.html
http://kaiyuan.me/2017/07/29/KCP源码分析/
http://perthcharles.github.io/2015/10/31/wiki-network-tcp-early-retrans/

[rUDP] KCP梳理相关推荐

  1. Linux下环境变量配置方法梳理(.bash_profile和.bashrc的区别)

    博客园 首页 新随笔 联系 管理 订阅 <div class="blogStats"><!--done--> 随笔- 556  文章- 38  评论- 77 ...

  2. 【转】Vue.js 2.0 快速上手精华梳理

    Vue.js 2.0 快速上手精华梳理 Sandy 发掘代码技巧:公众号:daimajiqiao 自从Vue2.0发布后,Vue就成了前端领域的热门话题,github也突破了三万的star,那么对于新 ...

  3. Docker | Docker技术基础梳理(五) - Docker网络管理

    Docker | Docker技术基础梳理(五) - Docker网络管理 链接: 原文链接 原文链接: https://gitbook.cn/books/5b8f3c471966b44b00d265 ...

  4. 常用rsync命令操作梳理

    作为一个运维工程师,经常可能会面对几十台.几百台甚至上千台服务器,除了批量操作外,环境同步.数据同步也是必不可少的技能. 说到"同步",不得不提的利器就是rsync.rsync不但 ...

  5. 分解例题及解析_【高考物理】考前梳理,高中物理经典常考例题(带解析),收藏起来考试不低于90+!...

    物理应该是理综里最让同学们头疼的一科.最后的压轴大题更是让很多人不知道该怎么下手,题型复杂难理解,简直是丢分"小能手". 别怕!学姐来拯救你们了!高中物理经典常考例题(带解析),考 ...

  6. js 多个定时器_从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理(二)

    作者:撒网要见鱼   https://segmentfault.com/a/1190000012925872 本文接上篇 <从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理(一)> ...

  7. 插件化知识梳理(7) 类的动态加载入门

    一.前言 在 插件化知识梳理(6) - Small 源码分析之 Hook 原理 这一章的学习完成之后,下一步我们将进入插件化加载的精髓,动态加载类的学习,在此之前,我们需要先准备一些关于类加载的知识. ...

  8. 致广大关注《网络规划设计师考试案例梳理、真题透解与强化训练》读者朋友的一封信...

    致广大关注<网络规划设计师考试案例梳理.真题透解与强化训练>读者朋友的一封信 书是人类进步的阶梯,读书是增强个人软实力的佳径. 好读书是你的美德,读好书是你的选择,书好读是我们的承诺! 如 ...

  9. Python培训教程之Python基础知识点梳理

    Python语言是入门IT行业比较快速且简单的一门编程语言,学习Python语言不仅有着非常大的发展空间,还可以有一个非常好的工作,下面小编就来给大家分享一篇Python培训教程之Python基础知识 ...

最新文章

  1. Win64 驱动内核编程-4.内核里操作字符串
  2. boost::mp11::mp_all相关用法的测试程序
  3. haproxy+keepalived实现负载均衡及高可用
  4. 自编码 Autoencoder
  5. 安卓查看php文件是否存在,Android_Android编程判断SD卡是否存在及使用容量查询实现方法,本文实例讲述了Android编程判断 - phpStudy...
  6. 串口通信工具android,Android串口通信工具
  7. MySQL Study之--MySQL schema_information数据库
  8. 高清电视开播:大多用户仍难跨入
  9. linux系统vmd软件如何使用,科学网—VMD (linux下分子可视化软件) - 刘雪静的博文...
  10. 分布式系统架构网络之IDC机房
  11. 戴尔(DELL)成就Vostro15-7580 15.6英寸八代混合独显便携商务笔记本 5699元
  12. 让笔记本的无线网卡指示灯不再狂闪的方法
  13. 转载:性格与健康(刘善人)之二
  14. git--基本知识点--1--工作区/暂存区/版本库
  15. 10.32/10.33 rsync通过服务同步 10.34 linux系统日志 10.35 scre
  16. 正则表达式的用法和常用正则表达式大全(转)
  17. 论文图片格式要求具体有哪些?
  18. webstorm批量查找,批量替换快捷键
  19. 《每天五分钟冲击python基础之函数参数》(十八)
  20. tcp协议用来提供什么服务器,关于TCP协议,我想你应该懂了!

热门文章

  1. 转发:9个offer,12家公司,35场面试,从微软到谷歌,应届计算机毕业生的求职之路
  2. Docker 操作记录
  3. [CVPR2022-oral]I M Avatar: Implicit Morphable Head Avatars from Videos
  4. 中小企业ERP项目需要顾问吗?
  5. RISCV学习笔记6.2--vcs和verdi开发蜂鸟e203
  6. 360“隔离沙箱”强劲升级:运行软件不中毒
  7. 该怎么学习区块链技术?
  8. layui框架使用与代码编写
  9. CSDN博客背景皮肤设置
  10. Python接口下载文件