《MMO 服务器开发》7.2 顶号、踢人与连接迁移
同账号第二次登录,该不该踢掉旧连接?踢了之后,旧连接迟到的断开事件又会不会把新会话一起删掉?本节给 aegis 写 Registrar,把「先摘旧、后关旧、再通知」的顺序钉死,用 go test -race 复现那个最经典的顶号竞态,并讲清「踢人」与「迁移」两种语义的区别。
tag
同账号第二次登录,该不该踢掉旧连接?踢了之后,旧连接迟到的断开事件又会不会把新会话一起删掉?本节给 aegis 写 Registrar,把「先摘旧、后关旧、再通知」的顺序钉死,用 go test -race 复现那个最经典的顶号竞态,并讲清「踢人」与「迁移」两种语义的区别。
一个账号、一条连接、一份会话,三者一旦不同步就会出顶号误删、幽灵在线这类经典事故。本节给 aegis 写一张连接与账号的双向索引表:单锁版本、64 分片版本,以及顶号时「先摘旧、后关旧」的原子替换顺序,全部用 go test -race 真跑验证,并给出本机并发的实测数字。
玩家掉线后必须能秒回游戏,但又不能凭一个账号名就免密登入。本节给 aegis 设计一张重连票据:用 HMAC-SHA256 签发、绑定账号与连接序号、带过期时间、且只能使用一次。我们用标准库把它从零写出来,并用测试逐一证明正常校验、签名被篡改、过期、以及同一票据二次使用这四种路径的行为。
长连接最大的敌人不是掉线,而是你以为它还活着。本节给 aegis 写一个真实的 WebSocket 服务端:一条读超时、一个看门狗、一路可选的服务端 ping,然后用真起端口、真连的 L2 测试验证「不发心跳的客户端会被主动关闭并清理会话」,最后报告本机实测的超时精度与 1000 个空连接的心跳开销。
浏览器与小程序只能发起 HTTP 请求,MMO 网关的长连接入口因此多半是 WebSocket。本节不引任何第三方库,用标准库手写 RFC 6455 里网关真正需要的一小块:从 Sec-WebSocket-Key 拼 GUID、SHA-1、Base64 回 101,到帧头的 FIN、opcode、MASK 与 126/127 扩展长度,再到客户端必须掩码的逐字节 XOR 与分片重组。
本节用一个固定种子的确定性弱网模拟器,把 5.2 的 KCP 放在 0%/1%/5%/20% 四档丢包下实测:记录每条消息的端到端延迟 P50/P99 与冗余发送率,并与 5.1 的单流 TCP 式基线正面对比。同一丢包率下,KCP 靠快速重传把 P99 明显压低,代价是 20% 丢包时约 1.5 倍的冗余发包。
本节从零实现一个教学版 KCP:24 字节报文头、PUSH/ACK/WASK 三种命令、基于 SRTT 与 RTTVAR 的 Jacobson RTO 估计、una 累积确认的滑动窗口、以及靠重复 ACK 触发的快速重传。接口只有 Send/Recv/Input/Update 四个,五个测试覆盖正常收发、丢包重传、乱序重组、重复去重与 RTO 收敛。
TCP 在实时对战里有三个硬伤:队头阻塞让一个丢包卡住后面所有消息,重传策略不区分消息重要性,拥塞控制在弱网下退让过猛。本节写一个确定性模拟器,在同一丢包率与 RTT 下对比单条有序流与多条独立通道的延迟分布,把队头阻塞的代价变成可以复现的数字。
同一条移动通知,用 JSON、手写扁平化协议与手写 varint 二进制分别编解码,本机跑出 ns/op、B/op 与 allocs/op。JSON 编解码要几百纳秒和几次堆分配,手写二进制只要十几到二十几纳秒、零到一次分配。本节给出真实数字表格与选型判据,并说明 MessagePack 与 Protobuf 为什么不在本节实测。
编解码一半时间花在拷贝上。本节对照 Go 与 Java 两种零拷贝:Go 侧用切片视图在同一底层数组上切出 payload,改一个字节即可证明不复制;Java 侧用 JDK 自带 ByteBuffer 的 slice/duplicate/asReadOnlyBuffer,并在 Temurin 21 容器里真跑。最后给出切片够用与必须池化的判定。
二进制编解码是 MMO 服务端最底层的性能开关。本节从零写一个 internal/codec 包:Writer 按协议顺序压入 uint8/16/32/64、varint、字符串与字节块,Reader 逐字段读取并对每次越界返回错误而不是 panic;再用它编解码真实的 MoveNotify 结构体,test 覆盖往返一致、varint 边界与越界读取。
《MMO 服务器开发》卷一《网络与并发内核》全书导航页:说明本卷定位、读者画像与前置知识、学习节奏与三条阅读路径,交代贯穿游戏《晨曦大陆》与从零服务端 aegis,讲清 Go 主线加 Java 对照的双栈口径与 L1–L4 可运行性分级,并按六个部分列出 18 章 54 节的完整章节索引,另含序、前言、附录 A–D 与常见问题速查表,方便读者按需定位、跳读与随时查阅。
《MMO 服务器开发》卷一《网络与并发内核》全 61 篇,用 Go 从零实现 MMO 服务端 aegis 的网络底座:从分层与职责边界起步,经 I/O 模型与 Reactor、goroutine-per-connection 与 Java 虚拟线程对照、协议头与长度前缀、粘包拆包、二进制编解码、可靠 UDP 与 KCP、WebSocket 长连接、会话表与网关、消息路由、时间轮、Actor 邮箱、幂等去重、状态同步与帧同步、客户端预测与服务端校正、时间同步与延迟补偿、服务器权威与反作弊、协议 fuzz 与封包加密,最后装配成一个能登录、能移动、能广播的最小服务器,并跑出第一组真实承载数字。
《MMO 服务器开发》卷一序言:在框架随手可得的今天,为什么还要从零写一个 MMO 服务端。文章用三个真实场景说明「自己写一遍」换来的判断力,讲清本套书为什么选 Go 主线加 Java 对照、这一卷写什么不写什么、它和站内既有游戏服务端专题的分工,并给读者一段开始前的鼓励。
《MMO 服务器开发》卷一前言:交代本卷的技术选型与环境准备。列出实测的 Go 1.26 与 JDK 21 版本、Docker 拉取 Java 对照环境的完整命令与挂载坑、从零搭建 aegis 工作区的步骤,解释代码块头的 file/tier/verify 三行约定与 L1–L4 加 B 的可运行性分级,并给出 macOS 文件描述符上限、accept backlog、命令统计工具等实测踩坑清单。
客户端永远比服务端慢半步:你发了新版本,总有人还没更新。本节处理协议演进——登录时上报版本、服务端回带可用区间,以及字段级的向后兼容解码(新增字段填默认值、未知字段安全跳过),并真跑老客户端对新服务端、新客户端对老服务端两个方向的兼容用例。
TCP 是字节流,send 的边界不会传到对端——粘包与拆包由此而来。本节用长度前缀把流切成帧:给出 wire 布局、可复用缓冲的编码器、逐字节补齐的解码器与不可信长度的上限防护,并真跑一组覆盖粘包、拆包、截断、超长与畸形长度字段的测试用例。
字节流本身没有消息边界,双方必须先约定头几个字节表示什么。本节定下 aegis 的固定消息头——长度、命令号、序号与标志位四个字段,讲清「高字节模块号 + 低字节序号」的命令号分配策略、标志位的位域压缩,并给出可运行的 header.go 与边界测试。
把 Go 的 goroutine、JDK 21 的虚拟线程与 Netty 的 EventLoop 放进同一张表:三者的栈内存模型、调度器和 GC 交互各有什么不同。10 万个执行体的实跑对照给出真实数字,最后落在「什么条件下选哪条路线」的可判定结论上。
一个连接一个 goroutine 的成本到底在哪:2KB 起始栈、GMP 调度、netpoller 挂起与唤醒。本节真起进程跑 2000 条并发 TCP 连接,量出连接建立耗时、RTT 的 P50/P99 与吞吐,再用 Little 定律把延迟读成容量。