序
KCP是一個非常簡單的可做流控的可靠傳輸協議,他的實現很多地方都借鑑了TCP的協議實現。通過學習KCP源碼,能夠更加熟悉ACK、重傳、流控等實現方法,對TCP的理解也能有很大幫助
目錄
1. KCP簡介
KCP是一個快速可靠協議,底層通過不可靠數據傳輸,通過浪費帶寬的代價來實現降低延遲的效果。純算法實現的數據協議,可以通過任何形式(UDP、TCP等)發送數據,但其作用是提供低延遲、可靠、流控機制,加到TCP等已經提供相關功能的傳輸方法而言純屬畫蛇添足。所以目前看來,基於UDP來實現KCP的應用是最爲合適的一種方法。即擁有UDP簡單、快速的傳輸效果,又能夠提供可靠的傳輸機制。
具體的介紹可以移步:https://github.com/skywind3000/kcp,整個工程其實就兩個文件,相對比較簡單。
簡單列舉下KCP特性:
- 自定義RTO:可以設置選擇不同的RTO時間計算方式
- 選擇性重傳:只重傳丟失的數據段
- 快速重傳:發現數據段丟失時,不等待RTO,直接發起數據段重傳
- UNA + ACK:收到UNA及ACK都會確認數據段,取消相應數據段重傳
- 非延遲ACK:可以設置ACK是否延遲發送
- 流量控制:發送端及接收端分別設有發送窗口及接收窗口,同時存在阻塞窗口。當出現丟包或失序的情況時,減小阻塞窗口以控制發送方的流量。
後面會從源碼的角度對以上特性一一分析來了解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進行解封裝裝、重組和其他一些控制邏輯才能拿到真實的發送數據。
我們先說一下這個封裝體的各個字段的作用:
- conv:保存了一次會話的ID,C-S兩端要用同樣的id進行通訊。可以直接理解爲sesionid
- cmd:數據報的控制指令類型,有4種:
- IKCP_CMD_PUSH:表示該報文段是用戶數據的推送
- IKCP_CMD_ACK:表示該報文段是對PUSH報文段的確認
- IKCP_CMD_WASK:表示該報文是詢問對端的接收窗口大小
- IKCP_CMD_WINS:與IKCP_CMD_WASK對應,表示該報文是通知對端自己的接收窗口大小
- frg:數據分段序號,從大到小排列,最後一個分段序號爲0。每個數據報有mtu(最大傳輸單元)和mss(最大報文長度的概念)。如果調用ikcp_send的數據超過了mss,那麼他就會被分成兩個報文段。frg就是表示了這兩個報文段的序號。如果調用ikcp_send的數據沒有超過mss,那麼frg始終爲0
- wnd:對方的接收窗口大小。用於流量控制。
- ts:數據報發送時的時鐘
- sn:數據報的序號,每發送一個數據報sn遞增1。注意跟frg區別,frg是當用戶要發送的數據太大時被分成兩個數據報的序號,sn是所有發送的數據報的序號。
- una:對端下一個要接收的數據報序號
- 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,也就是用戶發送的真實數據,那麼會執行:
- 產生對這個數據報的ACK,並放到ACK隊列。第24行:ikcp_ack_push(kcp, sn, ts);
- 創建數據報的數據結構,並放到隊列(這裏還不確定會放到哪個隊列裏面)。第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的接收有也有兩個隊列分別是:
- rcv_buf:接收到的數據隊列
- 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中。這樣就實現了逐漸擴大發送流量。