精华内容
下载资源
问答
  • kcp
    2021-01-21 09:09:39

    什么是KCP?为什么要使用KCP?
    KCP是一个快速可靠协议。它主要的设计目的是为了解决在网络拥堵的情况下TCP协议网络速度慢的问题,增大网络传输速率,但相当于TCP而言,会相应的牺牲一部分带宽。
    kcp没有规定下层传输协议,一般用UDP作为下层传输协议。kcp层协议的数据包在UDP数据报文的基础上增加控制头。当用户数据很大,大于一个UDP包能承担的范围时(大于MSS),kcp会将用户数据分片存储在多个kcp包中。因此每个kcp包称为一个分片。

    首先我们先复习一下网络协议的一些基本的概念,这对我们理解KCP有很大的帮助。

    【超时与重传】
    超时重传指的是,发送数据包在一定的时间内没有收到相应的ACK,等待一定的时间,超时之后就认为这个数据包丢失,就会重新发送。这个等待时间被称为RTO,即重传超时时间。

    【滑动窗口】
    TCP通过确认机制来保证数据传输的可靠性。在早期的时候,发送数据方在发送数据之后会启动定时器,在一定时间内,如果没有收到发送数据包的ACK报文,就会重新发送数据,直到发送成功为止。但是这种停等的重传机制必须等待确认之后才能发送下一个包,传输速度比较慢。
    为了提高传输速度,发送方不必在每发送一个包之后就进行等待确认,而是可以发送多个包出去,然后等待接收方一 一确认。但是接收方不可能同时处理无限多的数据,因此需要限制发送方往网络中发送的数据数量。接收方在未收到确认之前,发送方在只能发送wnd大小的数据,这个机制叫做滑动窗口机制。TCP的每一端都可以收发数据。每个TCP活连接的两端都维护一个发送窗口和接收窗口结构。

    kcp结构体字段含义
    snd_una:第一个未确认的包;
    snd_nxt:下一个待分配的包的序号;

    KCP通过以下方式提高速率:
    (1)RTO。
    TCP的RTO是以2倍的方式来计算的。当丢包的次数多的时候,重传超时时间RTO就非常非常的大了,重传就非常的慢,效率低,性能差。而KCP的RTO可以以1.5倍的速度增长,相对于TCP来说,有更短的重传超时时间。
    (2)快速重传机制—无延迟ACK回复模式
    假如开启KCP的快速重传机制,并且设置了当重复的ACK个数大于resend时候,直接进行重传。 当发送端发送了1,2,3,4,5五个包,然后收到远端的ACK:1,3,4,5。当收到ACK3时,KCP知道2被跳过1次,当收到ACK4的时候,KCP知道2被跳过2次,当次数大于等于设置的resend的值的时候,不用等到超时,可直接重传2号包。这就是KCP的快速重传机制。
    下面是设置快速重传机制的源码:

    //nodelay::0 不启用,1启用快速重传模式
    //interval:  内部flush刷新时间
    //resend:   0(默认)表示关闭。可以自己设置值,若设置为2(则2次ACK跨越将会直接重传)
    //nc:         是否关闭拥塞控制,0(默认)代表不关闭,1代表关闭
    int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
    {
        if (nodelay >= 0)              //大于0表示启用快速重传模式
        {             
            kcp->nodelay = nodelay;
            if (nodelay) {
                kcp->rx_minrto = IKCP_RTO_NDL;    //最小重传超时时间(如果需要可以设置更小)
            } else{
                kcp->rx_minrto = IKCP_RTO_MIN;  
            }
        }
        if (interval >= 0) {
            if (interval > 5000) 
                interval = 5000;
            else if (interval < 10) 
                interval = 10;
            kcp->interval = interval;           //内部flush刷新时间
        }
        if (resend >= 0) {                     // ACK被跳过resend次数后直接重传该包, 而不等待超时
            kcp->fastresend = resend           // fastresend : 触发快速重传的重复ack个数
        }
        if (nc >= 0) {
            kcp->nocwnd = nc;
        }
        return 0;
    }
    

    (3)选择重传
    KCP采用滑动窗口机制来提高发送速度。由于UDP是不可靠的传输方式,会存在丢包和乱序。而KCP是可靠的且保证数据有序的协议。为了保证包的顺序,接收方会维护一个接收窗口,接收窗口有一个起始序号 rcv_nxt(待接收消息序号)以及尾序号 rcv_nxt + rcv_wnd(接收窗口大小)。如果接收窗口收到序号为 rcv_nxt 的分片(属于接收窗口的消息序号),那么 rcv_nxt 就加1,也就是滑动窗口右移,并把该数据放入接收队列供应用层取用。如果收到的数据在窗口范围内但不是 rcv_nxt ,那么就把数据缓存起来,等收到rcv_nxt序号的分片时再一并放入接收队列供应用层取用。
    当丢包发生的时候,假设第n个包丢失了,但是第n+1,n+2个包都已经传输成功了,此时只重传第n个包,而不重传成功传输的n+1,n+2号包,这就是选择重传。为了能够做到选择重传,接收方需要告诉发送方哪些包它收到了。比如在返回的ACK中包含rcv_nxt和sn,rcv_nxt的含义是接收方已经成功按顺序接收了rcv_nxt序号之前的所有包,大于rcv_nxt的序号sn表示的是在接收窗口内的不连续的包。那么根据这两个参数就可以计算出哪些包没有收到了。发送方接收到接收方发过来的数据时,首先解析rcv_nxt,把所有小于rcv_nxt序号的包从发送缓存队列中移除。然后再解析sn(大于rcv_nxt),遍历发送缓存队列,找到所有序号小于sn的包,根据我们设置的快速重传的门限,对每个分片维护一个快速重传的计数,每收到一个ack解析sn后找到了一个分片,就把该分片的快速重传的计数加一,如果该计数达到了快速重传门限,那么就认为该分片已经丢失,可以触发快速重传,该门限值在kcp中可以设置。
    (4)拥塞窗口
    当网络状态不好的时候,KCP会限制发送端发送的数据量,这就是拥塞控制。拥塞窗口(cwnd)会随着网络状态的变化而变化。这里采用了慢启动机制,慢启动也就是控制拥塞窗口从0开始增长,在每收到一个报文段确认后,把拥塞窗口加1,多增加一个MSS的数值。但是为了防止拥塞窗口过大引起网络阻塞,还需要设置一个慢机制的的门限(ssthresh即拥塞窗口的阈值)。当拥塞窗口增长到阈值以后,就减慢增长速度,缓慢增长。
    但是当网络很拥堵的情况下,导致发送数据出现重传时,这时说明网络中消息太多了,用户应该减少发送的数据,也就是拥塞窗口应该减小。怎么减小呢,在快速重传的情况下,有包丢失了但是有后续的包收到了,说明网络还是通的,这时采取拥塞窗口的退半避让,拥塞窗口减半,拥塞门限减半。减小网络流量,缓解拥堵。当出现超时重传的时候,说明网络很可能死掉了,因为超时重传会出现,原因是有包丢失了,并且该包之后的包也没有收到,这很有可能是网络死了,这时候,拥塞窗口直接变为1,不再发送新的数据,直到丢失的包传输成功。

    KCP主要工作过程:
    1、把要发送的buffer分片成KCP的数据包格式,插入待发送队列中;
    当用户的数据超过一个MSS(最大分片大小)的时候,会对发送的数据进行分片处理。KCP采用的是流的方式进行分片处理。通过frg进行排序区分,frg即message中的segment分片ID,在message中的索引,由大到小,0表示最后一个分片。比如3,2,1,0。即把message分成了四个分片,分片的ID分别是4,3,2,1;

    分片方式共有两种:
    消息方式:将用户数据分片,为每个分片设置ID,将分片后的数据一个一个地存入发送队列,接收方通过id解析原来的包,消息方式一个分片的数据量可能不能达到MSS;
    流方式:检测每个发送队列里的分片是否达到最大MSS,如果没有达到就会用新的数据填充分片;
    网络速度:流方式 > 消息方式
    接收数据:流方式一个分片一个分片的的接收。消息方式kcp的接收函数会把自己原本属于一个数据的分片重组。

    int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
    {
        IKCPSEG *seg;
        int count, i;
    
        assert(kcp->mss > 0);
        if (len < 0) return -1;
    
        //根据len计算出需要多少个分片
        if (len <= (int)kcp->mss) 
            count = 1;
        else 
            count = (len + kcp->mss - 1) / kcp->mss;   
    
        if (count > 255) 
            return -2;
    
        if (count == 0) 
            count = 1;
    
        // fragment
        for (i = 0; i < count; i++) {
            int size = len > (int)kcp->mss ? (int)kcp->mss : len;   //获取当前分片的长度,存放到size中
            seg = ikcp_segment_new(kcp, size);      
            assert(seg);
            if (seg == NULL) {
                return -2;
            }
            if (buffer && len > 0) {
                memcpy(seg->data, buffer, size);
            }
            seg->len = size;
            seg->frg = count - i - 1;    //frg用来表示被分片的序号,从大到小递减
            iqueue_init(&seg->node);
            iqueue_add_tail(&seg->node, &kcp->snd_queue);   //把segment分片插入到发送队列中
            kcp->nsnd_que++;
            if (buffer) {
                buffer += size;
            }
            len -= size;
        }
    
        return 0;
    }
    

    2、将发送队列中的数据通过下层协议UDP进行发送
    void ikcp_flush(ikcpcb *kcp)主要处理一下四种情况:
    (1)发送ack

    // flush acknowledges
    count = kcp->ackcount;
    for (i = 0; i < count; i++) {
        size = (int)(ptr - buffer);
        if (size + IKCP_OVERHEAD > IKCP_OVERHEAD) {
            ikcp_output(kcp, buffer, size);
            ptr = buffer;
        }
        ikcp_ack_get(kcp, i, &seg.sn, &seg.ts);   //sn:message分片segment的序号,ts:message发送时刻的时间戳
        ptr = ikcp_encode_seg(ptr, &seg);
    }
    
    kcp->ackcount = 0;
    

    (2)发送探测窗口消息

    // probe window size (if remote window size equals zero)
    if (kcp->rmt_wnd == 0) {                                //远端接收窗口大小为0的时候
        if (kcp->probe_wait == 0) {                         //探查窗口需要等待的时间为0
            kcp->probe_wait = IKCP_PROBE_INIT;              //设置探查窗口需要等待的时间
            kcp->ts_probe = kcp->current + kcp->probe_wait; //设置下次探查窗口的时间戳 = 当前时间 + 探查窗口等待时间间隔
        }    
        else {
            if (_itimediff(kcp->current, kcp->ts_probe) >= 0) { //当前时间 > 下一次探查窗口的时间
                if (kcp->probe_wait < IKCP_PROBE_INIT) 
                    kcp->probe_wait = IKCP_PROBE_INIT;
                kcp->probe_wait += kcp->probe_wait / 2;   //等待时间变为之前的1.5倍
                if (kcp->probe_wait > IKCP_PROBE_LIMIT)
                    kcp->probe_wait = IKCP_PROBE_LIMIT;   //若超过上限,设置为上限值
                kcp->ts_probe = kcp->current + kcp->probe_wait;  //计算下次探查窗口的时间戳
                kcp->probe |= IKCP_ASK_SEND;         //设置探查变量。IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小
            }
        }
    }    else {
        kcp->ts_probe = 0;
        kcp->probe_wait = 0;
    }
    
    // flush window probing commands。IKCP_ASK_SEND表示请求远端告知窗口大小
    if (kcp->probe & IKCP_ASK_SEND) {
        seg.cmd = IKCP_CMD_WASK;
        size = (int)(ptr - buffer);
        if (size + IKCP_OVERHEAD > IKCP_OVERHEAD) {
            ikcp_output(kcp, buffer, size);     //KCP的下层输出协议,通过设置回调函数来实现
            ptr = buffer;
        }
        ptr = ikcp_encode_seg(ptr, &seg);
    }
    
    // flush window probing commands。IKCP_ASK_TELL表示告知远端窗口大小
    if (kcp->probe & IKCP_ASK_TELL) {
        seg.cmd = IKCP_CMD_WINS;
        size = (int)(ptr - buffer);
        if (size + IKCP_OVERHEAD > IKCP_OVERHEAD) {
            ikcp_output(kcp, buffer, size);
            ptr = buffer;
        }
        ptr = ikcp_encode_seg(ptr, &seg);
    }
    
    // flash remain no data segments
    size = (int)(ptr - buffer);
    if (size > 0) {
        ikcp_output(kcp, buffer, size);
        ptr = buffer;
    }
    
    kcp->probe = 0;
    

    (3)计算拥塞窗口大小

    // calculate window size
    cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);    //cwnd = 发送窗口大小 和 远端接收窗口大小的最小值
    if (kcp->nocwnd == 0)                         //不取消拥塞控制
        cwnd = _imin_(kcp->cwnd, cwnd);           //拥塞窗口 = 当前拥塞窗口和cwnd的最小值(也就是取当前拥塞窗口、发送窗口、接收窗口的最小值)
    

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

    
    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:发送消息的队列
    
        iqueue_del(&newseg->node);                      //从发送消息队列中,删除节点
        iqueue_add_tail(&newseg->node, &kcp->snd_buf);  //然后把删除的节点,加入到kcp的发送缓存队列中
        kcp->nsnd_que--; 
        kcp->nsnd_buf++;
    
        newseg->conv = kcp->conv;     //会话id
        newseg->cmd = IKCP_CMD_PUSH;  //cmd:用来区分分片的作用。IKCP_CMD_PUSH:数据分片,IKCP_CMD_ACK:ack分片,IKCP_CMD_WASK:请求告知窗口大小,IKCP_CMD_WINS:告知窗口大小
        newseg->wnd = seg.wnd;  
        newseg->ts = current;           
        newseg->sn = kcp->snd_nxt++;  //下一个待发报的序号
        newseg->una = kcp->rcv_nxt;   //待收消息序号
        newseg->resendts = current;   //下次超时重传的时间戳
        newseg->rto = kcp->rx_rto;    //由ack接收延迟计算出来的重传超时时间
        newseg->fastack = 0;          //收到ack时计算的该分片被跳过的累计次数
        newseg->xmit = 0;             //发送分片的次数,每发送一次加一
    }
    

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

    
    // flush data segments
    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) {
            needsend = 1;
            segment->xmit++;                 //发送分片的次数
            segment->rto = kcp->rx_rto;     //该分片超时重传的时间戳
            segment->resendts = current + segment->rto + rtomini;  //下次超时重传的时间戳
        }
        else if (_itimediff(current, segment->resendts) >= 0) {   //当前时间>下次重传时间。说明没有重传,即丢包了?
            needsend = 1;
            segment->xmit++;
            kcp->xmit++;
            if (kcp->nodelay == 0) {        //0:表示不启动快速重传模式
                segment->rto += kcp->rx_rto;    //不启动快速重传模式,每次重传之后rto的时间就是之前的2倍
            }    else {
                segment->rto += kcp->rx_rto / 2;  //启用快速重传之后,rto变成原来的1.5倍
            }
            segment->resendts = current + segment->rto;
            lost = 1;
        }
        else if (segment->fastack >= resent) {     //fastack:表示收到ack计算的该分片被跳过的累积次数
            needsend = 1;
            segment->xmit++;
            segment->fastack = 0;
            segment->resendts = current + segment->rto;
            change++;
        }
    
        if (needsend) {
            int size, need;
            segment->ts = current;
            segment->wnd = seg.wnd;       //剩余接收窗口大小。即接收窗口大小-接收队列大小
            segment->una = kcp->rcv_nxt;  //待接收消息序号
    
            size = (int)(ptr - buffer);
            need = IKCP_OVERHEAD + segment->len;   //segment报文默认大小 + segment的长度
    
            // 禁止数据包合包
            if (size + need > IKCP_OVERHEAD) {
                ikcp_output(kcp, buffer, size, IKCP_RETRY_FLAG);
                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;
            }
    
            // 重试次数打日志
            if (segment->xmit > 1)
            {
                ikcp_log(kcp, 0x80000000, "xmit: %d, sn: %d, rto: %u", segment->xmit, segment->sn, segment->rto);
            }
        }
    }
    
    // flash remain segments
    size = (int)(ptr - buffer);
    if (size > 0) {
        ikcp_output(kcp, buffer, size, IKCP_RETRY_FLAG);
    }
    
    

    (6)根据重传数据更新发送窗口大小;

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

    if (change) {
        IUINT32 inflight = kcp->snd_nxt - kcp->snd_una;   //下一个要分配的包 - 第一个未确认的包
        kcp->ssthresh = inflight / 2;                     //change=1说明发生过快速重传。当发生快速重传的时候,会将慢启动阈值调整为当前发送窗口的一半
        if (kcp->ssthresh < IKCP_THRESH_MIN)
            kcp->ssthresh = IKCP_THRESH_MIN;   
        kcp->cwnd = kcp->ssthresh + resent;   //并把拥塞窗口大小 = 拥塞窗口阈值 + 触发快速重传的ack大小
        kcp->incr = kcp->cwnd * kcp->mss;
    }
    

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

    if (lost) {
        kcp->ssthresh = cwnd / 2;   //丢包了。窗口的大小需要减半
        if (kcp->ssthresh < IKCP_THRESH_MIN)
            kcp->ssthresh = IKCP_THRESH_MIN;
        kcp->cwnd = 1;
        kcp->incr = kcp->mss;
    }
    

    3、kcp接收到下层协议UDP传进来的数据底层数据buffer转换成kcp的数据包格式;

    int ikcp_input(ikcpcb *kcp, const char *data, long size)
    

    KCP报文分为ACK报文、数据报文、探测窗口报文、响应窗口报文四种。
    kcp报文的una字段(snd_una:第一个未确认的包)表示对端希望接收的下一个kcp包序号,也就是说明接收端已经收到了所有小于una序号的kcp包。解析una字段后需要把发送缓冲区里面包序号小于una的包全部丢弃掉。

    ack报文则包含了对端收到的kcp包的序号,接到ack包后需要删除发送缓冲区中与ack包中的发送包序号(sn)相同的kcp包。

    if (cmd == IKCP_CMD_ACK) {
        if ((_itimediff(kcp->current, ts) >= 0) && (_itimediff(sn, kcp->maxsn) >= 0)) {
            ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
        }
    
        ikcp_parse_ack(kcp, sn);
        ikcp_shrink_buf(kcp);
        if (ikcp_canlog(kcp, IKCP_LOG_IN_ACK)) {
            ikcp_log(kcp, IKCP_LOG_IN_DATA, 
                "input ack: sn=%lu rtt=%ld rto=%ld", sn, 
                (long)_itimediff(kcp->current, ts),
                (long)kcp->rx_rto);
        }
    }
    

    解析ACK报文:

    static void ikcp_parse_ack(ikcpcb *kcp, IUINT32 sn)
    {
        struct IQUEUEHEAD *p, *next;
    
        if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0)
            return;
    
        for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) {
            IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
            next = p->next;
            if (sn == seg->sn) {
                iqueue_del(p);
    
                kcp->sumxmit += seg->xmit;
                ++kcp->sumseg;
    
                ikcp_segment_delete(kcp, seg);
                kcp->nsnd_buf--;
                break;
            }
            else {
                // 序号为sn的被跳过了
                seg->fastack++;
            }
        }
    }
    

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

    else if (cmd == IKCP_CMD_PUSH) {    //数据报文
        if (ikcp_canlog(kcp, IKCP_LOG_IN_DATA)) {
            ikcp_log(kcp, IKCP_LOG_IN_DATA, 
                "input psh: sn=%lu ts=%lu", sn, ts);
        }
        if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) {
            ikcp_ack_push(kcp, sn, ts);     //sn:message分片segment的序号,ts:message发送时刻的时间戳
            if (_itimediff(sn, kcp->rcv_nxt) >= 0) {
                seg = ikcp_segment_new(kcp, len);
                seg->conv = conv;
                seg->cmd = cmd;
                seg->frg = frg;
                seg->wnd = wnd;
                seg->ts = ts;
                seg->sn = sn;
                seg->una = una;
                seg->len = len;
    
                if (len > 0) {
                    memcpy(seg->data, data, len);
                }
    
                ikcp_parse_data(kcp, seg);
            }
        }
    }
    
    

    如果snd_una增加了那么就说明对端正常收到且回应了发送方发送缓冲区第一个待确认的包,此时需要更新cwnd(拥塞窗口)

     if (_itimediff(kcp->snd_una, una) > 0) {     //如果第一个未确认的包的序号>待接收消息序号
        if (kcp->cwnd < kcp->rmt_wnd) {          //用拥塞口大小 < 远端接收窗口大小
           IUINT32 mss = kcp->mss;
            if (kcp->cwnd < kcp->ssthresh) {     //拥塞窗口大小 < 拥塞窗口阈值
                kcp->cwnd++;                     //拥塞窗口+1
                 kcp->incr += mss;                //可发送最大数据量增加最大分片个大小
             }   else {
               if (_itimediff(kcp->snd_una, una) > 0) {
            if (kcp->cwnd < kcp->rmt_wnd) {
                IUINT32 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 / 16);
                    if ((kcp->cwnd + 1) * mss <= kcp->incr) {
                        kcp->cwnd++;
                    }
                }
                if (kcp->cwnd > kcp->rmt_wnd) {
                    kcp->cwnd = kcp->rmt_wnd;
                    kcp->incr = kcp->rmt_wnd * mss;
                }
            }
        }
    

    4、kcp将接收到的kcp数据包还原成之前kcp发送的buffer数据。
    int ikcp_recv(ikcpcb *kcp, char *buffer, int len)
    如果有分片,则执行merge操作。

    更多相关内容
  • node-kcp:Node.js的KCP协议

    2021-05-25 18:28:14
    节点kcp Node.js的如何建造: npm install -g node-gypnode-gyp configuregit clone git@github.com:leenjewel/node-kcpcd node-kcpgit submodule initgit submodule updatenode-gyp build例子:通过npm安装 npm ...
  • KCP C#版。 支持目标框架: dotnetstandard2.0 dotnetstandard1.1 开箱即用。也可以使用Nuget搜索KCP。 新增异步API标准接口 附带一个基本实现。 新增kcpSegment泛型化,可以实现用户自定义高级实现。 链接: c...
  • KCP-一种快速可靠的ARQ协议 简介 KCP是一个快速可靠的协议,能以比TCP浪费10%-20%的带宽的代价,换取平均延迟降低30%-40%,并且最大延迟降低三倍的传输效果。纯算法实现,并不负责任协议(如UDP)的收发,需要...
  • KCP是一种快速可靠的协议,可以达到平均延迟降低30%到40%,最大延迟降低三倍的传输效果,代价是浪费了10%到20%的带宽比TCP。 使用纯算法实现,不负责底层协议(如UDP)的发送和接收,需要用户自己定义底层数据包的...
  • kcp库代码 c c++

    2019-02-20 09:16:30
    KCP库代码,github上下载。KCP是一个快速可靠协议。它主要的设计目的是为了解决在网络拥堵的情况下TCP协议网络速度慢的问题,增大网络传输速率,但相当于TCP而言,会相应的牺牲一部分带宽。
  • kcp-netty 基于Netty的KCP的Java实现 要使用Maven添加依赖项,请执行以下操作: < groupId>io.jpower.kcp < artifactId>kcp-netty < version>1.4.10 如何使用 您可以在目录中找到。
  • AP-KCPArmor-Piercing KCP (装甲穿透KCP/破甲KCP)基于KCP修改和优化,用于穿透恶劣网络环境的高性能可靠传输协议(ARQ)。使用 Rust 实现。拥有基于 ring 的密码学支持,和基于 smol 的异步运行时。下图为在校园网的...
  • java-Kcp 基于netty版本实现的kcp(包含fec功能的实现) KCP是一个基于udp的快速可靠协议(rudp),能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。 maven地址: ...
  • 介绍KCP-GO是生产级可靠-UDP库 。 该库旨在通过UDP数据包提供平滑,有弹性,有序,经过错误检查和匿名的流交付,并已通过开源项目进行了测试。 数以百万计的设备(从低端MIPS路由器到高端服务器)已经以各种形式...
  • kcp-csharp KCP-一种快速可靠的ARQ协议去做 基于里德-所罗门码的FEC(前向纠错) 数据包级加密链接天风3000 xtaci
  • tidus-java-kcp 本项目中 KCP.java 来自于 KCPC.java 基于以上项目进行修改 并尽量和原版 接近,方便后续进行更新。 相比hkspirt/kcp-java 做了如下修改 将 ArrayList 改为用LinkedList。 在遍历并移除时开销更小 将...
  • 1.基于kcp协议,实现UDP消息客户端。2.基于kcp协议,实现UDP消息服务器。
  • KCP 是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP) 的收发,需要使用者自己定义下层数据包的发送方式...
  • kcp是一个最小的 Kubernetes API 服务器 到底有多小? kcp不知道 s 或 s,更不用说 s、 s、 s 等。 默认情况下, kcp只知道: 和类型,如和 s 和 s,用于存储配置数据 s,定义新类型一些其他低级资源,如 s、 Event ...
  • kcp-go - 一个全功能的可靠UDP通信Go库
  • kcp4sharp kcp是一种独立于长期通信协议的重传算法,kcp4sharp适用于客户端场景,只需要继承相关的类即可;用户不用担心udp和kcp的使用细节可以轻松驾驭驭moba类等需要高速传输环境的应用开发 请参考TestKcp.cs文件...
  • kcptun 是一个基于 KCP 的稳定且安全的隧道,具有 N:M 复用和 FEC。 适用于 ARM、MIPS、386 和 AMD64。 kcptun 附带由各种块加密算法提供支持的内置数据包加密,并在密码反馈模式下工作,对于每个要发送的数据包,...
  • python库。 资源全名:kcp_wrapper-0.2.1.tar.gz
  • kcp-server, kcp服务器一个密钥安装外壳,用于 https kcp服务器##作为kcptun的搬运工,我只是提供了一键安装脚本,至于使用的原理啊、功能啊、bug啊请各位移步到kcptun项目,我真的无能为力。电子邮件服务器安装wget...
  • 免责声明:kcptun维护一个网站— 。...要求目标最低限度推荐的系统aix达尔文蜻蜓freebsd linux netbsd openbsd solaris窗口linux 记忆> 20MB > 32MB 中央处理器任何带有AES-NI和AVX2的amd64快速开始增加服务器上打开...
  • 资源来自pypi官网。 资源全名:kcp_net-0.1.25.tar.gz
  • KCP协议:从TCP到UDP家族QUIC/KCP/ENET

    千次阅读 2022-03-27 17:42:44
    KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程以及重传...

    行文前先安利下《再深谈TCP/IP三步握手&四步挥手原理及衍生问题—长文解剖IP 》、《再谈UDP协议—浅入理解深度记忆

    KCP协议科普

    KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。

    纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。本文传输协议之考虑UDP的情况。

    整个KCP协议主要依靠一个循环ikcp_update来驱动整个算法的运转,所有的数据发送,接收,状态变化都依赖于此,所以如果有操作占用每一次update的周期过长,或者设置内部刷新的时间间隔过大,都会导致整个算法的效率降低。在ikcp_update中最终调用的是ikcp_flush,这是协议中的一个核心函数,将数据,确认包,以及窗口探测和应答发送到对端。

    KCP使用ikcp_send发送数据,该函数调用ikcp_output发送数据,实际上最终调用事先注册的发送回调发送数据。KCP通过ikcp_recv将数据接收出来,如果被分片发送,将在此自动重组,数据将与发送前保持一致。

    KCP为什么存在?

    首先要看TCP与UDP的区别,TCP与UDP都是传输层的协议,比较两者的区别主要应该是说TCP比UDP多了什么?

    • 面向连接:TCP接收方与发送方维持了一个状态(建立连接,断开连接),双方知道对方还在。

    • 可靠的:发送出去的数据对方一定能够接收到,而且是按照发送的顺序收到的。

    • 流量控制与拥塞控制:TCP靠谱通过滑动窗口确保,发送的数据接收方来得及收。TCP无私,发生数据包丢失的时候认为整个网络比较堵,自己放慢数据发送速度。

    TCP/UDP/KCP

    TCP

    • TCP协议的可靠性让使用TCP开发更为简单,同时它的这种设计也导致了慢的特点。

    • TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽

    • TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程以及重传策略。由于TCP内置在系统协议栈中,极难对其进行改进。 

    UDP

    • UDP协议简单,所以它更快。但是,UDP毕竟是不可靠的,应用层收到的数据可能是缺失、乱序的。

    • UDP协议以其简单、传输快的优势,在越来越多场景下取代了TCP,如网页浏览、流媒体、实时游戏、物联网。

    随着网络技术飞速发展,网速已不再是传输的瓶颈,CDN服务商Akamai报告从2008年到2015年7年时间,各个国家网络平均速率由1.5Mbps提升为5.1Mbps,网速提升近4倍。网络环境变好,网络传输的延迟、稳定性也随之改善,UDP的丢包率低于5%,如果再使用应用层重传,能够完全确保传输的可靠性。

    KCP

    KCP协议就是在保留UDP快的基础上,提供可靠的传输,应用层使用更加简单——TCP可靠简单,但是复杂无私,所以速度慢。KCP尽可能保留UDP快的特点下,保证可靠。

    • TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。

    • KCP是为流速设计的(单个数据包从一端发送到一端需要多少时间),以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。

    TCP信道是一条流速很慢,但每秒流量很大的大运河,而KCP是水流湍急的小激流。

    MOBA类和“吃鸡”游戏多使用帧同步为主要同步算法,竞技性也较高,无论从流畅性,还是从公平性要求来说,对响应延迟的要求都最高,根据业内经验,当客户端与服务器的网络延迟超过150ms时,会开始出现卡顿,当延迟超过250ms时,会对玩家操作造成较大影响,游戏无法公平进行。类似地,“吃鸡”游戏(如《绝地求生》)玩法对玩家坐标、动作的同步要求极高,延迟稍大导致的数据不一致对体验都会造成较大影响,其实时性要求接近MOBA类游戏。而对于传统mmorpg来说,多采用状态同步算法,以属性养成和装备获取为关注点,也有一定竞技性,出于对游戏流畅性的要求,对延迟也有一定要求,同步算法的优化程度不一样,这一要求也不一样,一般情况下为保证游戏正常进行,需要响应延迟保持在300ms以下。相比之下,对于炉石传说、斗地主、梦幻西游等回合制游戏来说,同时只有一个玩家在操作双方数据,无数据竞争,且时间粒度较粗,甚至可通过特效掩盖延迟,因此对网络延迟的要求不高,即便延迟达到500ms~1000ms,游戏也能正常进行

    不同传输层协议在可靠性、流量控制等方面都有差别,而这些技术细节会对延迟造成影响。

    tcp追求的是完全可靠性和顺序性,丢包后会持续重传直至该包被确认,否则后续包也不会被上层接收,且重传采用指数避让策略,决定重传时间间隔的RTO(retransmission timeout)不可控制,linux内核实现中最低值为200ms,这样的机制会导致丢包率短暂升高的情况下应用层消息响应延迟急剧提高,并不适合实时性高、网络环境复杂的游戏。

    基于udp定制传输层协议,引入顺序性和适当程度或者可调节程度的可靠性,修改流控算法。适当放弃重传,如:设置最大重传次数,即使重传失败,也不需要重新建立连接。比较知名的tcp加速开源方案有:quic、enet、kcp、udt。

    kcp/quic/enet协议的区别

    先安利下《浅谈QUIC协议原理与性能分析及部署方案》,

    • quic 是一个完整固化的 http 应用层协议,目前已经更名 http/3,指定使用 udp(虽然本质上并不一定需要 udp)。其主要目的是为了整合TCP协议的可靠性和udp协议的速度和效率,其主要特性包括:避免前序包阻塞、减少数据包、向前纠错、会话重启和并行下载等,然而QUIC对标的是TCP+TLS+SPDY,相比其他方案更重,目前国内用于网络游戏较少

    • kcp 只是一套基于无连接的数据报文之上的连接和拥塞控制协议,对底层【无连接的数据报文】没有具体的限制,可以基于 udp,也可以基于伪造的 tcp/icmp 等,也可以基于某些特殊环境的非 internet 网络(比如各种现场通信总线)

    • enet: 有ARQ协议。收发不用自己实现,提供连接管理,心跳机制。支持人数固定。自己实现跨平台。支持可靠无序通道。没有拥塞控制。线程不安全

    其实kcp不能和quic对比(quic vs enet),只是讲到UDP的时候,顺带搭上QUIC协议,类似的还有WebRTC

    为什么采用UDP,而不是其他的协议呢?比如SCTP天生就具备TCP/UDP所不具备的各种优点(支持多宿主多流分帧可无序抗syn flooding),但是就比如Windows系统,各种路由器、网关都不支持,无法铺开(除非在私有网络或者专用网络中用)。况且,TCP/UDP的各种问题很多都已经通过技术或技巧给解决了。

    KCP的配置模式

    在网络中,我们认为传输是不可靠的,而在很多场景下我们需要的是可靠的数据,所谓的可靠,指的是数据能够正常收到,且能够顺序收到,于是就有了ARQ协议,TCP之所以可靠就是基于此。

    ARQ协议(Automatic Repeat-reQuest),即自动重传请求,是传输层的错误纠正协议之一,它通过使用确认和超时两个机制,在不可靠的网络上实现可靠的信息传输。

    ARQ协议有两种模式:

    停等ARQ协议

    同步请求响应模式,基于超时重传保证可靠。

    1. A会为每个即将发送的数据编号,编号的目的是为了标识数据和给数据排序

    2. A发送完数据之后,会给这次发送的数据设置一个超时计时器

    3. B收到数据,将会返回一个确认,该确认也有自己的编号

    4. A收到确认,将删除副本且取消超时计时器,保留副本的原因是传输可能出错

    5. B收到错误的数据,或者数据在传输过程中出错,总之就是说B没有收到想要的数据

    6. A在超时计时器的设置时间内没有收到确认,此时重发数据

    所以可靠的TCP有32位序列号和32位确认号,TCP和UDP都有16位校验和。

    连续ARQ协议

    可以连续发送多个分组,而不必每发完一个分组就停下来等待对方确认。

    是不是想到了HTTP1.1中的管道模式与HTTP1.0停等模式,但这里有些许区别,HTTP1.1是中服务器按照顺序响应客户端请求,但连续ARQ协议不会响应每个数据段,而是仅仅响应编号最大的这个数据段,表示之前的数据都收到了,这个叫做UNA模式,而停等ARQ协议可以看作是ACK模式。

    现在已经能够在不可靠的网络中传输可靠的数据,但这不意味着可以随意发送数据,带宽是有限的,接收方的负载也是有限的,所以引入了窗口协议,做流量控制。

    窗口协议中有两种:

    拥塞窗口

    防止过多的数据注入到网络中,这样可以使网络中的路由器 和链路不至于过载。

    拥塞控制相关的有慢启动、退半避让、快重传、快恢复等

    慢启动是在刚开始发送数据时让窗口缓慢扩张,退半避让是在网络拥堵时窗口大小减半,快重传是在网络恢复时及时给予响应,与之配合的就是快恢复。

    滑动窗口

    接收方告知发送方自己可以接收缓冲区的大小,通常与连续ARQ协议配合使用。

    TCP协议中的16位窗口大小就是为窗口协议提供支持的。而UDP协议的目标是尽最大努力交付,不管你收到没有,所以没有该字段。

    TCP协议是面向连接的协议,在数据传输前通过三次握手建立连接,传输完成后通过四次挥手断开连接,整个过程表示一次完整的数据传输,所以需要4位头长告知哪些是正在传输的数据。

    UDP协议是无连接的,两次数据传输没有任何联系,所以需要16位长度告知本次传输的数据有多少。同时注意,UDP协议每次传输的数据量并不是2^16 - 1 - 8 - 20(8表示UDP头长,20表示IP头长),而是与MTU有关,即数据链路层的最大传输单元(Maximum Transmission Unit),值是1500。

    TCP协议中的8位标志位表示不同的功能,例如当SYN = 1时表示建立连接时让ack = seq + 1而不做任何验证,当URG = 1时16位紧急指针生效,紧急指针表示正常数据的起始位置,而之前的数据则表示额外的紧要数据,可以被尽快处理。

    当清楚TCP和UDP的工作流程,KCP就很容易理解了。

    KCP工作模式:

    KCP协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:

    int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)

    • nodelay :是否启用 nodelay模式,0不启用;1启用。

    • interval :协议内部工作的 interval,单位毫秒,比如 10ms或者 20ms

    • resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传)

    • nc :是否关闭流控,默认是0代表不关闭,1代表关闭。

    KCP有正常模式和快速模式两种,通过以下策略达到提高流速的结果:

    • 普通模式/正常模式: ikcp_nodelay(kcp, 0, 40, 0, 0);

    • 极速模式/快速模式: ikcp_nodelay(kcp, 1, 10, 2, 1)

    最大窗口:

    int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);

    该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32. 这个可以理解为 TCP的 SND_BUF 和 RCV_BUF,只不过单位不一样 SND/RCV_BUF 单位是字节,这个单位是包。

    最大传输单元:

    纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。

    最小RTO:

    TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度

    KCP对比TCP配置

    RTO翻倍vs不翻倍:

    • TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖

    • KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度

    选择性重传 vs 全部重传:

    • TCP丢包时会全部重传从丢的那个包开始以后的数据

    • KCP是选择性重传,只重传真正丢失的数据包。(TCP同样有选择重传SACK,但有区别,后续文章再介绍)。

    快速重传:

    与TCP相同,都是通过累计确认实现的,发送端发送了1,2,3,4,5几个包,然后收到远端的ACK:1,3,4,5,当收到ACK = 3时,KCP知道2被跳过1次,收到ACK = 4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。1字节cmd = 81时,sn相当于TCP中的seq,cmd = 82 时,sn相当于TCP中的ack。cmd相当于WebSocket协议中的openCode,即操作码。

    延迟ACK vs 非延迟ACK:

    TCP在连续ARQ协议中,不会将一连串的每个数据都响应一次,而是延迟发送ACK,即上文所说的UNA模式,目的是为了充分利用带宽,但是这样会计算出较大的RTT时间,延长了丢包时的判断过程,而KCP的ACK是否延迟发送可以调节。

    • TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程。

    • KCP的ACK是否延迟发送可以调节。

    UNA vs ACK+UNA:

    ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。

    非退让流控:

    KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果

    在传输及时性要求很高的小数据时,可以通过配置忽略上文所说的窗口协议中的拥塞窗口机制,而仅仅依赖于滑动窗口。2字节wnd与TCP协议中的16位窗口大小意义相同,值得一提的是,KCP协议的窗口控制还有其它途径,当cmd = 83时,表示询问远端窗口大小,当cmd = 84时,表示告知远端窗口大小。

    4字节conv表示会话匹配数字,为了在KCP基于UDP实现时,让无连接的协议知道哪个是哪个,相当于WEB系统HTTP协议中的SessionID。

    1字节frg表示拆数据时的编号,4字节len表示整个数据的长度,相当于WebSocket协议中的len。

    IKCPCB结构

    IKCPCB是KCP中最重要的结构,也是在会话开始就创建的对象,代表着这次会话,所以这个结构体体现了一个会话所需要涉及到的所有组件。其中一些参数在IKCPSEG中已经描述,不再多说。

    • conv:标识这个会话;

    • mtu:最大传输单元,默认数据为1400,最小为50;

    • mss:最大分片大小,不大于mtu;

    • state:连接状态(0xFFFFFFFF表示断开连接);

    • snd_una:第一个未确认的包;

    • snd_nxt:下一个待分配的包的序号;

    • rcv_nxt:待接收消息序号。为了保证包的顺序,接收方会维护一个接收窗口,接收窗口有一个起始序号rcv_nxt(待接收消息序号)以及尾序号 rcv_nxt + rcv_wnd(接收窗口大小);

    • ssthresh:拥塞窗口阈值,以包为单位(TCP以字节为单位);

    • rx_rttval:RTT的变化量,代表连接的抖动情况;

    • rx_srtt:smoothed round trip time,平滑后的RTT;

    • rx_rto:由ACK接收延迟计算出来的重传超时时间;

    • rx_minrto:最小重传超时时间;

    • snd_wnd:发送窗口大小;

    • rcv_wnd:接收窗口大小;

    • rmt_wnd:远端接收窗口大小;

    • cwnd:拥塞窗口大小;

    • probe:探查变量,IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小;

    • interval:内部flush刷新间隔,对系统循环效率有非常重要影响;

    • ts_flush:下次flush刷新时间戳;

    • xmit:发送segment的次数,当segment的xmit增加时,xmit增加(第一次或重传除外);

    • rcv_buf:接收消息的缓存;

    • nrcv_buf:接收缓存中消息数量;

    • snd_buf:发送消息的缓存;

    • nsnd_buf:发送缓存中消息数量;

    • rcv_queue:接收消息的队列

    • nrcv_que:接收队列中消息数量;

    • snd_queue:发送消息的队列;

    • nsnd_que:发送队列中消息数量;

    • nodelay:是否启动无延迟模式。无延迟模式rtomin将设置为0,拥塞控制不启动;

    • updated:是否调用过update函数的标识;

    • ts_probe:下次探查窗口的时间戳;

    • probe_wait:探查窗口需要等待的时间;

    • dead_link:最大重传次数,被认为连接中断;

    • incr:可发送的最大数据量;

    • acklist:待发送的ack列表;

    • ackcount:acklist中ack的数量,每个ack在acklist中存储ts,sn两个量;

    • ackblock:2的倍数,标识acklist最大可容纳的ack数量;

    • user:指针,可以任意放置代表用户的数据,也可以设置程序中需要传递的变量;

    • buffer:存储消息字节流;

    • fastresend:触发快速重传的重复ACK个数;

    • nocwnd:取消拥塞控制;

    • stream:是否采用流传输模式;

    • logmask:日志的类型,如IKCP_LOG_IN_DATA,方便调试;

    • output udp:发送消息的回调函数;

    • writelog:写日志的回调函数。

    参考文章:

    在网络中狂奔:KCP协议 在网络中狂奔:KCP协议 - 知乎

    可靠UDP,KCP协议快在哪? https://wetest.qq.com/lab/view/391.html

    KCP 协议与源码分析(一) https://github.com/skywind3000/kcp

    网络编程懒人入门(五):快速理解为什么说UDP有时比TCP更有优势 网络编程懒人入门(五):快速理解为什么说UDP有时比TCP更有优势-网络编程/专项技术区 - 即时通讯开发者社区!

    转载本站文章《KCP协议:从TCP到UDP家族QUIC/KCP/ENET》,
    请注明出处:KCP协议:从TCP到UDP家族QUIC/KCP/ENET - Network - 周陆军的个人网站

    展开全文
  • 每个企业关注的中心不一样,从降低企业成本,提高企业运行效率,实现正确的决策...而KCP提供了相关的支持,从实时监控到项目管理,从团队协作到个人办公,在这个平台上,每个人都能找到他的位子,更好的发挥他的能力。
  • 可靠udp协议kcp

    2019-01-14 13:06:49
    kcp可靠udp协议,c源码,仅供参考,可以编译到任何游戏中。
  • KCP讨论QQ组:364933586,KCP集成,调优,免责声明:kcptun维护一个网站— github.com/xtaci/kcptun。 xtaci不认可github.com/xtaci/kcptun以外的任何网站。 要求目标最低推荐系统aix darwin蜻蜓freebsd linux ...
  • Linux网络编程中网络传输KCP协议原理解析

    多人点赞 热门讨论 2022-06-06 22:37:18
    一、KCP概述 二、kcp协议头部 三、KCP流程 1.kcp数据接收 3.kcp确认机制 4.kcp重传机制 四、KCP实现原理 五、KCP源码分析 1.首先来看包发送的逻辑,我们会调用 ikcp_send方法 2.看完这个flush方法,我们基本了解发送...

    系列文章目录



    前言


    一、KCP概述

    • 对于游戏开发,尤其是MOBA(多人在线竞技)游戏,延迟是需要控制的。但是对于传统的TCP(网络友好,很棒),并不利于包的实时性传输,因为他的超时重传和拥塞控制都是网络友好,对于我们包的实时性,没有优势。所以一般都是需要基于UDP去实现一套自己的网络协议,保证包的实时,以及可靠。其实就是牺牲TCP的友好,牺牲带宽,以空间换时间。基于UDP,网上有一些优秀的协议,比如KCP。

    • KCP是一种网络传输协议(ARQ,自动重传请求),可以视它为TCP的代替品,但是它运行于用户空间,它不管底层的发送与接收,只是个纯算法实现可靠传输,它的特点是牺牲带宽来降低延迟。因为TCP协议的大公无私,经常牺牲自己速度来减少网络拥塞,它是从大局上考虑的。而KCP是自私的,它只顾自己的传输效率,从不管整个网络的拥塞情况。举个例子,TCP检测到丢包的时候,首先想到的是网络拥塞了,要放慢自己的速度别让网络更糟,而KCP想到的赶紧重传别耽误事。

    • kcp可以理解为可靠的udp协议。udp是面向无连接的协议,由于其实时性较好,通常用于游戏或音视频通话中,同时由于其不需要提前建立连接,能节省设备资源,也广泛应用于嵌入式设备中。另外,对于大量的数据传输如下载文件等场景,以及DNS中客户端请求域名对应的地址这个环节中,使用的是udp。注意,DNS另一部分,域名服务器节点之间的数据同步用的是tcp。

    • 为了提高udp可靠性,在udp协议上封装一层可靠性传输机制(类似tcp的ACK机制、重传机制、序号机制、重排机制、窗口机制),就做到了兼具tcp的安全性(流量控制和拥塞控制等)和udp的实时性,并且具备一定的灵活性(超时重传、ack等),其中一个代表是kcp协议。

    • TCP的特点是可靠传输(累积确认、超时重传、选择确认)、流量控制(滑动窗口)、拥塞控制(慢开始、拥塞避免、快重传、快恢复)、面向连接。KCP对这些参数基本都可配,也没用建立/关闭连接的过程。

    二、kcp协议头部

    在这里插入图片描述

    • conv:连接号。UDP是无连接的,conv用于表示来自于哪个客户端,是对连接的一种替代。
    • cmd:命令字。如,IKCP_CMD_ACK确认命令,IKCP_CMD_WASK接收窗口大小询问命令,IKCP_CMD_WINS接收窗口大小告知命令。
    • frg:分片,用户数据可能会被分成多个KCP包,发送出去。
    • wnd:接收窗口大小,发送方的发送窗口不能超过接收方给出的数值。
    • ts:时间戳。
    • sn:序列号。
    • una:下一个可接收的序列号。其实就是确认号,类似tcp的ack。
    • len:数据长度。
    • data:用户数据。

    三、KCP流程

    注意收发数据时,数据要先放到缓存重排然后再进行收发。

    1.kcp数据接收

    在这里插入图片描述

    接收数据时,kcp会先把数据放到rcv_buf中进行缓存,注意rcv_buf中的数据是已经按sn排好序的,但是不一定完整连续,因为传输过程中可能有丢失,rcv_queue是从rcv_buf中拷贝出的连续完整的数据分片。用户调用ikcp_recv()时是从rcv_que中读取数据。还要注意一点,ikcp_recv中的len一定要是整个数据的长度,要一次性读完,这和recvfrom的size是一样的。
    在这里插入图片描述

    与接收过程类似,只不过顺序相反,用户调用ikcp_send()只是将数据放到了snd_buf中,不过会自动进行数据分片,使得数据大小不超过mtu。然后,kcp会通过流量控制和拥塞控制将数据放到snd_buf中再进行发送。

    注意区分一下,不断调用ikcp_send(),数据会累积在snd_queue中,如果对方没有接收,对方的数据则会累积在对方的rcv_buf中。
    说了这么多,其实kcp底层调用的,还是recvfrom()和sendto(),这一点不要忘记。

    3.kcp确认机制

    snd_buf中存放的是已发送但未收到对方ack的数据包和未发送但可发送的数据包,如果收到了ack,相应的数据就会从snd_buf中删除。如上图中,8包已收到ack,从snd_buf删除,9、10包已发送但未收到ack。这里提一下,kcp的确认机制有两种,一种是una,与tcp的ack类似,代表una之前的包全部收到;还有一种是单独ack,只对某一个单独的包进行确认。kcp优先检测una。

    4.kcp重传机制

    kcp的重传机制体现了其较于tcp而言,灵活的地方。一是超时重传时间,可以自己设置,tcp是超时一次就将RTO×2,kcp可以×1.5,缩短了重传等待时间;二是建立了快速重传机制,如果一个数据包之后的指定个数据包已收到ack,那么即便未到该包的rto,也立即重传。取3包未收到ack,而4包和5包已经收到了ack,如果指定的是跳过2个包就认为包丢失,那么此时3包就立即重传,无论是否经过了3包的RTO。此外,kcp还可以选择是否设置延迟ack,超时只重传丢失包,采用非退让流控等,这些都使kcp的传输效率得以提高。

    四、KCP实现原理

    在这里插入图片描述

    KCP只是简单的算法实现,并没有涉及到任何的底层调用。我们只需要在UDP系统调用的时候,注册KCP回调函数,即可使用。所以可以将它理解为一个应用层协议。

    • 对比TCP:
    • TCP的RTO翻倍。这个概念是很恐怖的。KCP为1.5倍。
    • 选择性重传,只会传输丢失的数据包。
    • 快速重传,不会等到超时。默认若干次重新传输。
    • TCP会延时发送ACK。KCP可设置。
    • 非退让流控。发送窗口可以只取决于发送缓存大小和接收端剩余接收缓存大小。

    KCP为了实现选择性重传(ARQ),会维护一个接收窗口(滑动窗口)。如果收到有序数据会将其放到接收队列,以待应用层消费。如果存在包丢失,会判断。超过设置的次数,会让其选择重传对应的包。

    其实就是通过一个rcv_nxt(接收窗口当前偏移)来判断当前需要接受的数据包。如果收到的包在窗口范围,但是不是rcv_nxt。先保存,等包连续之后才会将连续的数据包放入到接受队列供应用层消费。

    同样网络不好的情况,KCP也会实现拥塞控制,限制发送端的包。
    在这里插入图片描述

    五、KCP源码分析

    首先在分析之前我们应该去github看一下使用方法。其实很简单,初始化kcp对象,然后实现回调函数,其实就是实现自己底层UDP系统调用。每次我们通过KCP发包的时候,他都会调用这个回调。

    UDP收到包之后调用ikcp_input函数,即可。我们最终只需要通过ikcp_send和ikcp_recv收发数据。

    在看代码前,先看看kcp数据包的结构,Segement。

    struct IKCPSEG
    {
       struct IQUEUEHEAD node;
       IUINT32 conv;     //会话编号,两方一致才会通信
       IUINT32 cmd;      //指令类型,四种下面会说
       IUINT32 frg;      //分片编号 倒数第几个seg。主要就是用来合并一块被分段的数据。
       IUINT32 wnd;      //自己可用窗口大小    
       IUINT32 ts;
       IUINT32 sn;       //编号 确认编号或者报文编号
       IUINT32 una;      //代表编号前面的所有报都收到了的标志
       IUINT32 len;
       IUINT32 resendts; //重传的时间戳。超过当前时间重发这个包
       IUINT32 rto;      //超时重传时间,根据网络去定
       IUINT32 fastack;  //快速重传机制,记录被跳过的次数,超过次数进行快速重传
       IUINT32 xmit;     //重传次数
       char data[1];     //数据内容
    };
    
    

    Kcp就是通过数据包的这些字段,实现稳定通信,针对不同的点可以去做优化。从上面的字段,也可以看出kcp借助UNA和ACK实现了选择性重传。

    1.首先来看包发送的逻辑,我们会调用 ikcp_send方法

    这个方法,首先会判断kcp流。并尝试将包追加到前一段,如果可能的话。否则进行分片传输。

        if (len <= (int)kcp->mss) count = 1;
         else count = (len + kcp->mss - 1) / kcp->mss;
         if (count >= (int)IKCP_WND_RCV) return -2;
         if (count == 0) count = 1;
         // fragment
         for (i = 0; i < count; i++) {
             int size = len > (int)kcp->mss ? (int)kcp->mss : len;
             seg = ikcp_segment_new(kcp, size);
             assert(seg);
             if (seg == NULL) {
                 return -2;
             }
             if (buffer && len > 0) {
                 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++;
             if (buffer) {
                 buffer += size;
             }
             len -= size;
         }
    
         return 0;
    
    

    上面的代码逻辑中count其实就是包的分片数。然后循环,创建segment,segment的数据结构主要就是保存了分片的数据包信息。比如eg->frg保存当前分片的编号。完事之后调用iqueue_add_tail方法将segment传入到发送队列。这些方法通过宏定义实现。其实就是链表操作。队列是一个双向链表。逻辑很简单。

    那么这一步之时将数据分片放入到队列。具体发送逻辑在哪实现呢,继续往下看。

    我们看一下回调的逻辑,其实就是ikcp_output方法,这个方法会在ikcp_flush 中调用。也就是ikcp_output做的是最终的数据发送。那是如何驱动的呢?我先来看看这个方法。

    1、这个方法首先发送ack。遍历所有ack。调用ikcp_output 方法发送。

        count = kcp->ackcount;
         for (i = 0; i < count; i++) {
             size = (int)(ptr - buffer);
             if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) {
                 ikcp_output(kcp, buffer, size);
                 ptr = buffer;
             }
             ikcp_ack_get(kcp, i, &seg.sn, &seg.ts);
             ptr = ikcp_encode_seg(ptr, &seg);
         }
    
         kcp->ackcount = 0;
    
    

    2、判断当前是否需要进行窗口探测,因为如果窗口为0,是不能发数据,所以必须进行窗口探测才行。探测结束之后,如果需要,设置探测窗口时间。发送探测窗口的请求或者窗口恢复的请求。主要就是请求对端窗口大小,以及告知远端窗口大小。完事之后将结果放入seg中。

    if (kcp->rmt_wnd == 0) {
       if (kcp->probe_wait == 0) {
          kcp->probe_wait = IKCP_PROBE_INIT;
          kcp->ts_probe = kcp->current + kcp->probe_wait;
       }  
       else {
          if (_itimediff(kcp->current, kcp->ts_probe) >= 0) {
             if (kcp->probe_wait < IKCP_PROBE_INIT) 
                kcp->probe_wait = IKCP_PROBE_INIT;
             kcp->probe_wait += kcp->probe_wait / 2;
             if (kcp->probe_wait > IKCP_PROBE_LIMIT)
                kcp->probe_wait = IKCP_PROBE_LIMIT;
             kcp->ts_probe = kcp->current + kcp->probe_wait;
             kcp->probe |= IKCP_ASK_SEND;
          }
       }
    }  else {
       kcp->ts_probe = 0;
       kcp->probe_wait = 0;
    }
    
    

    3、计算本次发送可用的窗口大小,由多个因素决定,KCP有选择性配置。可以选择不结合流控窗口。

    4、将发送队列中的消息放到发送缓冲区,其实就是发送窗口。也就是说所有发送后的数据都会在这个缓存区。发送数据之前,还需要设置对应的重传次数和间隔。这个逻辑就比较简单了,其实就从发送窗口队列拿出一个seg。然后设置对应的参数。并且更新缓冲队列。以及缓冲队列的大小。如果设置nodelay,重传时间有*2 变成1.5。

        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);
             iqueue_del(&newseg->node);
             iqueue_add_tail(&newseg->node, &kcp->snd_buf);
             kcp->nsnd_que--;
             kcp->nsnd_buf++;
             newseg->conv = kcp->conv;
             newseg->cmd = IKCP_CMD_PUSH;
             newseg->wnd = seg.wnd;
             newseg->ts = current;
             newseg->sn = kcp->snd_nxt++;
             newseg->una = kcp->rcv_nxt;
             newseg->resendts = current;
             newseg->rto = kcp->rx_rto;
             newseg->fastack = 0;
             newseg->xmit = 0;
         }
    
    

    5、遍历发送窗口队列。判断是否有需要发送的数据(包括重新传输的)。其实就是拿到对应的segment,然后根据信息进行逻辑判断是否需要重新传输。或者需要发送。判断结束之后进行重新传输。

    逻辑也很简单:

    • 如果包是第一次传输,直接发。
    • 如果到了包的重传时间,再次传输,并且记录丢失标志。
    • 如果被跳过的次数超过了fastack,重新传输。

    其实lost和change是用来更新窗口大小的字段。并且两个更新算法不一样。

    if (segment->xmit == 0) {
       needsend = 1;
       segment->xmit++;
       segment->rto = kcp->rx_rto;
       segment->resendts = current + segment->rto + rtomin;
    }
    else if (_itimediff(current, segment->resendts) >= 0) {
       needsend = 1;
       segment->xmit++;
       kcp->xmit++;
       if (kcp->nodelay == 0) {
          segment->rto += kcp->rx_rto;
       }  else {
          segment->rto += kcp->rx_rto / 2;
       }
       segment->resendts = current + segment->rto;
        //记录包丢失
       lost = 1;
    }
    else if (segment->fastack >= resent) {
       if ((int)segment->xmit <= kcp->fastlimit || 
          kcp->fastlimit <= 0) {
          needsend = 1;
          segment->xmit++;
          segment->fastack = 0;
          segment->resendts = current + segment->rto;
          //用来标示发生了快速重传  
          change++;
       }
    }
    
    

    基本上所有的快速重传和超时重传的逻辑都在这个方法中。如果出现超时重传(丢包),就会进入慢启动,拥塞窗口减半,滑动窗口变为1。如果发生了快速重传,也会更新拥塞窗口。具体算法可看代码。

    2.看完这个flush方法,我们基本了解发送数据的逻辑。然后就看他在哪调用的

    其实就是在ikcp_update方法中就行调用,这个方法需要应用层反复调用,一般可以为10ms和100ms,时间将决定数据发送的实时性。也就是说他会定时刷新判断发送窗口队列的数据或者需要重传的数据,并通过底层UDP进行数据发送。这个方法没有什么逻辑。

    void ikcp_update(ikcpcb *kcp, IUINT32 current)
    {
         IINT32 slap;
         kcp->current = current;
         if (kcp->updated == 0) {
             kcp->updated = 1;
             kcp->ts_flush = kcp->current;
         }
         slap = _itimediff(kcp->current, kcp->ts_flush);
         if (slap >= 10000 || slap < -10000) {
             kcp->ts_flush = kcp->current;
             slap = 0;
         }
         if (slap >= 0) {
             kcp->ts_flush += kcp->interval;
             if (_itimediff(kcp->current, kcp->ts_flush) >= 0) {
                 kcp->ts_flush = kcp->current + kcp->interval;
             }
             ikcp_flush(kcp);
         }
    }
    
    

    3.我们再来看一下底层接受数据的方法 ikcp_input

    这个方法是在底层UDP接收到网络数据之后调用。其实就是解析对应的数据。在KCP中,主要有四种报文格式,ACK报文、数据报文、探测window报文、响应窗口报文四种。

    1、首先就是解析对应的头部数据,大概是24字节。然后根据字段去调用ikcp_parse_una和ikcp_shrink_buf方法。前者是解析una,确定已经发出去的数据包,有哪些对方接收到了。如果收到了直接重接受窗口移除。后者是更新kcp的send_una。send_una代表之前的包已经确定收到。

    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, &len);
    size -= IKCP_OVERHEAD;
    
    
    kcp->rmt_wnd = wnd;
    ikcp_parse_una(kcp, una);
    ikcp_shrink_buf(kcp);
    
    

    2、如果是ACK指令,其实就是做了一些处理。ikcp_update_ack主要就是更新kcp的一些参数,包括rtt以及rto, 首先ikcp_parse_ack方法主要就是根据sn,去移除发送队列中对应的segment。然后就是更新maxack以及时间,并且记录日志

    if (cmd == IKCP_CMD_ACK) {
       if (_itimediff(kcp->current, ts) >= 0) {
          ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
       }
       ikcp_parse_ack(kcp, sn);
       //根据snd队列去更新una     
       ikcp_shrink_buf(kcp);
       if (flag == 0) {
          flag = 1;
          maxack = sn;
          latest_ts = ts;
       }  else {
          if (_itimediff(sn, maxack) > 0) {
          #ifndef IKCP_FASTACK_CONSERVE
            //记录最大ACK
             maxack = sn;
             latest_ts = ts;
          #else
             if (_itimediff(ts, latest_ts) > 0) {
                maxack = sn;
                latest_ts = ts;
             }
          #endif
          }
       }
    //打印日志
    }
    
    

    3、如果收到的是数据包,这个逻辑其实很简单,就是检测数据,并将有效的数据放到接受队列,首先就是判断数据包是否有效,如果是,构造一个segment。将数据放入。,然后调用ikcp_parse_data方法。这个方法逻辑也比较简单,其实就是判断是否有效,如果已经被接收过的话,就丢弃,否则根据sn(编号)插入到接收队列。如果是询问窗口大小的包。这个其实就做个标记,因为每个kcp的header都有win大小。剩下的操作就是根据网络状况更新拥塞以及窗口大小了。

    else if (cmd == IKCP_CMD_PUSH) {
       if (ikcp_canlog(kcp, IKCP_LOG_IN_DATA)) {
          ikcp_log(kcp, IKCP_LOG_IN_DATA, 
             "input psh: sn=%lu ts=%lu", (unsigned long)sn, (unsigned long)ts);
       }
       if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) {
          ikcp_ack_push(kcp, sn, ts);
          if (_itimediff(sn, kcp->rcv_nxt) >= 0) {
             seg = ikcp_segment_new(kcp, len);
             seg->conv = conv;
             seg->cmd = cmd;
             seg->frg = frg;
             seg->wnd = wnd;
             seg->ts = ts;
             seg->sn = sn;
             seg->una = una;
             seg->len = len;
             if (len > 0) {
                memcpy(seg->data, data, len);
             }
             ikcp_parse_data(kcp, seg);
          }
       }
    }
    
    

    六、KCP快在哪里

    • 没用使用任何系统调用接口。
    • 无需建立/关闭连接(就KCP本身来说)。
    • 很多影响速度的参数都可配。

    七、KCP使用场景

    丢包率高的网络环境下KCP的优点才会显示出来。如果不丢包,那么TCP和KCP的效率不会差别很大,可能就是少了连接建立/关闭而已。一般来讲,在公网上传输的都可以使用,特别是对实时性要求较高的程序,如LOL。

    八、KCP有何缺点

    • 学习成本
    • 据说有些运营商对UDP有限制?

    九、KCP总结

    看了kcp的实现,其实发现和传输层的TCP差不多,只不过进行一下微调和可控。比如牺牲流控保证数据包的实时传输。或者加速重传等等。还有通过una和ack实现选择性重传。总的来说用于游戏帧同步或者数据实时传输领域还是有一定的优势。

    展开全文
  • .NET的Socket+KCP+Protobuf的Unity局域网联机测试范例, 大量功能待补充。
  • KCP播放器套装

    2015-08-08 11:35:19
    国外论坛开发的内置多种解码器的播放器,挺好用的
  • Java中使用KCP协议

    2021-12-21 22:44:22
    KCP就是这样的一个协议 不过网上说的再天花乱坠,我们也得亲自调研,分析源码和它的机制,并测试它的性能,是否满足项目上线要求。本文从C版本的源码入手理解KCP的机制,再研究各种Java版本的实现 一、KCP协议 原版...

    传统游戏项目一般使用TCP协议进行通信,得益于它的稳定和可靠,不过在网络不稳定的情况下,会出现丢包严重。
    不过近期有不少基于UDP的应用层协议,声称对UDP的不可靠进行了改造,这意味着我们既可以享受网络层提供稳定可靠的服务,又可以享受它的速度。
    KCP就是这样的一个协议

    不过网上说的再天花乱坠,我们也得亲自调研,分析源码和它的机制,并测试它的性能,是否满足项目上线要求。本文从C版本的源码入手理解KCP的机制,再研究各种Java版本的实现

    一、KCP协议

    原版源码(C代码):https://github.com/skywind3000/kcp

    基于底层协议(一般是UDP)之上,完全在应用层实现类TCP的可靠机制(快速重传,拥塞控制等)

    二、KCP特性

    KCP实现以下特性,也可参考github中README中对KCP的定义

    特性说明源码位置
    RTO优化超时时间计算优于TCPikcp_update_ack
    选择性重传KCP只重传真正丢失的数据包,TCP会全部重传丢失包之后的全部数据ikcp_parse_fastack,ikcp_flush
    快速重传根据配置,可以在丢失包被跳过一定次数后直接重传,不等RTO超时ikcp_parse_fastack,ikcp_flush
    UNA + ACKARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP),ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。ikcp_flush(每次update,都发送ACK)
    非延迟ACKKCP可配置是否延迟发送ACKikcp_update_ack
    流量控制同TCP的公平退让原则,发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定ikcp_input,
    ikcp_flush

    三、KCP报文

    1. 报文解析源码

    源码中对报文解析部分代码如下

    data = ikcp_decode32u(data, &conv);
    if (conv != kcp->conv) return -1;
    
    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, &len);
    

    2. 报文定义

    报文中标识的定义

    名词全称备注作用
    convconversation id会话ID每个连接的唯一标识
    cmdcommand命令每个数据包指定逻辑
    frgfragment count数据分段序号根据mtu(最大传输单元)和mss(最大报文长度)的数据分段
    wndwindow size接收窗口大小流量控制
    tstimestamp时间戳数据包发送时间记录
    snserial number数据报的序号确保包的有序
    unaun-acknowledged serial number对端下一个要接收的数据报序号确保包的有序

    3. 消息类型

    KCP报文的四种消息类型

    const IUINT32 IKCP_CMD_PUSH = 81;     // cmd: push data: 推送数据
    const IUINT32 IKCP_CMD_ACK  = 82;     // cmd: ack: 对推送数据的确认
    const IUINT32 IKCP_CMD_WASK = 83;     // cmd: window probe (ask): 询问窗口大小
    const IUINT32 IKCP_CMD_WINS = 84;     // cmd: window size (tell): 回复窗口大小
    
    1. 报文结构

    报文结构.png

    四、源码解析

    在网络四层模型中,KCP和TCP/UDP(传输层),IP(网络层)等协议有着本质上区别,理论上KCP是属于应用层协议。
    KCP并不提供协议实际收发处理,它只是在传输层只上对消息和链接的一层中间管理。

    在KCP的源码中,它仅仅包含ikcp.c和ikcp.h两个文件,仅提供KCP的数据管理和数据接口,而用户需要在应用层进行KCP的调度

    1. 结构体定义

    KCP分包结构KCP对象结构体定义

    struct IKCPSEG
    {
        struct IQUEUEHEAD node;
        IUINT32 conv; //用来标记这个seg属于哪个kcp
        IUINT32 cmd;//这个包的指令是: // 数据 ack 询问/应答窗口大小
        IUINT32 frg; //分包时,分包的序号,0为终结
        IUINT32 wnd;//发送这个seg的这个端的 窗口大小--> 远端的接收窗口大小
        IUINT32 ts; //我不知道为什么要用时间轴,这个都1秒,有什么用 ??
        IUINT32 sn;//相当于tcp的ack
        IUINT32 una;//una 远端等待接收的一个序号
        IUINT32 len; //data的长度
        IUINT32 resendts;//重发的时间轴
        IUINT32 rto;//等于发送端kcp的 rx_rto->由 计算得来
        IUINT32 fastack;//ack跳过的次数,用于快速重传
        IUINT32 xmit;// fastack resend次数
        char data[1];//当malloc时,只需要  malloc(sizeof(IKCPSEG)+datalen) 则,data长=数据长度+1 刚好用来放0
    };
    
    struct IKCPCB
    {
        //会话ID,最大传输单元,最大分片大小,状态   mss=mtu-sizeof(IKCPSEG)
        IUINT32 conv, mtu, mss, state;
        //第一个未接收到的包,待发送的包(可以认为是tcp的ack自增),接收消息的序号-> 用来赋seg的una值
        IUINT32 snd_una, snd_nxt, rcv_nxt;
        //前两个不知道干嘛  拥塞窗口的阈值 用来控制cwnd值变化的
        IUINT32 ts_recent, ts_lastack, ssthresh;
        //这几个变量是用来更新rto的
        // rx_rttval 接收ack的浮动值
        // rx_srtt 接收ack的平滑值
        // rx_rto 计算出来的rto
        // rx_minrto 最小rto
        IINT32 rx_rttval, rx_srtt, rx_rto, rx_minrto;
        //发送队列的窗口大小
        //接收队列的窗口大小
        //远端的接收队列的窗口大小
        //窗口大小
        //probe 用来二进制标记
        IUINT32 snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe;
        //时间轴 时间间隔 下一次flush的时间  xmit发射多少次? 看不到有什么地方用到
        IUINT32 current, interval, ts_flush, xmit;
        //接收到的数据seg个数
        //需要发送的seg个数
        IUINT32 nrcv_buf, nsnd_buf;
        //接收队列的数据 seg个数
        //发送队列的数据 seg个数
        IUINT32 nrcv_que, nsnd_que;
        //是否为nodelay模式:如果开启,rto计算范围更小
        //updated 在调用flush时,有没有调用过update
        IUINT32 nodelay, updated;
        //请求访问窗口的时间相关 当远程端口大小为0时
        IUINT32 ts_probe, probe_wait;
        IUINT32 dead_link, incr;
        //发送队列
        struct IQUEUEHEAD snd_queue;
        //接收队列
        struct IQUEUEHEAD rcv_queue;
        //待发送队列
        struct IQUEUEHEAD snd_buf;
        //待接收队列
        struct IQUEUEHEAD rcv_buf;
        //用来缓存自己接收到了多少个ack
        IUINT32 *acklist;
        IUINT32 ackcount;
        IUINT32 ackblock;
    
        //用户信息
        void *user;
        //好像就用来操作数据的中转站
        char *buffer;
        //快速重传的阈值
        int fastresend;
        //快速重传的上限
        int fastlimit;
        //是否无视重传等其它设置窗口
        //steam模式的话,会将几个小包合并成大包
        int nocwnd, stream;
        int logmask;
        int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);
        void (*writelog)(const char *log, struct IKCPCB *kcp, void *user);
    };
    

    2. 接口分析

    分析C源码,KCP作为中间管理层,主要提供以下接口

    //---------------------------------------------------------------------
    // interface
    //---------------------------------------------------------------------
    
    // create a new kcp control object, 'conv' must equal in two endpoint
    // from the same connection. 'user' will be passed to the output callback
    // output callback can be setup like this: 'kcp->output = my_udp_output'
    // 创建kcp对象,conv必须在两个端之间相同,user会被传递到output回调,
    // output回调这样设置:kcp->output = my_udp_output
    ikcpcb* ikcp_create(IUINT32 conv, void *user);
    
    // release kcp control object
    // 释放kcp对象
    void ikcp_release(ikcpcb *kcp);
    
    // set output callback, which will be invoked by kcp
    // 设置kcp调用的output回调
    void ikcp_setoutput(ikcpcb *kcp, int (*output)(const char *buf, int len, 
       ikcpcb *kcp, void *user));
    
    // user/upper level recv: returns size, returns below zero for EAGAIN
    // 用户层/上层 接收消息:返回接收长度,数据读取错误返回值小于0
    int ikcp_recv(ikcpcb *kcp, char *buffer, int len);
    
    // user/upper level send, returns below zero for error
    // 用户层/上层 发送消息,错误返回值小于0
    int ikcp_send(ikcpcb *kcp, const char *buffer, int len);
    
    // update state (call it repeatedly, every 10ms-100ms), or you can ask 
    // ikcp_check when to call it again (without ikcp_input/_send calling).
    // 'current' - current timestamp in millisec. 
    // 更新状态(每10ms-100ms调用一次),或者你可以通过调用ikcp_check,
    // 来得知什么时候再次调用(不调用ikcp_input/_send)
    // current - 当前时间戳(毫秒)
    void ikcp_update(ikcpcb *kcp, IUINT32 current);
    
    // Determine when should you invoke ikcp_update:
    // returns when you should invoke ikcp_update in millisec, if there 
    // is no ikcp_input/_send calling. you can call ikcp_update in that
    // time, instead of call update repeatly.
    // Important to reduce unnacessary ikcp_update invoking. use it to 
    // schedule ikcp_update (eg. implementing an epoll-like mechanism, 
    // or optimize ikcp_update when handling massive kcp connections)
    // 决定你什么时候调用ikcp_update
    // 返回你多少毫秒后应该调用ikcp_update,如果没有ikcp_input/_send调用,你可以在那个时间
    // 调用ikcp_updates来代替自己驱动update调用
    // 用于减少不必要的ikcp_update调用。用这个来驱动ikcp_update(比如:实现类epoll的机制,
    // 或者优化处理大量kcp连接时的ikcp_update调用)
    IUINT32 ikcp_check(const ikcpcb *kcp, IUINT32 current);
    
    // when you received a low level packet (eg. UDP packet), call it
    // 接收下层数据包(比如:UDP数据包)时调用
    int ikcp_input(ikcpcb *kcp, const char *data, long size);
    
    // flush pending data
    // 刷新数据
    void ikcp_flush(ikcpcb *kcp);
    
    // check the size of next message in the recv queue
    // 检测接收队列里下条消息的长度
    int ikcp_peeksize(const ikcpcb *kcp);
    
    // change MTU size, default is 1400
    // 修改MTU长度,默认1400
    int ikcp_setmtu(ikcpcb *kcp, int mtu);
    
    // set maximum window size: sndwnd=32, rcvwnd=32 by default
    // 设置最大窗口大小,默认值:sndwnd=32, rcvwnd=32
    int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
    
    // get how many packet is waiting to be sent
    // 获取准备发送的数据包
    int ikcp_waitsnd(const ikcpcb *kcp);
    
    // fastest: ikcp_nodelay(kcp, 1, 20, 2, 1)
    // nodelay: 0:disable(default), 1:enable
    // interval: internal update timer interval in millisec, default is 100ms 
    // resend: 0:disable fast resend(default), 1:enable fast resend
    // nc: 0:normal congestion control(default), 1:disable congestion control
    // 快速设置:ikcp_nodelay(kcp, 1, 20, 2, 1)
    // nodelay:0:使用(默认),1:使用
    // interval:update时间(毫秒),默认100ms
    // resend:0:不适用快速重发(默认), 其他:自己设置值,若设置为2(则2次ACK跨越将会直接重传)
    // nc:0:正常拥塞控制(默认), 1:不适用拥塞控制
    int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc);
    
    void ikcp_log(ikcpcb *kcp, int mask, const char *fmt, ...);
    
    // setup allocator
    // 设置kcp allocator
    void ikcp_allocator(void* (*new_malloc)(size_t), void (*new_free)(void*));
    
    // read conv
    // 获取conv
    IUINT32 ikcp_getconv(const void *ptr);
    

    3. 调度逻辑

    KCP调度逻辑.png

    KCP关键接口:

    • 更新(上层驱动KCP状态更新)
      ikcp_update:kcp状态更新接口,需要上层进行调度,判断flush时间,满足条件调用ikcp_flush刷新数据,同时也负责对收到数据的kcp端回复ACK消息
    • 发送
      ikcp_send -> ikcp_update -> ikcp_output
      ikcp_send:上层调用发送接口,把数据根据mss值进行分片,设置分包编号,放到snd_queue队尾
      ikcp_flush:发送数据接口,根据对端窗口大小,拷贝snd_queue的数据到snd_buf,遍历snd_buf,满足条件则调用output回调(调用网络层的发送)
    • 接收
      ikcp_input -> ikcp_update -> ikcp_recv
      ikcp_input:解析上层输入数据,拷贝rcv_buf到rcv_queue
      ikcp_recv:数据接收接口,上层从rcv_queue中复制数据到网络层buffer

    五、Java版本

    目前github上有几个高star的java版本实现,选取最高的三个进行分析

    1. https://github.com/szhnet/kcp-netty.git(star:212)

    实现原理:

    1.KCP逻辑是源码的Java翻译版(一模一样)
    2.UkcpServerChannel继承ServerChannel,UkcpServerBootStrap
    3.用Boss线程EventLoopGroup的read事件来驱动KCP逻辑

    优点:使用Netty的Boss线程Read事件来驱动KCP,不用while(true)的驱动;使用简单,只需使用指定的ServerChannel和ServerBootStrap来启动Netty
    缺点:无明显缺点

    2. https://github.com/beykery/jkcp.git(star:172)

    实现原理:

    1.KCP逻辑是源码的Java翻译版(一模一样)
    2.启动指定线程数的KcpThread自定义IO线程池,进行KCP逻辑调度
    3.Netty读消息时抛到KcpThread自定义IO线程

    // 通过hash选择IO线程处理
    InetSocketAddress sender = dp.sender();
    int hash = sender.hashCode();
    hash = hash < 0 ? -hash : hash;
    this.workers[hash % workers.length].input(dp);
    

    优点:代码简单明了,容易理解,核心是翻译版源码,外壳套的是Netty+自定义IO线程池
    缺点:IO线程池会while(true)的调用KCP的update

    3. https://github.com/l42111996/java-Kcp.git(star:187)

    实现原理:

    1.KCP逻辑是源码的Java翻译版(一模一样)
    2.Netty读消息时,扔到定时器,1ms后,抛出任务到自定义IO线程

    优点:拥有1的全部优点,也在Netty的读消息,把消息抛到定时器去调用KCP的逻辑,避免了2的无意义的while(true),同时实现功能更全,有上线项目验证(据作者描述)
    缺点:Netty相关逻辑完全封装起来,不能修改任何Netty参数(不过源码中对Netty的参数已配置的很好了)

    目前看来,第三种实现(https://github.com/l42111996/java-Kcp.git)是最理想的方式

    如果大家感兴趣,后边会对第三种实现进行详细的源码分析

    六、性能测试

    近期准备做性能测试进行对比,感兴趣的朋友可以关注下

    // TODO
    
    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,714
精华内容 1,085
关键字:

kcp