kcp-go源碼解析
對kcp-go的源碼解析,有錯誤之處,請一定告之。
sheepbao 2017.0612
概念
ARQ:自動重傳請求(Automatic Repeat-reQuest,ARQ)是OSI模型中數據鏈路層的錯誤糾正協議之一.
RTO:Retransmission TimeOut
FEC:Forward Error Correction
kcp簡介
kcp是一個基於udp實現快速、可靠、向前糾錯的的協議,能以比TCP浪費10%-20%的帶寬的代價,換取平均延遲降低30%-40%,且最大延遲降低三倍的傳輸效果。純算法實現,並不負責底層協議(如UDP)的收發。查看官方文檔kcp
kcp-go是用go實現了kcp協議的一個庫,其實kcp類似tcp,協議的實現也很多參考tcp協議的實現,滑動窗口,快速重傳,選擇性重傳,慢啟動等。
kcp和tcp一樣,也分客戶端和監聽端。
kcp協議
layer model
KCP header
KCP Header Format
代碼結構
着重研究兩個文件kcp.go
和sess.go
kcp淺析
kcp是基於udp實現的,所有udp的實現這里不做介紹,kcp做的事情就是怎么封裝udp的數據和怎么解析udp的數據,再加各種處理機制,為了重傳,擁塞控制,糾錯等。下面介紹kcp客戶端和服務端整體實現的流程,只是大概介紹一下函數流,不做詳細解析,詳細解析看后面數據流的解析。
kcp client整體函數流
和tcp一樣,kcp要連接服務端需要先撥號,但是和tcp有個很大的不同是,即使服務端沒有啟動,客戶端一樣可以撥號成功,因為實際上這里的撥號沒有發送任何信息,而tcp在這里需要三次握手。
客戶端大體的流程如上面所示,先Dial
,建立udp連接,將這個連接封裝成一個會話,然后啟動一個go程,接收udp的消息。
kcp server整體函數流
服務端的大體流程如上圖所示,先Listen
,啟動udp監聽,接着用一個go程監控udp的數據包,負責將不同session的數據寫入不同的udp連接,然后解析封裝將數據交給上層。
kcp 數據流詳細解析
不管是kcp的客戶端還是服務端,他們都有io行為,就是讀與寫,我們只分析一個就好了,因為它們讀寫的實現是一樣的,這里分析客戶端的讀與寫。
kcp client 發送消息
讀寫都是在sess.go
文件中實現的,Write方法:
假設發送一個hello消息,Write方法會先判斷發送窗口是否已滿,滿的話該函數阻塞,不滿則kcp.Send("hello"),而Send函數實現根據mss的值對數據分段,當然這里的發送的hello,長度太短,只分了一個段,並把它們插入發送的隊列里。
接着判斷參數writeDelay
,如果參數設置為false,則立馬發送消息,否則需要任務調度后才會觸發發送,發送消息是由flush函數實現的。
flush函數非常的重要,kcp的重要參數都是在調節這個函數的行為,這個函數只有一個參數ackOnly
,意思就是只發送ack,如果ackOnly
為true的話,該函數只遍歷ack列表,然后發送,就完事了。 如果不是,也會發送真實數據。 在發送數據前先進行windSize探測,如果開啟了擁塞控制nc=0
,則每次發送前檢測服務端的winsize,如果服務端的winsize變小了,自身的winsize也要更着變小,來避免擁塞。如果沒有開啟擁塞控制,就按設置的winsize進行數據發送。
接着循環每個段數據,並判斷每個段數據的是否該重發,還有什么時候重發:
- 如果這個段數據首次發送,則直接發送數據。
- 如果這個段數據的當前時間大於它自身重發的時間,也就是RTO,則重傳消息。
- 如果這個段數據的ack丟失累計超過resent次數,則重傳,也就是快速重傳機制。這個resent參數由
resend
參數決定。 - 如果這個段數據的ack有丟失且沒有新的數據段,則觸發ER,ER相關信息ER
最后通過kcp.output發送消息hello,output是個回調函數,函數的實體是sess.go
的:
output函數才是真正的將數據寫入內核中,在寫入之前先進行了fec編碼,fec編碼器的實現是用了一個開源庫github.com/klauspost/reedsolomon,編碼以后的hello就不是和原來的hello一樣了,至少多了幾個字節。 fec編碼器有兩個重要的參數reedsolomon.New(dataShards, parityShards, reedsolomon.WithMaxGoroutines(1)),dataShards
和parityShards
,這兩個參數決定了fec的冗余度,冗余度越大抗丟包性就越強。
kcp的任務調度器
其實這里任務調度器是一個很簡單的實現,用一個全局變量updater
來管理session,代碼文件為updater.go
。其中最主要的函數
任務調度器實現了一個堆結構,每當有新的連接,session都會插入到這個堆里,接着for循環每隔interval時間,遍歷這個堆,得到entry
然后執行entry.s.update()
。而entry.s.update()
會執行s.kcp.flush(false)
來發送數據。
總結
這里簡單介紹了kcp的整體流程,詳細介紹了發送數據的流程,但未介紹kcp接收數據的流程,其實在客戶端發送數據后,服務端是需要返回ack的,而客戶端也需要根據返回的ack來判斷數據段是否需要重傳還是在隊列里清除該數據段。處理返回來的ack是在函數kcp.Input()函數實現的。具體詳細流程下次再介紹。