kcp-go 源码分析(二)

 一:基础

        上一篇主要是分析kcp-go上层应用包装,这一篇再来分析kcp算法层面的东西。kcp是一个快速可靠协议,设计的目的就是解决在网络拥堵的情况下TCP协议网络速度慢这个问题。相比TCP而言,KCP增大了网络传输速率但是牺牲了部分带宽。有个形象的比如就是TCP是一条大河,大河里边的水流速慢但是能容纳的水多,而KCP就是小激流,水流速快但是少。

        KCP并没有规定下层使用什么协议来传输数据,它只是对数据层进行包装。一般情况下下层是使用UDP作为传输协议,KCP层的数据包是在UDP数据报文的基础上加了控制头,这样数据报就变大了。当用户的数据很大时,大于一个UDP报文能曾受的范围(大于MSS),KCP会将数据分片存储到多个KCP包中,因此每个KCP包称为一个分片。

        由于KCP算法是仿照TCP来的,弄清楚网络协议的一些基本概念对于理解TCP和KCP很有帮助。TCP通过确认机制来保证数据传输的可靠性,下面我们来复习一下基本的知识

1:超时重传(RTO)

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

2:确认和重传机制

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

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

  关于滑动窗口请参考:TCP协议详解 滑动窗口_sandyznb的博客-CSDN博客_tcp协议的滑动窗口

二:正式进入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中。

        

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是在 Unity 中使用 kcp 的示例代码: 首先,需要在 Unity 中导入 kcp 的 C# 实现代码。可以从以下链接下载: https://github.com/xtaci/kcp-csharp 将所有的 .cs 文件添加到 Unity 工程中。 接下来,可以编写一个简单的 kcp 客户端和服务器程序。以下是客户端代码: ```csharp using System; using System.Net; using System.Threading; using KcpClient; public class KcpClientTest : IDisposable { private KcpClient.KcpClient _client; public void Start() { _client = new KcpClient.KcpClient(); _client.Connect(IPAddress.Parse("127.0.0.1"), 12345); new Thread(() => { while (true) { if (_client.Connected) { var data = new byte[1024]; var length = _client.Receive(data, out var remote); if (length > 0) { var message = System.Text.Encoding.UTF8.GetString(data, 0, length); Console.WriteLine($"Received message: {message}"); } } Thread.Sleep(10); } }).Start(); } public void Send(string message) { var data = System.Text.Encoding.UTF8.GetBytes(message); _client.Send(data, data.Length); } public void Dispose() { _client.Dispose(); } } ``` 以下是服务器端代码: ```csharp using System; using System.Net; using System.Threading; using KcpServer; public class KcpServerTest : IDisposable { private KcpServer.KcpServer _server; public void Start() { _server = new KcpServer.KcpServer(); _server.Bind(IPAddress.Any, 12345); new Thread(() => { while (true) { if (_server.ConnectedClients.Count > 0) { foreach (var client in _server.ConnectedClients) { var data = new byte[1024]; var length = client.Receive(data, out var remote); if (length > 0) { var message = System.Text.Encoding.UTF8.GetString(data, 0, length); Console.WriteLine($"Received message: {message}"); client.Send($"Received message: {message}"); } } } Thread.Sleep(10); } }).Start(); } public void Dispose() { _server.Dispose(); } } ``` 在 Unity 中使用以上代码时,需要在场景中添加一个空物体,然后将客户端和服务器端代码分别添加到该物体的脚本组件中。在客户端脚本中,可以调用 `Start` 方法启动客户端,并使用 `Send` 方法发送消息。在服务器端脚本中,调用 `Start` 方法启动服务器端。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值