KCP一种基于非可靠传输的可靠传输协议(源码分析)

KCP是一个非常简单的可做流控的可靠传输协议,他的实现很多地方都借鉴了TCP的协议实现。通过学习KCP源码,能够更加熟悉ACK、重传、流控等实现方法,对TCP的理解也能有很大帮助

 

目录

1. KCP简介

2. 使用方法

2.1 客户端

2.2 服务端

3. 基本收发逻辑

3.1 数据封装

3.2 数据发送逻辑

3.3 数据接收逻辑

4. 重传

4.1 RTT、RTO计算

4.2 选择性重传

4.3 快速重传

5. UNA + ACK

5.1 ACK

5.1.1 产生ACK

5.1.2 发送ACK

5.1.3 处理ACK

5.2 UNA

5.3 非延迟ACK

6. 流量控制

6.1 阻塞窗口


 

1. KCP简介

KCP是一个快速可靠协议,底层通过不可靠数据传输,通过浪费带宽的代价来实现降低延迟的效果。纯算法实现的数据协议,可以通过任何形式(UDP、TCP等)发送数据,但其作用是提供低延迟、可靠、流控机制,加到TCP等已经提供相关功能的传输方法而言纯属画蛇添足。所以目前看来,基于UDP来实现KCP的应用是最为合适的一种方法。即拥有UDP简单、快速的传输效果,又能够提供可靠的传输机制。

具体的介绍可以移步:https://github.com/skywind3000/kcp,整个工程其实就两个文件,相对比较简单。

简单列举下KCP特性:

  1. 自定义RTO:可以设置选择不同的RTO时间计算方式
  2. 选择性重传:只重传丢失的数据段
  3. 快速重传:发现数据段丢失时,不等待RTO,直接发起数据段重传
  4. UNA + ACK:收到UNA及ACK都会确认数据段,取消相应数据段重传
  5. 非延迟ACK:可以设置ACK是否延迟发送
  6. 流量控制:发送端及接收端分别设有发送窗口及接收窗口,同时存在阻塞窗口。当出现丢包或失序的情况时,减小阻塞窗口以控制发送方的流量。

后面会从源码的角度对以上特性一一分析来了解KCP的工作机制。

在后面的讨论中,都会使用UDP作为底层的传输方式。

 

2. 使用方法

使用方法非常简单,最简单的demo中,客户端 + 服务端代码不超过100行就能搞定。

2.1 客户端

客户端中需要先创建UDP的socket并定义UDP的数据包发送方法,然后初始化KCP。

客户端初始化逻辑:

static int m_fd;
static sockaddr_in server;
static ikcpcb *kcp;
 
 
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user) {
    ::sendto(m_fd, buf, len, 0, (struct sockaddr *) &server, sizeof(server));
    return 0;
}
 
void initSocket() {
    static const std::string i_host = "127.0.0.1";
    static const int i_port = 1120;
 
    m_fd = ::socket(AF_INET, SOCK_DGRAM, 0));
    hostent* he = gethostbyname(i_host.c_str());
    char ip[32];
    inet_ntop(he->h_addrtype, he->h_addr, ip, sizeof(ip));
 
    int flags;
    flags = fcntl(m_fd, F_GETFL, NULL));
    fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
 
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    inet_aton(ip, &server.sin_addr);
    server.sin_port = htons(i_port);
}
 
 
void initKcp() {
    *kcp = ikcp_create(0x11223344, NULL);
    kcp->output = udp_output;
}

非常简单,不用多讲。注意倒数第二行:kcp->output = udp_output,这里给KCP定义了数据的传输方式

客户端收发逻辑:

char buff[2048];
struct sockaddr_in peer;
 
IUINT32 tLastSend = iclock();
const int TIME_SEND_INTERVAL = 3 * 1000;
int cntSend = 0;
while(1) {
    IUINT32 now = iclock();
    if (now - tLastSend > TIME_SEND_INTERVAL) {
        sprintf(buff, "send %d", ++cntSend);
        tLastSend = iclock();
        int res_send = ikcp_send(kcp, buff, strlen(buff) + 1);
        printf("send: <%s>, res:%d, strlen:%d\n", buff, res_send, strlen(buff));
    }
    ikcp_update(kcp, now);
 
    socklen_t len = sizeof(peer);
    ssize_t r_len = ::recvfrom(m_fd, buff, sizeof(buff), 0, (struct sockaddr*) &peer, &len);
    if (r_len > 0) {
        int res_input = ikcp_input(kcp, buff, r_len);
        int res_recv = ikcp_recv(kcp, buff, r_len);
        printf("receive: <%s>, res_input:%d, res_recv:%d, r_len:%d\n", buff, res_input, res_recv, r_len);
    }
}

这段代码的含义是每3秒向服务端发送一个字符串,并接收服务端传回的字符串。因为在初始化时设置了recv为非阻塞方式,所以,这里会一直循环不会停。

与普通UDP收发方式不同的是,发送数据只用了了ikcp_send方法;接受数据依然使用UDP的接受方式,但是在收到数据之后必须调用ikcp_input和ikcp_recv。

此外,在每次循环里都调用了ikcp_update,这个方法非常重要,需要不断循环调用。随所是需要不断调用,其实也没有必要每次循环都要调用这个方法,可以使用ikcp_check来获取下次调用ikcp_update方法的时间。

2.2 服务端

与客户端类似,先进行UDP的socket初始化和数据包发送方法,然后初始化KCP

服务端初始化逻辑:

static int m_fd = 0;
static ikcpcb *kcp;
 
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user) {
    ::sendto(m_fd, buf, len, 0, (struct sockaddr*)&client, sizeof(client));
    return 0;
}
 
 
void initSocket() {
    m_fd = ::socket(AF_INET, SOCK_DGRAM, 0));
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(i_host.c_str());
    server.sin_port = htons(i_port);
 
 
    ::bind(m_fd, (struct sockaddr*)&server, sizeof(server));
 
 
    int flags;
    flags = fcntl(m_fd, F_GETFL, NULL));
    fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
}
 
 
void initKcp() {
    kcp = ikcp_create(0x11223344, NULL);
    kcp->output = udp_output;
}
与客户端类似,不解释了

服务端收发逻辑:

struct sockaddr_in client;
char buff[2048] = {0};
int cntSend = 0;
while (1) {
    socklen_t len = sizeof(client);
    ssize_t r_len = ::recvfrom(m_fd, buff, sizeof(buff), 0, (sockaddr *) &client, &len);
    if (r_len > 0) {
        int res_input = ikcp_input(kcp, buff, r_len);
        int res_recv = ikcp_recv(kcp, buff, r_len);
 
        if (res_recv > 0) {
            printf("receive: <%s>, res_input:%d, res_recv:%d\n", buff, res_input, res_recv);
            sprintf(buff, "back send: %d", ++cntSend);
            ikcp_send(kcp, buff, strlen(buff) + 1);
        }
    }
 
    ikcp_update(kcp, iclock());
}

与客户端类似,这段代码含义是一直等待接收数据,收到数据后变向客户端返回一个数据。

同样,发送数据使用ikcp_send,收到数据后需要调用ikcp_input和ikcp_recv,也需要不断调用ikcp_update。

比较简单,以上就完成了整个利用KCP的客户端服务端收发逻辑

 

3. 基本收发逻辑

下面来看下KCP最基本的收发逻辑

3.1 数据封装

简介中说过,KCP实际上是一个传输协议,他定义了一套自己的报文格式。KCP一种基于非可靠传输的可靠传输协议(源码分析)_第1张图片

因为kcp有自己的一套报文封装格式,所以在上面使用的时候,要发送的数据是不能通过udp直接发送的,要通过调用ikcp_send来进行发送,发送前会对要发送的数据进行分组和封装。同理,udp收到的数据实际上也是不能直接用的,要通过ikcp_input、ikcp_recv进行解封装装、重组和其他一些控制逻辑才能拿到真实的发送数据。

我们先说一下这个封装体的各个字段的作用:

  1. conv:保存了一次会话的ID,C-S两端要用同样的id进行通讯。可以直接理解为sesionid
  2. cmd:数据报的控制指令类型,有4种:
    1. IKCP_CMD_PUSH:表示该报文段是用户数据的推送
    2. IKCP_CMD_ACK:表示该报文段是对PUSH报文段的确认
    3. IKCP_CMD_WASK:表示该报文是询问对端的接收窗口大小
    4. IKCP_CMD_WINS:与IKCP_CMD_WASK对应,表示该报文是通知对端自己的接收窗口大小
  3. frg:数据分段序号,从大到小排列,最后一个分段序号为0。每个数据报有mtu(最大传输单元)和mss(最大报文长度的概念)。如果调用ikcp_send的数据超过了mss,那么他就会被分成两个报文段。frg就是表示了这两个报文段的序号。如果调用ikcp_send的数据没有超过mss,那么frg始终为0
  4. wnd:对方的接收窗口大小。用于流量控制。
  5. ts:数据报发送时的时钟
  6. sn:数据报的序号,每发送一个数据报sn递增1。注意跟frg区别,frg是当用户要发送的数据太大时被分成两个数据报的序号,sn是所有发送的数据报的序号。
  7. una:对端下一个要接收的数据报序号
  8. len:后面data的数据长度,当cmd不是IKCP_CMD_PUSH时,len都为0

数据报头的最大大小是MTU,真实数据的最大大小是MSS,实际上MSS就是MTU - 24。这在初始化的代码中能够看到:

const IUINT32 IKCP_OVERHEAD = 24;
 
 
ikcpcb* ikcp_create(IUINT32 conv, void *user)
{
   ...
   kcp->mss = kcp->mtu - IKCP_OVERHEAD;
   ...
   return kcp;
}

对应到相应的代码中,封装有独立的方法:

