Skip to content

通信协议设计

协议设计是网络游戏的"语言"——它决定了客户端和服务器如何对话。设计不当的协议会导致性能瓶颈、安全漏洞,甚至无法支持新功能。

参考书籍:本章基于《网络游戏核心技术与实战》(中嶋谦互)的协议设计框架,结合《百万在线》中的高并发协议实践。


1. 协议设计为什么重要

协议设计是游戏服务器开发中最容易被低估的环节。很多团队在项目初期随便定义了一些 JSON 接口,等到游戏上线后才发现:消息体积太大导致带宽不够、协议不兼容导致老版本客户端无法更新、没有版本管理导致接口混乱。

协议一旦上线就很难修改——因为你要同时兼容新老版本的客户端。所以协议设计需要"向前看",预留扩展空间,考虑未来的业务需求。

一个反面案例:某手游在 v1.0 版本使用了简单的 JSON 协议,消息 ID 就是字符串(如 "login"、"move")。等到 v3.0 需要支持跨服玩法时,发现字符串消息 ID 无法高效路由,不得不做一次痛苦的协议迁移。如果一开始就用数字编码的模块化消息 ID,这个问题根本不会出现。

1.1 协议设计的三个原则

  1. 可扩展:新功能不需要修改已有协议,只需要添加新消息
  2. 高效:消息体积尽可能小,编解码速度尽可能快
  3. 安全:防篡改、防重放、防刷

2. 协议的八种类型

网络游戏中的协议可以按传输方向、可靠性和使用场景分为八种基本类型。理解这八种类型,才能为不同的业务场景选择合理的协议。

2.1 八种类型总览

类型方向可靠性典型场景传输层
请求-响应C→S→C可靠登录、背包操作、商店购买TCP/WebSocket
服务端推送S→C可靠系统公告、邮件、任务完成TCP/WebSocket
广播通知S→C不可靠聊天消息、世界公告UDP
实时同步C↔S不可靠位置同步、战斗状态UDP
心跳包C↔S不可靠连接保活、延迟探测UDP
确认包C↔S可靠操作确认、数据校验TCP
批量同步S→C可靠登录初始数据、全量同步TCP
跨服消息S↔S可靠跨服匹配、跨服聊天gRPC/TCP

2.2 为什么需要八种类型

不同业务场景对"可靠性"和"延迟"的需求完全不同:

  • 登录操作:需要可靠,延迟可以稍高。用请求-响应协议。
  • 位置同步:需要低延迟,偶尔丢包可以接受。用 UDP 实时同步。
  • 系统公告:需要可靠(不能丢),但延迟不敏感。用服务端推送。

如果你把所有业务都用 TCP 请求-响应来做,位置同步的延迟会很高(TCP 的重传机制会导致延迟抖动)。如果你把登录也用 UDP 做,登录失败时你不知道是网络问题还是服务器拒绝——因为 UDP 不保证送达。

2.3 选型决策树

需要可靠传输?
  ├── 是 → TCP/WebSocket
  │        ├── 客户端发起? → 请求-响应
  │        └── 服务器发起? → 服务端推送 / 批量同步
  └── 否 → UDP
           ├── 高频小包? → 实时同步
           └── 低频通知? → 广播通知

需要跨服通信?
  └── 是 → gRPC / 内部 TCP

3. API 规范设计

3.1 消息命名规范

好的命名规范让协议自解释——看到消息 ID 就知道它是哪个模块的什么操作。常用的命名格式是:模块_动作_方向

模块说明消息 ID 前缀
AUTH认证相关0x01
PLAYER玩家相关0x02
BATTLE战斗相关0x03
CHAT聊天相关0x04
SHOP商店相关0x05
GUILD公会相关0x06
MAIL邮件相关0x07
ACTIVITY活动相关0x08

方向标记:REQ(请求)、RSP(响应)、NTF(通知)、SYNC(同步)。

示例:AUTH_LOGIN_REQ = 认证模块的登录请求,消息 ID = EncodeMsgID(0x01, 0x0001, 0x01)

3.2 消息 ID 编码

