kcp 介绍与源代码分析_KCP-GO源码解析

原标题: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论坛提供下载链接)

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。返回搜狐,查看更多

责任编辑:

你可能感兴趣的:(kcp,介绍与源代码分析)