//---------------------------------------------------------------------
// ikcp_encode_seg
//---------------------------------------------------------------------
static char *ikcp_encode_seg(char *ptr, const IKCPSEG *seg)
{
   ptr = ikcp_encode32u(ptr, seg->conv);
   ptr = ikcp_encode8u(ptr, (IUINT8)seg->cmd);
   ptr = ikcp_encode8u(ptr, (IUINT8)seg->frg);
   ptr = ikcp_encode16u(ptr, (IUINT16)seg->wnd);
   ptr = ikcp_encode32u(ptr, seg->ts);
   ptr = ikcp_encode32u(ptr, seg->sn);
   ptr = ikcp_encode32u(ptr, seg->una);
   ptr = ikcp_encode32u(ptr, seg->len);
   return ptr;
}
 
 
/* encode 32 bits unsigned int (lsb) */
static inline char *ikcp_encode32u(char *p, IUINT32 l)
{
#if IWORDS_BIG_ENDIAN
   *(unsigned char*)(p + 0) = (unsigned char)((l >>  0) & 0xff);
   *(unsigned char*)(p + 1) = (unsigned char)((l >>  8) & 0xff);
   *(unsigned char*)(p + 2) = (unsigned char)((l >> 16) & 0xff);
   *(unsigned char*)(p + 3) = (unsigned char)((l >> 24) & 0xff);
#else
   *(IUINT32*)p = l;
#endif
   p += 4;
   return p;
}

解封装是在ikcp_input里实现的,没有单独的方法:

int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   ...
   while (1) {
      IUINT32 ts, sn, len, una, conv;
      ...
      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);
      ...
      data += len;
      size -= len;
   }
 
   return 0;
}

每个数据报是由一个单独的结构体来维护的,这个结构体是一个双向链表:

struct IQUEUEHEAD {
   struct IQUEUEHEAD *next, *prev;
};
 
 
//=====================================================================
// SEGMENT
//=====================================================================
struct IKCPSEG
{
   struct IQUEUEHEAD node;
   IUINT32 conv;
   IUINT32 cmd;
   IUINT32 frg;
   IUINT32 wnd;
   IUINT32 ts;
   IUINT32 sn;
   IUINT32 una;
   IUINT32 len;
   IUINT32 resendts;
   IUINT32 rto;
   IUINT32 fastack;
   IUINT32 xmit;
   char data[1];
};

3.2 数据发送逻辑

先不要管流量控制、超时重传、丢包这些东西,这些会在下面讲到。我们只来讨论最基本的发送逻辑。

//---------------------------------------------------------------------
// user/upper level send, returns below zero for error
//---------------------------------------------------------------------
int ikcp_send(ikcpcb *kcp, const char *buffer, int len)
{
   IKCPSEG *seg;
   int count, i;
   ...
   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;
}

前面说了,当你要发送数据的时候需要调用ikcp_send方法,他会对你的数据进行分组和封装。

count 是发送传入的数据需要使用的数据报个数。传如的数据可能很大,一个数据报可能装不下(前面说了最大数据报是mss),第9~14行就是来计算数据报的数量,分组的数据报序号被倒序写入frg里面(28行)。

19行创建了一个报文数据结构,25行将要发送的数据拷贝到数据结构里面,30行将数据报文添加到待发送队列队尾。

kcp->snd_queue是待发送队列,强调一下是“待发送队列”。是一个按找SN号排列的有序队列,31行的kcp->nsnd_que是待发送队列的长度。

看到这里ikcp_send其实已经完了,但是并没有实际发送数据,而只是将数据放到了待发送队列里面。

最关键的在ikcp_update里面:

//---------------------------------------------------------------------
// 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.
//---------------------------------------------------------------------
void ikcp_update(ikcpcb *kcp, IUINT32 current)
{
   IINT32 slap;
 
   kcp->current = current;
   ...
   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);
   }
}

current是当前的时钟变量。KCP将时钟的定义交给了使用者,使用者只要调用这个方法传入当前平台的系统时钟即可。

kcp->ts_flush是下次需要执行ikcp_flush的时间,kcp→interval表示执行ikcp_flush的时间间隔。

最最重要的部分是在ikcp_update的最后调用了ikcp_flush,ikcp_flush是真正去发送数据的方法。以上也说明了在主逻辑里面为什么要不断的调用ikcp_update方法,他为整个KCP的运行提供了时钟并调用flush清空待发送队列。

来看ikcp_flush:

//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
   ...
   // calculate window size
   cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
   if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
 
   // move data from snd_queue to snd_buf
   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;
   }
 
   // calculate resent
   resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
   rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
 
   // 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 + rtomin;
      }
      ...
 
      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;
 
         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);
   }
   ...
}
 
// output segment
static int ikcp_output(ikcpcb *kcp, const void *data, int size)
{
   assert(kcp);
   assert(kcp->output);
   if (ikcp_canlog(kcp, IKCP_LOG_OUTPUT)) {
      ikcp_log(kcp, IKCP_LOG_OUTPUT, "[RO] %ld bytes", (long)size);
   }
   if (size == 0) return 0;
   return kcp->output((const char*)data, size, kcp, kcp->user);
}

第8行计算阻塞窗口的大小(后面流量控制再讨论)

第12行的循环意思是:当对端接收窗口允许的情况下,将待发送队列的数据报复制到kcp→snd_buf中,kcp→snd_buf表示已发送队列待确认队列,重复“已发送队列待确认队列”

第40行的循环表示:对所有的已发送待确认队列,如果是第一次发送(未重传),则构造真实的数据报到ptr中。ptr和buffer这两个也是很重要的变量,之前没讲。buffer是一个固定大小的内存数组,大小是MTU的3倍。ptr算是buffer的pc指针。ptr开始指向buffer,每写一些数据,ptr遍向后移动一些距离,ptr - buffer也就表示了已经写入buffer的数据大小。

