kcp-go的源码分析

1,234 阅读8分钟

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.gosess.go

kcp浅析

kcp是基于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进行数据发送。
接着循环每个段数据,并判断每个段数据的是否该重发,还有什么时候重发:

  1. 如果这个段数据首次发送,则直接发送数据。 2. 如果这个段数据的当前时间大于它自身重发的时间,也就是RTO,则重传消息。 3. 如果这个段数据的ack丢失累计超过resent次数,则重传,也就是快速重传机制。这个resent参数由resend参数决定。 4. 如果这个段数据的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/r…,编码以后的hello就不是和原来的hello一样了,至少多了几个字节。 fec编码器有两个重要的参数reedsolomon.New(dataShards, parityShards, reedsolomon.WithMaxGoroutines(1)),dataShardsparityShards,这两个参数决定了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()函数实现的。

具体流程

前置

1:超时重传(RTO)

超时重传指的是,发送数据包在一定的时间内没有收到相应的ACK,等待一定的时间,超时之后就认为这个数据包丢失,就会重新发送。这个等待时间被称为RTO,即重传超时时间。

2:确认和重传机制

a:停等机制。在早期的时候是采用停等的模式来实现的,具体就是发送方在发送完数据之后会启动定时器,在规定的时间内没有收到ACK报文就认为发送失败,会重新发送数据知道成功为止。必须等待确认之后才能发送下一个包,传输速度慢,效率低。

b:滑动窗口机制。为了提高传输速度,发送方没必要发一个数据包然后等待回复,可以一下子发送多个数据包然后再等对方一一确认,发送方也不是说想发送多少就发送多少的,这得看接收方能接收多少,所以说需要限制一下发送方往网络中发送的数据量。在没有收到确认之前,发送方最多只能发送wnd大小的数据,这个就是滑动窗口机制。TCP的每一端都可以收发数据,每个TCP连接的a、b两端都维护一个发送窗口和接收窗口。其实弄明白4个概念就很好理解滑动窗口:已发送已确认数据段 已发送未确认数据段 可发送还未发送数据段 不可发送数据

正式进入KCP

1:源码中的有关的名词解析

MSS:最大报文长度Maximum Segment Size

MTU:最大传输单元Maximum Transmission Unit

snd_una:第一个未确认的包

snd_nxt:下一个待分配包的序号

rcv_nxt:待接收消息序号

snd_wnd:当前端的发送窗口 默认大小 IKCP_WND_SND = 32

rcv_wnd:当前端的接收窗口 默认大小 IKCP_WND_RCV = 32

rmt_wnd:远端的接收窗口 默认大小 IKCP_WND_RCV = 32

cwnd:当前端的拥塞窗口

nocwnd:值为1时 表示取消拥塞控制(字面理解就是没有拥塞窗口)

stream:值为1时 表示会将几个小包合并成一个大包

2:kcp的协议组成说明

来看看KCP协议头segment具体字段的含义

type segment struct {
        conv     uint32    //会话编号,和TCP的con一样,conv一致才会通信
        cmd      uint8     //指令类型 有4种
        frg      uint8     //标识segment分片ID,用户数据可能被分成多个kcp包发送,倒序->0
        wnd      uint16    //剩余接收窗口大小
        ts       uint32    //发送时刻的时间戳
        sn       uint32    //分片segment的序号,按1累加递增
        una      uint32    //待接收消息序号,代表编号前面的所有报都收到了的标志
        rto      uint32    //超时重传时间,根据网络去定
        xmit     uint32    //重传次数
        resendts uint32    //重传的时间戳。超过当前时间重发这个包
        fastack  uint32    //快速重传机制,记录被跳过的次数,超过次数进行快速重传
        acked    uint32    // mark if the seg has acked
        data     []byte    //数据内容
}

conv一般都是服务器先分配好然后发给客户端,kcp-go是随机生成的。 CMD 4种类型: IKCP_CMD_PUSH = 81 // cmd: push data 数据分片 IKCP_CMD_ACK = 82 // cmd: ack ack分片 IKCP_CMD_WASK = 83 // cmd: window probe (ask) 请求告知窗口大小 IKCP_CMD_WINS = 84 // cmd: window size (tell) 告知窗口大小 3:KCP类分析