消息 ID 的编码方式推荐:模块(8bit) + 动作(16bit) + 类型(8bit)。这样做的好处是:

  • 8bit 模块 = 最多 256 个模块,足够
  • 16bit 动作 = 每个模块最多 65536 个动作,足够
  • 8bit 类型 = 4 种消息类型(REQ/RSP/NTF/SYNC),足够

消息 ID 是 uint32 类型,总共 4 字节,在网络传输中非常高效。

3.3 错误码设计

错误码应该包含足够的信息,让客户端知道"出了什么问题"以及"怎么告诉玩家"。推荐格式:模块(8bit) + 错误类型(8bit) + 错误号(16bit)

错误类型通常分为:参数错误(0x01)、逻辑错误(0x02)、系统错误(0x03)。这样客户端可以根据错误类型做不同的处理——参数错误提示"请检查输入",逻辑错误提示具体原因,系统错误提示"请稍后重试"。


4. 包格式设计

4.1 通用包头设计

包头是协议的"信封",包含了路由、校验、控制等元信息。一个好的包头设计应该包含:

字段大小说明
Magic2 bytes魔数,用于快速识别有效包(如 0x474D = "GM")
Version1 byte协议版本号
Flags1 byte标志位(压缩/加密/分片/优先级)
MsgID4 bytes消息 ID
SeqNum4 bytes序列号(用于防重放和消息去重)
BodyLength4 bytes消息体长度
Checksum4 bytes校验和

包头固定 20 字节。为什么要用魔数?因为 TCP 是流式协议,没有"包"的概念。客户端收到的是一串字节流,你需要一个标记来告诉它"从这里开始是一个完整的包"。如果第一个字节不是魔数,说明数据错位了,需要跳过一个字节重新寻找。

4.2 大消息的分片处理

当消息体超过 MTU(通常 1500 字节)时需要分片。分片的关键是:每个分片都要携带分片信息(分片 ID、总分片数、当前分片索引),接收方需要缓存所有分片,收齐后重组。

分片重组的一个常见陷阱是:超时清理。如果某个分片一直不到(网络丢包),重组器会一直占用内存。需要设置超时时间,超时后丢弃不完整的分片组。


5. 序列化格式的选择

5.1 Protobuf:游戏协议的首选

Protobuf 是 Google 开发的序列化框架,是游戏协议的首选。它的核心优势是:

  • 体积小:比 JSON 小 60-80%
  • 速度快:编解码速度比 JSON 快 5-10 倍
  • 强类型:有 .proto 定义文件,编译时检查
  • 代码生成:自动生成各语言的编解码代码

Protobuf 的缺点是:不可读(二进制格式)、需要预定义 schema、调试时不如 JSON 方便。但对于游戏协议来说,这些缺点远小于优点。

5.2 何时用 JSON,何时用 Protobuf

场景推荐格式原因
游戏核心逻辑Protobuf体积小、速度快
登录/支付接口JSON可读性好、调试方便
管理后台 APIJSON浏览器原生支持
日志/监控JSONELK 生态支持
实时同步数据Protobuf/FlatBuffers性能要求高

5.3 二进制编码技巧

对于性能要求极高的场景(如位置同步),可以使用手动二进制编码而非 Protobuf。关键技巧包括:

  • Varint 编码:小数字用更少的字节(1-10 使用 1 字节,128 以上使用 2 字节)
  • 坐标压缩:将浮点坐标转为定点整数(精度 0.01,范围 -10000~10000)
  • 位域编码:用一个字节存储 8 个布尔值(如角色状态标记)

6. 压缩技术

6.1 压缩策略选择

不是所有消息都需要压缩。压缩有 CPU 开销,对于小消息来说可能得不偿失。

消息类型判断:
  ├── 小消息(< 64 bytes)→ 不压缩
  ├── 中等消息(64-256 bytes)→ 根据频率决定
  ├── 大消息(> 256 bytes)→ 压缩
  └── 批量数据 → 需要压缩

压缩算法选择:
  ├── LZ4 → 速度快,压缩率低(适合实时消息)
  ├── zlib → 平衡(适合一般消息)
  ├── zstd → 压缩率高,速度适中(适合大文件)
  └── Snappy → 极快,压缩率最低(适合日志)

6.2 Delta 压缩(增量同步)

Delta 压缩是游戏同步中最重要的优化之一:只发送变化的数据,而非全量数据

