« COC Like 游戏中的寻路算法 | 返回首页 | 如何让玩家相信游戏是公平的 »

在移动网络上创建更稳定的连接

我们的手机游戏发布有一段时间了。立项之前我写的一篇 blog , 在移动设备上开发游戏需要克服的两大技术难点: 移动网络的不稳定性以及手机硬件资源的约束。由于开发时间所限,第一点我们并没有专门去做。

我一直不想动手去做一个临时方案解决 TCP 断线重连问题,因为实现一个 TCP over TCP 是没有太大意义的。移动网络发展迅速的今天,整个行业都在努力提高移动网络的稳定性,所以费力做这个事情很可能在两年之后就变得完全没有必要。

比如,iOS 7.0 发布 后,让 MultiPath TCP 技术为更多人所知。从许多中文资料对其的解读,主要集中在 MPTCP 提供了更大的带宽上;甚至一些网络喷子借机来喷国内的 3G 收费高的问题,认为同时利用 3G 网络和 wifi 下载没有意义。但我认为其对于移动网络的真正意义在于提供一个更加稳定的连接。

顾名思义,MPTCP 允许在同一 TCP 连接的通讯两端建立多条通讯路径,如这篇文章 所言:Just like IP can hide routing changes, MPTCP can hide the details of which paths it is using at any given time.

这两天,我们在自己的服务器上安装了支持 MPTCP 的新内核做了测试。发现:如有可能,设备会为新的 IP 地址建立新的通讯路径。如果连接两端各有两个 IP ,那么在初始的 TCP 连接建立后,通过协商,最终会建立 4 条 TCP 连接出来,交叉连接了所有的 IP 。任何一条通路有效都不影响通讯。btw, 如果你的机房有网通,电信两个 IP 的话,如果客户端设备支持 MPTCP ,那么会自动同时使用两个通路同时维持一个逻辑上的连接。这对国内的网络环境非常有利,不需要使用 bgp 机房,也不需要在多线机房配置复杂的 DNS 了。

当你的手机从 3G 网络切换到新的 wifi 热点时,设备会自动利用新的 wifi 网络做数据传输;离开 wifi 热点后,又能无缝切换回 3G ;再次进入新的 wifi 热点范围,还可以重新利用新的 wifi 网络。这样,移动设备可以穿梭于多个网络之间而永不断开连接。

可惜的是,Apple 目前并没有完全开放 MPTCP 给应用层使用。经我的测试,只有 Siri 的连接才会发送 MPTCP 握手协商。这篇 blog 也证实了这一点

ps. 经过这两天的测试,还发现 MPTCP 似乎只能利用第一次连接的通路做控制信息交换。当第一次连接的 IP 实效后,不能把后来的通路提升为主控连接。所以 MPTCP 看起来不能在只有一个网络设备上正常工作。(我原先预期它可以在同一个设备上切换 IP 还可以正常建立新的子流,看来是搞错了)


借着阅读 MPTCP 的协议文档,我也想了许多。我觉得在现阶段在应用层上实现一个更稳定的 TCP 连接也是可行的。但协议设计要考虑的很多,下面记录一下我的设计方案:

我希望针对游戏服务器的特性,实现一个不对称的连接协议。即,只能由客户端发起连接,而发起连接的一方无法主动断开连接。服务器只接受连接,有权利断开连接。

这个协议基于已有的 TCP 协议,通讯是基于带长度信息的包构成。客户端到服务器的前两个包为握手包,服务器只用回应第一个握手包,客户端发送的第二个握手包用于校验,当服务器不认可握手过程,可直接断开连接。

连接建立过程如下:

.1. 客户端向服务器发起一个 TCP 连接,并发送第一个握手信号:包含一个 0 和按 Diffie-Hellman 密钥交换算法产生一串随机量 A 。

.2. 服务器收到第一个握手信号后,检查第一个字段,若不为 0 则进入连接修复阶段 2.2,否则继续创建连接过程 2.1。

.2.1 此时服务器生成另一个随机串 B ,并通过 DH 密钥交换算法得到了一个 secret 。此时回应 DH 算法需要的 B ,以及一个新的随机串 E (用于校验)。

