0%

Java中使用KCP协议——性能测试及应用

上一篇文章简单介绍了KCP基本的机制和原理,以及github上的三种java版本。但是上次留了一个小小的坑,缺少了性能测试部分。
这几天通过写测试服务器和测试客户端,简单测了一下TCP和KCP分别在内网和外网的延迟。

一. Java版KCP

上一篇文章介绍的github的三种java版本,对比之下,我最终选择了 这个版本 进行测试。
不过当我测完之后,发现这个版本的实现有个小小的缺陷:虽然它底层也是Netty的实现版本,但是它的底层实在封装的太严实了,而对我们设计中以下两点不太友好:

  1. 由于我们打算根据C原版作者的建议使用TCP/KCP双通道共同运作,原TCPServer我们是基于Netty实现的,有一套相对完整的抽象接口,本来我打算把KCPServer也通过封装和抽象集成到这套接口里面,但这套库的对外接口全部是自己封装的,完全不涉及Netty,以致于两套Server完全是不一样的实现思路。解决办法就是把KCP的层级做的高一点,就不太好利用一些公共方法了,可能还需要再做一层抽象和封装。
  2. 之前实现TCPServer的时候,编解码操作是作为Handler添加到Netty的响应链中的,也就是说编解码操作都是在Netty的IO线程操作的,而这套库的所有对外接口都是以ByteBuf为单位的。这意味着我们需要自己在外部实现编解码,这就涉及到线程的使用。目前是以下解决方案:
    • 修改底层源码,改动可能较大
    • 自定义一套编解码处理线程,代替Netty的IO线程处理编解码的效果,相对改动较小
    • 换一个基于Netty实现的版本

很不幸,最终我换了一个基于Netty实现的版本,主要我们当前的Server接口比较融合,准备先接进来,和前端联调,同时做好抽象,随时准备切换回这一套版本,并使用上面的第二个解决方案。主要考虑到这个库是有线上项目经验验证

二、测试方式

如果大家有需要,我可以把测试代码上传到github

自定一个测试协议TestKcp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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;
}

测试客户端逻辑

  1. 客户端启动并连接服务端
  2. 启动定时器,每100ms,Proto编码一个条Test消息,消息中content长度为M,发送N条
  3. 发送编码后的消息
  4. 收到服务端回复的SC消息
  5. Proto解码,统计延迟(客户端收到第N条消息的时候,关闭客户端,统计所有消息的来回延迟)

测试服务端逻辑

  1. 启动服务端并监听端口
  2. 收到客户端消息
  3. Proto解码消息
  4. Proto编码SC消息
  5. 回复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登录

  1. BattleLogin的时候,服务器创建KcpChannel对象,设置convId规则:index * 100000 + 6位随机数,*并*绑定TCP和KCP的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 生成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);
}

  1. BattleLogin_S2C返回该convId

2.KCP登录

  1. 检测KCP传过来的uid的玩家是否有Human对象,有则该玩家完成了TCP登录
  2. 检测该玩家身上是否有KCP连接的convId,是否和当前KCP的convId是同一个id
  3. 检测TCP的IP和KCP的IP是否是同一个来源
  4. 服务器增加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方法,其中读取当前网络方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
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

大爷赏点儿呗.

欢迎关注我的其它发布渠道