type KCP struct {
    conv, mtu, mss, state                  uint32
        snd_una, snd_nxt, rcv_nxt              uint32
        ssthresh                               uint32
        rx_rttvar, rx_srtt                     int32
        rx_rto, rx_minrto                      uint32
        snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe uint32
        interval, ts_flush                     uint32
        nodelay, updated                       uint32
        ts_probe, probe_wait                   uint32
        dead_link, incr                        uint32
        fastresend     int32
        nocwnd, stream int32
        snd_queue []segment
        rcv_queue []segment
        snd_buf   []segment
        rcv_buf   []segment
        acklist []ackItem
        buffer   []byte
        reserved int
        output   output_callback
}

conv:会话ID

mtu:最大传输单元,默认数据是1400,最小是50

mss:最大分片大小 不大于mtu

state:连接状态 0xFFFFFFFF表示断开连接

snd_una:第一个未确认的包

snd_nxt:待发送包的序号

rcv_nxt:待接收消息的序号。

sshthresh:拥塞窗口的阈值,以包为单位(TCP以字节为单位)

rx_rttval:ack接收rtt浮动值 代表连接的抖动情况

rx_srtt:ack接收rtt平滑值(smoothed)

rx_rto:由ACK接收延迟计算出来的重传超时时间

rx_minrto:最小重传超时时间

snd_wnd:发送窗口大小

rcv_wnd:接收窗口大小

rmt_wnd:远端接收窗口大小

cwnd:拥塞窗口大小

probe:探查变量。IKCP_ASK_TELL表示告知远端窗口大小,IKCP_ASK_SEND表示请求远端告知窗口大小

current:当前的时间戳

interval:内部flush刷新间隔

ts_flush:下次flush刷新时间戳

nodelay:是否启动无延迟模式

updated:是否调用过update函数的标记(kcp需要上层通过不断的update和check来驱动kcp收发)

ts_probe:下次探查窗口的时间戳

probe_wait:探查窗口需要等待的时间

dead_link:最大重传次数 超过了就被认为连接中断

incr:可发送的最大数据量

snd_queue:发送消息的队列

rcv_queue:接收消息的队列

snd_buf:发送消息的缓存

rcv_buf:接收消息的缓存

ack_list:等待发送的ack列表

buffer:储存消息字节流的内存

fastresend:触发快速重传的重复ack个数

nocwnd:取消拥塞控制

stream:是否采用流传输模式。Send的时候用到。为1的话 frg都是0,为0的话frg 倒序--->0

reserved:保留的字节数,这个变量是kcp-go自己加的,目的是为加密和FEC这部分功能预留位置

4:kcp相关api分析

func (kcp *KCP) ReserveBytes(n int) bool {
        if n >= int(kcp.mtu-IKCP_OVERHEAD) || n < 0 {
                return false
        }
        kcp.reserved = n
        kcp.mss = kcp.mtu - IKCP_OVERHEAD - uint32(n)
        return true
}
/*
    newUDPSession中
    if sess.block != nil {
                sess.headerSize += cryptHeaderSize
        }
        if sess.fecEncoder != nil {
                sess.headerSize += fecHeaderSizePlus2
        }
        sess.kcp.ReserveBytes(sess.headerSize)
*/

ReserveBytes是为上层引用的加密和FEC提供的,不考虑这些的话mss = mtu-head,为了实现加密和前向纠错就得给它预留空间,这样的话mss就得重置

func (kcp *KCP) PeekSize() (length int) {
        if len(kcp.rcv_queue) == 0 {
                return -1
        }

        seg := &kcp.rcv_queue[0]
        if seg.frg == 0 {
                return len(seg.data)
        }

        if len(kcp.rcv_queue) < int(seg.frg+1) {
                return -1
        }

        for k := range kcp.rcv_queue {
                seg := &kcp.rcv_queue[k]
                length += len(seg.data)
                if seg.frg == 0 {
                        break
                }
        }
        return
}

PeekSize是提前获取一个完整包的长度,通过frg来判断一个包是不是被分成了多片,如果有多个分片(比如3个),frg是倒序来赋值的(frg分别是2 1 0),这样的话碰到frg为0 就表示当前这个分片是最后一个分片。当queue里边没有数据 或者 有数据但是不能组成一个完整的包时,返回-1

