断线重连最危险的不是掉线,而是旧连接回来得太晚

旧连接与新连接争夺同一席位的技术示意图

真正的竞态

游戏里的断线重连,表面上像是一条连接断开,再建立一条新连接。真正棘手的情况却不是“没有连接”,而是新旧两条连接在一小段时间内同时影响同一个玩家席位。

网络连接、房间消息和异步任务不会按照业务叙事排队。旧连接可能已经失效,但它退出时发出的断开消息仍在邮箱中等待处理;与此同时,新连接已经完成鉴权和入房。如果房间随后仅凭玩家编号或席位编号执行“标记离线”,迟到的旧消息就会把刚上线的新连接一起清掉。

这不是概率很低的边角问题。只要断开通知经过异步队列,而重连允许更换底层连接,系统就必须定义:此刻到底是哪条连接拥有这个席位。

谁拥有席位

这次分析的服务端实现中,每条战斗连接都会创建独立的发送通道。玩家第一次进入房间时,席位保存旧通道;同一身份重新进入时,房间用新通道替换席位当前通道,并向旧通道发送“席位已被占用”的通知。

关键不是“新连接来了”这件事,而是替换动作建立了新的所有权事实:席位当前保存的发送通道,就是当前会话身份。旧连接即使尚未完全退出,也不再拥有修改席位在线状态的权利。

这种做法没有另外引入一套会话注册表,而是复用房间本来就需要保存的发送通道。对当前架构而言,它足够直接,也避免了玩家编号同时指向多条有效连接时的含糊状态。

回调为何过期

连接任务退出时,会把自己的原始发送通道连同席位编号一起放进 Disconnected 消息。房间处理这条消息时,不会立即标记离线,而是先比较消息携带的通道与席位当前通道是否为同一条通道。

如果两者相同,说明退出的连接仍是当前所有者,可以执行离线清理;如果不同,说明所有权已经在重连时转移,这条断开消息只是过期回调,必须忽略。

旧连接断开消息晚于新连接入房的处理时序

图里的顺序很重要:旧连接先入房,新连接随后替换它,最后旧连接的断开消息才到达。只按玩家身份清理会得到错误结果;加入通道身份比较后,旧消息无法越过所有权检查,新连接仍能继续收发消息。

清理先验权

这条规则可以概括为一句话:创建资源可以依据业务身份,销毁资源必须证明实例所有权。

类似问题不只存在于游戏房间。登录会话、语音通话、设备长连接和实时协作都可能出现同一逻辑身份快速更换物理连接。若底层通道没有稳定的身份比较能力,可以为每次绑定分配递增的会话代号。建立新连接时更新当前代号,断开回调携带创建时的代号,清理前再做相等比较。

无论使用通道身份还是会话代号,都应让判断与状态修改在房间的串行消息处理边界内完成。否则“检查通过”和“标记离线”之间仍可能插入一次新的重连,竞态只是换了位置。

等待旧连接彻底退出后再允许重连,并不是可靠替代方案。移动网络切换、进程暂停和中间设备超时都可能让连接生命周期拖得很长,玩家却需要立即恢复。给断开操作加锁也不够:锁只能约束同时修改,不能告诉系统拿到锁的回调是否仍代表当前会话。真正需要比较的是实例身份,而不是把所有操作排成一条更慢的队列。

业务身份和会话身份因此必须承担不同职责。玩家编号用于找回同一个席位、权限和对局数据;会话身份只回答这一次连接是否仍然有效。前者稳定,后者短暂。用稳定身份寻找资源,再用短暂身份授权清理,边界才清楚。

重连还没结束

服务端保护住新连接,只解决了会话所有权问题。玩家重新看到一致的战局,还需要客户端完成进度对齐。

当前实现会在入房确认中返回服务器已累计帧数,客户端把它当作排他的追帧终点。客户端将它与本地期望帧比较,发现缺口后请求从期望帧到终点前一帧的区间,再在每次 ReconnectController.Tick 中最多应用十六帧。重试等待采用一秒、两秒、四秒、八秒、十六秒的递增间隔,之后封顶十六秒;从首次断线起累计三十秒仍未恢复,才进入放弃状态。

这里应把两类职责分开:会话切换保证“谁有资格继续操作”,帧追赶保证“新连接看到什么状态”。如果前者不稳定,后者收到的补帧也可能被一次迟到清理中断;如果只做前者而不追帧,连接虽然在线,画面与模拟进度仍可能落后。

怎样验证

验证这种竞态不能只测试一次普通重连。最小的服务端用例应主动构造反常顺序:旧连接入房,新连接以同一身份替换旧连接,然后再投递旧连接的断开消息,最后由新连接发送一次探测消息并确认收到响应。

本次写作前实际运行了两项服务端聚焦测试:一项验证迟到断开不会清理新连接,另一项验证同一身份重连会替换旧连接,两项均通过。客户端网络测试套件也实际运行并通过四十九项测试,其中覆盖等待、重试、超时放弃、缺帧请求和分批追赶。

生产环境还应记录“过期断开被忽略”的次数,并关联席位、旧会话和当前会话标识。这是可观测性建议,不代表当前项目已经具备对应指标。它能帮助团队判断保护分支是在处理偶发网络抖动,还是在掩盖频繁的连接生命周期异常。

最终判断

断线重连的核心不是重新拨号,而是一次有顺序约束的会话所有权转移。新连接成功入房后,旧连接产生的任何延迟清理都必须被视为可能过期的命令。

最可靠的实现通常并不复杂:席位保存当前会话身份,断开消息携带原会话身份,清理前比较二者,并在同一个串行状态边界里完成判断与修改。随后再用服务器帧号和缺帧追赶恢复战局进度。把所有权与状态同步拆开验证,才能让“重连成功”从接口返回值变成可证明的系统行为。