kcp-go原始碼解析
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一樣,也分客戶端和監聽端。
+-+-+-+-+-+ +-+-+-+-+-+
| Client | | Server |
+-+-+-+-+-+ +-+-+-+-+-+
|------ kcp data ------>|
|<----- kcp data -------|
kcp協議
layer model
+----------------------+
| Session |
+----------------------+
| KCP(ARQ) |
+----------------------+
| FEC(OPTIONAL) |
+----------------------+
| CRYPTO(OPTIONAL)|
+----------------------+
| UDP(Packet) |
+----------------------+
KCP header
KCP Header Format
4 1 1 2 (Byte)
+---+---+---+---+---+---+---+---+
| conv |cmd|frg| wnd |
+---+---+---+---+---+---+---+---+
| ts | sn |
+---+---+---+---+---+---+---+---+
| una | len |
+---+---+---+---+---+---+---+---+
| |
+ DATA +
| |
+---+---+---+---+---+---+---+---+
程式碼結構
src/vendor/github.com/xtaci/kcp-go/
├── LICENSE
├── README.md
├── crypt.go 加解密實現
├── crypt_test.go
├── donate.png
├── fec.go 向前糾錯實現
├── frame.png
├── kcp-go.png
├── kcp.go kcp協議實現
├── kcp_test.go
├── sess.go 會話管理實現
├── sess_test.go
├── snmp.go 資料統計實現
├── updater.go 任務排程實現
├── xor.go xor封裝
└── xor_test.go
著重研究兩個檔案kcp.go
和sess.go
kcp淺析
kcp是基於udp實現的,所有udp的實現這裡不做介紹,kcp做的事情就是怎麼封裝udp的資料和怎麼解析udp的資料,再加各種處理機制,為了重傳,擁塞控制,糾錯等。下面介紹kcp客戶端和服務端整體實現的流程,只是大概介紹一下函式流,不做詳細解析,詳細解析看後面資料流的解析。
kcp client整體函式流
和tcp一樣,kcp要連線服務端需要先撥號,但是和tcp有個很大的不同是,即使服務端沒有啟動,客戶端一樣可以撥號成功,因為實際上這裡的撥號沒有傳送任何資訊,而tcp在這裡需要三次握手。
DialWithOptions(raddr string, block BlockCrypt, dataShards, parityShards int)
V
net.DialUDP("udp", nil, udpaddr)
V
NewConn()
V
newUDPSession() {初始化UDPSession}
V
NewKCP() {初始化kcp}
V
updater.addSession(sess) {管理session會話,任務管理,根據使用者設定的internal引數間隔來輪流喚醒任務}
V
go sess.readLoop()
V
go s.receiver(chPacket)
V
s.kcpInput(data)
V
s.fecDecoder.decodeBytes(data)
V
s.kcp.Input(data, true, s.ackNoDelay)
V
kcp.parse_data(seg) {將分段好的資料插入kcp.rcv_buf緩衝}
V
notifyReadEvent()
客戶端大體的流程如上面所示,先Dial
,建立udp連線,將這個連線封裝成一個會話,然後啟動一個go程,接收udp的訊息。
kcp server整體函式流
ListenWithOptions()
V
net.ListenUDP()
V
ServerConn()
V
newFECDecoder()
V
go l.monitor() {從chPacket接收udp資料,寫入kcp}
V
go l.receiver(chPacket) {從upd接收資料,併入佇列}
V
newUDPSession()
V
updater.addSession(sess) {管理session會話,任務管理,根據使用者設定的internal引數間隔來輪流喚醒任務}
V
s.kcpInput(data)`
V
s.fecDecoder.decodeBytes(data)
V
s.kcp.Input(data, true, s.ackNoDelay)
V
kcp.parse_data(seg) {將分段好的資料插入kcp.rcv_buf緩衝}
V
notifyReadEvent()
服務端的大體流程如上圖所示,先Listen
,啟動udp監聽,接著用一個go程監控udp的資料包,負責將不同session的資料寫入不同的udp連線,然後解析封裝將資料交給上層。
kcp 資料流詳細解析
不管是kcp的客戶端還是服務端,他們都有io行為,就是讀與寫,我們只分析一個就好了,因為它們讀寫的實現是一樣的,這裡分析客戶端的讀與寫。
kcp client 傳送訊息
s.Write(b []byte)
V
s.kcp.WaitSnd() {}
V
s.kcp.Send(b) {將資料根據mss分段,並存在kcp.snd_queue}
V
s.kcp.flush(false) [flush data to output] {
if writeDelay==true {
flush
}else{
每隔`interval`時間flush一次
}
}
V
kcp.output(buffer, size)
V
s.output(buf)
V
s.conn.WriteTo(ext, s.remote)
V
s.conn..Conn.WriteTo(buf)
讀寫都是在sess.go
檔案中實現的,Write方法:
// Write implements net.Conn
func (s *UDPSession) Write(b []byte) (n int, err error) {
for {
...
// api flow control
if s.kcp.WaitSnd() < int(s.kcp.snd_wnd) {
n = len(b)
for {
if len(b) <= int(s.kcp.mss) {
s.kcp.Send(b)
break
} else {
s.kcp.Send(b[:s.kcp.mss])
b = b[s.kcp.mss:]
}
}
if !s.writeDelay {
s.kcp.flush(false)
}
s.mu.Unlock()
atomic.AddUint64(&DefaultSnmp.BytesSent, uint64(n))
return n, nil
}
...
// wait for write event or timeout
select {
case <-s.chWriteEvent:
case <-c:
case <-s.die:
}
if timeout != nil {
timeout.Stop()
}
}
}
假設傳送一個hello訊息,Write方法會先判斷髮送視窗是否已滿,滿的話該函式阻塞,不滿則kcp.Send("hello"),而Send函式實現根據mss的值對資料分段,當然這裡的傳送的hello,長度太短,只分了一個段,並把它們插入傳送的佇列裡。
func (kcp *KCP) Send(buffer []byte) int {
...
for i := 0; i < count; i++ {
var size int
if len(buffer) > int(kcp.mss) {
size = int(kcp.mss)
} else {
size = len(buffer)
}
seg := kcp.newSegment(size)
copy(seg.data, buffer[:size])
if kcp.stream == 0 { // message mode
seg.frg = uint8(count - i - 1)
} else { // stream mode
seg.frg = 0
}
kcp.snd_queue = append(kcp.snd_queue, seg)
buffer = buffer[size:]
}
return 0
}
接著判斷引數writeDelay
,如果引數設定為false,則立馬傳送訊息,否則需要任務排程後才會觸發傳送,傳送訊息是由flush函式實現的。
// flush pending data
func (kcp *KCP) flush(ackOnly bool) {
var seg Segment
seg.conv = kcp.conv
seg.cmd = IKCP_CMD_ACK
seg.wnd = kcp.wnd_unused()
seg.una = kcp.rcv_nxt
buffer := kcp.buffer
// flush acknowledges
ptr := buffer
for i, ack := range kcp.acklist {
size := len(buffer) - len(ptr)
if size+IKCP_OVERHEAD > int(kcp.mtu) {
kcp.output(buffer, size)
ptr = buffer
}
// filter jitters caused by bufferbloat
if ack.sn >= kcp.rcv_nxt || len(kcp.acklist)-1 == i {
seg.sn, seg.ts = ack.sn, ack.ts
ptr = seg.encode(ptr)
}
}
kcp.acklist = kcp.acklist[0:0]
if ackOnly { // flash remain ack segments
size := len(buffer) - len(ptr)
if size > 0 {
kcp.output(buffer, size)
}
return
}
// probe window size (if remote window size equals zero)
if kcp.rmt_wnd == 0 {
current := currentMs()
if kcp.probe_wait == 0 {
kcp.probe_wait = IKCP_PROBE_INIT
kcp.ts_probe = current + kcp.probe_wait
} else {
if _itimediff(current, kcp.ts_probe) >= 0 {
if kcp.probe_wait < IKCP_PROBE_INIT {
kcp.probe_wait = IKCP_PROBE_INIT
}
kcp.probe_wait += kcp.probe_wait / 2
if kcp.probe_wait > IKCP_PROBE_LIMIT {
kcp.probe_wait = IKCP_PROBE_LIMIT
}
kcp.ts_probe = current + kcp.probe_wait
kcp.probe |= IKCP_ASK_SEND
}
}
} else {
kcp.ts_probe = 0
kcp.probe_wait = 0
}
// flush window probing commands
if (kcp.probe & IKCP_ASK_SEND) != 0 {
seg.cmd = IKCP_CMD_WASK
size := len(buffer) - len(ptr)
if size+IKCP_OVERHEAD > int(kcp.mtu) {
kcp.output(buffer, size)
ptr = buffer
}
ptr = seg.encode(ptr)
}
// flush window probing commands
if (kcp.probe & IKCP_ASK_TELL) != 0 {
seg.cmd = IKCP_CMD_WINS
size := len(buffer) - len(ptr)
if size+IKCP_OVERHEAD > int(kcp.mtu) {
kcp.output(buffer, size)
ptr = buffer
}
ptr = seg.encode(ptr)
}
kcp.probe = 0
// calculate window size
cwnd := _imin_(kcp.snd_wnd, kcp.rmt_wnd)
if kcp.nocwnd == 0 {
cwnd = _imin_(kcp.cwnd, cwnd)
}
// sliding window, controlled by snd_nxt && sna_una+cwnd
newSegsCount := 0
for k := range kcp.snd_queue {
if _itimediff(kcp.snd_nxt, kcp.snd_una+cwnd) >= 0 {
break
}
newseg := kcp.snd_queue[k]
newseg.conv = kcp.conv
newseg.cmd = IKCP_CMD_PUSH
newseg.sn = kcp.snd_nxt
kcp.snd_buf = append(kcp.snd_buf, newseg)
kcp.snd_nxt++
newSegsCount++
kcp.snd_queue[k].data = nil
}
if newSegsCount > 0 {
kcp.snd_queue = kcp.remove_front(kcp.snd_queue, newSegsCount)
}
// calculate resent
resent := uint32(kcp.fastresend)
if kcp.fastresend <= 0 {
resent = 0xffffffff
}
// check for retransmissions
current := currentMs()
var change, lost, lostSegs, fastRetransSegs, earlyRetransSegs uint64
for k := range kcp.snd_buf {
segment := &kcp.snd_buf[k]
needsend := false
if segment.xmit == 0 { // initial transmit
needsend = true
segment.rto = kcp.rx_rto
segment.resendts = current + segment.rto
} else if _itimediff(current, segment.resendts) >= 0 { // RTO
needsend = true
if kcp.nodelay == 0 {
segment.rto += kcp.rx_rto
} else {
segment.rto += kcp.rx_rto / 2
}
segment.resendts = current + segment.rto
lost++
lostSegs++
} else if segment.fastack >= resent { // fast retransmit
needsend = true
segment.fastack = 0
segment.rto = kcp.rx_rto
segment.resendts = current + segment.rto
change++
fastRetransSegs++
} else if segment.fastack > 0 && newSegsCount == 0 { // early retransmit
needsend = true
segment.fastack = 0
segment.rto = kcp.rx_rto
segment.resendts = current + segment.rto
change++
earlyRetransSegs++
}
if needsend {
segment.xmit++
segment.ts = current
segment.wnd = seg.wnd
segment.una = seg.una
size := len(buffer) - len(ptr)
need := IKCP_OVERHEAD + len(segment.data)
if size+need > int(kcp.mtu) {
kcp.output(buffer, size)
current = currentMs() // time update for a blocking call
ptr = buffer
}
ptr = segment.encode(ptr)
copy(ptr, segment.data)
ptr = ptr[len(segment.data):]
if segment.xmit >= kcp.dead_link {
kcp.state = 0xFFFFFFFF
}
}
}
// flash remain segments
size := len(buffer) - len(ptr)
if size > 0 {
kcp.output(buffer, size)
}
// counter updates
sum := lostSegs
if lostSegs > 0 {
atomic.AddUint64(&DefaultSnmp.LostSegs, lostSegs)
}
if fastRetransSegs > 0 {
atomic.AddUint64(&DefaultSnmp.FastRetransSegs, fastRetransSegs)
sum += fastRetransSegs
}
if earlyRetransSegs > 0 {
atomic.AddUint64(&DefaultSnmp.EarlyRetransSegs, earlyRetransSegs)
sum += earlyRetransSegs
}
if sum > 0 {
atomic.AddUint64(&DefaultSnmp.RetransSegs, sum)
}
// update ssthresh
// rate halving, https://tools.ietf.org/html/rfc6937
if change > 0 {
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
}
// congestion control, https://tools.ietf.org/html/rfc5681
if lost > 0 {
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
}
}
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
的:
func (s *UDPSession) output(buf []byte) {
var ecc [][]byte
// extend buf's header space
ext := buf
if s.headerSize > 0 {
ext = s.ext[:s.headerSize+len(buf)]
copy(ext[s.headerSize:], buf)
}
// FEC stage
if s.fecEncoder != nil {
ecc = s.fecEncoder.Encode(ext)
}
// encryption stage
if s.block != nil {
io.ReadFull(rand.Reader, ext[:nonceSize])
checksum := crc32.ChecksumIEEE(ext[cryptHeaderSize:])
binary.LittleEndian.PutUint32(ext[nonceSize:], checksum)
s.block.Encrypt(ext, ext)
if ecc != nil {
for k := range ecc {
io.ReadFull(rand.Reader, ecc[k][:nonceSize])
checksum := crc32.ChecksumIEEE(ecc[k][cryptHeaderSize:])
binary.LittleEndian.PutUint32(ecc[k][nonceSize:], checksum)
s.block.Encrypt(ecc[k], ecc[k])
}
}
}
// WriteTo kernel
nbytes := 0
npkts := 0
// if mrand.Intn(100) < 50 {
for i := 0; i < s.dup+1; i++ {
if n, err := s.conn.WriteTo(ext, s.remote); err == nil {
nbytes += n
npkts++
}
}
// }
if ecc != nil {
for k := range ecc {
if n, err := s.conn.WriteTo(ecc[k], s.remote); err == nil {
nbytes += n
npkts++
}
}
}
atomic.AddUint64(&DefaultSnmp.OutPkts, uint64(npkts))
atomic.AddUint64(&DefaultSnmp.OutBytes, uint64(nbytes))
}
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
。其中最主要的函式
func (h *updateHeap) updateTask() {
var timer <-chan time.Time
for {
select {
case <-timer:
case <-h.chWakeUp:
}
h.mu.Lock()
hlen := h.Len()
now := time.Now()
if hlen > 0 && now.After(h.entries[0].ts) {
for i := 0; i < hlen; i++ {
entry := heap.Pop(h).(entry)
if now.After(entry.ts) {
entry.ts = now.Add(entry.s.update())
heap.Push(h, entry)
} else {
heap.Push(h, entry)
break
}
}
}
if hlen > 0 {
timer = time.After(h.entries[0].ts.Sub(now))
}
h.mu.Unlock()
}
}
任務排程器實現了一個堆結構,每當有新的連線,session都會插入到這個堆裡,接著for迴圈每隔interval時間,遍歷這個堆,得到entry
然後執行entry.s.update()
。而entry.s.update()
會執行s.kcp.flush(false)
來傳送資料。
總結
這裡簡單介紹了kcp的整體流程,詳細介紹了傳送資料的流程,但未介紹kcp接收資料的流程,其實在客戶端傳送資料後,服務端是需要返回ack的,而客戶端也需要根據返回的ack來判斷資料段是否需要重傳還是在佇列裡清除該資料段。處理返回來的ack是在函式kcp.Input()函式實現的。具體詳細流程下次再介紹。
相關文章
- Java Timer原始碼解析(定時器原始碼解析)Java原始碼定時器
- 【原始碼解析】- ArrayList原始碼解析,絕對詳細原始碼
- ReactNative原始碼解析-初識原始碼React原始碼
- Koa 原始碼解析原始碼
- Koa原始碼解析原始碼
- RxPermission原始碼解析原始碼
- Express原始碼解析Express原始碼
- redux原始碼解析Redux原始碼
- CopyOnWriteArrayList原始碼解析原始碼
- LeakCanary原始碼解析原始碼
- ArrayBlockQueue原始碼解析BloC原始碼
- ReentrantLock原始碼解析ReentrantLock原始碼
- OKio原始碼解析原始碼
- ReentrantReadWriteLock原始碼解析原始碼
- CyclicBarrier原始碼解析原始碼
- Semaphore原始碼解析原始碼
- Exchanger原始碼解析原始碼
- SDWebImage原始碼解析Web原始碼
- AbstractQueuedSynchronizer原始碼解析原始碼
- LinkedList原始碼解析原始碼
- HandlerThread原始碼解析thread原始碼
- ButterKnife原始碼解析原始碼
- SpringMVC原始碼解析SpringMVC原始碼
- RecyclerView原始碼解析View原始碼
- MyBatis原始碼解析MyBatis原始碼
- CountDownLatch原始碼解析CountDownLatch原始碼
- Promise 原始碼解析Promise原始碼
- Mansonry原始碼解析原始碼
- Observer原始碼解析Server原始碼
- SparseArray 原始碼解析原始碼
- Ribbon原始碼解析原始碼
- AsyncTask原始碼解析原始碼
- linker原始碼解析原始碼
- vuex原始碼解析Vue原始碼
- LeakCanary 原始碼解析原始碼
- Vue原始碼解析Vue原始碼
- Hive原始碼解析Hive原始碼
- Javapoet原始碼解析Java原始碼
- React原始碼解析React原始碼