ikcp_input
先從下層協議將資料讀出來,並將對應的包頭資訊解析出來,根據不同的包頭命令進入不同的處理邏輯。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
IUINT32 prev_una = kcp->snd_una;
IUINT32 maxack = 0, // 收到的這組包裡最大的ack
latest_ts = 0; // 以及最近的時間戳
int flag = 0; // 接收到的資料包裡有CMD_ACK則為1
if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) {
ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", (int)size);
}
if (data == NULL || (int)size < (int)IKCP_OVERHEAD) return -1;
// 迴圈將底層收到的資料讀取並解析
while (1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
// 資料包文長度小於 kcp 包頭大小, 頭部資訊不完整, 退出
if (size < (int)IKCP_OVERHEAD) break;
// 取得所有包頭資訊
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);
// 減去包頭大小
size -= IKCP_OVERHEAD;
// 資料包文長度小於包頭指定的資料長度,資料被破壞,退出
if ((long)size < (long)len || (int)len < 0) return -2;
// 非法cmd, 退出
if (cmd != IKCP_CMD_PUSH && cmd != IKCP_CMD_ACK &&
cmd != IKCP_CMD_WASK && cmd != IKCP_CMD_WINS)
return -3;
kcp->rmt_wnd = wnd;
ikcp_parse_una(kcp, una);
ikcp_shrink_buf(kcp);
...
}
}
先看一下最後兩行的 ikcp_parse_una 和 ikcp_shrink_buf,kcp 收到對方傳送的 una,說明 una 之前的包已經收到,那麼接收方就可以將 snd_buf裡的 una 之前的包刪掉,ikcp_parse_una 就是做這個的。
// 從snd_buf(雙向迴圈連結串列)中刪除小於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;
// 如果該sendbuf的序列號小於una, 則說明該包已經被確認, 從snd_buf中刪除
if (_itimediff(una, seg->sn) > 0) {
iqueue_del(p); // 把seg裡邊的node指標斷了
ikcp_segment_delete(kcp, seg);
kcp->nsnd_buf--;
} else {
break;
}
}
}
上邊的函式把 snd_buf 裡的資料包刪了,對應的 snd_una 也需要更新,ikcp_shrink_buf 就是更新 snd_una 的。
// 呼叫這個函式之前刪除了一些被確認的包, 更新snd_una為當前傳送緩衝裡的最小序列號
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_CMD_ACK
當收到的命令為IKCP_CMD_ACK,說明該包是一個 ACK,需要將被ACK的包從 snd_buf 裡刪除,然後更新 snd_una,用的還是上邊那兩個函式。然後是更新maxack(這組包裡最大的ack)和latest_ts(最近的時間戳),後邊用於計算snd_buf裡哪些包被跳過了(失序),決定是否快速重傳。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
// 迴圈將底層收到的資料讀取並解析
while (1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
...
if (cmd == IKCP_CMD_ACK) {
// ts為該資料包傳送時間, current為當前時間(收到該資料包的ack), 兩者之差為rtt
if (_itimediff(kcp->current, ts) >= 0) {
// 計算超時重傳時間
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
}
ikcp_parse_ack(kcp, sn);
ikcp_shrink_buf(kcp);
// 下邊的程式碼就是更新maxack和latest_ts,
if (flag == 0) { // 只有在解析第一個IKCP_CMD_ACK包進入這個分支
flag = 1;
maxack = sn;
latest_ts = ts;
} else {
if (_itimediff(sn, maxack) > 0) { // 這裡不能用max函式,因為有序號迴繞的問題
#ifndef IKCP_FASTACK_CONSERVE
maxack = sn;
latest_ts = ts;
#else
if (_itimediff(ts, latest_ts) > 0) {
maxack = sn;
latest_ts = ts;
}
#endif
}
}
}
...
}
}
// 計算超時重傳時間, 和計網自頂向下3.5.3節的公式一樣
static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt)
{
// rtt:該資料包的往返時間
// rx_srtt: 平滑的rtt,近8次rtt平均值
// rx_rttval: 近4次rtt和srtt的平均差值,反應了rtt偏離srtt的程度
// rx_rto: 重傳超時時間
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); // rx_srtt + 4*rx_rttval表示一種比較壞的情況
kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX);
}
IKCP_CMD_PUSH
當收到的命令為IKCP_CMD_ACK,說明是資料包。新建一個 IKCPSEG,將頭資訊和data 複製進去。然後呼叫 ikcp_ack_push 將該資料包加入 acklist,以便傳送 ack 。
然後檢查該 seg 是否已經接受過,如果接收過則忽略,如果沒有接收過則將其加入 rcv_buf,之後將 rcv_buf 裡連續序號的包轉移到 rcv_que供上層讀取,透過檢查rcv_nxt即可判斷資料包是否連續。這個檢查過程是 ikcp_parse_data 實現的。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
// 迴圈將底層收到的資料讀取並解析
while (1) {
IUINT32 ts, sn, len, una, conv;
IUINT16 wnd;
IUINT8 cmd, frg;
IKCPSEG *seg;
...
else if (cmd == IKCP_CMD_PUSH) {
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) {
// (to , from, len)
memcpy(seg->data, data, len); // 複製資料
}
// 檢查newseg資料包,如果沒有接受過,就將其放在接收快取內,
// 並將快取內連續的資料放到接受佇列裡,可供上層應用讀取。
ikcp_parse_data(kcp, seg);
}
}
}
...
}
}
// 收到對方傳送的sn號包,更新acklist,acknode的格式為(sn, ts), sn這個包的發出時間為ts
// 注意可能收到多個sn相同的包
static void ikcp_ack_push(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
IUINT32 newsize = kcp->ackcount + 1;
IUINT32 *ptr;
// 申請更大的acklist空間
if (newsize > kcp->ackblock) {
IUINT32 *acklist;
IUINT32 newblock;
for (newblock = 8; newblock < newsize; newblock <<= 1); // 倍增的方式擴容
acklist = (IUINT32*)ikcp_malloc(newblock * sizeof(IUINT32) * 2); // 申請newblock個acknode空間
if (acklist == NULL) {
assert(acklist != NULL);
abort();
}
if (kcp->acklist != NULL) { // 將舊的acklist複製到新的acklist中
IUINT32 x;
for (x = 0; x < kcp->ackcount; x++) { // 從這裡看出來每個acknode佔用2個IUINT32
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++;
}
// 檢查newseg資料包,如果沒有接受過,就將其放在接收快取內,並將快取內連續的資料放到接受佇列裡,可供上層應用讀取。
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;
}
// 判斷是否接受過sn包
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;
}
// 當前包序號大於被檢查的包,之後就不可能有 seg->sn 了,因為 rcv_buf 的連結串列 sn 是遞增的。
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);
}
// 將收到的連續資料包從rcv_buf 轉移到 rcv_queue 供上層應用讀取
while (! iqueue_is_empty(&kcp->rcv_buf)) {
IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
// 透過檢查rcv_nxt即可判斷資料包是否連續
if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) { //資料包連續 且 rcv_queue 不滿
iqueue_del(&seg->node);
kcp->nrcv_buf--;
iqueue_add_tail(&seg->node, &kcp->rcv_queue);
kcp->nrcv_que++;
kcp->rcv_nxt++;
} else {
break;
}
}
...
}
IKCP_CMD_WASK
當收到的命令為 IKCP_CMD_WASK,說明對方想探測你的視窗大小。只需要在本地標記一下,等到傳送的時候會檢查這個標記然後傳送視窗大小。
else if (cmd == IKCP_CMD_WASK) { // 傳送方探測接收方的視窗大小
// ready to send back IKCP_CMD_WINS in ikcp_flush
// tell remote my window size
kcp->probe |= IKCP_ASK_TELL;
}
IKCP_CMD_WINS
當收到的命令為 IKCP_CMD_WASK,說明對方回答了你的視窗探測,在這裡不需要做處理。
檢查失序(冗餘)ACK
當收完資料包,KCP 會檢查哪些資料包被跳過了,然後標記其被跳過的次數。不過有人會發現透過 maxack 去檢查哪個資料包被跳過會不會有問題?因為小於 maxack 的包可能也收到了,但也要記錄一次被跳過。答案是不會的,因為在收到IKCP_CMD_ACK的時候會呼叫ikcp_parse_ack; ikcp_shrink_buf ; 這兩個函式,而這兩個函式會把所有收到ack 的包從 snd_buf 刪除掉,那麼在ikcp_parse_fastack 中也就不會檢查到這個包了。
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
// 迴圈將底層收到的資料讀取並解析
while (1) {
...
if (cmd == IKCP_CMD_ACK) ...
else if(cmd == IKCP_CMD_PUSH) ...
else if(cmd == IKCP_CMD_WASK) ...
else if(cmd == IKCP_CMD_WINS) ...
else return -3;
...
}
// 只要收到了IKCP_CMD_PUSH,flag 就為 true
if (flag != 0) {
ikcp_parse_fastack(kcp, maxack, latest_ts);
}
}
// 計算快速重傳的引數fastack(傳送快取裡的包被跳過的總次數,冗餘ACK)
static void ikcp_parse_fastack(ikcpcb *kcp, IUINT32 sn, IUINT32 ts)
{
struct IQUEUEHEAD *p, *next;
if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0)
return;
// 統計maxack之前的有幾個沒有被ack包
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) { // 被跳過一次
#ifndef IKCP_FASTACK_CONSERVE
seg->fastack++; // 該資料包被跳過一次
#else
if (_itimediff(ts, seg->ts) >= 0)
seg->fastack++;
#endif
}
}
}
計算擁塞視窗
上邊的工作做完後,需要計算並更新擁塞視窗,
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
...
/*
接收時,擁塞視窗增加
擁塞視窗控制, 慢啟動:每收到一個ACK,擁塞視窗就加1,一個RTT擁塞視窗會翻倍增長。
擁塞避免:每收到一個ACK,擁塞視窗就加1/cwnd, 一個RTT擁塞視窗加1
下邊傳送時,擁塞視窗減少
*/
if (_itimediff(kcp->snd_una, prev_una) > 0) {
if (kcp->cwnd < kcp->rmt_wnd) {
IUINT32 mss = kcp->mss;
if (kcp->cwnd < kcp->ssthresh) { // 擁塞視窗小於慢啟動閾值,快速增加擁塞視窗
kcp->cwnd++; // 慢啟動
kcp->incr += mss;
// 達到閾值的時候 cwnd*mss = incr
} else { // 擁塞視窗大於慢啟動閾值,擁塞避免模式
if (kcp->incr < mss) kcp->incr = mss;
// 經過計算,差不多是每個RTT增加一個mss, 間隔略小於一個RTT,也就是更快地增加擁塞視窗
// incr = k*mss , 擁塞視窗等於floor(k)
kcp->incr += (mss * mss) / kcp->incr + (mss / 16);
if ((kcp->cwnd + 1) * mss <= kcp->incr) {
#if 1
kcp->cwnd = (kcp->incr + mss - 1) / ((mss > 0)? mss : 1);
#else
kcp->cwnd++;
#endif
}
}
if (kcp->cwnd > kcp->rmt_wnd) {
kcp->cwnd = kcp->rmt_wnd;
kcp->incr = kcp->rmt_wnd * mss;
}
}
}
}
ikcp_recv
ikcp_recv 是使用者呼叫的介面,期待收到長度最多為 len 的訊息。
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); // 檢視使用者訊息大小,主要檢查buffer大小能不能裝下這個訊息
if (peeksize < 0)
return -2;
// 使用者的 buffer 不夠裝
if (peeksize > len)
return -3;
// 這裡發現接收佇列裡資料太多,則標記一下,將會向對方傳送一個視窗大小防止對面太快的發資料
if (kcp->nrcv_que >= kcp->rcv_wnd)
recover = 1;
// 將一個資料包複製進使用者 buffer
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 (ispeek == 0) {
iqueue_del(&seg->node);
ikcp_segment_delete(kcp, seg);
kcp->nrcv_que--;
}
// 非位元組流模式 : 讀到最後一段,已經組裝出一個完整的上層資料包
// 位元組流模式:每個包的frg都為0,每次recv只會讀到一個包
// 也有可能遇不到fragment為0的包,訊息的其他部分還在接收快取或者網路中
if (fragment == 0)
break;
}
assert(len == peeksize);
// move available data from rcv_buf -> rcv_queue
// ikcp_input裡已經轉移過一次了, 使用者把資料讀走後rcv_que變小了, 這裡再轉移一次
while (! iqueue_is_empty(&kcp->rcv_buf)) {
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;
}
}
// fast recover
if (kcp->nrcv_que < kcp->rcv_wnd && recover) {
// ready to send back IKCP_CMD_WINS in ikcp_flush
// tell remote my window size
kcp->probe |= IKCP_ASK_TELL;
}
return len;
}
// 返回訊息的大小(應用層的一個資料包大小, 如果分段, 則要計算所有段的和),如果是流模式,返回一個KCP包的大小
int ikcp_peeksize(const ikcpcb *kcp)
{
struct IQUEUEHEAD *p;
IKCPSEG *seg;
int length = 0;
assert(kcp);
if (iqueue_is_empty(&kcp->rcv_queue)) return -1;
seg = iqueue_entry(kcp->rcv_queue.next, IKCPSEG, node);
if (seg->frg == 0) return seg->len;
// 如果接收佇列裡的資料不足一個包,返回-1
if (kcp->nrcv_que < seg->frg + 1) return -1;
for (p = kcp->rcv_queue.next; p != &kcp->rcv_queue; p = p->next) {
seg = iqueue_entry(p, IKCPSEG, node);
length += seg->len;
if (seg->frg == 0) break;
}
return length;
}