一、KCP协议简介
官方简介:
KCP是一个快速可靠协议,能以比 TCP 浪费 10%-20% 的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。
KCP可以当作一个应用层的协议,通常使用UDP作为底层协议,在提供可靠传输机制的情况下提高传输速度。那为什么不直接使用TCP呢?
TCP是为流量设计的,讲究的是充分利用带宽。TCP协议会在网络拥塞的情况下会减少自身的传输速率;
而KCP是为流速设计的,它会抢占流量,而不管整个网络的拥塞情况。从更高的角度而言,KCP协议牺牲了网络协议的公平性来贪婪的占用网速
设计目的:解决在网络拥堵的情况下TCP协议网络速度慢的问题(弱网环境下)
注意在网络情况较好的情况下TCP协议更快
使用场景: 实时性较高的场景,如Moba游戏、FPS游戏,视频直播
二、KCP协议的特点
KCP 主要利用了如下思想来加快数据在网络中的传输:
1.RTO计算: TCP超时后 RTO *= 2 而KCP采用 RTO *= 1.5,避免RTO快速膨胀
2.选择重传: TCP 丢包时会全部重传从丢的那个包开始以后的数据,KCP 是选择性重传,只重传真正丢失的数据包。
3.快重传: 可以开启KCP的快速重传机制,并且设置了当重复的ACK个数大于resend时候,直接进行重传,而不用等待超时
4.延迟确认: TCP 为了充分利用带宽,延迟发送 ACK(NODELAY 都没用),这样超时会得到较大 RTT 时间,延长了丢包时的判断过程。KCP 的 ACK 是否延迟发送可以调节。
5.ARQ 模型响应有两种,UNA(此编号前所有包已收到,如TCP)和 ACK(该编号包已收到),光用 UNA 将导致全部重传,光用 ACK 则丢失成本太高,以往协议都是二选其一,而 KCP 协议中,除去单独的ACK包外,所有包都有UNA 信息
6.非退让流控: KCP 正常模式同 TCP 一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。
(TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差)
三、KCP源码学习
kcp源码: https://github.com/skywind3000/kcp/blob/master/ikcp.h
基础结构
- IKCPSEG,用于存储发送和接收的数据段状态
struct IQUEUEHEAD {
struct IQUEUEHEAD *next, *prev;
};
struct IKCPSEG
{
struct IQUEUEHEAD node; //用来串接多个segment,也就是前向后向指针(如上)
IUINT32 conv; //会话编号,和TCP的con一样,确保双方需保证conv相同,相互的数据包才能被接收
//conv唯一标识一个会话
IUINT32 cmd; //区分不同的类型的segment
//IKCP_CMD_PUSH数据分片
//IKCP_CMD_ACK:ack分片
//IKCP_CMD_WASK:请求告知窗口大小
//IKCP_CMD_WINS:告知窗口大小
IUINT32 frg; // 当输出数据大于 MSS 时,需要将数据进行分片
// 标识segment分片ID,用户数据可能被分成多个kcp包发送, 为0时代表数据可以被接收了
// 流模式下所有segment分片都为0
IUINT32 wnd; // 剩余接收窗口大小(接收窗口大小-接收队列大小),发送方的发送窗口不能超过接收方给出的数值
IUINT32 ts; // 发送时刻的时间戳,用来估计RTT
IUINT32 sn; // 为 data 报文的编号或者 ack 报文的确认编号
IUINT32 una; // 未确认的序号
IUINT32 len; // 数据长度
IUINT32 resendts; // 下次重发该报文的时间
IUINT32 rto; // 重传超时时间
IUINT32 fastack; // 收到ack时计算该分片被跳过的累计次数,
// 此字段用于快速重传,自定义需要几次确认开始快速重传
IUINT32 xmit; // 发送分片的实际次数,每发一次加1
//发送的次数对RTO的计算有影响,但是比TCP来说,影响会小一些.
char data[1]; //实际传输的数据
//用于索引结构体尾部的数据,额外分配的内存扩展了运行时的 data 字段数组的实际长度
};
2.IKCPCB,KCP控制块,该结构存储了KCP控制对象的所有数据、上下文状态、以及回调函数。由于成员太多,可以通过对照发送和接收的过程理解。
struct IKCPCB
{
IUINT32 conv; // 会话编号
IUINT32 mtu; // 最大传输单元,默认数据为1400,最小为50
IUINT32 mss; // 最大分片大小,不大于mtu
//MSS是用来限制application层最大的发送字节数。
//如果底层物理接口MTU= 1500 byte,则 MSS = 1500- 20(IP Header) -8 (UDP Header) = 1472 byte
IUINT32 state; // 连接状态(0xffffffff表示断开连接)
IUINT32 snd_una; // 第一个未确认的包
IUINT32 snd_nxt; // 下一个待分配包的序号,这个值实际是用来分配序号的
IUINT32 rcv_nxt; // 待接收消息序号.为了保证包的顺序,接收方会维护一个接收窗口,接收窗口有一个起始序号rcv_nxt
// 以及尾序号rcv_nxt + rcv_wnd(接收窗口大小)
IUINT32 ts_recent;
IUINT32 ts_lastack;
IUINT32 ssthresh; // 拥塞窗口的阈值
IINT32 rx_rttval; // RTT的变化量,代表连接的抖动情况
IINT32 rx_srtt; // smoothed round trip time,平滑后的RTT;
IINT32 rx_rto; // 收ACK接收延迟计算出来的重传超时时间
IINT32 rx_minrto; // 最小重传超时时间
IUINT32 snd_wnd; // 发送窗口大小
IUINT32 rcv_wnd; // 接收窗口大小,本质上而言如果接收端一直不去读取数据则rcv_queue就会满(达到rcv_wnd)
IUINT32 rmt_wnd; // 远端接收窗口大小
IUINT32 cwnd; // 拥塞窗口大小, 动态变化
IUINT32 probe; // 探查变量, IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小;
IUINT32 current;
IUINT32 interval; // 内部flush刷新间隔,对系统循环效率有非常重要影响, 间隔小了cpu占用率高, 间隔大了响应慢
IUINT32 ts_flush; // 下次flush刷新的时间戳
IUINT32 xmit; // 发送segment的次数, 当segment的xmit增加时,xmit增加(重传除外)
IUINT32 nrcv_buf; // 接收缓存中的消息数量
IUINT32 nsnd_buf; // 发送缓存中的消息数量
IUINT32 nrcv_que; // 接收队列中消息数量
IUINT32 nsnd_que; // 发送队列中消息数量
IUINT32 nodelay; // 是否启动无延迟模式。无延迟模式rtomin将设置为0,拥塞控制不启动;
IUINT32 updated; //是 否调用过update函数的标识;
IUINT32 ts_probe; // 下次探查窗口的时间戳;
IUINT32 probe_wait; // 探查窗口需要等待的时间;
IUINT32 dead_link; // 最大重传次数,被认为连接中断;
IUINT32 incr; // 可发送的最大数据量;
struct IQUEUEHEAD snd_queue; //发送消息的队列
struct IQUEUEHEAD rcv_queue; //接收消息的队列, 是已经确认可以供用户读取的数据
struct IQUEUEHEAD snd_buf; //发送消息的缓存 和snd_queue有什么区别
struct IQUEUEHEAD rcv_buf; //接收消息的缓存, 还不能直接供用户读取的数据
IUINT32 *acklist; //待发送的ack的列表 当收到一个数据报文时,将其对应的 ACK 报文的 sn 号以及时间戳 ts
//同时加入到acklist 中,即形成如 [sn1, ts1, sn2, ts2 …] 的列表
IUINT32 ackcount; //是当前计数, 记录 acklist 中存放的 ACK 报文的数量
IUINT32 ackblock; //是容量, acklist 数组的可用长度,当 acklist 的容量不足时,需要进行扩容
void *user; // 指针,可以任意放置代表用户的数据,也可以设置程序中需要传递的变量;
char *buffer; //
int fastresend; // 触发快速重传的重复ACK个数;
int fastlimit;
int nocwnd; // 取消拥塞控制
int stream; // 是否采用流传输模式
int logmask; // 日志的类型,如IKCP_LOG_IN_DATA,方便调试
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);//发送消息的回调函数
void (*writelog)(const char *log, struct IKCPCB *kcp, void *user); // 写日志的回调函数
};
发送和接收报文逻辑
1.发送数据
由于 KCP 是应用层协议,在使用 KCP 之前,需要先实现底层的回调函数,如ikcpcb中的output函数。然后上层应用可以调用ikcp_send来发送数据。
ikcpcb中定义了发送相关的缓冲队列和buf,分别是snd_queue和snd_buf。应用层调用ikcp_send后,数据将会进入到snd_queue中,而ikcp_flush将会决定将多少数据从snd_queue中移到snd_buf中,最后调用output函数发送。
发送数据过程:
1.调用ikcp_send后:
根据MSS进行分片,分片后的包插入snd_queue中
2主循环中不断调用(每次循环sleep一段时间)ikcp_update,检查到有更新会调用ikcp_flush
数据从snd_queue放入到snd_buf中,ikcp_flush将数据移动到send_buf中
3.最后通过调用output函数发送
2.接收数据
1.应用层recvfrom接收到数据,调用ikcp_input将数据写入rev_buf中交给kcp协议进行解析
2.ikcp_update会调用ikcp_flush,数据从rcv_buf进行包排序等处理并放入recv_queue中
3.通过ikcp_recv中读取recv_queue中的数据,应用层获得解析后的数据