.2.2 当第一个字段不为 0 则认为是需要修复一个已有连接,这个数字表示在旧连接上客户端已收到过服务器发过来的数据包数量。此时,第二个字段应理解为旧连接上已收到数据包的指纹。服务器根据包数量和指纹可以核对所有保持的有效连接,如果不能找到匹配的连接(指纹相同),就断开客户端。否则回应客户端在旧连接上一共收到客户端发送的数据包数量,以及一个新的随机串 E ,用于确认客户端是否知道旧连接的 secret 。(这个校验是有必要的,否则会有人监听到链接重建过程,而重复发送这个握手包来踢掉合法用户刚修复的连接)

.3. 客户端收到随机串 E 后,和 secret 连接在一起做一次 hash (可以使用 md5 算法) H,回应服务器。这可以让服务器校验客户端是否真的拥有 secret 。

.4. 服务器收到二次握手信号 H 后,用同样的 hash 算法做一次 secret 校验,确认是合法的客户端后继续通讯;若是非法连接则立即断开。

.5.1 如果是新连接,那么服务器利用得到的 secret 初始化 RC4 加密算法需要的 s-box ,之后的通讯利用 RC4 算法加密。

.5.2 如果是旧连接修复,那么服务器将客户端未收到的数据包重发一次。并从旧通道上复制 RC4 所用的 s-box 以及 secret 用于后续通讯。

.6. 此后的每次数据通讯,在数据打包后,都利用 RC4 算法做一次加密,并利用数据更新数据指纹(可以用加密后的数据流的 CRC 值)。每个数据包都记录当前的指纹,并 cache 最近发送的 128 个数据包用于事后的连接修复。

.7. 设定一个超时时间,定期清理没有数据来往的 TCP 连接。

这个协议的好处是,客户端在握手完成后,任何时间都可以向服务器发起一个新的 TCP 连接取代旧的连接(无须利用旧连接是否还有效),而对应用层来说,连接重来没有中断过。

应用层可以做一些配合工作:比如设计一分钟一次的心跳,如果长时间没有收到心跳包,就主动发起新的 TCP 连接去取代旧的。这对无线网络能增加网络的稳定性。比如你切换 wifi 网络时,由于 IP 地址的变化, TCP 连接不可能保持,但这套协议可以帮助你自动修复它。在没有 MPTCP 支持时,它还可以尽量去使用更高质量的网络(只要重新连接时去尝试新的网络设备即可)。


我计划在 skynet 中实现一个和网络 API 无关的 C 模块作为中间层来完成以上工作:

接口大约是这样:

struct socket_pool;

// when sz == 0 and buffer == NULL, fd is closed
// when sz > 0 and buffer != NULL, buffer is the data 
struct socket_package {
    int fd;
    int sz;
    const char * buffer;
};

struct socket_pool * socketpool_new();
void socketpool_release(struct socket_pool *sp);
void socketpool_timeout(struct socket_pool *sp);

void socketpool_pushinput(struct socket_pool *sp, int fd, const void * buffer, int sz);
void socketpool_pushoutput(struct socket_pool *sp, int id, const void * buffer, int sz);

int socketpool_popoutput(struct socket_pool *sp, struct socket_package *p);
int socketpool_popinput(struct socket_pool *sp, struct socket_package *p);

void socketpool_closefd(struct socket_pool *sp, int fd);
void socketpool_close(struct socket_pool *sp, int id);

fd 是底层的 socket handle ,id 是应用层的连接 id 。

当网络层有任一 fd 收到数据时,通过 pushinput 接口把数据推送到 socketpool 中。调用 popinput 会报告哪个 id 上有新的数据包(或是没有新的数据包)。

向一个 id 写数据只需要调用 pushoutput ,然后反复调用 popoutput 可以得到真正需要将哪些数据写入具体的 fd ,把它们交给网络层 API 发送即可。

如果有 fd 断开,或向主动关闭 id ,可以调用 closefd / close ;而 popoutput 则有可能收到一个 fd 关闭的信号,然后调用网络层 API 去对应的 fd 即可。

这个模块会处理数据打包加密,修复连接重新补发包等问题,并将这些隐藏在实现中。


2 月 14 日补充:

我实现了一个开源版本, API 有所不同。目前尚未仔细测试,有兴趣的同学可以一起来完善它。

