侯烁
(杭州碧游信息技术有限公司 浙江杭州 310012)
传统TCP协议运用在移动设备游戏上所遇到的问题。与有线网络的情况不同,有线网络的丢包(Packet Loss)可以被一个正态分布随机过程来描述,即在丢包不存在时间相关性随机事件。而无线网络的丢包往往可以描述成有两个状态的马尔可夫过程[1-2]。当进入平稳期时,丢包会呈现为一个正态分布过程、但是丢包概率还是远大于有线传输;而进入屏障期时,链路中传输的数据包会出现完全损失。移动网络还具有一个异构性特点,即用户会在不同的网络环境下进行切换。
目前,TCP 协议一直是被最广泛使用的实现网络游戏客户端与服务端可靠通信的协议。但是在TCP协议设计时还没有出现移动网络,TCP 是完全按照有线网络的设计思路下的产物,因此直接在TCP 协议之上设计移动端网络游戏的通信架构,会带来以下几个方面的问题。
(1)在双方互不发送信息时,即使网络层已经发生改变无法通信,没有收发信息造成的网络层错误信息(SocketError),不能立刻判断出现了断线的状态,因此很多网络架构中如果长时间没有网络交互的情况下,会定期发送PING 消息来相互确认网络状态,因此PING 消息的间隔决定了基于TCP 协议的网络层断线重连的敏感度和反应速度,但即使每秒发送一次PING也会让玩家感受到明显的卡顿,而过高频率的PING消息则消耗了网络和服务器计算资源。而过于灵敏的PING机制会在移动网络进入短暂的屏障期后,由于没有收到PING消息对断线进行“误判”,从而对用户的体验进行打断。
(2)在移动端设备经常出现的网络切换后,由于源地址变更后TCP 协议无法收到ACK,不能确定对方是否收到在断线之前发送的数据包,这样“可靠”的连接在发生断线之后就变得“不可靠”了。因此,很多移动端游戏在发生断线TCP重连后往往需要进行打断当前游戏体验进行重新登录操作,重新“可靠的”让客户端与服务端的数据进行一次同步。而这样的体验,对于在公交车或者地铁上使用的用户来说是不可接受的。
(3)无线网络在稳定期丢包是因为随机信号干扰导致的随机丢包。而TCP协议由于设计之初视为有线网络服务,拥塞控制认为丢包意味着网络拥塞,需要礼貌性地“慢启动”缩小发送窗口进行退让[3]。而且TCP的收发机制由于是在操作系统内核态中进行处理,程序无法对其进行控制,当出现丢包后,必须等待ACK超时机制进行重发而程序无法干预。这些问题导致TCP 在无线信道环境下延迟较高,且无法持续达到最高速率进行传输。
之所以出现上述问题,是因为TCP 将端对端的传输以及拥塞控制,以及可靠传输这两个功能,耦合在了一个协议中。而这个协议被操作系统层实现,代码无法控制。在客户端和服务端的进程都正常运行时,即使出现了断网现象,双方的可靠传输机制中的数据包序号、超时重传机制仍然有效,可以在网络层将数据包传送到对方之后继续工作。KCP协议是一个运行在会话层的,负责通过在其KCP 包头的CONV 字段(会话号)进行会话的唯一性标识,与TCP可靠传输原理类似的可靠传输协议,但是它并不关注数据是如何被传输的。而UDP协议只负责端到端的数据传输,但是它不关心数据是否能够被对方收到。将KCP 运行在UDP之上,类似实现了将TCP 协议的端对端传输与可靠性保证两个功能做了解耦。
但是这样的设计并没有TCP 的“三次握手”,客户端无法自发“优雅”地与服务端协商会话号从而建立连接,因此在建立连接时客户端与服务端需要先通过第三方使用TCP或者HTTP协议协商会话号,在会话号被同步到客户端与服务端之后才能进行通信,建立连接流程具体见图1。
图1 LoginServer协助KCP协商会话号Conv,并与服务端通信
同样由于这个过程没有“四次挥手”,因此在长时间收不到对方消息的情况下,一方面可能是网络的确长时间存在问题,另一方面可能是对方进程已经退出所以我方也应该进行退出操作。在无法判断这两种情况下,仍然需要借助第三方通过TCP或者HTTP协议进行对方状态的查询,具体见图2。
图2 UDP端口收不到消息,向LoginServer查询服务端状态
由于UDP 的无连接属性[4],客户端无论由于网络切换造成源地址变换,还是客户端链通信恢复后,不需要像TCP那样先要发现网络断开、再重新发送“三次握手”请求,即可相互发送数据,大大降低了传输层恢复的速度。而只要通信双方的KCP 连接对象存活,KCP层都可以通过超时重发机制进行可靠的通信。而且由于KCP 数据包是在用户程序层面生成的,程序控制利用UDP发送KCP数据包的频率,可以设计更加适合无线信道环境的拥塞控制机制,甚至可以通过一次发送对一个KCP 数据包多次发送的方法,来抵抗随机丢包重发造成的延时。
一个经典的MMO 服务端,KBEngine,网络架构图可以由图3 来表示[5-6],客户端通过服务端的Gate 进程与服务端的Game 进程中的实体进行通信。Gate 进程会维护每个与之通信的服务端实体(Entity)所在的Game 进程的路由表,当服务端实体在Game 进程之间迁移时,服务端实体需要跟对应Gate进行相应的维护。因此可以认为,使用TCP协议与固定的Gate进行通信,通过Gate路由之后,即可以跟Game进程进行双向收发RPC消息。
图3 KBEngine服务器框架
在引入KCP 连接的概念后,在对原有服务端架构不进行大规模修改的前提下,基于KCP 的服务端架构由图4可知,额外添加一个负责登录和查询的微服务,并在Gate之外额外添加一层UDPGate进程。
图4 基于KCP的网络游戏服务器框架
新的登录流程可以用图5 来表示,当客户端通过HTTP 与登录微服务发送Login 请求并通过后,微服务随机选择一个Game 进程生成一个实体,Game 实体向Gate 注册后,由Gate 进程生成一个KCP 对象并申请一个全服唯一的KCP 会话号,并将KCP 会话号到对应Gate 的路由信息写入一个Redis 进程,然后会话号和UDPGate 的IP 和端口通过TCP 的LoginSuccess 发送给客户端完成正常登录流程。
图5 新的登录流程
客户端与服务端的RPC通信流程可以用图6来表示。UDPGate进程负责维护KCP的会话号到Gate进程之间的路由和消息传递,当UDPGate 收到一个UDP 带有一个陌生的会话号时,会去Redis进程中查找对应的Gate 进程并将这个路由信息保存下来,UDPGate 负责将收到的UDP 消息发送到对应Gate 进程,Gate 进程持有KCP对象对UDP消息进行解包,获得原始的RPC消息,从而转发到对应的服务端实体进行RPC调用。
图6 客户端与服务端的RPC通信流程
与现在大部分基于TCP协议的游戏服务器框架相比,这个服务器框架,通过在KCP层将传输层的断线进行隐藏,解决了上层业务逻辑被链路断线所打断的问题,也可以给用户带来网络无缝切换的体验。在这个框架下,客户端可以通过与不仅仅一个UDPGate 进行通信,在不登出游戏的情况下,客户端可以进行无缝的选择在最快的接入点之间进行切换。而且这个游戏服务器框架这是在原有服务器框架下进行了一些进程节点的添加,改动量小,可以在现有服务器上进行快速的部署升级。