KCP协议和源码解析

一、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

基础结构

  1. 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_queuesnd_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中的数据,应用层获得解析后的数据

  • 36
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值