传统游戏项目一般使用TCP协议进行通信,得益于它的稳定和可靠,不过在网络不稳定的情况下,会出现丢包严重。
不过近期有不少基于UDP的应用层协议,声称对UDP的不可靠进行了改造,这意味着我们既可以享受网络层提供稳定可靠的服务,又可以享受它的速度。
KCP就是这样的一个协议
不过网上说的再天花乱坠,我们也得亲自调研,分析源码和它的机制,并测试它的性能,是否满足项目上线要求。本文从C版本的源码入手理解KCP的机制,再研究各种Java版本的实现
一、KCP协议
原版源码(C代码):https://github.com/skywind3000/kcp
基于底层协议(一般是UDP)之上,完全在应用层实现类TCP的可靠机制(快速重传,拥塞控制等)
二、KCP特性
KCP实现以下特性,也可参考github中README中对KCP的定义
特性 | 说明 | 源码位置 |
---|---|---|
RTO优化 | 超时时间计算优于TCP | ikcp_update_ack |
选择性重传 | KCP只重传真正丢失的数据包,TCP会全部重传丢失包之后的全部数据 | ikcp_parse_fastack,ikcp_flush |
快速重传 | 根据配置,可以在丢失包被跳过一定次数后直接重传,不等RTO超时 | ikcp_parse_fastack,ikcp_flush |
UNA + ACK | ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP),ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。 | ikcp_flush(每次update,都发送ACK) |
非延迟ACK | KCP可配置是否延迟发送ACK | ikcp_update_ack |
流量控制 | 同TCP的公平退让原则,发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定 | ikcp_input, |
ikcp_flush |
三、KCP报文
1. 报文解析源码
源码中对报文解析部分代码如下
1 | data = ikcp_decode32u(data, &conv); |
2. 报文定义
报文中标识的定义
名词 | 全称 | 备注 | 作用 |
---|---|---|---|
conv | conversation id | 会话ID | 每个连接的唯一标识 |
cmd | command | 命令 | 每个数据包指定逻辑 |
frg | fragment count | 数据分段序号 | 根据mtu(最大传输单元)和mss(最大报文长度)的数据分段 |
wnd | window size | 接收窗口大小 | 流量控制 |
ts | timestamp | 时间戳 | 数据包发送时间记录 |
sn | serial number | 数据报的序号 | 确保包的有序 |
una | un-acknowledged serial number | 对端下一个要接收的数据报序号 | 确保包的有序 |
3. 消息类型
KCP报文的四种消息类型
1 | const IUINT32 IKCP_CMD_PUSH = 81; // cmd: push data: 推送数据 |
四、源码解析
在网络四层模型中,KCP和TCP/UDP(传输层),IP(网络层)等协议有着本质上区别,理论上KCP是属于应用层协议。
KCP并不提供协议实际收发处理,它只是在传输层只上对消息和链接的一层中间管理。
在KCP的源码中,它仅仅包含ikcp.c和ikcp.h两个文件,仅提供KCP的数据管理和数据接口,而用户需要在应用层进行KCP的调度
1. 结构体定义
KCP分包结构KCP对象结构体定义
1 | struct IKCPSEG |
2. 接口分析
分析C源码,KCP作为中间管理层,主要提供以下接口
1 | //--------------------------------------------------------------------- |
3. 调度逻辑
KCP关键接口:
- 更新(上层驱动KCP状态更新)
ikcp_update:kcp状态更新接口,需要上层进行调度,判断flush时间,满足条件调用ikcp_flush刷新数据,同时也负责对收到数据的kcp端回复ACK消息 - 发送
ikcp_send -> ikcp_update -> ikcp_output
ikcp_send:上层调用发送接口,把数据根据mss值进行分片,设置分包编号,放到snd_queue队尾
ikcp_flush:发送数据接口,根据对端窗口大小,拷贝snd_queue的数据到snd_buf,遍历snd_buf,满足条件则调用output回调(调用网络层的发送) - 接收
ikcp_input -> ikcp_update -> ikcp_recv
ikcp_input:解析上层输入数据,拷贝rcv_buf到rcv_queue
ikcp_recv:数据接收接口,上层从rcv_queue中复制数据到网络层buffer
五、Java版本
从源码来看,作者对于KCP的设计仅仅是应用层的通信管理,对内是数据和连接管理,对外是上层需要调用的接口,所以理论上要在Java中使用,核心逻辑部分照着翻译成Java版本,在网络层实现一套KCP的调度即可。
实际调研看来,github上确实已有不上已经实现的版本,其中有不少代码实现非常优秀的版本,有着良好的封装,优秀的线程模型管理,各种附加优化等等。本着不重复造轮子的思想(对,就是懒),我们完全可以直接采用大佬们的作品,避开大佬们踩过的坑。
目前github上有几个高star的java版本实现,选取最高的三个进行分析
1. https://github.com/szhnet/kcp-netty.git(star:212)
实现原理:
1.KCP逻辑是源码的Java翻译版(一模一样)
2.UkcpServerChannel继承ServerChannel,UkcpServerBootStrap
3.用Boss线程EventLoopGroup的read事件来驱动KCP逻辑
优点:使用Netty的Boss线程Read事件来驱动KCP,不用while(true)的驱动;使用简单,只需使用指定的ServerChannel和ServerBootStrap来启动Netty
缺点:无明显缺点
2. https://github.com/beykery/jkcp.git(star:172)
实现原理:
1.KCP逻辑是源码的Java翻译版(一模一样)
2.启动指定线程数的KcpThread自定义IO线程池,进行KCP逻辑调度
3.Netty读消息时抛到KcpThread自定义IO线程
1 | // 通过hash选择IO线程处理 |
优点:代码简单明了,容易理解,核心是翻译版源码,外壳套的是Netty+自定义IO线程池
缺点:IO线程池会while(true)的调用KCP的update
3. https://github.com/l42111996/java-Kcp.git(star:187)
实现原理:
1.KCP逻辑是源码的Java翻译版(一模一样)
2.Netty读消息时,扔到定时器,1ms后,抛出任务到自定义IO线程
优点:拥有1的全部优点,也在Netty的读消息,把消息抛到定时器去调用KCP的逻辑,避免了2的无意义的while(true),同时实现功能更全,有上线项目验证(据作者描述)
缺点:Netty相关逻辑完全封装起来,不能修改任何Netty参数(不过源码中对Netty的参数已配置的很好了)
目前看来,第三种实现(https://github.com/l42111996/java-Kcp.git)是最理想的方式
如果大家感兴趣,后边会对第三种实现进行详细的源码分析
六、性能测试
近期准备做性能测试进行对比,感兴趣的朋友可以关注下
1 | // TODO |