[rUDP] KCP梳理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sai_j/article/details/82598692

背景

先介绍下写这篇博客时的背景。无意间就看到了某网站需要懂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. 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;
        IKCPSEG *seg = ikcp_segment_new(kcp, size);
        memcpy(seg->data, buffer, size);
        seg->len = size;
        seg->frg = (kcp->stream == 0)? (count - i - 1) : 0;
        iqueue_init(&seg->node);
        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、窗口探测相关的逻辑;针对这部分,我们暂且跳过,因为这部分其实牵涉接收过程(一个主机既可以是发送方,也可以是接收方,)

持续更新

参考文献:
https://wetest.qq.com/lab/view/391.html
http://kaiyuan.me/2017/07/29/KCP%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
http://perthcharles.github.io/2015/10/31/wiki-network-tcp-early-retrans/

猜你喜欢

转载自blog.csdn.net/sai_j/article/details/82598692
Kcp