Showing Posts From
Protobuf
Proto3 基础:message / enum / repeated 和订阅协议模式
行情推送、IM、游戏后端这类双向长连接服务,常见的做法是走 TCP/WebSocket + Protobuf 编码。Proto3 定义消息的基础语法不多,但组合起来能表达很复杂的协议。按一个典型的"订阅推送"协议模式走一遍。 一个订阅推送协议的骨架 先看一个典型定义(脱敏后的通用示例): syntax = "proto3";package sample.pushv1;// ============ 消息类型枚举 ============ enum MsgID { MSG_UNKNOWN = 0; MSG_HEART_BEAT = 1; MSG_AUTH = 2; MSG_SUBSCRIBE = 10; MSG_UNSUBSCRIBE = 11; MSG_DATA_BROADCAST = 20; MSG_STATUS = 30; MSG_ERROR = 99; }enum SubscribeFlag { SUBSCRIBE = 0; KEEP = 1; // 保持订阅(心跳) UNSUBSCRIBE = 2; }// ============ 主消息体 ============ message Envelope { MsgID msg_id = 1; sint32 seq = 2; // 双工:request 客户端来,response 服务器来 Request request = 4; repeated Response response = 5; // 备用 JSON 通道(调试用) string json_req = 8; string json_resp = 9; string err_msg = 17; }message Request { repeated string codes = 1; SubscribeFlag flag = 2; Auth auth = 3; }message Response { repeated DataField data = 1; sint32 err_code = 2; string msg = 3; }message Auth { string apptype = 1; bytes token = 2; }message DataField { string code = 1; fixed64 ts = 2; double value = 3; }看着长,本质就四件事:枚举定消息类型 → 主 envelope 包一层 → Request / Response 承载数据 → 心跳保活。 核心语法 message 数据结构声明,类似 struct: message User { int32 id = 1; string name = 2; bytes data = 3; }每个字段有唯一 tag(= 1, = 2 ...),wire format 上按 tag 编码,改字段名不影响兼容。 enum enum Color { COLOR_UNKNOWN = 0; // proto3 里第一个必须是 0 RED = 1; GREEN = 2; BLUE = 3; }Proto3 强制第一项 = 0(作为默认值)。 repeated 数组 / 列表: message Order { repeated string items = 1; repeated User buyers = 2; }对应各语言里的 List<T> / []T。 常用标量Proto3 类型 说明 对应 Java Goint32 变长整数(负数编码差) int int32sint32 变长整数(负数省字节) int int32uint32 无符号变长整数 int uint32fixed32 固定 4 字节(大数字省) int uint32sfixed32 有符号固定 4 字节 int int32float 4 字节浮点 float float32double 8 字节浮点 double float64boolboolean boolstring UTF-8 字符串 String stringbytes 二进制 ByteString []bytetimestamp 别用 int32——2038 年就爆。用 int64 或 google.protobuf.Timestamp。 oneof(互斥字段) message Notification { string title = 1; oneof body { TextBody text = 2; ImageBody image = 3; VideoBody video = 4; } }三选一,永远只有一个字段被设置。适合"消息内容多种类型"这种场景。 map map<string, int32> counts = 1;底层其实是 repeated Entry,但语法糖后用起来就是 map。 订阅推送协议的常用模式 1. 一个大 Envelope 包一层 避免为每种消息设 endpoint,客户端 / 服务端都只处理一种 message,靠 msg_id 分发: switch (envelope.getMsgId()) { case MSG_HEART_BEAT: handleHeartbeat(envelope); break; case MSG_SUBSCRIBE: handleSubscribe(envelope.getRequest()); break; case MSG_DATA_BROADCAST: handleData(envelope.getResponse()); break; }好处:加新消息只加个 enum 值 + 新 case,不用改协议框架。 2. Request / Response 分离 一个 envelope 同时能塞 request(客户端来)和 response(服务器来)。虽然一次只填一边,但复用同一个消息类型简化编解码。 3. 心跳保活 TCP 长连接会被 NAT / 中间设备清理。定期发 MSG_HEART_BEAT 保持连接: 每 30 秒 → client → { msg_id: MSG_HEART_BEAT, seq: N } <-- server ack或者复用订阅消息:SubscribeFlag = KEEP 表示"我还在,别断"。 4. 序列号 seq 请求带 seq、响应带同样 seq——做请求 - 响应对齐。异步双工协议里没这个就乱了。 5. 备用 JSON 通道 json_req / json_resp 备用字段——调试时可以传 JSON,服务端也返 JSON,跳过 proto 编解码。方便排查协议问题。 编译 # 装 protoc brew install protobuf # macOS apt install protobuf-compiler # Ubuntu# 生成代码 protoc --java_out=./gen-java --go_out=./gen-go quote.proto各语言都有 protoc 插件,一个 .proto 文件能同时给 Java / Go / Python / Kotlin / TS 用。这是 Protobuf 最大的价值。 常见坑 1. 默认值不会传输 Proto3 里字段等于默认值(0、空串、false)不会占字节——但也意味着接收端分不清"没设"和"设为 0"。 需要区分时用 oneof 或 google.protobuf.Int32Value 之类的 wrapper 类型。 2. tag 号一旦发布就不能改 改了 tag 号 = 完全不同的字段。老客户端拿到新数据会解析错误。 3. 删除字段用 reserved message User { reserved 3, 5; reserved "old_name"; int32 id = 1; // 不能再用 tag 3 或 5 }避免后人不小心用了同一个 tag。 4. proto2 vs proto3 syntax = "proto2"; 有 optional / required,字段能显式 null。syntax = "proto3"; 简化了很多,新项目都用 proto3。 一句话总结 订阅推送协议的常用模式:Envelope + MsgID enum + Request/Response + seq + 心跳。Proto3 的核心就是 message / enum / repeated / oneof / map。一份 .proto 编成多语言,是它最大的优势。
