跳转至

Map 频道二进制协议(v0.1 冻结)

线上传输格式说明。帧类型/负载字段的语义名与编码的权威字典是业务层两枚枚举: `packages/demo/src/Protocol/FrameType.php`(帧类型)与 `packages/demo/src/Protocol/PayloadKey.php`(负载字段)。 编码一经发布不得复用(客户端与服务器共享契约);新增帧/字段必须同步两端。

1. 双通道并存

频道 序列化 用途
Map(18081 战斗直连) `BinaryBatchSerializer`(二进制,枚举压缩) 移动/战斗/视野/拾取
Social(Gateway 18285) `JsonSerializer`(自描述 JSON) 登录/账号/社交
  • 服务器到 Map 客户端:每连接每帧恰好一个二进制批量包(`FrameMerger::drain` 一包多帧)。
  • 客户端到服务器:请求以「批量含 1 帧」的批量包发送;`MapServer::dispatchSafe` 校验恰好 1 帧,否则 422。
  • 单测/降级路径可用 `JsonBatchSerializer`(JSON 数组批量或单帧对象,与二进制同构):harness 与 JSON 社交层互不干扰。

2. 批量包布局(全部大端)

``` [4B 魔数 "NX" + 0x00 0x02] [4B 帧数 count] { 逐帧: [4B 帧长 len] [len 字节帧体] } ```

3. 帧体布局

``` [2B 字段数] { 逐字段: [2B keyCode] [1B valueType] [值负载] } ```

保留 keyCode(高位段,负载字段从 1 起自由分配):

keyCode 字段 类型 说明
0xF3 type STRING 恒有;帧类型名(如 `entity_moved`)
0xF2 requestId STRING 可选;有值才编码
0xF1 timestamp FLOAT 可选;当前默认不编码(客户端以帧边界为时间基准)

4. 值类型(valueType)

类型 负载
0x00 NULL
0x01 INT 有符号 64 位(pack('q'),机器序)
0x02 FLOAT 双精度(pack('d'))
0x03 STRING [1B 长度] [UTF-8](≤255B)
0x04 STRING32 [4B 长度] [UTF-8](>255B)
0x05 LIST [4B 元素数] { 每元素 [1B 元素类型] [值负载] }
0x06 POS [2B int16 x] [2B int16 y](坐标专用,仅 payload['position'] 形状 `{x:int, y:int}`)
0x07 EMPTY_STRING 无(空串)
0x08 TYPE_CODE [1B typeCode](仅 type 字段;词表 ProtocolVocabulary 反查,码值经 manifestVersion 对齐)
0xF0 / 0xF1 TRUE / FALSE

5. 失败路径(快速失败,强制维护枚举)

  • 编码时未知帧类型/未知负载字段 → `ProtocolException`(业务代码必须扩展对应枚举)。
  • 解码时未知 keyCode/值类型/魔数不匹配/截断 → `DecodeException`;`MapServer` 回 400/422 错误帧。 截断为穷尽拒绝:帧长声明超出缓冲在帧级首道闸即拒(`帧体越界`),LIST 元素字节缺失按截断抛错—— 绝不静默补 null(2026-09 修复;回归 `testTruncatedListElementsThrowInsteadOfNullPadding` 与 `testFrameLengthBeyondBufferThrows`,异常面 40 档截断对拍见 `benchmarks/probe-protocol-ab.php`)。

6. 实现位置

  • 引擎:`packages/engine/src/Protocol/BinaryBatchSerializer.php`(编解码)、`ProtocolVocabulary.php`(词表)、 `BatchSerializerInterface.php`(批量契约,extends SerializerInterface)、`JsonBatchSerializer.php`(JSON 兼容批量)。
  • 业务字典:`packages/demo/src/Protocol/FrameType.php`、`PayloadKey.php`(中英双语注释)。
  • 工厂:`packages/demo/src/Protocol/MapCodec.php`(由两枚枚举组装词表,返回二进制序列化器)。
  • 传输:`WorkermanWebSocketServer::handleConnect` 设置 `websocketType = BINARY_TYPE_ARRAYBUFFER`(二进制 WebSocket 帧)。

7. 协议版本与清单对齐(ADR-030,v2)

  • v2 = 当前唯一线上形态(魔数尾字节 0x02):type 字段恒以 0x08 TYPE_CODE 携带 1B 码值(词表反查), 明文帧类型编码已于一次切换中退役、不留 v1 兼容——v1 包(魔数 0x01 / 明文 type)一律 DecodeException 拒绝。
  • 握手协商三件套:客户端 auth 携带 version(v2 起缺省 2);服务端 auth_ok 回带 version + manifestVersion (typeCodes+keyCodes 双码表 CRC32 指纹,MapCodec::manifestVersion());客户端与编译期生成物比对, 不一致即断开升级(A 模型:拒绝而非适配)——杜绝「能解码字节、不会处理事件」的静默裂缝。
  • 码表同步链路FrameType/PayloadKey 枚举为唯一事实源 → generate-definitions.php 派生 .d.ts/TS; nythros-client.js 码表按铁律手工同步;跨语言黄金向量(同一条 hex)钉死在 BinaryBatchSerializerTest::testV2GoldenBytesMatchClientJsCrossEncodercodec.test.mjs 两侧—— 改动 wire 格式必须两端同步重生成。
  • 清单演进纪律:码值一经发布不得复用/改义;加新事件 = 枚举末尾追加 → manifestVersion 变化 → 客户端随版本同步升级。 varint 化 INT、1B keyCode、位图事件打包(EVENT_BUNDLE)为后续提案,均须走 ADR。