上一篇文章简单介绍了KCP基本的机制和原理,以及github上的三种java版本。但是上次留了一个小小的坑,缺少了性能测试部分。
这几天通过写测试服务器和测试客户端,简单测了一下TCP和KCP分别在内网和外网的延迟。
一. Java版KCP
上一篇文章介绍的github的三种java版本,对比之下,我最终选择了 这个版本 进行测试。
不过当我测完之后,发现这个版本的实现有个小小的缺陷:虽然它底层也是Netty的实现版本,但是它的底层实在封装的太严实了,而对我们设计中以下两点不太友好:
- 由于我们打算根据C原版作者的建议使用TCP/KCP双通道共同运作,原TCPServer我们是基于Netty实现的,有一套相对完整的抽象接口,本来我打算把KCPServer也通过封装和抽象集成到这套接口里面,但这套库的对外接口全部是自己封装的,完全不涉及Netty,以致于两套Server完全是不一样的实现思路。解决办法就是把KCP的层级做的高一点,就不太好利用一些公共方法了,可能还需要再做一层抽象和封装。
- 之前实现TCPServer的时候,编解码操作是作为Handler添加到Netty的响应链中的,也就是说编解码操作都是在Netty的IO线程操作的,而这套库的所有对外接口都是以ByteBuf为单位的。这意味着我们需要自己在外部实现编解码,这就涉及到线程的使用。目前是以下解决方案:
- 修改底层源码,改动可能较大
- 自定义一套编解码处理线程,代替Netty的IO线程处理编解码的效果,相对改动较小
- 换一个基于Netty实现的版本
很不幸,最终我换了一个基于Netty实现的版本,主要我们当前的Server接口比较融合,准备先接进来,和前端联调,同时做好抽象,随时准备切换回这一套版本,并使用上面的第二个解决方案。主要考虑到这个库是有线上项目经验验证。
二、测试方式
如果大家有需要,我可以把测试代码上传到github
自定一个测试协议TestKcp
// KCP测试协议
message TestKcp {
required int64 clientTime = 1;
required int32 msgIndex = 2;
optional string content = 3;
}
// KCP测试协议
message TestKcp_S2C {
required int64 clientTime = 1;
required int64 serverTime = 2;
required int32 msgIndex = 3;
optional string content = 4;
}
测试客户端逻辑
- 客户端启动并连接服务端
- 启动定时器,每100ms,Proto编码一个条Test消息,消息中content长度为M,发送N条
- 发送编码后的消息
- 收到服务端回复的SC消息
- Proto解码,统计延迟(客户端收到第N条消息的时候,关闭客户端,统计所有消息的来回延迟)
测试服务端逻辑
- 启动服务端并监听端口
- 收到客户端消息
- Proto解码消息
- Proto编码SC消息
- 回复SC消息
在内网环境(同机房机器),外网环境(上海机器)分别部署TCP和KCP的服务端
三、测试结果
以下是通过我的测试逻辑跑出来的测试结果,结果是通过把测试数据交给python的matplotlib库所画出来(几行代码搞定,很简单)
1. KCP测试
1.1 KCP:内网环境 VS 外网环境平均延迟对比
KCP平均延迟测试1.2 KCP:内网环境 VS 外网环境最高延迟对比
KCP最高延迟测试1.3 KCP:内网环境 VS 外网环境最低延迟对比
KCP最低延迟测试2. TCP测试
2.1 TCP:内网环境 VS 外网环境平均延迟对比
TCP平均延迟测试2.2 TCP:内网环境 VS 外网环境最高延迟对比
TCP最高延迟测试2.3 TCP:内网环境 VS 外网环境最低延迟对比
TCP最低延迟测试3. TCP VS KCP对比测试
3.1 TCP VS KCP:平均延迟对比
TCP VS KCP平均延迟测试TCP VS KCP平均延迟测试
3.2 TCP VS KCP:最高延迟对比
TCP VS KCP最高延迟测试TCP VS KCP最高延迟测试
3.3 TCP VS KCP:最低延迟对比
TCP VS KCP最低延迟测试TCP VS KCP最低延迟测试
四、 测试结论
现象描述:
- KCP的外网环境平均延迟稳定在30ms左右,内网环境平均延迟稳定在3ms左右
- KCP的外网环境比内网环境延迟要高,但差距不大,和内网环境相比,平均延迟最高达到10倍(内网最高1ms,外网最高33ms,即使达到外网最高延迟,相对也是比较稳定的)
- TCP的外网环境非常不稳定,最高达到3400ms,最高与内网相差几乎达到3400倍,而最低也有150ms
- KCP和TCP对比,KCP内网,TCP内网,KCP外网,平均延迟都相对稳定,内网环境下,TCP大部分时间都优于KCP,一旦到了外网环境,KCP表现非常稳定,而TCP外网非常不稳定,平均延迟忽高忽低
结论:
- 较好的网络环境下(测试中的内网环境),TCP大部分时候都优于KCP
- 较差的网络环境下(测试中的外网环境),KCP延迟稳定性远大于TCP
- 国内部分网络防火墙可能丢弃UDP包
- 建议战斗内(Lua层)同步协议使用KCP,战斗进入前其他协议继续使用TCP
- 国内部分网络可能出现UDP不通的情况,因此建议同时保留TCP和KCP,指定一个通信策略,比如优先KCP,KCP不通的情况下,退回到TCP通信;或者根据网络状况,动态选择TCP还是KCP
- KCP作者的使用建议(https://github.com/skywind3000/kcp/wiki/Cooperate-With-Tcp-Server)
这个测试代码也可以用来测试并调整KCP参数
五、KCP实际应用
根据KCP的特性,以下是实际应用设计
1. TCP登录
- BattleLogin的时候,服务器创建KcpChannel对象,设置convId规则:index * 100000 + 6位随机数,并绑定TCP和KCP的关系
/**
* 生成kcp唯一id
* FIXME 这个方法到INT最大值的临界点会有多线程问题,可能产生相同id,想想怎么优化
* KCP convId规则:redis index * 100000
*/
public int generateKcpConvId() {
long incrNum = RedisManager.getTemplate().incr(DBEnum.global_new_kcp_conv_Id, 0);
if (incrNum >= KCP_CONV_REDIS_MAX_INDEX) {
RedisManager.getTemplate().set(DBEnum.global_new_kcp_conv_Id, 0, "0");
}
// 生成6位随机数
int random = MathUtils.intRand(kcpConvRandom, KCP_CONV_RANDOM_MIN, KCP_CONV_RANDOM_MAX);
return (int) (incrNum * KCP_CONV_RANDOM_MIN + random);
}
- BattleLogin_S2C返回该convId
2.KCP登录
- 检测KCP传过来的uid的玩家是否有Human对象,有则该玩家完成了TCP登录
- 检测该玩家身上是否有KCP连接的convId,是否和当前KCP的convId是同一个id
- 检测TCP的IP和KCP的IP是否是同一个来源
- 服务器增加KCP通道管理
如果有必要,可以在消息包里再加个随机的token,防止udp包造假,或者在每条心跳消息都下发新的token
3.TCP/KCP网络切换
由于国内部分地区UDP无法击穿,因此需要TCP/UDP双线路设计。
网络切换.png服务端同时提供TCP和KCP网络,并且可随时切换,把网络选择权交给客户端
- 服务器启动时同时启动TCPServer和KCPServer,同时工作,同时处理消息
- Human对象身上记录当前网络状态,默认TCP
- TCP Server收到战斗消息(Lua层的状态同步消息),切换Human为TCP模式
- KCP Server收到战斗消息(Lua层的状态同步消息),切换Human为KCP模式
- Human提供统一push方法,其中读取当前网络方法:
protected NetMessageType getNetMessageType(Packet packet) {
// 默认推送TCP网络
NetMessageType netMessageType = NetMessageType.NET_TCP;
int code = packet.getHeader();
// 战斗消息
IPSEnum ipsEnum = ThrudServerApplicationContext.getEnum(code);
if (ipsEnum != null && ipsEnum.isBattleMsg()) {
// 战斗消息,根据玩家当前网络通道推送
netMessageType = this.battleMessageNet;
}
return netMessageType;
}
4.TCP/KCP Idle超时
- 服务器TCP和KCP均设定超时30s,客户端需要定时发送心跳消息(BattleHeartBeat),发送TCP或KCP都可以
- TCP和KCP均触发读超时后,服务器断开玩家所有连接,并踢下线
5.其他
不管任何情况下,TCP断开,服务器也会同时断开KCP(清除KCP连接信息),因此TCP走重连的时候,也需要同时走KCP的重连
个人微信公众号:Henry游戏开发
个人博客:https://hjcenry.com
CSDN:https://blog.csdn.net/hjcenry
简书:https://www.jianshu.com/u/7fa742801aeb
网友评论