原标题:KCP-GO源码解析
原文作者:张伯雨 golang技术社区
概念
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一样,也分客户端和监听端。
1+-+-+-+-+-+ +-+-+-+-+-+
2| Client || Server |
3+-+-+-+-+-+ +-+-+-+-+-+
4|------ kcp data ------>|
5|
kcp协议layer model
1+----------------------+
2| Session |
3+----------------------+
4| KCP(ARQ) |
5+----------------------+
6| FEC(OPTIONAL) |
7+----------------------+
8| CRYPTO(OPTIONAL)|
9+----------------------+
10| UDP(Packet) |
11+----------------------+
KCP header
KCP Header Format
14112(Byte)
2+---+---+---+---+---+---+---+---+
3| conv |cmd |frg|wnd |
4+---+---+---+---+---+---+---+---+
5| ts | sn |
6+---+---+---+---+---+---+---+---+
7| una |len |
8+---+---+---+---+---+---+---+---+
9| |
10+ DATA +
11| |
12+---+---+---+---+---+---+---+---+
13
代码结构
1src/vendor/github.com/xtaci/kcp- go/
2├── LICENSE
3├── README.md
4├── crypt. go加解密实现
5├── crypt_test. go
6├── donate.png
7├── fec. go向前纠错实现
8├── frame.png
9├── kcp- go.png
10├── kcp. gokcp协议实现
11├── kcp_test. go
12├── sess. go会话管理实现
13├── sess_test. go
14├── snmp. go数据统计实现
15├── updater. go任务调度实现
16├── xor. goxor封装
17└── xor_test. go
着重研究两个文件 kcp.go 和 sess.go
kcp浅析
kcp是基于udp实现的,所有udp的实现这里不做介绍,kcp做的事情就是怎么封装udp的数据和怎么解析udp的数据,再加各种处理机制,为了重传,拥塞控制,纠错等。下面介绍kcp客户端和服务端整体实现的流程,只是大概介绍一下函数流,不做详细解析,详细解析看后面数据流的解析。
kcp client整体函数流
和tcp一样,kcp要连接服务端需要先拨号,但是和tcp有个很大的不同是,即使服务端没有启动,客户端一样可以拨号成功,因为实际上这里的拨号没有发送任何信息,而tcp在这里需要三次握手。
1DialWithOptions(raddr string, block BlockCrypt, dataShards, parityShards int)
2V
3net.DialUDP( "udp", nil, udpaddr)
4V
5NewConn( )
6V
7newUDPSession( ) {初始化UDPSession}
8V
9NewKCP( ) {初始化kcp}
10V
11updater.addSession(sess) {管理session会话,任务管理,根据用户设置的 internal参数间隔来轮流唤醒任务}
12V
13go sess.readLoop
14V
15go s.receiver(chPacket)
16V
17s.kcpInput(data)
18V
19s.fecDecoder.decodeBytes(data)
20V
21s.kcp.Input(data, true, s.ackNoDelay)
22V
23kcp.parse_data(seg) {将分段好的数据插入kcp.rcv_buf缓冲}
24V
25notifyReadEvent( )
26
客户端大体的流程如上面所示,先Dial,建立udp连接,将这个连接封装成一个会话,然后启动一个go程,接收udp的消息。
kcp server整体函数流
1ListenWithOptions
2V
3net.ListenUDP
4V
5ServerConn
6V
7newFECDecoder
8V
9go l.monitor {从chPacket接收udp数据,写入kcp}
10V
11go l.receiver(chPacket) {从upd接收数据,并入队列}
12V
13newUDPSession
14V
15updater.addSession(sess) {管理session会话,任务管理,根据用户设置的 internal参数间隔来轮流唤醒任务}
16V
17s.kcpInput( data)`
18V
19s.fecDecoder.decodeBytes( data)
20V
21s.kcp.Input( data, true, s.ackNoDelay)
22V
23kcp.parse_data(seg) {将分段好的数据插入kcp.rcv_buf缓冲}
24V
25notifyReadEvent
服务端的大体流程如上图所示,先Listen,启动udp监听,接着用一个go程监控udp的数据包,负责将不同session的数据写入不同的udp连接,然后解析封装将数据交给上层。
kcp 数据流详细解析
不管是kcp的客户端还是服务端,他们都有io行为,就是读与写,我们只分析一个就好了,因为它们读写的实现是一样的,这里分析客户端的读与写。
kcp client 发送消息
1s.Write(b [] byte)
2V
3s.kcp.WaitSnd {}
4V
5s.kcp.Send(b) {将数据根据mss分段,并存在kcp.snd_queue}
6V
7s.kcp. flush( false) [ flushdata to output] {
8ifwriteDelay== true{
9flush
10} else{
11每隔`interval`时间 flush一次
12}
13}
14V
15kcp. output(buffer, size)
16V
17s. output(buf)
18V
19s.conn.WriteTo(ext, s.remote)
20V
21s.conn..Conn.WriteTo(buf)
读写都是在sess.go文件中实现的,Write方法:
1// Write implements net.Conn
2func(s *UDPSession)Write(b [] byte)(n int, err error){
3for{
4...
5// api flow control
6ifs.kcp.WaitSnd < int(s.kcp.snd_wnd) {
7n = len(b)
8for{
9iflen(b) <= int(s.kcp.mss) {
10s.kcp.Send(b)
11break
12} else{
13s.kcp.Send(b[:s.kcp.mss])
14b = b[s.kcp.mss:]
15}
16}
17if!s.writeDelay {
18s.kcp.flush( false)
19}
20s.mu.Unlock
21atomic.AddUint64(&DefaultSnmp.BytesSent, uint64(n))
22returnn, nil
23}
24...
25// wait for write event or timeout
26select{
27case
28case
29case
30}
31iftimeout != nil{
32timeout.Stop
33}
34}
35}
假设发送一个hello消息,Write方法会先判断发送窗口是否已满,满的话该函数阻塞,不满则kcp.Send(“hello”),而Send函数实现根据mss的值对数据分段,当然这里的发送的hello,长度太短,只分了一个段,并把它们插入发送的队列里。
1func(kcp *KCP)Send(buffer [] byte)int{
2...
3fori := 0; i < count; i++ {
4varsize int
5iflen(buffer) > int(kcp.mss) {
6size = int(kcp.mss)
7} else{
8size = len(buffer)
9}
10seg := kcp.newSegment(size)
11copy(seg.data, buffer[:size])
12ifkcp.stream == 0{ // message mode
13seg.frg = uint8(count - i - 1)
14} else{ // stream mode
15seg.frg = 0
16}
17kcp.snd_queue = append(kcp.snd_queue, seg)
18buffer = buffer[size:]
19}
20return0
21}
接着判断参数writeDelay,如果参数设置为false,则立马发送消息,否则需要任务调度后才会触发发送,发送消息是由flush函数实现的。
1// flushpending data
2func (kcp *KCP) flush(ackOnly bool) {
3varseg Segment
4seg.conv = kcp.conv
5seg.cmd = IKCP_CMD_ACK
6seg.wnd = kcp.wnd_unused
7seg.una = kcp.rcv_nxt
8buffer := kcp.buffer
9// flushacknowledges
10ptr := buffer
11fori, ack := rangekcp.acklist {
12size:= len(buffer) - len(ptr)
13ifsize+IKCP_OVERHEAD > int(kcp.mtu) {
14kcp.output(buffer, size)
15ptr = buffer
16}
17// filter jitters caused bybufferbloat
18ifack.sn >= kcp.rcv_nxt || len(kcp.acklist) -1== i {
19seg.sn, seg.ts = ack.sn, ack.ts
20ptr = seg.encode(ptr)
21}
22}
23kcp.acklist = kcp.acklist[ 0: 0]
24ifackOnly { // flash remain ack segments
25size:= len(buffer) - len(ptr)
26ifsize> 0{
27kcp.output(buffer, size)
28}
29return
30}
31// probe window size( ifremote window sizeequals zero)
32ifkcp.rmt_wnd == 0{
33current:= currentMs
34ifkcp.probe_wait == 0{
35kcp.probe_wait = IKCP_PROBE_INIT
36kcp.ts_probe = current+ kcp.probe_wait
37} else{
38if_itimediff( current, kcp.ts_probe) >= 0{
39ifkcp.probe_wait < IKCP_PROBE_INIT {
40kcp.probe_wait = IKCP_PROBE_INIT
41}
42kcp.probe_wait += kcp.probe_wait / 2
43ifkcp.probe_wait > IKCP_PROBE_LIMIT {
44kcp.probe_wait = IKCP_PROBE_LIMIT
45}
46kcp.ts_probe = current+ kcp.probe_wait
47kcp.probe |= IKCP_ASK_SEND
48}
49}
50} else{
51kcp.ts_probe = 0
52kcp.probe_wait = 0
53}
54// flushwindow probing commands
55if(kcp.probe & IKCP_ASK_SEND) != 0{
56seg.cmd = IKCP_CMD_WASK
57size:= len(buffer) - len(ptr)
58ifsize+IKCP_OVERHEAD > int(kcp.mtu) {
59kcp.output(buffer, size)
60ptr = buffer
61}
62ptr = seg.encode(ptr)
63}
64// flushwindow probing commands
65if(kcp.probe & IKCP_ASK_TELL) != 0{
66seg.cmd = IKCP_CMD_WINS
67size:= len(buffer) - len(ptr)
68ifsize+IKCP_OVERHEAD > int(kcp.mtu) {
69kcp.output(buffer, size)
70ptr = buffer
71}
72ptr = seg.encode(ptr)
73}
74kcp.probe = 0
75// calculate window size
76cwnd := _imin_(kcp.snd_wnd, kcp.rmt_wnd)
77ifkcp.nocwnd == 0{
78cwnd = _imin_(kcp.cwnd, cwnd)
79}
80// sliding window, controlled bysnd_nxt && sna_una+cwnd
81newSegsCount := 0
82fork := rangekcp.snd_queue {
83if_itimediff(kcp.snd_nxt, kcp.snd_una+cwnd) >= 0{
84break
85}
86newseg := kcp.snd_queue[k]
87newseg.conv = kcp.conv
88newseg.cmd = IKCP_CMD_PUSH
89newseg.sn = kcp.snd_nxt
90kcp.snd_buf = append(kcp.snd_buf, newseg)
91kcp.snd_nxt++
92newSegsCount++
93kcp.snd_queue[k].data = nil
94}
95ifnewSegsCount > 0{
96kcp.snd_queue = kcp.remove_front(kcp.snd_queue, newSegsCount)
97}
98// calculate resent
99resent := uint32(kcp.fastresend)
100ifkcp.fastresend <= 0{
101resent = 0xffffffff
102}
103// checkforretransmissions
104current:= currentMs
105varchange, lost, lostSegs, fastRetransSegs, earlyRetransSegs uint64
106fork := rangekcp.snd_buf {
107segment:= &kcp.snd_buf[k]
108needsend := false
109ifsegment.xmit == 0{ // initialtransmit
110needsend = true
111segment.rto = kcp.rx_rto
112segment.resendts = current+ segment.rto
113} elseif_itimediff( current, segment.resendts) >= 0{ // RTO
114needsend = true
115ifkcp.nodelay == 0{
116segment.rto += kcp.rx_rto
117} else{
118segment.rto += kcp.rx_rto / 2
119}
120segment.resendts = current+ segment.rto
121lost++
122lostSegs++
123} elseifsegment.fastack >= resent { // fastretransmit
124needsend = true
125segment.fastack = 0
126segment.rto = kcp.rx_rto
127segment.resendts = current+ segment.rto
128change++
129fastRetransSegs++
130} elseifsegment.fastack > 0&& newSegsCount == 0{ // early retransmit
131needsend = true
132segment.fastack = 0
133segment.rto = kcp.rx_rto
134segment.resendts = current+ segment.rto
135change++
136earlyRetransSegs++
137}
138ifneedsend {
139segment.xmit++
140segment.ts = current
141segment.wnd = seg.wnd
142segment.una = seg.una
143size:= len(buffer) - len(ptr)
144need := IKCP_OVERHEAD + len(segment.data)
145ifsize+need > int(kcp.mtu) {
146kcp.output(buffer, size)
147current= currentMs // timeupdatefora blocking call
148ptr = buffer
149}
150ptr = segment.encode(ptr)
151copy(ptr, segment.data)
152ptr = ptr[ len(segment.data):]
153ifsegment.xmit >= kcp.dead_link {
154kcp.state = 0xFFFFFFFF
155}
156}
157}
158// flash remain segments
159size:= len(buffer) - len(ptr)
160ifsize> 0{
161kcp.output(buffer, size)
162}
163// counter updates
164sum:= lostSegs
165iflostSegs > 0{
166atomic.AddUint64(&DefaultSnmp.LostSegs, lostSegs)
167}
168iffastRetransSegs > 0{
169atomic.AddUint64(&DefaultSnmp.FastRetransSegs, fastRetransSegs)
170sum+= fastRetransSegs
171}
172ifearlyRetransSegs > 0{
173atomic.AddUint64(&DefaultSnmp.EarlyRetransSegs, earlyRetransSegs)
174sum+= earlyRetransSegs
175}
176ifsum> 0{
177atomic.AddUint64(&DefaultSnmp.RetransSegs, sum)
178}
179// updatessthresh
180// rate halving, https://tools.ietf.org/html/rfc6937
181ifchange> 0{
182inflight := kcp.snd_nxt - kcp.snd_una
183kcp.ssthresh = inflight / 2
184ifkcp.ssthresh < IKCP_THRESH_MIN {
185kcp.ssthresh = IKCP_THRESH_MIN
186}
187kcp.cwnd = kcp.ssthresh + resent
188kcp.incr = kcp.cwnd * kcp.mss
189}
190// congestion control, https://tools.ietf.org/html/rfc5681
191iflost > 0{
192kcp.ssthresh = cwnd / 2
193ifkcp.ssthresh < IKCP_THRESH_MIN {
194kcp.ssthresh = IKCP_THRESH_MIN
195}
196kcp.cwnd = 1
197kcp.incr = kcp.mss
198}
199ifkcp.cwnd < 1{
200kcp.cwnd = 1
201kcp.incr = kcp.mss
202}
203}
flush函数非常的重要,kcp的重要参数都是在调节这个函数的行为,这个函数只有一个参数ackOnly,意思就是只发送ack,如果ackOnly为true的话,该函数只遍历ack列表,然后发送,就完事了。 如果不是,也会发送真实数据。 在发送数据前先进行windSize探测,如果开启了拥塞控制nc=0,则每次发送前检测服务端的winsize,如果服务端的winsize变小了,自身的winsize也要更着变小,来避免拥塞。如果没有开启拥塞控制,就按设置的winsize进行数据发送。
接着循环每个段数据,并判断每个段数据的是否该重发,还有什么时候重发:
1. 如果这个段数据首次发送,则直接发送数据。
2. 如果这个段数据的当前时间大于它自身重发的时间,也就是RTO,则重传消息。
3. 如果这个段数据的ack丢失累计超过resent次数,则重传,也就是快速重传机制。这个resent参数由 resend 参数决定。
4. 如果这个段数据的ack有丢失且没有新的数据段,则触发ER,ER相关信息ER
最后通过kcp.output发送消息hello,output是个回调函数,函数的实体是 sess.go 的:
1func(s *UDPSession)output(buf [] byte){
2varecc [][] byte
3// extend buf's header space
4ext := buf
5ifs.headerSize > 0{
6ext = s.ext[:s.headerSize+ len(buf)]
7copy(ext[s.headerSize:], buf)
8}
9// FEC stage
10ifs.fecEncoder != nil{
11ecc = s.fecEncoder.Encode(ext)
12}
13// encryption stage
14ifs.block != nil{
15io.ReadFull(rand.Reader, ext[:nonceSize])
16checksum := crc32.ChecksumIEEE(ext[cryptHeaderSize:])
17binary.LittleEndian.PutUint32(ext[nonceSize:], checksum)
18s.block.Encrypt(ext, ext)
19ifecc != nil{
20fork := rangeecc {
21io.ReadFull(rand.Reader, ecc[k][:nonceSize])
22checksum := crc32.ChecksumIEEE(ecc[k][cryptHeaderSize:])
23binary.LittleEndian.PutUint32(ecc[k][nonceSize:], checksum)
24s.block.Encrypt(ecc[k], ecc[k])
25}
26}
27}
28// WriteTo kernel
29nbytes := 0
30npkts := 0
31// if mrand.Intn(100) < 50 {
32fori := 0; i < s.dup+ 1; i++ {
33ifn, err := s.conn.WriteTo(ext, s.remote); err == nil{
34nbytes += n
35npkts++
36}
37}
38// }
39ifecc != nil{
40fork := rangeecc {
41ifn, err := s.conn.WriteTo(ecc[k], s.remote); err == nil{
42nbytes += n
43npkts++
44}
45}
46}
47atomic.AddUint64(&DefaultSnmp.OutPkts, uint64(npkts))
48atomic.AddUint64(&DefaultSnmp.OutBytes, uint64(nbytes))
49}
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 。其中最主要的函数
1func(h *updateHeap)updateTask{
2vartimer
3for{
4select{
5case
6case
7}
8h.mu.Lock
9hlen := h.Len
10now := time.Now
11ifhlen > 0&& now.After(h.entries[ 0].ts) {
12fori := 0; i < hlen; i++ {
13entry := heap.Pop(h).(entry)
14ifnow.After(entry.ts) {
15entry.ts = now.Add(entry.s.update)
16heap.Push(h, entry)
17} else{
18heap.Push(h, entry)
19break
20}
21}
22}
23ifhlen > 0{
24timer = time.After(h.entries[ 0].ts.Sub(now))
25}
26h.mu.Unlock
27}
28}
任务调度器实现了一个堆结构,每当有新的连接,session都会插入到这个堆里,接着for循环每隔interval时间,遍历这个堆,得到 entry 然后执行 entry.s.update 。而 entry.s.update 会执行 s.kcp.flush(false) 来发送数据。
总结
这里简单介绍了kcp的整体流程,详细介绍了发送数据的流程,但未介绍kcp接收数据的流程,其实在客户端发送数据后,服务端是需要返回ack的,而客户端也需要根据返回的ack来判断数据段是否需要重传还是在队列里清除该数据段。处理返回来的ack是在函数kcp.Input函数实现的。具体详细流程下次再介绍。
github:https://github.com/Golangltd/kcp-go
Google资深工程师深度讲解
Go语言基础到实战系列
(www.Golang.Ltd论坛提供下载链接)
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。返回搜狐,查看更多
责任编辑: