目前网上已经有非常多的KCP的原理机制、以及各种版本的KCP实现的相关资料。我在之前做了两篇文章的KCP相关分析,分别是原理机制和性能测试实践。
我们当前的项目是一个对实时性要求比较高的游戏,理论上,按传统实时游戏做法,TCP的性能也是够用,但为了追求更好的效果和更流畅的体验,我们决定在战斗中使用KCP作为网络层通信。
根据调研以及性能测试,总结原因如下:
一开始,我们分析github大佬开源的Java版本实现库(https://github.com/l42111996/java-Kcp),理论上是完全够用的。并且这个版本的开源库在原版C的基础上,结合Java中Netty的基于事件驱动,以及多核CPU的利用,对KCP消息的flush策略又做了优化.
/**
* 执行flush
* flush策略
* 1,在send调用后检查缓冲区如果可以发送直接调用update得到时间并存在uktucp内
* 2,定时任务到了检查uktucp的时间和自己的定时,如果可以发送则直接发送,时间延后则重新定时,定时任务发送成功后检测缓冲区 是否触发发送时间
* 3,读事件触发后检测检测缓冲区触发写事件
*/
从源码来看,作者非常熟悉Netty和KCP,对这两者又做了较好的融合。并且根据作者文档说明,这版实现在腾讯是有5款上线项目验证的。
因此我在大致摸透这套框架之后,再结合C版本的原作者对KCP的使用建议(https://github.com/skywind3000/kcp/wiki/Cooperate-With-Tcp-Server),对网络层进行了初版的改造,做了如下设计:
保留原有的TCP通道,新增一条KCP/UDP通道,在游戏逻辑层做网络切换逻辑。检测到客户端通过哪条信道发过来消息,我们就切换玩家网络为当前信道。
基于这种实现,我们在项目中可以实现如下功能:
在单网络情况下的战斗内通信中,无论玩家使用TCP还是KCP/UDP通道,都是没有任何问题的。
但很快我们就发现这种模式在多网络切换下的一些弊端,一旦涉及到网络切换的一些边界情况,就可能会出现问题
如上面序列图所示:
当然了,基于这套通信架构,解决方案也是有:
这三种方案中,显然前两种实现起来更简单,但这还只是能想到的网络切换中会遇到问题,实际情况中,可能遇到的问题会更多。
既然问题出在网络切换和包序列管理上,那为什么不在统一的入口和出口来处理网络消息包呢?
这个分割线,代表以下内容开始进入正题了
和大佬们一番讨论之后,决定使用一种更为激进,却也是一劳永逸的方法。即以TCP和UDP为双通道网络通信,以KCP进行统一的数据包管理为模型的通信架构。
大概用了一周的时间,我基于这套开源库进行改造,实现了一套以KCP为应用层,TCP和UDP为底层通信协议的双通道网络层。这样的架构下,消息包的序列,分片,窗口大小,流量控制等,都完全交给KCP去做,而底层网络,想用什么用什么,想起几个起几个。因为网络消息的管理统一交给了KCP处理,因此它不再有网络之间切换的问题。
为了让它更灵活,更容易扩展,我把这套网络层抽象出一个开源库,修改为下层支持多通道网络,并且为了使用方便,我在原框架的接口中,把更多参数修改为可配置,既是方便自己,同时也开放给大家,让这套框架最大限度的开放KCP应用层和底层网络通信的配置。
该框架已发布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
实现功能
基于原项目的新增和优化:
通信架构
应用层
┌┴┐
UDP TCP ...(N个网络)
└┬┘
KCP
优化和新增
根据原作者对KCP的使用建议(https://github.com/skywind3000/kcp/wiki/Cooperate-With-Tcp-Server)
实际使用中,最好是通过TCP和UDP结合的方式使用:
结合以上需求,这套开源库的目的就是整合TCP和UDP网络到同一套KCP机制中,甚至可以支持启动多TCP多UDP服务。
并且最大程度的开放底层Netty配置权限,用户可根据自己的需求定制化自己的网络框架
欢迎大家使用,有任何bug以及优化需求,欢迎提issue讨论
好了,废话不多说了,我们直接上手看看它怎么使用吧
<dependency>
<groupId>io.github.hjcenrygroupId>
<artifactId>ktucp-netartifactId>
<version>1.1version>
dependency>
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);
// 这里可以配置大部分的参数
// ...
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);
}
};
KtucpServer ktucpServer = new KtucpServer();
// 默认启动一个UDP端口
ktucpServer.init(ktucpListener, channelConfig, 8888);
[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}
===========================================================
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);
// 这里可以配置大部分的参数
// ...
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);
}
};
// 默认启动一个UDP端口
KtucpClient ktucpClient = new KtucpClient();
ktucpClient.init(ktucpListener, channelConfig, new InetSocketAddress("127.0.0.1", 8888));
[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
有一部分直接引用原作者的例子
篇幅有些过长了,剩余内容列入计划中
微信:hjcenry 欢迎交流讨论