func (kcp *KCP) Recv(buffer []byte) (n int) {
        peeksize := kcp.PeekSize()
        if peeksize < 0 {
                return -1
        }

        if peeksize > len(buffer) {
                return -2
        }

        var fast_recover bool
        if len(kcp.rcv_queue) >= int(kcp.rcv_wnd) {
                fast_recover = true
        }

        // merge fragment
        count := 0
        for k := range kcp.rcv_queue {
                seg := &kcp.rcv_queue[k]
                copy(buffer, seg.data)
                buffer = buffer[len(seg.data):]
                n += len(seg.data)
                count++
                kcp.delSegment(seg)
                if seg.frg == 0 {
                        break
                }
        }
        if count > 0 {
                kcp.rcv_queue = kcp.remove_front(kcp.rcv_queue, count)
        }

        // move available data from rcv_buf -> rcv_queue
        count = 0
        for k := range kcp.rcv_buf {
                seg := &kcp.rcv_buf[k]
                if seg.sn == kcp.rcv_nxt && len(kcp.rcv_queue)+count < int(kcp.rcv_wnd) {
                        kcp.rcv_nxt++
                        count++
                } else {
                        break
                }
        }

        if count > 0 {
                kcp.rcv_queue = append(kcp.rcv_queue, kcp.rcv_buf[:count]...)
                kcp.rcv_buf = kcp.remove_front(kcp.rcv_buf, count)
        }

        // fast recover
        if len(kcp.rcv_queue) < int(kcp.rcv_wnd) && fast_recover {
                // ready to send back IKCP_CMD_WINS in ikcp_flush
                // tell remote my window size
                kcp.probe |= IKCP_ASK_TELL
        }
        return
}

Recv()主要是上层调用收数据的逻辑,先通过调用前边的PeekSize探测一个完整包的长度,如果rcv_queue没有包(peeksize=-1)或者queue里边完整包的数量超过了buff的剩余空间的话都返回错误码,正常情况下从rcv_queue中把data拷贝到buffer中,然后从rcv_queue中弹出已经copy好的数据,这个时候rcv_queue已经腾出空间了就从rcv_buf中拷贝一些合法数据到rcv_queue。

从这可以看出 读取数据的流程是 rcv_buf->rcv_queue->buffer。rcv_buf中数据包的sn是连续的,rcv_queue中如果数据包被分片了,那frg也是要连续的。

func (kcp *KCP) Send(buffer []byte) int {
        var count int
        if len(buffer) == 0 {
                return -1
        }

        // append to previous segment in streaming mode (if possible)
        if kcp.stream != 0 {
                n := len(kcp.snd_queue)
                if n > 0 {
                        seg := &kcp.snd_queue[n-1]
                        if len(seg.data) < int(kcp.mss) {
                                capacity := int(kcp.mss) - len(seg.data)
                                extend := capacity
                                if len(buffer) < capacity {
                                        extend = len(buffer)
                                }

                                // grow slice, the underlying cap is guaranteed to
                                // be larger than kcp.mss
                                oldlen := len(seg.data)
                                seg.data = seg.data[:oldlen+extend]
                                copy(seg.data[oldlen:], buffer)
                                buffer = buffer[extend:]
                        }
                }

                if len(buffer) == 0 {
                        return 0
                }
        }

        if len(buffer) <= int(kcp.mss) {
                count = 1
        } else {
                count = (len(buffer) + int(kcp.mss) - 1) / int(kcp.mss)
        }

        if count > 255 {
                return -2
        }

        if count == 0 {
                count = 1
        }

        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
}

Send是应用层把数据发送到KCP里边。如果kcp启用了stream模式,就看看snd_queue中最后一个seg里边还能不能加数据,如果len(seg.data)<kcp.mss就表示上一个seg中data里边还能塞一些数据,随后就是计算能塞多少的问题,extend的计算就是取 capacity和len(buffer)的最小值。现在已经计算出extend的值,那么把buffer拷贝进seg.data的时候就知道存在的具体位置了,然后计算剩余的buffer。

如果说剩余的buffer没有了,也就是完全塞到上一个seg里边了的话直接返回,本次send结束。如果还剩余有buffer,那么就要生成新的seg 然后进行相应的动作。计算剩余的buffer需要seg的个数count,count最小为1最大不能超过255,为啥不能超过255尼?因为frg的值跟count有关系,如果count>=256,那么第一个seg的frg就>=255了,而kcp的协议头里边frg是由一个字节长度存放的,最多只能存255,所以这里对于count的大小是有要求的。cout计算完了之后,剩余的操作就是创建seg然后把seg存放到snd_queue中。