第81行,将所有需要发送的数据发出去。61行也有同样的调用,意思是,如果buffer满了,先发送一批。在ikcp_output方法中,最终调用了kcp->output方法。可以回到最上面“客户端使用方法”代码中的第33行,他实际上就是调用了udp的接口将数据发出去。

还有一些变量需要补充一下:

第43行segment->xmit,表示该数据报被发送的次数,包括后面要说的重传在内,每发送一次这个变量自增1。

第47行segment->resendts表示如果该数据报未被确认时数据重传的时间kcp->rx_rto,是重传超时时间,后面会讨论,rtomin是重传延迟,可以通过ikcp的接口进行设置。

第55行的kcp->rcv_nxt,表示当前端下一个要接收的数据报的序号。他跟减少确认报文的策略有关,后面再讨论。

3.3 数据接收逻辑

前面有说,数据接收时数据是用udp的接口进行接收的。但是收到的数据是不能用的,因为他包含了KCP协议的报文头。需要执行ikcp_input、ikcp_recv将数据解开报文头,如果数据太大还要进行数据重组。那么先来看下ikcp_input的工作:

//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   IUINT32 una = kcp->snd_una;
   IUINT32 maxack = 0;
   int flag = 0;
 
   if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
      ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", size);
   }
 
   if (data == NULL || (int)size < (int)IKCP_OVERHEAD) return -1;
 
   while (1) {
      ...
      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);
            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);
            }
         }
      }
      ...
   }
   ...
 
   return 0;
}

第16行,一个循环来对data进行解包,因为data可能包含不止一个数据报,所以用循环来执行。直到解完最后一个数据报跳出循环。

第18行,cmd就是数据报头的cmd字段(前面“数据解包”部分代码已经展示了是怎么拿到这个变量的),如果cmd是IKCP_CMD_PUSH,也就是用户发送的真实数据,那么会执行:

  1. 产生对这个数据报的ACK,并放到ACK队列。第24行:ikcp_ack_push(kcp, sn, ts);
  2. 创建数据报的数据结构,并放到队列(这里还不确定会放到哪个队列里面)。第26 ~ 40行

其他的:

_itimediff方法传入两个参数A,B,可以理解为A-B

第23行:判断收到的数据报是否小于接收窗口尾部,在则处理,不在丢弃数据报。kcp->rcv_nxt表示下一个要接收的数据报序号,可以理解为接收窗口头部。kcp->rcv_wnd表示接收窗口。

第25行:判断收到的数据报是否大于接收窗口头部,在则处理,不在丢弃。

要知道,因为涉及到丢包重传的机制,而且ACK包也有可能会出现丢包现象。所以很有可能会出现接收端已经正确接收了某个数据报,但是发送端没有收到这个数据报的ACK,所以发送端可能会重传一次,这种情况(当然这只是其中一种情况)可能就会出现接收端收到了小于接收窗口头部的包。这种情况就把收到的包直接丢掉。

第24行的ikcp_ack_push(kcp, sn, ts);暂且不讲,这里就先记住,收到的所有PUSH数据报都会产生一个ACK,并放到ACK队列中。

下面来看ikcp_parse_data。前面说,收到的数据会先放到队列里面,但是还不确定是那个队列。为什么不知道是那个队列,因为收到的这个数据报可能是后发送的数据报。因为网络链路或者丢包等原因,发送端是按序发送数据报,但是接收端不一定是按序收到数据报,如果接收端先收到了后面的数据报,那就先将他暂存起来。

KCP的接收有也有两个队列分别是:

  1. rcv_buf:接收到的数据队列
  2. rcv_queue:待返回给调用这的数据队列,后面称待反队列
//---------------------------------------------------------------------
// parse data
//---------------------------------------------------------------------
void ikcp_parse_data(ikcpcb *kcp, IKCPSEG *newseg)
{
   struct IQUEUEHEAD *p, *prev;
   IUINT32 sn = newseg->sn;
   int repeat = 0;
    
   if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) >= 0 ||
      _itimediff(sn, kcp->rcv_nxt) < 0) {
      ikcp_segment_delete(kcp, newseg);
      return;
   }
 
   for (p = kcp->rcv_buf.prev; p != &kcp->rcv_buf; p = prev) {
      IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
      prev = p->prev;
      if (seg->sn == sn) {
         repeat = 1;
         break;
      }
      if (_itimediff(sn, seg->sn) > 0) {
         break;
      }
   }
 
   if (repeat == 0) {
      iqueue_init(&newseg->node);
      iqueue_add(&newseg->node, p);
      kcp->nrcv_buf++;
   }  else {
      ikcp_segment_delete(kcp, newseg);
   }
 
#if 0
   ikcp_qprint("rcvbuf", &kcp->rcv_buf);
   printf("rcv_nxt=%lu\n", kcp->rcv_nxt);
#endif
 
   // move available data from rcv_buf -> rcv_queue
   while (! iqueue_is_empty(&kcp->rcv_buf)) {
      IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
      if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) {
         iqueue_del(&seg->node);
         kcp->nrcv_buf--;
         iqueue_add_tail(&seg->node, &kcp->rcv_queue);
         kcp->nrcv_que++;
         kcp->rcv_nxt++;
      }  else {
         break;
      }
   }
 
#if 0
   ikcp_qprint("queue", &kcp->rcv_queue);
   printf("rcv_nxt=%lu\n", kcp->rcv_nxt);
