轉載請注明出處:https://www.cnblogs.com/lihaiping/p/kcp.html
author: lihaiping1603@aliyun.com
date: 2019/02/08
介紹(https://github.com/skywind3000/kcp)
摘自官方介紹
KCP是一個快速可靠協議,能以比 TCP浪費10%-20%的帶寬的代價,換取平均延遲降低 30%-40%,且最大延遲降低三倍的傳輸效果。純算法實現,並不負責底層協議(如UDP)的收發,需要使用者自己定義下層數據包的發送方式,以 callback的方式提供給 KCP。 連時鍾都需要外部傳遞進來,內部不會有任何一次系統調用。
整個協議只有 ikcp.h, ikcp.c兩個源文件,可以方便的集成到用戶自己的協議棧中。也許你實現了一個P2P,或者某個基於 UDP的協議,而缺乏一套完善的ARQ可靠協議實現,那么簡單的拷貝這兩個文件到現有項目中,稍微編寫兩行代碼,即可使用。
技術特效
摘自官方介紹
TCP是為流量設計的(每秒內可以傳輸多少KB的數據),講究的是充分利用帶寬。而 KCP是為流速設計的(單個數據包從一端發送到一端需要多少時間),以10%-20%帶寬浪費的代價換取了比 TCP快30%-40%的傳輸速度。TCP信道是一條流速很慢,但每秒流量很大的大運河,而KCP是水流湍急的小激流。KCP有正常模式和快速模式兩種,通過以下策略達到提高流速的結果:
RTO翻倍vs不翻倍:
TCP超時計算是RTOx2,這樣連續丟三次包就變成RTOx8了,十分恐怖,而KCP啟動快速模式后不x2,只是x1.5(實驗證明1.5這個值相對比較好),提高了傳輸速度。
選擇性重傳 vs 全部重傳:
TCP丟包時會全部重傳從丟的那個包開始以后的數據,KCP是選擇性重傳,只重傳真正丟失的數據包。
快速重傳:
發送端發送了1,2,3,4,5幾個包,然后收到遠端的ACK: 1, 3, 4, 5,當收到ACK3時,KCP知道2被跳過1次,收到ACK4時,知道2被跳過了2次,此時可以認為2號丟失,不用等超時,直接重傳2號包,大大改善了丟包時的傳輸速度。
延遲ACK vs 非延遲ACK:
TCP為了充分利用帶寬,延遲發送ACK(NODELAY都沒用),這樣超時計算會算出較大 RTT時間,延長了丟包時的判斷過程。KCP的ACK是否延遲發送可以調節。
UNA vs ACK+UNA:
ARQ模型響應有兩種,UNA(此編號前所有包已收到,如TCP)和ACK(該編號包已收到),光用UNA將導致全部重傳,光用ACK則丟失成本太高,以往協議都是二選其一,而 KCP協議中,除去單獨的 ACK包外,所有包都有UNA信息。
非退讓流控:
KCP正常模式同TCP一樣使用公平退讓法則,即發送窗口大小由:發送緩存大小、接收端剩余接收緩存大小、丟包退讓及慢啟動這四要素決定。但傳送及時性要求很高的小數據時,可選擇通過配置跳過后兩步,僅用前兩項來控制發送頻率。以犧牲部分公平性及帶寬利用率之代價,換取了開着BT都能流暢傳輸的效果。
KCP協議源碼結構圖(https://github.com/lihp1603/kcp/tree/note)
這里介紹一篇寫的很不錯的源碼筆記供參考(https://blog.csdn.net/yongkai0214/article/details/85156452),我也引用一下他當中的一幅圖:

然后接下來的內容,就屬於我個人對於源碼的一些閱讀認識和讀書筆記了。(主要方便自己日后翻閱,如果錯誤,請聯系我。)
首先我們對幾個東西需要弄清楚: 對於接收方,我們收到的數據,經過input函數解析的時候,先是放入rcv_buf中,進行排序,對於排好序的有序數據,我們會從rcv_buf再移動到rcv_queue接收隊列中,然后用戶調用recv函數的時候,我們就直接從rcv_queue隊列中取出來,這樣用戶得到的數據就是有序的數據包了。 而對於kcp->rcv_nxt;這個是接收方用於標識我們接下來待接收的數據,也就是說rcv_nxt前面的數據,我們已經有序的全部收到了。 接收方在發送應答ack的時候,ack消息中,就會將這個rcv_nxt的值設置為ack中的una來告知發送方。
對於發送方,用戶調用send函數的時候,我們會先將數據分包按序放入snd_queue中,然后在flush函數中,在發送之前,我們會再從snd_queue中根據窗口大小,取出這次能發送的新數據包,重新打包,並按序存入到snd_buf中。其中打包的時候,sn的序號是依次通過分包的時候,snd_nxt++來進行編號的,所以可以認為snd_nxt為我們打包的最右側數據包序號。同時打包的時候,還會將這次需要發送消息的una設置為rcv_nxt。
其次我們來看一下發送方在收到對方網絡傳送過來的ack應答以后的處理,先調用input函數,然后根據ack消息中的una,我們先從snd_buf緩存數據中刪除sn為una之前的所有數據包,因為una在ack中的值為對端kcp->rcv_nxt的值,其次是更新發送端的kcp的kcp_snd_una的值,所以發送端的kcp->snd_una表示的是我們在snd_buf中緩存的最小或者說最左側的數據包序號。 然后接着根據發生方發送過來ack中的sn序號,我們從snd_buf中,再刪除掉這部分對端收到的數據包。 經過上述兩個步驟以后,於是snd_buf中緩存的數據就是我們沒有收到ack的數據包了,這部分數據包,我們在下次調用flush的時候會根據是否超時,是否被ack跳包等情況來進行重傳和發送。
看下kcp的擁塞控制機制: cwnd為發送端的擁塞控制窗口大小,ssthresh為擁塞窗口的閥值。
如果網絡情況比較好的話,我們就逐步加大擁塞窗口的大小,發送更多的數據。
//網絡比較好的時候,調整擁塞窗口大小的算法
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;
}
}
}
如果因為ack跳過出現數據可能的丟包,我們將通過算法進行調整,將擁塞窗口的大小減少為發送出去數據量的一半,
即下次發送的時候,我們發送上次一半的數據量,來避免網絡擁堵。
if (change) {//如果是因為ack被跳過一定次數認為的丟包的情況
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;
}
如果出現超時丟包重傳的情況,說明網絡情況已經很糟糕的,因為ack啥的都可能丟失了,
所以這個時候,我們直接將擁塞窗口大小設置為最小,大量減少下次的數據網絡傳輸。
//超時丟包的情況,說明網絡情況更加糟糕,需要進一步對擁塞窗口進行計算
if (lost) {//數據發送超時,認為丟包的情況
kcp->ssthresh = cwnd / 2;
if (kcp->ssthresh < IKCP_THRESH_MIN)
kcp->ssthresh = IKCP_THRESH_MIN;
kcp->cwnd = 1;
kcp->incr = kcp->mss;
}
再來看一下發送方在計算rto的時候的如何計算的,根據接收方發送過來的ts時間戳信息,和kcp->current時間戳,我們調用
ikcp_update_ack(kcp, _itimediff(kcp->current, ts));
函數,根據兩端的消息時間戳,我們來更新rrt,rto等信息。
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);
}
源碼閱讀的注釋可以在https://github.com/lihp1603/kcp/tree/note中看到。
