Skip to content

MortarMQ v0 线协议 ​

版本 1.0(2026-09-30;冻结后修订)

本文是 v0 线协议的唯一事实来源。修订遵循冻结规则:只有写得出理由的改动才进本文,且下面每条规则自身站得住,不依赖任何引用。

1. 帧格式与长度语义 ​

text
Frame = u32_be length || u8 op || payload[length - 1]
length: number of bytes that follow — the op byte plus payload (NOT the total frame size)
valid range: 1..=MAX_FRAME_LEN
ConstantValueDescription
MAX_FRAME_LEN16 MiB (16777216)Frame cap (including the op byte); neither broker nor client may raise it
MAX_VALUE_LEN16 MiB − 1 KiB (16776192)Cap on PUB/MSG value; reserves 1 KiB for field headers + max key + future trailer fields. A 16 MiB value does not fit in a 16 MiB frame — the two caps must be staggered
MAX_KEY_LEN255 BKey cap (UTF-8 not enforced)
MAX_TOPIC_LEN255 BTopic-name cap (UTF-8)
MAX_ERR_MESSAGE512 BCap on ERR diagnostic text
MAX_OK_RESULT_LEN16 MiB − 6 B (16777210)Cap on OK result; reserves the op byte, request_op, and result_len
WIRE_VERSION1First field of CONNECT
Keepalive30 sThe server closes the connection after 1.5×30 s without a complete frame (semantics aligned with MQTT §3.1.2.10)
Half-frame timeout10 sClose on timeout; never attempt resynchronization
  • 所有多字节整数都是大端;v0 帧不带 CRC(TCP 校验和够用;安全边界见威胁模型)。
  • length=0、length=MAX+1、半帧、半帧超时全部是 golden 用例。
  • 背压 / 流控只在完整帧边界上执行 —— 绝不在帧中间停止读取。

2. Op 表(15 个 op:9 核心 + 6 扩展) ​

核心 op 0x01–0x09(客户端命令面 + 常用响应);扩展 op 0x0A–0x0F(投递 / 消费 / 流控面)。0x00 非法,必须断开连接;0x10–0x7F 预留给新业务 op;0x80–0xFE 预留给管理/内部 op;0xFF 非法。

计数规则: 「9 核心 op + 6 扩展 op = 15 op」。丢掉这个拆分的说法一律算错。

OpValueDirectionPayload (field order = byte order)Success responseMain errors
CONNECT0x01C→Sprotocol_version:u8, client_flags:u16, token_len:u16, token:bytes, reserved:u32OK0x0004 / 0x0006 / 0x0007
DECLARE0x02C→Stopic_len:u16, topic:bytes, partition_count:u16, flags:u16OK.result=topic_id:u16, partition_count:u16, created:u8 (0=CREATED, 1=IDEMPOTENT_OK)0x2001 / 0x2003
PUB0x03C→Stopic_id:u16, partition:u16, producer_id:u64, producer_seq:u64, flags:u16, key_len:u16, key:bytes, value_len:u32, value:bytesOK.result=sequence:u64, durability:u8, leader_epoch:u64, commit_seq:u64, result_flags:u8 (bit0=deduplicated)0x0010–0x0013 / 0x4001–0x4004 / 0x6xxx
SUB0x04C→Sgroup_len:u16, group:bytes, topic_id:u16, partition:u16, start_policy:u8, credit_count:u16, credit_bytes:u32, flags:u16OK.result=subscription_id:u16, member_id:u64, membership_epoch:u32, assignment_epoch:u320x0024 / 0x2002 / 0x2005
ACK0x05C→Ssubscription_id:u16, topic_id:u16, partition:u16, seq:u64, delivery_id:u64, ownership_epoch:u64, flags:u16OK0x0020 / 0x0022 / 0x0023
NACK0x06C→Ssubscription_id:u16, topic_id:u16, partition:u16, seq:u64, delivery_id:u64, ownership_epoch:u64, reason:u8, flags:u16OKsame as ACK
PING0x07C→SemptyOK—
ERR0x08bothcode:u16, request_op:u8, message_len:u16, message:bytesnone—
OK0x09bothrequest_op:u8, result_len:u32, result:bytesnone—
MSG0x0AS→Csubscription_id:u16, topic_id:u16, partition:u16, sequence:u64, delivery_id:u64, attempt:u16, ownership_epoch:u64, producer_id:u64, producer_seq:u64, flags:u16, key_len:u16, key:bytes, value_len:u32, value:bytesnone (client answers with ACK/NACK)—
PULL0x0BC→Ssubscription_id:u16, member_id:u64, membership_epoch:u32, assignment_epoch:u32, ownership_epochs_len:u16, ownership_epochs:[]u64, max_msgs:u16, max_bytes:u32, wait_ms:u16, min_bytes:u32OK(empty), or MSG×N then OK0x0020 / 0x0021 / 0x4001
HEARTBEAT0x0CC→Ssubscription_id:u16, member_id:u64, membership_epoch:u32, assignment_epoch:u32, state:u8 (0=ACTIVE, 1=LEAVE)OK0x0020 / 0x0026
NOTICE0x0DS→Cnotice_id:u64, kind:u8, group_id:u16, membership_epoch:u32, assignment_epoch:u32, ownership_epoch:u64, topic_id:u16, partition:u16, member_id:u64 (kind: 0=ASSIGN, 1=REVOKE)none (client answers with CONTROL_ACK)—
CONTROL_ACK0x0EC→Snotice_id:u64, notice_kind:u8, group_id:u16, membership_epoch:u32, assignment_epoch:u32, ownership_epoch:u64, result:u8 (0=OK, 1=STALE, 2=ERROR)OK0x0020
CREDIT0x0FC→Ssubscription_id:u16, membership_epoch:u32, ownership_epoch:u64, mode:u8, credit_count:u16, credit_bytes:u32, reserved:u32 (mode: 0=SET, 1=ADD)OK.result=granted_count:u16, granted_bytes:u32, outstanding_count:u16, outstanding_bytes:u32, credit_epoch:u640x0020 / 0x4005