#endif
 
#if 1
// printf("snd(buf=%d, queue=%d)\n", kcp->nsnd_buf, kcp->nsnd_que);
// printf("rcv(buf=%d, queue=%d)\n", kcp->nrcv_buf, kcp->nrcv_que);
#endif
}

前面说过,KCP里面的队列基本都是按SN的有序队列,第16 ~ 26行就是把收到的数据报插入到rcv_buf队列中,并保证有序。

第42 ~ 53行中遍历所有rcv_buf队列中的数据报,如果数据报的SN(数据报序号)与下一个要接收的数据报序号相同则将数据报放入待反队列。再说一下各个变量的含义,kcp->rcv_nxt表示下一个要接收的数据报序号,也可以理解为接收窗口头部,kcp->nrcv_que表示待反队列中的数据报个数,kcp->rcv_wnd表示待反队列的窗口大小(就理解为待反队列的最大长度吧)。如果满足条件,就将数据报从rcv_buf转移到kcp->rcv_queue(都是按序的)。如果不满足条件跳出循环,此时rcv_buf可能还有一些数据报,因为可能会出现乱序到达的情况,乱序到达时由rcv_buf先暂失序的数据报。

第20行的repeat表示该数据报已经接收过了,因为可能网络的一些原因发送端没有收到该数据报的ACK,所以又重传了一次。如果有repeat在第28行丢弃他。

第10 ~ 14行也是判断数据报是否在接收窗口内,不在,丢弃他。不用担心未处理的数据报被丢弃,因为不会产生这种数据报的ACK,所以发送端会重传这些数据报。

下面到了最后一个步骤,将收到的所有数据返给调用者:

//---------------------------------------------------------------------
// user/upper level recv: returns size, returns below zero for EAGAIN
//---------------------------------------------------------------------
int ikcp_recv(ikcpcb *kcp, char *buffer, int len)
{
   struct IQUEUEHEAD *p;
   int ispeek = (len < 0)? 1 : 0;
   int peeksize;
   int recover = 0;
   IKCPSEG *seg;
   assert(kcp);
 
   if (iqueue_is_empty(&kcp->rcv_queue))
      return -1;
 
   if (len < 0) len = -len;
 
   peeksize = ikcp_peeksize(kcp);
 
   if (peeksize < 0)
      return -2;
 
   if (peeksize > len)
      return -3;
 
   if (kcp->nrcv_que >= kcp->rcv_wnd)
      recover = 1;
 
   // merge fragment
   for (len = 0, p = kcp->rcv_queue.next; p != &kcp->rcv_queue; ) {
      int fragment;
      seg = iqueue_entry(p, IKCPSEG, node);
      p = p->next;
 
      if (buffer) {
         memcpy(buffer, seg->data, seg->len);
         buffer += seg->len;
      }
 
      len += seg->len;
      fragment = seg->frg;
 
      if (ikcp_canlog(kcp, IKCP_LOG_RECV)) {
         ikcp_log(kcp, IKCP_LOG_RECV, "recv sn=%lu", seg->sn);
      }
 
      if (ispeek == 0) {
         iqueue_del(&seg->node);
         ikcp_segment_delete(kcp, seg);
         kcp->nrcv_que--;
      }
 
      if (fragment == 0)
         break;
   }
 
   assert(len == peeksize);
   ...
   return len;
}

比较简单了,一个循环遍历待反队列,并将数据拷贝到调用者传入的buffer里面。还是要注意,数据大的时候是要分组的,而且分组数(frg)在数据报中是倒序排放的,所以到每个frg为0的时候就跳出循环。

如果待反队列里面的数据比较多,看似是要不断的调用ikcp_recv以获取数据。

 

4. 重传

4.1 RTT、RTO计算

保证可靠传输的很重要一个策略就是超时重传,超时重传的一个关键问题是如何计算RTT及RTO。

RTO(Retransmission TimeOut):重传超时时间,即从数据发送时刻起,超过这个时间将进行数据报重传。

RTT(Round Trip Time):往返时间,即数据报发送到收到确认的时差。

在“数据封装”中提到过,数据报的第8 ~ 11字节为ts字段,ts表示数据报发送时刻的时钟,他在计算RTT中起到了关键作用。

先看下计算RTT的时机:

//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   ...
   while (1) {
      ...
      if (cmd == IKCP_CMD_ACK) {
         if (_itimediff(kcp->current, ts) >= 0) {
            ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
         }
         ikcp_parse_ack(kcp, sn);
         ikcp_shrink_buf(kcp);
         if (flag == 0) {
            flag = 1;
            maxack = sn;
         }  else {
            if (_itimediff(sn, maxack) > 0) {
               maxack = sn;
            }
         }
         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);
         }
      }
      ...
   }
   ...
   return 0;
}

如果收到的数据报为ACK数据报,那么就会在第11行执行ikcp_update_ack,这个方法就是计算RTT、RTO的。ts是数据报中的ts字段,表示数据报发送是的时钟,不过注意ts不是ACK这个数据报发送的时钟,而是ACK要确认的PUSH数据报发送的时钟。

来看下如何计算RTT和RTO的:

