KCP 是一個(gè)快速可靠協(xié)議,能以比 TCP浪費(fèi)10%-20%的帶寬的代價(jià),換取平均延遲降低30%-40%,且最大延遲降低三倍的傳輸效果。純算法實(shí)現(xiàn),并不負(fù)責(zé)底層協(xié)議(如UDP)的收發(fā),需要使用者自己定義下層數(shù)據(jù)包的發(fā)送方式,以 callback的方式提供給 KCP。連時(shí)鐘都需要外部傳遞進(jìn)來,內(nèi)部不會(huì)有任何一次系統(tǒng)調(diào)用。
整個(gè)協(xié)議只有 ikcp.h, ikcp.c兩個(gè)源文件,可以方便的集成到用戶自己的協(xié)議棧中。也許你實(shí)現(xiàn)了一個(gè)P2P,或者某個(gè)基于 UDP的協(xié)議,而缺乏一套完善的ARQ可靠協(xié)議實(shí)現(xiàn),那么簡單的拷貝這兩個(gè)文件到現(xiàn)有項(xiàng)目中,稍微編寫兩行代碼,即可使用。HTTP://www.mobanhou.COM;;;;;;;;;;https://github.com/skywind3000/kcp
技術(shù)特性
TCP是為流量設(shè)計(jì)的(每秒內(nèi)可以傳輸多少KB的數(shù)據(jù)),講究的是充分利用帶寬。而KCP是為流速設(shè)計(jì)的(單個(gè)數(shù)據(jù)包從一端發(fā)送到一端需要多少時(shí)間),以10%-20%帶寬浪費(fèi)的代價(jià)換取了比 TCP快30%-40%的傳輸速度。TCP信道是一條流速很慢,但每秒流量很大的大運(yùn)河,而KCP是水流湍急的小激流。KCP有正常模式和快速模式兩種,通過以下策略達(dá)到提高流速的結(jié)果:
RTO翻倍vs不翻倍:
TCP超時(shí)計(jì)算是RTOx2,這樣連續(xù)丟三次包就變成RTOx8了,十分恐怖,而KCP啟動(dòng)快速 模式后不x2,只是x1.5(實(shí)驗(yàn)證明1.5這個(gè)值相對比較好),提高了傳輸速度。
選擇性重傳 vs 全部重傳:
TCP丟包時(shí)會(huì)全部重傳從丟的那個(gè)包開始以后的數(shù)據(jù),KCP是選擇性重傳,只重傳真正 丟失的數(shù)據(jù)包。
快速重傳:
發(fā)送端發(fā)送了1,2,3,4,5幾個(gè)包,然后收到遠(yuǎn)端的ACK: 1, 3, 4, 5,當(dāng)收到ACK3時(shí), KCP知道2被跳過1次,收到ACK4時(shí),知道2被跳過了2次,此時(shí)可以認(rèn)為2號丟失,不用 等超時(shí),直接重傳2號包,大大改善了丟包時(shí)的傳輸速度。
延遲ACK vs 非延遲ACK:
TCP為了充分利用帶寬,延遲發(fā)送ACK(NODELAY都沒用),這樣超時(shí)計(jì)算會(huì)算出較大 RTT時(shí)間,延長了丟包時(shí)的判斷過程。KCP的ACK是否延遲發(fā)送可以調(diào)節(jié)。
UNA vs ACK+UNA:
ARQ模型響應(yīng)有兩種,UNA(此編號前所有包已收到,如TCP)和ACK(該編號包已收到 ),光用UNA將導(dǎo)致全部重傳,光用ACK則丟失成本太高,以往協(xié)議都是二選其一,而 KCP協(xié)議中,除去單獨(dú)的 ACK包外,所有包都有UNA信息。
非退讓流控:
KCP正常模式同TCP一樣使用公平退讓法則,即發(fā)送窗口大小由:發(fā)送緩存大小、接收 端剩余接收緩存大小、丟包退讓及慢啟動(dòng)這四要素決定。但傳送及時(shí)性要求很高的小 數(shù)據(jù)時(shí),可選擇通過配置跳過后兩步,僅用前兩項(xiàng)來控制發(fā)送頻率。以犧牲部分公平 性及帶寬利用率之代價(jià),換取了開著BT都能流暢傳輸?shù)男Ч?/p>
基本使用
創(chuàng)建 KCP對象:
// 初始化 kcp對象,conv為一個(gè)表示會(huì)話編號的整數(shù),和tcp的 conv一樣,通信雙 // 方需保證 conv相同,相互的數(shù)據(jù)包才能夠被認(rèn)可,user是一個(gè)給回調(diào)函數(shù)的指針 ikcpcb *kcp = ikcp_create(conv, user);
設(shè)置回調(diào)函數(shù):
1 2 3 4 5 6 7 8 9 | // KCP的下層協(xié)議輸出函數(shù),KCP需要發(fā)送數(shù)據(jù)時(shí)會(huì)調(diào)用它
// buf/len 表示緩存和長度
// user指針為 kcp對象創(chuàng)建時(shí)傳入的值,用于區(qū)別多個(gè) KCP對象
int udp_output( const char *buf, int len, ikcpcb *kcp, void *user)
{
....
}
// 設(shè)置回調(diào)函數(shù)
kcp->output = udp_output;
|
循環(huán)調(diào)用 update:
1 2 3 | // 以一定頻率調(diào)用 ikcp_update來更新 kcp狀態(tài),并且傳入當(dāng)前時(shí)鐘(毫秒單位)
// 如 10ms調(diào)用一次,或用 ikcp_check確定下次調(diào)用 update的時(shí)間不必每次調(diào)用
ikcp_update(kcp, millisec);
|
輸入一個(gè)下層數(shù)據(jù)包:
1 2 | // 收到一個(gè)下層數(shù)據(jù)包(比如UDP包)時(shí)需要調(diào)用:
ikcp_input(kcp, received_udp_packet, received_udp_size);
|
處理了下層協(xié)議的輸出/輸入后 KCP協(xié)議就可以正常工作了,使用 ikcp_send 來向遠(yuǎn)端發(fā)送數(shù)據(jù)。而另一端使用 ikcp_recv(kcp, ptr, size)來接收數(shù)據(jù)。
協(xié)議配置
協(xié)議默認(rèn)模式是一個(gè)標(biāo)準(zhǔn)的 ARQ,需要通過配置打開各項(xiàng)加速開關(guān):
工作模式:
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
nodelay :是否啟用 nodelay模式,0不啟用;1啟用。interval :協(xié)議內(nèi)部工作的 interval,單位毫秒,比如 10ms或者 20msresend :快速重傳模式,默認(rèn)0關(guān)閉,可以設(shè)置2(2次ACK跨越將會(huì)直接重傳)nc :是否關(guān)閉流控,默認(rèn)是0代表不關(guān)閉,1代表關(guān)閉。普通模式:`ikcp_nodelay(kcp, 0, 40, 0, 0);極速模式: ikcp_nodelay(kcp, 1, 10, 2, 1);
最大窗口:
1 | int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
|
該調(diào)用將會(huì)設(shè)置協(xié)議的最大發(fā)送窗口和最大接收窗口大小,默認(rèn)為32.
最大傳輸單元:
純算法協(xié)議并不負(fù)責(zé)探測 MTU,默認(rèn) mtu是1400字節(jié),可以使用ikcp_setmtu來設(shè)置該值。該值將會(huì)影響數(shù)據(jù)包歸并及分片時(shí)候的最大傳輸單元。
最小RTO:
不管是 TCP還是 KCP計(jì)算 RTO時(shí)都有最小 RTO的限制,即便計(jì)算出來RTO為40ms,由于默認(rèn)的 RTO是100ms,協(xié)議只有在100ms后才能檢測到丟包,快速模式下為30ms,可以手動(dòng)更改該值:
最佳實(shí)踐
內(nèi)存分配器
默認(rèn)KCP協(xié)議使用 malloc/free進(jìn)行內(nèi)存分配釋放,如果應(yīng)用層接管了內(nèi)存分配,可以 用ikcp_allocator來設(shè)置新的內(nèi)存分配器,注意要在一開始設(shè)置:
ikcp_allocator(my_new_malloc, my_new_free);
前向糾錯(cuò)注意
為了進(jìn)一步提高傳輸速度,下層協(xié)議也許會(huì)使用前向糾錯(cuò)技術(shù)。需要注意,前向糾錯(cuò)會(huì)根據(jù)冗余信息解出原始數(shù)據(jù)包。相同的原始數(shù)據(jù)包不要兩次input到KCP,否則將會(huì)導(dǎo)致kcp以為對方重發(fā)了,這樣會(huì)產(chǎn)生更多的ack占用額外帶寬。
比如下層協(xié)議使用最簡單的冗余包:單個(gè)數(shù)據(jù)包除了自己外,還會(huì)重復(fù)存儲(chǔ)一次上一個(gè)數(shù)據(jù)包,以及上上一個(gè)數(shù)據(jù)包的內(nèi)容:
Fn = (Pn, Pn-1, Pn-2) P0 = (0, X, X) P1 = (1, 0, X) P2 = (2, 1, 0) P3 = (3, 2, 1)
這樣幾個(gè)包發(fā)送出去,接收方對于單個(gè)原始包都可能被解出3次來(后面兩個(gè)包任然會(huì)重復(fù)該包內(nèi)容),那么這里需要記錄一下,一個(gè)下層數(shù)據(jù)包只會(huì)input給kcp一次,避免過多重復(fù)ack帶來的浪費(fèi)。
管理大規(guī)模連接
如果需要同時(shí)管理大規(guī)模的 KCP連接(比如大于3000個(gè)),比如你正在實(shí)現(xiàn)一套類 epoll的機(jī)制,那么為了避免每秒鐘對每個(gè)連接調(diào)用大量的調(diào)用 ikcp_update,我們可以使用ikcp_check來大大減少 ikcp_update調(diào)用的次數(shù)。 ikcp_check返回值會(huì)告訴你需要在什么時(shí)間點(diǎn)再次調(diào)用 ikcp_update(如果中途沒有 ikcp_send, ikcp_input的話,否則中途調(diào)用了 ikcp_send, ikcp_input的話,需要在下一次interval時(shí)調(diào)用 update)
標(biāo)準(zhǔn)順序是每次調(diào)用了 ikcp_update后,使用 ikcp_check決定下次什么時(shí)間點(diǎn)再次調(diào)用ikcp_update,而如果中途發(fā)生了 ikcp_send, ikcp_input的話,在下一輪 interval 立馬調(diào)用 ikcp_update和 ikcp_check。 使用該方法,原來在處理2000個(gè) kcp連接且每個(gè)連接每10ms調(diào)用一次update,改為 check機(jī)制后,cpu從 60%降低到 15%。
相關(guān)應(yīng)用
- dog-tunnel: GO開發(fā)的網(wǎng)絡(luò)隧道,使用 KCP極大的改進(jìn)了傳輸速度,并移植了一份 GO版本 KCP
- lua-kcp:KCP的 Lua擴(kuò)展,用于 Lua服務(wù)器
- asio-kcp: 使用 KCP的完整 UDP網(wǎng)絡(luò)庫,完整實(shí)現(xiàn)了基于 UDP的鏈接狀態(tài)管理,會(huì)話控制,KCP協(xié)議調(diào)度等
- KCP協(xié)議開源地址:https://github.com/skywind3000/kcp
協(xié)議比較
如果永遠(yuǎn)不丟包那么 KCP和 TCP性能差不多,但網(wǎng)絡(luò)會(huì)卡,造成卡的原因就是丟包和抖動(dòng)。在內(nèi)網(wǎng)里直接比較,大家都差不多,但是放到公網(wǎng)上,放到3G/4G網(wǎng)絡(luò)情況下,或者使用內(nèi)網(wǎng)丟包模擬,差距就很明顯了。公網(wǎng)在高峰期有平均接近10%的丟包,wifi/3g/4g下更糟糕,這正是造成各種網(wǎng)絡(luò)卡頓的元兇。
感謝 asio-kcp 的作者 zhangyuan 對 KCP 與 enet, udt做過的一次橫向評測,結(jié)論如下:
- ASIO-KCP has good performace in wifi and phone network(3G, 4G).
- Extra using 20% ~ 50% network flow for speed improvement.
- The kcp is the first choice for realtime pvp game.
- The lag is less than 1 second when network lag happen. 3 times better than enet when lag happen.
- The enet is a good choice if your game allow 2 second lag.
- UDT is a bad idea. It always sink into badly situation of more than serval seconds lag. And the recovery is not expected.
- enet has the problem of lack of doc. And it has lots of functions that you may intrest.
- kcp's doc is chinese. Good thing is the function detail which is writen in code is english. And you can use asio_kcp which is a good wrap.
- The kcp is a simple thing. You will write more code if you want more feature.
- UDT has a perfect doc. UDT may has more bug than others as I feeling.