2.1 字段语义说明 ​

  1. PUB 幂等字段:producer_id=0 && producer_seq=0 允许(非幂等发布,计入指标);producer_id=0 && producer_seq>0 被拒(0x0013);其余情况按 (topic_id, partition, producer_id) 窗口去重。
  2. MSG 携带 ownership_epoch:客户端不解释它 —— 只在 ACK/NACK 里原样回显,由接收端用来围栏 owner。attempt 首次投递为 1。MSG 是服务端推送而非响应,因此不占用单连接 FIFO 的请求槽(NOTICE 同理)。
  3. ACK flags bit0=PROGRESS:刷新工作截止时间,不消耗投递次数,也不归还 credit。此举取代 v0.1 的 NACK(reason=SLOW)(SLOW=3 已保留但未实现)。
  4. NOTICE 携带 notice_id:CONTROL_ACK 必须原样回显,这样乱序到达的控制确认才可归属。
  5. SUB 不隐式 DECLARE:对不存在的 topic 发 SUB 返回 0x0008。v0 中 partition 字段必须是 0xFFFF(归属由协调器分配);其他取值预留给 v1 的直连分区模式。credit_count 与 credit_bytes 一起设定初始 credit 窗口(默认 256 条 / 2 MiB)。
  6. 单连接 FIFO:请求与响应严格交错,靠 request_op 归属响应。v0 无 request_id(要并发就开多条连接)。
  7. 先认证:CONNECT 之前只允许 CONNECT/PING/ERR;第二次 CONNECT 被拒。
  8. topic_id 配对分配:DECLARE 成功时为源 topic 与其保留的 .dlq 孪生各分配一个 id——源取奇数槽、孪生取偶数槽(1,2 / 3,4 / …)。注册表按对计数,容量上限 32767 对(共 0xFFFF 个槽位),因此 topic_id 不保证跨 topic 连续。

3. 错误码注册表(全局注册) ​

注册表是查表而非叙述 —— 四个已注册段落共 51 个码,每行都带「客户端该做什么」列。它单独成页,为的是查它时不必读规范。

→ 错误码注册表 —— 0x0xxx 协议/会话 · 0x2xxx 元数据 · 0x4xxx 背压/配额 · 0x6xxx 复制。

段落按固定的四段划分;未注册的段一律保留。上面的码有两条规矩:BACKPRESSURE / QUOTA_EXCEEDED 落在 0x4xxx 而不是 0x000a/0x000b,让背压与配额码集中在同一段(wire_version=1 尚未发布,无兼容负担);复制占用 0x6001–0x600c。

4. 版本规则 ​

  1. 兼容变更:可选字段只能追加在载荷尾部 + 用 flags 位声明;旧对端忽略未知尾部。
  2. 破坏性变更:新增 op / 改字段顺序 / 改长度语义 → 递增 wire_version。
  3. 错误码只增不改:已发布码的语义永不变化,码也永不复用。

5. 范围边界 ​

  • 复制 RPC(APPEND/PREPARE/INSTALL/…)走 listen_peer,不属于本协议;客户端只会见到 0x6xxx 错误码。
  • 管理 HTTP(/metrics、/healthz、/stats)与 CLI 读路径不使用本协议。CLI 写路径(topics create/delete)复用本文定义的 DECLARE 等 op。

6. 修订历史 ​

DateChange
2026-09-309 ops → 15 ops consolidation; PUB gains producer_id/producer_seq; ACK/NACK gain subscription_id/delivery_id/ownership_epoch; new MSG/PULL/HEARTBEAT/NOTICE/CONTROL_ACK/CREDIT
2026-09-30Added MAX_VALUE_LEN = 16 MiB − 1 KiB, resolving the frame/value cap clash
2026-09-30Full error-code registry adopted; BACKPRESSURE/QUOTA moved to 0x4xxx; 0x600b added; AUTH_FAILED naming unified
2026-10-110x600c ELECTION_TIMEOUT added to the replication segment
2026-10-16topic_id 配对分配(源奇/孪生偶,32767 对);注册表 51 码:0x2007 MUTATION_NOT_PACKABLE
2026-09-30ACK flags=PROGRESS; NOTICE notice_id; SUB no implicit DECLARE + credit_bytes; SLOW=3 reserved