//---------------------------------------------------------------------
// parse ack
//---------------------------------------------------------------------
static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt)
{
   IINT32 rto = 0;
   if (kcp->rx_srtt == 0) {
      kcp->rx_srtt = rtt;
      kcp->rx_rttval = rtt / 2;
   }  else {
      long delta = rtt - kcp->rx_srtt;
      if (delta < 0) delta = -delta;
      kcp->rx_rttval = (3 * kcp->rx_rttval + delta) / 4;
      kcp->rx_srtt = (7 * kcp->rx_srtt + rtt) / 8;
      if (kcp->rx_srtt < 1) kcp->rx_srtt = 1;
   }
   rto = kcp->rx_srtt + _imax_(kcp->interval, 4 * kcp->rx_rttval);
   kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX);
}

有兴趣的同学可以去翻阅《计算机网络 自顶向下方法 原书第七版》中文版中的第158页“3.5.3 往返时间的估计与超时”。这里的计算方法和书上讲的计算方法一模一样。

kcp->rx_srtt就是当前计算出的RTT时间。

kcp->rx_rttval是当前RTT偏差。

kcp->rx_rto就是之后用来重传的超时时间(在普通模式下,这个超时时间还会加一个延迟,在后面“非延迟ACK”中会有提到)。

RTT的计算,就是纯数学计算,真要探究为啥这么算可能要去研究其数学模型了。

4.2 选择性重传

后面“处理ACK”的部分会讨论

4.3 快速重传

快速重传即不等待超时,当探测到达丢包的可能行达到一定程度之后,直接对未确认的数据报机型重传。KCP中设置kcp->fastresend可以启动快速重传模式。

//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   ...
   while (1) {
      ...
      if (cmd == IKCP_CMD_ACK) {
         if (_itimediff(kcp->current, ts) >= 0) {
            ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
         }
         ikcp_parse_ack(kcp, sn);
         ikcp_shrink_buf(kcp);
         if (flag == 0) {
            flag = 1;
            maxack = sn;
         }  else {
            if (_itimediff(sn, maxack) > 0) {
               maxack = sn;
            }
         }
         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);
         }
      }
      ...
   }
 
   if (flag != 0) {
      ikcp_parse_fastack(kcp, maxack);
   }
   ...
   return 0;
}
 
 
static void ikcp_parse_fastack(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 (_itimediff(sn, seg->sn) < 0) {
         break;
      }
      else if (sn != seg->sn) {
         seg->fastack++;
      }
   }
}

发送端收到ACK数据报后还会去检查失序数据报(第16行和第33行)

在不断调用ikcp_input的过程中,seg->fastack自增,该值表示了数据报失序到达的次数。

//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
   ...
   // calculate resent
   resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
   rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
 
   // flush data segments
   for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) {
      IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node);
      ...
      else if (segment->fastack >= resent) {
         needsend = 1;
         segment->xmit++;
         segment->fastack = 0;
         segment->resendts = current + segment->rto;
         change++;
      }
      ...
   }
   ...
}

在ikcp_flush中,如果设置了快速重传失序数(第八行的kcp->fastresend),一旦超过这个失序数,失序的数据报便会不等待超时时间,直接执行重传操作。

 

5. UNA + ACK

5.1 ACK

5.1.1 产生ACK

在“数据接收逻辑”的代码第24行,调用了ikcp_ack_push。每当收到PUSH数据报时,都会通过这个方法产生数据报的ACK,并放到ACK队列中

//---------------------------------------------------------------------
// ack append
//---------------------------------------------------------------------
static void ikcp_ack_push(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
   size_t newsize = kcp->ackcount + 1;
   IUINT32 *ptr;
 
   if (newsize > kcp->ackblock) {
      IUINT32 *acklist;
      size_t newblock;
 
      for (newblock = 8; newblock < newsize; newblock <<= 1);
      acklist = (IUINT32*)ikcp_malloc(newblock * sizeof(IUINT32) * 2);
 
      if (acklist == NULL) {
         assert(acklist != NULL);
         abort();
      }
 
      if (kcp->acklist != NULL) {
         size_t x;
         for (x = 0; x < kcp->ackcount; x++) {
            acklist[x * 2 + 0] = kcp->acklist[x * 2 + 0];
            acklist[x * 2 + 1] = kcp->acklist[x * 2 + 1];
         }
         ikcp_free(kcp->acklist);
      }
 
      kcp->acklist = acklist;
      kcp->ackblock = newblock;
   }
 
   ptr = &kcp->acklist[kcp->ackcount * 2];
   ptr[0] = sn;
   ptr[1] = ts;
   kcp->ackcount++;
}

kcp->ackcount是还没发送的ACK的数量,kcp→ackblock当前ACK队列的容量。只不过ACK队列是用一个数组来表示的,这个数组的 X * 2 + 0位是数据报的序号,X * 2 + 1位是收到的PUSH数据报发送时的时钟。 强调是”收到的PUSH数据报发送时的时钟”,这个时钟就厉害了,他是计算RTT和RTO的重要参数,后面再讨论。

现在且记下会在ACK队列里面产生一个ACK,后面再讨论ACK的发送。

5.1.2 发送ACK

前面已经介绍了,接收端收到了PUSH的数据报之后会产生ACK放到ACK队列里面,kcp->acklist是以数组形式表示的ACK队列,kcp->ackcount表示了ACK队列中的元素个数。下面我们就来看下ACK是如何发送的。