Comments

云风说两年之后会解决移动网络不稳定的问题,如今已经过去8年了,问题仍未解决

总是server断开client, server端会不会有time_wait问题?

想法挺好的,这个和我之前做的游戏,天天炫斗,network层的做法基本一致(简单来说,就是RES/REQ配对检查+心跳包检查+C/S包序连贯,超时就发起重链,服务器根据重链请求包序判断是否重发缓存消息)

不过我现在有了更好的想法,不知云风大师是否有兴趣一起讨论下:利用可靠UDP库来代替现有TCP通讯,再利用可靠UDP库的可靠消息标签来代替心跳以及RES/REQ的配对检查。
就是把网络切换导致的网络重链,放到更底层去做,对应用层保持透明

我觉得如果要完全对上层透明,在MMORPG中128个包,肯定不够,广播一点聊天信息,场景移动信息,128个包,几秒钟就用光了。

不过可能陌陌争霸这种游戏可能128的确够了。

Socket API没办法得知底层的TCP数据是否已经被对面收到。

是不是需要引入类似ACK的协议告诉对面知道数据已经收到了?

cache 最近发送的 128 个数据包用于事后的连接修复。

这样是否太浪费内存了?
会不会有DoS隐患?

还有一种断线重连,属于应用层的断线重连。

比如客户端崩溃后重启,丢失了先前的应用层会话信息。

我觉得这类断线重连,可以通过设计协议,让其对服务端透明。

但客户端需要专门编码,重新发起请求获取游戏世界的信息。

我想用Haxe做个类似的东西,以便跨平台使用。

因为不同平台的网络API不同,所以这个东西的接口必须做得抽象一点,允许适配到不同的网络API。

请问【cache 最近发送的 128 个数据包用于事后的连接修复】在游戏场合128个包够用么?

我现在境界貌似还远远不够,好多地方理解得不够透彻。。

通信协议是不是最好能跟游戏的类型来分析呢,如果卡牌类的的确需要适时,但是类似地下城之类的游戏能不能,在开始和结束的时候进行一次通信,过程就直接交给客户端来解决呢?这样用户体验是不是能更好一点点呢

又分析了一下这个问题,如果只是简单处理,可以把这当作一个重复登陆来对待。在定时内如果重复登陆,则只是关闭旧连接,把Player信息里对应的socket改为新的连接Scoket就行了。

在做手游,遇到同样的问题,学习了!

udp is rescue.

@est

如果你先关闭 3g, 用 wifi 连, 然后打开 3g, 再断开 wifi, 再连一个新的热点 (切换 wifi)

最后一步是切不过去的.

> 还发现 MPTCP 似乎只能利用第一次连接的通路做控制信息交换

应该可以的啊。我看mptcp视频都是wifi 3g分别各自强制断开,流量就自动过去了。连接不断的。

请问如果使用ZeroMQ+心跳,是不是也能达到类似的效果?因为您实现的这个协议,我觉得核心部分主要就是消息的cache和链接的修复。修复的基础又是cache(先不考虑RC4那部分)。
PS:我的Note2上玩陌陌争霸总退出,很郁闷啊!而且字体也偏小,在我那么大的屏上,都觉得不够大。吐槽一下……

可以看下 mosh

哎呀!第一次在大牛的博客里面说话,好激动啊!!嗷嗷

通信协议是不是最好能跟游戏的类型来分析呢,如果卡牌类的的确需要适时,但是类似地下城之类的游戏能不能,在开始和结束的时候进行一次通信,过程就直接交给客户端来解决呢?这样用户体验是不是能更好一点点呢?

为啥不用UDP协议呢?弱弱的问一句。

设计方案有点类似adobe的RTMFP协议了。不过,rtmfp是udp的。

MPTCP看起来在有点类似在电信领域使用多年的SCTP。不过SCTP协议终端和设备支持不好,貌似Linux内核已有但Windows就没支持,所以互联网应用中没人用

应用层做这个挺好。
MPTcp的类似协议没那么快有标准,
也不可能所有设备都支持,
那就只能在应用层做了。

苹果只跟自家玩就可以全用MPTcp

Post a comment

非这个主题相关的留言请到:留言本