目前网上已经有非常多的KCP的原理机制、以及各种版本的KCP实现的相关资料。我在之前做了两篇文章的KCP相关分析,分别是原理机制和性能测试实践。
我们当前的项目是一个对实时性要求比较高的游戏,理论上,按传统实时游戏做法,TCP的性能也是够用,但为了追求更好的效果和更流畅的体验,我们决定在战斗中使用KCP作为网络层通信。
根据调研以及性能测试,总结原因如下:
TCP在网络环境较差的时候,丢包率较高,且十分不稳定;KCP在内外网的环境中,表现都十分稳定。TCP的RTO延时计算不合理,会造成重传数据包时间过长;KCP对于重传发包以及RTO延时计算有着更加友好的算法设计。TCP是以控制网络带宽为目的而设计的协议,而当前网络环境下,带宽已经不是特别重要;KCP则是以控制流速为目的而设计。 二、初版实现
一开始,我们分析github大佬开源的Java版本实现库(https://github.com/l42111996/java-Kcp),理论上是完全够用的。并且这个版本的开源库在原版C的基础上,结合Java中Netty的基于事件驱动,以及多核CPU的利用,对KCP消息的flush策略又做了优化.
1.设计实现
从源码来看,作者非常熟悉Netty和KCP,对这两者又做了较好的融合。并且根据作者文档说明,这版实现在腾讯是有5款上线项目验证的。
因此我在大致摸透这套框架之后,再结合C版本的原作者对KCP的使用建议(https://github.com/skywind3000/kcp/wiki/Cooperate-With-Tcp-Server),对网络层进行了初版的改造,做了如下设计:
保留原有的TCP通道,新增一条KCP/UDP通道,在游戏逻辑层做网络切换逻辑。检测到客户端通过哪条信道发过来消息,我们就切换玩家网络为当前信道。
基于这种实现,我们在项目中可以实现如下功能:
利用TCP的安全可靠性,用TCP通道处理玩家在战斗服的登录,以及战斗场景的创建、进入、结算、和退出战斗场景流程利用KCP的网络稳定性以及较差网络环境下的低延迟特性,用KCP/UDP通道处理玩家战斗中的状态同步消息(我们的战斗是状态同步,但实际也可以用在帧同步的项目中。这一点不重要,就理解为战斗内通信消息即可)在某些UDP包不可达的网络下,战斗内可灵活切换网络通道,退回到TCP备用通道 2.问题分析
在单网络情况下的战斗内通信中,无论玩家使用TCP还是KCP/UDP通道,都是没有任何问题的。
但很快我们就发现这种模式在多网络切换下的一些弊端,一旦涉及到网络切换的一些边界情况,就可能会出现问题
如上面序列图所示:
在2.1 *** 作时,客户端发现TCP网络不通,2.2的ack包以及2.3的状态同步包被阻塞在网络中,经过很久才返回
给客户端(或者也有可能客户端的 *** 作包在发给服务端的时候,就已经被堵在路上了。图中的2.1、2.2、2.3任意一个包都有可能被堵住)这个时候,客户端切到KCP网络,并接着发后面的玩家 *** 作,服务端的战斗状态可能也已经做了很多改变。过了很久很久,客户端啪地收到一个服务端的状态同步包2.3,告诉你怪扣血100,但很有可能这个怪早就被打死了。
提问:客户端收到这个状态同步包,是处理还是不处理。
3.解决方案
当然了,基于这套通信架构,解决方案也是有:
- 给每个消息包加一个网络包序号,低于当前序号的包不做处理,但这种方式只能应用于状态同步游戏,状态同步是允许丢包的,只要保证玩家最终状态一致即可;但如果是帧同步游戏,就稍微有些头疼了,帧同步的游戏是绝对不允许丢包的,因为客户端会根据帧数据进行逻辑运算,而不是简单的同步状态,所以帧同步游戏一定需要一套完善的包序列管理机制。给每个消息包加一个时间戳,原理和加包序号相同,也只能适用于状态同步游戏。不管是走TCP还是KCP网络,服务端对客户端的每一包都做一个回复机制,客户端需要感知到它的消息服务端有没有收到。因为应用层感知不到网络底层收到ACK包,因此需要在应用层实现一套类似ACK的机制。
这三种方案中,显然前两种实现起来更简单,但这还只是能想到的网络切换中会遇到问题,实际情况中,可能遇到的问题会更多。
既然问题出在网络切换和包序列管理上,那为什么不在统一的入口和出口来处理网络消息包呢?
三、多通道网络实现
这个分割线,代表以下内容开始进入正题了
和大佬们一番讨论之后,决定使用一种更为激进,却也是一劳永逸的方法。即以TCP和UDP为双通道网络通信,以KCP进行统一的数据包管理为模型的通信架构。
大概用了一周的时间,我基于这套开源库进行改造,实现了一套以KCP为应用层,TCP和UDP为底层通信协议的双通道网络层。这样的架构下,消息包的序列,分片,窗口大小,流量控制等,都完全交给KCP去做,而底层网络,想用什么用什么,想起几个起几个。因为网络消息的管理统一交给了KCP处理,因此它不再有网络之间切换的问题。
为了让它更灵活,更容易扩展,我把这套网络层抽象出一个开源库,修改为下层支持多通道网络,并且为了使用方便,我在原框架的接口中,把更多参数修改为可配置,既是方便自己,同时也开放给大家,让这套框架最大限度的开放KCP应用层和底层网络通信的配置。
四、ktucp-netty该框架已发布release1.1版本,上传了github和maven中央仓库,大家可以根据我的说明通过maven导入使用
github:https://github.com/hjcenry/ktucp-netty
欢迎大家贡献一个小星星,开放出来既是方便自己也是方便大家,如果大家使用过程中有任何问题,可以在github中提issues,我都会解决。
取名简单粗暴,因为通信架构基于kcp/tcp/udp,所以干脆三合一ktucp,后缀netty代表整个框架以Netty为通信基础实现
以下内容摘自我github工程里的README,对这套框架的架构和使用,做一个简单的介绍。
基于原作者的开源项目的修改:https://github.com/l42111996/java-Kcp.git
原项目:
通信架构
应用层 <--> UDP <--> KCP
实现功能
java版kcp基本实现优化kcp的flush策略基于事件驱动,利用多核性能支持配置多种kcp参数支持配置conv或address(ip+port)确定唯一连接支持fec(降低延迟)支持crc32校验
基于原项目的新增和优化:
通信架构
应用层 ┌┴┐ UDP TCP ...(N个网络) └┬┘ KCP
优化和新增
支持配置多个TCP/UDP底层网络服务支持TCP和UDP通道切换支持自定义配置底层网络的Netty参数支持添加底层网络的自定义Handler支持自定义编解码支持切换KCP下层的网络支持强制使用某一个网络发送数据支持使用自定义时间服务(可以不用System.currentTimeMillis方法而使用自己系统的缓存时间系统) 五、为什么要使用多网络
根据原作者对KCP的使用建议(https://github.com/skywind3000/kcp/wiki/Cooperate-With-Tcp-Server)
实际使用中,最好是通过TCP和UDP结合的方式使用:
- 国内网络情况特殊,可能出现UDP包被防火墙拦下TCP网络在使用LB的情况下,两端中的一端可能出现感知不到对方断开的情况可通过TCP的可靠连接作为备用线路,UDP不通的情况下可使用备用TCP
结合以上需求,这套开源库的目的就是整合TCP和UDP网络到同一套KCP机制中,甚至可以支持启动多TCP多UDP服务。
并且最大程度的开放底层Netty配置权限,用户可根据自己的需求定制化自己的网络框架
欢迎大家使用,有任何bug以及优化需求,欢迎提issue讨论
六、快速开始好了,废话不多说了,我们直接上手看看它怎么使用吧
maven地址服务端 1. 创建ChannelConfigio.github.hjcenry ktucp-net1.1
ChannelConfig channelConfig = new ChannelConfig(); channelConfig.nodelay(true, 40, 2, true); channelConfig.setSndWnd(512); channelConfig.setRcvWnd(512); channelConfig.setMtu(512); channelConfig.setTimeoutMillis(10000); channelConfig.setUseConvChannel(true); // 这里可以配置大部分的参数 // ...2. 创建KtucpListener监听网络事件
KtucpListener ktucpListener = new KtucpListener() { @Override public void onConnected(int netId, Uktucp uktucp) { System.out.println("onConnected:" + uktucp); } @Override public void handleReceive(Object object, Uktucp uktucp) throws Exception { System.out.println("handleReceive:" + uktucp); ByteBuf byteBuf = (ByteBuf) object; // TODO read byteBuf } @Override public void handleException(Throwable ex, Uktucp uktucp) { System.out.println("handleException:" + uktucp); ex.printStackTrace(); } @Override public void handleClose(Uktucp uktucp) { System.out.println("handleClose:" + uktucp); System.out.println("snmp:" + uktucp.getSnmp()); } @Override public void handleIdleTimeout(Uktucp uktucp) { System.out.println("handleIdleTimeout:" + uktucp); } };3. 创建并启动KtucpServer
KtucpServer ktucpServer = new KtucpServer(); // 默认启动一个UDP端口 ktucpServer.init(ktucpListener, channelConfig, 8888);4. 观察日志
[main] INFO com.hjcenry.log.KtucpLog - KtucpServer Start : =========================================================== TcpNetServer{bindPort=8888, bossGroup.num=1, ioGroup.num=8} UdpNetServer{bindPort=8888, bossGroup.num=8, ioGroup.num=0} ===========================================================客户端 1. 创建ChannelConfig
ChannelConfig channelConfig = new ChannelConfig(); // 客户端比服务端多一个设置convId channelConfig.setConv(1); channelConfig.nodelay(true, 40, 2, true); channelConfig.setSndWnd(512); channelConfig.setRcvWnd(512); channelConfig.setMtu(512); channelConfig.setTimeoutMillis(10000); channelConfig.setUseConvChannel(true); // 这里可以配置大部分的参数 // ...2. 创建KtucpListener监听网络事件
KtucpListener ktucpListener = new KtucpListener() { @Override public void onConnected(int netId, Uktucp uktucp) { System.out.println("onConnected:" + uktucp); } @Override public void handleReceive(Object object, Uktucp uktucp) throws Exception { System.out.println("handleReceive:" + uktucp); ByteBuf byteBuf = (ByteBuf) object; // TODO read byteBuf } @Override public void handleException(Throwable ex, Uktucp uktucp) { System.out.println("handleException:" + uktucp); ex.printStackTrace(); } @Override public void handleClose(Uktucp uktucp) { System.out.println("handleClose:" + uktucp); System.out.println("snmp:" + uktucp.getSnmp()); } @Override public void handleIdleTimeout(Uktucp uktucp) { System.out.println("handleIdleTimeout:" + uktucp); } };3. 创建并启动KtucpClient
// 默认启动一个UDP端口 KtucpClient ktucpClient = new KtucpClient(); ktucpClient.init(ktucpListener, channelConfig, new InetSocketAddress("127.0.0.1", 8888));4. 观察日志
[main] INFO com.hjcenry.log.KtucpLog - KtucpClient Connect : =========================================================== TcpNetClient{connect= local:null -> remote:/127.0.0.1:8888, ioGroup.num=8} UdpNetClient{connect= local:null -> remote:/127.0.0.1:8888, ioGroup.num=0} ===========================================================
七、使用注意以上是简单的示例,可快速启动ktucp服务和客户端。关于多网络的详细使用方法,可参考下面的例子3和4
客户端对应实现:该框架仅实现了Java版本,其他版本的客户端需要根据此通信架构进行实现(单纯使用UDP通道的话,也是能和原版KCP兼容的)convId的唯一性:因不能校验udp的address或tcp的channel,只能依靠convId获取唯一Uktucp对象convId的有效性校验:需要判断convId的来源,防止伪造。因convId从消息包读取,框架底层对TCP连接的消息包做了Channel唯一性判断处理,但UDP暂时没有好的判断方法。如果有安全性需求,应用层需要自己做一个防伪检测,比如服务端给客户端分配一个token,客户端在每个消息包头把token带过来,服务端对每个包头的token做一个校验处理好多网络连接管理:因底层配置较为开放,默认为KCP超时即断开所有连接,如有其他配置,请注意连接释放时机 八、使用方法以及例子
有一部分直接引用原作者的例子
- server端示例client端实例多网络server端示例多网络client端实例最佳实践大量资料C#兼容版服务端C#客户端兼容kcp-go
- https://github.com/skywind3000/kcp 原版c版本的kcphttps://github.com/xtaci/kcp-go go版本kcp,有大量优化https://github.com/Backblaze/JavaReedSolomon java版本fechttps://github.com/LMAX-Exchange/disruptor 高性能的线程间消息传递库https://github.com/JCTools/JCTools 高性能并发库https://github.com/szhnet/kcp-netty java版本的一个kcphttps://github.com/l42111996/csharp-kcp 基于dotNetty的c#版本kcp,完美兼容https://github.com/l42111996/java-Kcp.git 此开源库的原版本
篇幅有些过长了,剩余内容列入计划中
对该框架做一次网络切换的性能测试分析github中写一份详细使用wiki文档,把所有可配参数以及调用接口以文档形势呈现以这套框架为基础,实现一些网络相关例子,如rpc调用、游戏服务器等有时间的话,可以考虑实现其他语言版本的客户端(或者看看大家有没有这方面的需求)大家对于这套框架有没有什么更好的想法或者优化方案,欢迎提意见或加入一起开发
微信:hjcenry 欢迎交流讨论
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)