位置还是在ikcp_flush里面,前面“数据发送逻辑”里面也介绍过,ikcp_flush是由ikcp_update调用的,ikcp_update需要不断的被调用这调用以便更新时钟以及清空发送队列。

//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
   IUINT32 current = kcp->current;
   char *buffer = kcp->buffer;
   char *ptr = buffer;
   int count, size, i;
   IUINT32 resent, cwnd;
   IUINT32 rtomin;
   struct IQUEUEHEAD *p;
   int change = 0;
   int lost = 0;
   IKCPSEG seg;
 
   // 'ikcp_update' haven't been called.
   if (kcp->updated == 0) return;
 
   seg.conv = kcp->conv;
   seg.cmd = IKCP_CMD_ACK;
   seg.frg = 0;
   seg.wnd = ikcp_wnd_unused(kcp);
   seg.una = kcp->rcv_nxt;
   seg.len = 0;
   seg.sn = 0;
   seg.ts = 0;
 
   // flush acknowledges
   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;
   ...
   // flash remain segments
   size = (int)(ptr - buffer);
   if (size > 0) {
      ikcp_output(kcp, buffer, size);
   }
   ...
}
 
 
static void ikcp_ack_get(const ikcpcb *kcp, int p, IUINT32 *sn, IUINT32 *ts)
{
   if (sn) sn[0] = kcp->acklist[p * 2 + 0];
   if (ts) ts[0] = kcp->acklist[p * 2 + 1];
}

比较简单了,不再介绍。

5.1.3 处理ACK

//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   ...
   while (1) {
      ...
      if (cmd == IKCP_CMD_ACK) {
         if (_itimediff(kcp->current, ts) >= 0) {
            ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
         }
         ikcp_parse_ack(kcp, sn);
         ikcp_shrink_buf(kcp);
         if (flag == 0) {
            flag = 1;
            maxack = sn;
         }  else {
            if (_itimediff(sn, maxack) > 0) {
               maxack = sn;
            }
         }
         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);
         }
      }
      ...
   }
   ...
   return 0;
}
 
 
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);
         ikcp_segment_delete(kcp, seg);
         kcp->nsnd_buf--;
         break;
      }
      if (_itimediff(sn, seg->sn) < 0) {
         break;
      }
   }
}
 
 
static void ikcp_shrink_buf(ikcpcb *kcp)
{
   struct IQUEUEHEAD *p = kcp->snd_buf.next;
   if (p != &kcp->snd_buf) {
      IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
      kcp->snd_una = seg->sn;
   }  else {
      kcp->snd_una = kcp->snd_nxt;
   }
}

还是在ikcp_input方法中,当收到ACK后第18行调用ikcp_parse_ack处理ACK。

第37 ~ 57行展示了ikcp_parse_ack处理ACK的过程,比较简单,就是找到ACK所代表数据报序号,将已发送待确认队列数据报删掉。

处理完ACK后,可以看到第19行调用了ikcp_shrink_buf。第60 ~ 69行展示了ikcp_shrink_buf的过程。他是将una向后移动,即发送窗口头部向后移动。

如何实现的快速重传:

结合“数据发送逻辑”中讨论的数据报发送的过程。所有的已发送但是还没有确认的数据报会被放到kcp->snd_buf这个队列当中,当收到ACK时,会在kcp->snd_buf队列中删掉已经确认过得数据报,然后在通过ikcp_shrink_buf方法移动发送窗口。同样“数据接收逻辑”中也讨论过,收到的数据报会在接收端先放到kcp->rcv_buf队列中,直到收到完整且连续的数据报后才会转移到kcp->rcv_queue并传给调用者。 当接收端出现失序或丢包情况时,发送端可能会先收到后面数据报的ACK,发送端就会先把kcp->snd_buf中后面的数据报删掉,不再重新发送。如果前面的数据报接收ACK超时,会只重发前面未收到ACK的数据报。当发送窗口前端连续的数据报都收到ACK后,便会向后移动发送窗口。这种逻辑就是前面说过的“选择性重传”,与之对应的是回退式的重传。

5.2 UNA

KCP在使用ACK确认的同时,也使用了捎带式的UNA确认。大概思路是:在收发的数据报中添加了UNA字段,该字段表示的是对端下一个要接收的数据报序号。这个字段也表示,所有序号小于UNA的数据报已经被对端正确接收。那么也就是说,所有未收到ACK的数据报如果SN小于UNA,都可以直接丢弃了,因为这些数据报已经被对端正确接收,不用再等待ACK进行重传了。

还是来看ikcp_input方法:

//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   IUINT32 una = kcp->snd_una;
   IUINT32 maxack = 0;
   int flag = 0;
 
   if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
      ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", size);
   }
 
   if (data == NULL || (int)size < (int)IKCP_OVERHEAD) return -1;
 
   while (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);
      ...
      ikcp_parse_una(kcp, una);
      ikcp_shrink_buf(kcp);
      ...
   }
   ...
   return 0;
}

第29行ikcp_parse_una(kcp, una);就是丢弃una之前的所有待发送数据报,代码如下:

