WebSocket最经典的用法是传JSON字符串,简单直观,调试方便。但当消息频率上来之后,比如单机每秒几千条推送,JSON的体积膨胀和解析开销就会暴露出来:一个包含嵌套对象的行情快照,JSON编码后可能有600字节,而换成二进制序列化可能只需要120字节。传输量直接砍掉八成,服务端CPU的编解码占用也明显下降。这篇文章围绕两个主流的二进制序列化方案——Protobuf和MessagePack,讲清楚它们的原理差异,并给出在WebSocket中落地的完整代码。

一、为什么WebSocket要改用二进制帧
WebSocket协议本身支持两种帧类型:文本帧和二进制帧。浏览器端的onmessage事件里,如果服务端发的是二进制帧,收到的就是ArrayBuffer或Blob,取决于binaryType属性的设置。很多团队一直用JSON.stringify加文本帧,问题主要有三个。
第一是体积。JSON的键名会随每条消息重复传输,字段越多浪费越明显。比如一个温度传感器上报,数据本体只有一个数值,但JSON里{"deviceId":"sensor-001","temperature":23.5,"humidity":45}光键名就占了三十多个字节。二进制方案要么用数字编号替代字段名,要么干脆去掉字段名,只留值。
第二是解析成本。JSON解析需要处理字符串转义、类型推断、对象分配,在V8里虽然已经高度优化,但和直接读取定长字节相比仍慢一个数量级。Protobuf的解码基本就是按varint规则读数字加一次对象构造,MessagePack解码也是查类型前缀后直接搬字节。
第三是数值精度。JavaScript的JSON.stringify对超过一定长度的整数会丢精度,而二进制协议可以用固定的64位整数表示,配合BigInt或long类型的polyfill处理,金融、计费类场景里这一点很关键。
二、Protobuf与MessagePack的核心差异
Protobuf是Google推出的强Schema协议。先写.proto文件定义消息结构,编译成各语言的类,字段用编号标识。它的优势是契约清晰:前后端共用一份定义文件,字段拼错在编译期就能发现;新增字段只要用新的编号,老客户端解析新消息时会自动跳过未知字段,版本兼容性天然好。劣势是工具链较重,改一次消息定义要重新生成代码。
MessagePack走的是另一条路,它本质上还是键值结构,只是把JSON的表示方式压缩成二进制。不需要预定义Schema,任何语言里一个字典丢进去就能编码。它的优势是接入成本几乎为零,动态语言里特别顺手;劣势是字段名(哪怕压缩过)仍在每条消息里传输,体积通常比Protobuf大百分之二十到五十,而且没有Schema约束,字段名写错只能在运行时发现。
两者还有一个容易被忽略的区别: Protobuf对整数的编码用了varint(变长整数),小数字只占一个字节,这对大量携带状态码、枚举值、ID的场景特别友好。MessagePack的整数是定长编码,正数最小一个字节,但大数固定占多字节。下面这个对比表可以作为选型参考:
| 维度 | Protobuf | MessagePack |
|---|---|---|
| Schema要求 | 必须定义proto文件 | 不需要,可选Schema |
| 体积效率 | 最优,varint压缩 | 较好,略逊于Protobuf |
| 跨语言 | 官方支持十几种语言 | 社区库覆盖广 |
| 动态性 | 弱,结构编译期固定 | 强,随编码随传 |
| 适用场景 | 长期维护的核心链路 | 快速原型、灵活结构 |
三、服务端与浏览器端代码落地
先看服务端,以Node.js为例,用protobufjs动态加载proto文件,WebSocket收到二进制帧后直接反序列化:
// message.proto:
// syntax = "proto3";
// message Tick {
// string symbol = 1;
// double price = 2;
// int64 volume = 3;
// int64 ts = 4;
// }
const protobuf = require('protobufjs');
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
protobuf.load('message.proto').then(root => {
const Tick = root.lookupType('Tick');
wss.on('connection', ws => {
ws.binaryType = 'arraybuffer';
ws.on('message', data => {
// 解码客户端发来的二进制帧
const msg = Tick.decode(new Uint8Array(data));
console.log('收到:', msg.symbol, msg.price);
// 编码并回发
const ack = Tick.encode({ symbol: msg.symbol, price: msg.price, volume: 100, ts: Date.now() }).finish();
ws.send(ack);
});
});
});
浏览器端的处理同样简单。关键是把binaryType设为arraybuffer,否则默认收到的是Blob,还得异步转换一次。protobufjs在浏览器里可以直接用.decode和.encode操作Uint8Array:
const root = await protobuf.load('/proto/message.proto');
const Tick = root.lookupType('Tick');
const ws = new WebSocket('ws://localhost:8080');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
const payload = Tick.encode({ symbol: 'BTC-USDT', price: 65000.5, volume: 2, ts: Date.now() }).finish();
ws.send(payload); // 直接发送Uint8Array,走二进制帧
};
ws.onmessage = evt => {
const msg = Tick.decode(new Uint8Array(evt.data));
console.log('服务端回包:', msg.symbol, msg.price);
};
如果选MessagePack,服务端换成@msgpack/msgpack这个库,写法甚至更短。值得注意的是发送端要传Uint8Array而不是字符串,WebSocket库检测到二进制类型才会使用二进制帧,这一点配错了就会退回文本帧,前功尽弃。
const { decode, encode } = require('@msgpack/msgpack');
ws.on('message', data => {
const obj = decode(data); // 二进制转对象
ws.send(encode({ ack: true })); // 对象转二进制
});
四、用AI加速协议定义与代码生成
接入二进制协议最繁琐的一环其实不是写收发代码,而是设计proto文件、决定字段编号、生成各端类型定义。这部分工作现在完全可以交给AI辅助完成。实践中比较有效的做法是:把业务消息的自然语言描述整理清楚,交给大模型生成初版proto定义,再让它同时产出服务端、浏览器端甚至移动端的编解码示例。
一个实用的提示词结构大致是:先描述业务场景和消息种类,再列出现有JSON报文的样例,最后说明目标语言和兼容性要求(比如要求新增字段不能破坏旧客户端)。模型通常会给出字段命名规范、合理的编号分配策略,以及reserved关键字的使用建议,避免编号被复用导致线上解析错乱。
需要提醒的是,AI生成的proto定义一定要人工审查两件事:一是字段编号是否有预留习惯,比如每加一个字段保留几个空位;二是optional语义在proto3里的变化,老版本默认scalar字段不序列化默认值,这会影响前端判断字段是否真的存在。建议把AI产出当成初稿,配合CI里的proto编译检查,形成生成、审查、落库的闭环,这样既快又稳。
五、踩坑记录与选型建议
落地过程中有几个高频坑值得记录。第一,浏览器和Node的Buffer与Uint8Array混用问题,ws库返回的可能是Buffer,protobufjs需要Uint8Array,虽然Buffer是其子类,但某些版本处理有差异,稳妥做法是统一用new Uint8Array(data)包一层。第二,代理层的问题,如果WebSocket经过了Nginx,确认proxy_buffering关闭,否则二进制帧可能被攒包,实时性反而变差。
选型上给一个直接的结论:长期维护、多端共用、字段相对稳定的核心链路,选Protobuf,Schema带来的工程收益远大于学习成本;快速验证、消息结构多变、以动态语言为主的内部系统,MessagePack半天就能接完。还有一种混合策略:控制面消息用JSON方便调试,数据面高频推送用二进制,通过消息第一个字节的魔数区分协议类型,浏览器端收到后按魔数分流解析,实现成本不高,却能兼顾调试效率和传输性能。
WebSocket二进制传输ProtobufMessagePack序列化修改时间:2026-09-06 04:42:43