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

我们的手机游戏发布有一段时间了。立项之前我写的一篇 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 有所不同。目前尚未仔细测试,有兴趣的同学可以一起来完善它。

发表评论

关闭菜单