static void ikcp_parse_una(ikcpcb *kcp, IUINT32 una)
{
   struct IQUEUEHEAD *p, *next;
   for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) {
      IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node);
      next = p->next;
      if (_itimediff(una, seg->sn) > 0) {
         iqueue_del(p);
         ikcp_segment_delete(kcp, seg);
         kcp->nsnd_buf--;
      }  else {
         break;
      }
   }
}

之后执行ikcp_shrink_buf,前面“处理ACK”部分也已经提到过了,是为了更新本地的una将发送窗口头部向后移动。

5.3 非延迟ACK

这是一个设置项,在kcp->nodelay中设置:

//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
   ...
   // calculate resent
   resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
   rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
 
   // 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 + 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) {
         needsend = 1;
         segment->xmit++;
         segment->fastack = 0;
         segment->resendts = current + segment->rto;
         change++;
      }
      ...
   }
   ...
}

如上代码segment->resendts表示的是如果为收到ACK,数据报重传的时间点。

第9行,如果设置了无延迟模式,即kcp->nodelay不是0,那么rtomin为0。rtomin实际上是代表了超时重传的延迟时间,这个数值会影响后面的重传时间。

第19行,如果是第一次发送数据报,超时时间直接设置到RTO之后并加上rtomin的时间点,非延迟模式下rtomin为0,超时时间就是RTO时间。

第25行,如果是已经出现重传的数据报,非延迟模式下,下次重传的时间是RTO/2。重传延迟是普通模式下的1/2。

其实可以看到,但出现数据报重传后非延迟模式下减小了数据重传的超时时间,网络中可能会出现多个重传的数据报,是一种用带宽换速度的策略。具体效果只能看实测数据,至少其github上写的是有效的。

 

6. 流量控制

6.1 阻塞窗口

kcp->cwnd这个变量记录了当前发送的窗口大小,其实这个就是我们所指的阻塞窗口。他是用来调节发送端的发送速度的。

//---------------------------------------------------------------------
// ikcp_flush
//---------------------------------------------------------------------
void ikcp_flush(ikcpcb *kcp)
{
   ...
   seg.conv = kcp->conv;
   seg.cmd = IKCP_CMD_ACK;
   seg.frg = 0;
   seg.wnd = ikcp_wnd_unused(kcp);
   seg.una = kcp->rcv_nxt;
   seg.len = 0;
   seg.sn = 0;
   seg.ts = 0;
   ...
   // calculate window size
   cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd);
   if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd);
 
   // move data from snd_queue to snd_buf
   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;
   }
 
   // calculate resent
   resent = (kcp->fastresend > 0)? (IUINT32)kcp->fastresend : 0xffffffff;
   rtomin = (kcp->nodelay == 0)? (kcp->rx_rto >> 3) : 0;
 
   // 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 + 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) {
         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;
 
         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
   if (change) {
      IUINT32 inflight = kcp->snd_nxt - kcp->snd_una;
      kcp->ssthresh = inflight / 2;
      if (kcp->ssthresh < IKCP_THRESH_MIN)
         kcp->ssthresh = IKCP_THRESH_MIN;
      kcp->cwnd = kcp->ssthresh + resent;
      kcp->incr = kcp->cwnd * kcp->mss;
   }
 
   if (lost) {
      kcp->ssthresh = cwnd / 2;
      if (kcp->ssthresh < IKCP_THRESH_MIN)
         kcp->ssthresh = IKCP_THRESH_MIN;
      kcp->cwnd = 1;
      kcp->incr = kcp->mss;
   }
 
   if (kcp->cwnd < 1) {
      kcp->cwnd = 1;
      kcp->incr = kcp->mss;
   }
}
 
 
static int ikcp_wnd_unused(const ikcpcb *kcp)
{
   if (kcp->nrcv_que < kcp->rcv_wnd) {
      return kcp->rcv_wnd - kcp->nrcv_que;
   }
   return 0;
}

第68行的lost表示了丢包的个数。

第75行的change表示了进行快速重传的包的个数。

第112 ~ 133行中展示了,一旦出现丢包或者有快速重传的现象存在,那么就要逐渐的减小kcp->cwnd发送窗口大小。

代码中在第21行将kcp->snd_queue的数据报放入kcp->snd_buf时,通过cwnd来限制转移的数据报数量。通过控制转移入kcp→snd_buf的数据报数量,来控制整个发送端的发送。

//---------------------------------------------------------------------
// input data
//---------------------------------------------------------------------
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
   ...
   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;
         }
      }
   }
 
   return 0;
}

kcp->rmt_wnd表示对端的接收窗口大小,这个数值是怎么来的?可以看上面flush方法里面的第10行,在每次flush时都会计算自己端的接收窗口,并通过数据报传到对端。具体的计算方法在上面代码中的最后ikcp_wnd_unused展示了。

在收到数据时判断kcp->snd_una与una的大小。kcp->snd_una是发送端下一个未收到ACK的数据报,una是已经收到的ACK。如果una大于kcp->snd_una,也就是说收到的ACK的una比下一个要确认的snd_una大,那么此时可能会有丢包现象的存在。相反,如果kcp->snd_una比una大,那么此时网络可能比较畅通。当网络畅通时,逐渐增加kcp->cwnd,这样在flush的时候就能够从kcp->snd_queue放入更多的数据报到kcp->snd_buf中。这样就实现了逐渐扩大发送流量。

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(KCP一种基于非可靠传输的可靠传输协议(源码分析))