例如,一个玩家的位置同步消息包含:坐标(x,y,z)、旋转(rotation)、速度(velocity)、状态(state)。如果只有坐标变化了,只需要发送坐标,不需要发送其他字段。这可以将消息体积减少 50-80%。

实现方式是:服务端保存每个玩家的"上次同步状态",每次同步时计算差异,只发送差异部分。如果差异超过全量的 50%,就发送全量(避免差异本身太大)。

6.3 消息合并压缩

将多个小消息合并为一个大消息后再压缩,可以显著减少网络开销。因为每个 TCP 包都有固定的头部开销(约 40 字节),发送 10 个小消息(每个 10 字节)比发送 1 个大消息(100 字节)多了 360 字节的头部开销。


7. 加密与安全

7.1 加密层次

┌─────────────────────────────────────┐
│           应用层加密                 │
│   (消息签名、防篡改)                  │
├─────────────────────────────────────┤
│           传输层加密                 │
│   (TLS/DTLS)                        │
├─────────────────────────────────────┤
│           网络层加密                 │
│   (IPSec/VPN)                       │
└─────────────────────────────────────┘

推荐策略:一般游戏使用 TLS 传输加密 + 消息签名;高安全需求使用 TLS + AES 应用层加密 + 消息签名。

7.2 防重放攻击

重放攻击是指攻击者截获一个合法消息,然后重复发送。比如截获一个"购买"消息,重复发送 100 次,就买了 100 份。

防御方式是序列号 + 滑动窗口:每个消息有一个递增的序列号,服务端维护一个"已处理序列号"的位图。如果收到的序列号已经处理过,就拒绝;如果超出窗口范围,也拒绝。

7.3 速率限制

速率限制是防止客户端发送过多消息导致服务端过载的关键机制。可以使用令牌桶算法:每个客户端有一个令牌桶,桶里有一定数量的令牌,每秒补充一定数量。发送消息消耗令牌,令牌不足时拒绝消息。


8. P2P 架构与网络对战

8.1 P2P vs C/S 架构对比

特性C/S 架构P2P 架构
权威方服务端(单一权威)任一客户端(去中心化)
延迟客户端→服务器→客户端(双跳)客户端↔客户端(单跳)
带宽成本高(服务端承担所有转发)低(客户端直连)
反作弊容易(服务端权威校验)困难(客户端互相校验)
适用场景MMO、MOBA、FPS(竞技)格斗、赛车、休闲对战

8.2 混合架构

大多数现代游戏使用混合架构:P2P 直连传输游戏数据 + 协调服务器处理匹配、房间管理、NAT 穿越。这种架构兼顾了 P2P 的低延迟和 C/S 的可靠性。

8.3 NAT 穿越

NAT 穿越是 P2P 架构的核心挑战。NAT 类型决定了两个客户端能否直接通信:

NAT 类型Full ConeRestricted ConePort Restricted ConeSymmetric
Full Cone
Restricted Cone
Port Restricted
Symmetric

NAT 穿越的常用技术:STUN(发现公网地址)、TURN(中继转发)、ICE(综合方案)。当 P2P 直连失败时,需要回退到 TURN 中继服务器。


9. 协议设计 Checklist

□ 1. 消息命名规范统一(模块_动作_方向)
□ 2. 消息 ID 分配合理,预留扩展空间
□ 3. 错误码设计清晰,便于调试
□ 4. 包头包含魔数、版本、长度等关键字段
□ 5. 大消息支持分片和重组
□ 6. 高频消息使用 UDP + 可靠性层
□ 7. 消息体支持压缩(阈值 + 算法选择)
□ 8. 关键操作使用 AES 加密
□ 9. 所有消息支持签名验证
□ 10. 实现防重放保护
□ 11. 实现速率限制
□ 12. 协议版本管理,支持旧版兼容
□ 13. 编写协议文档和测试用例
□ 14. 性能测试:编码/解码速度、压缩率
□ 15. 安全审计:加密强度、签名算法

下一步

协议设计完成后,接下来需要深入游戏逻辑的实现细节:从实体管理到状态同步,从战斗系统到 AI 系统,每一个模块都需要精心设计才能支撑大规模在线游戏的运行。

游戏后端知识体系