KCP特点

这里先罗列下以前学习过的技术方案,以后有机会的我们再慢慢一个一个来梳理;
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相关的“宏观”知识会更有助于理解。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值