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有自己的一套報文封裝格式,所以在上面使用的時候,要發送的數據是不能通過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中。這樣就實現了逐漸擴大發送流量。

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章