必须设 socket.binaryType = 'arraybuffer',否则 Uint8Array 会被转为 UTF-8 文本帧导致乱码;需在 onopen 中立即设置;'arraybuffer' 优于 'blob';推荐用 protobuf-ts 替代 protobuf.js。
必须设 socket.binaryType = 'arraybuffer',否则发出去就是乱码
websocket 默认把 send() 的任何值都转成 utf-8 文本帧,哪怕你传的是 uint8array。浏览器会默默调用 .tostring(),结果是把二进制字节当字符解释,服务端收到的就是一堆无法解析的垃圾。
正确做法是在 onopen 回调里立刻设置:
socket.onopen = () => {
socket.binaryType = 'arraybuffer'; // 必须写在这儿,不能晚于第一次 send()
};
- 如果在
send()之后才设,前面那几帧已经按文本发出去了 -
'arraybuffer'比'blob'更适合 Protobuf:解码时可直接构造Uint8Array,不用额外读取 Blob 内容 - 服务端(如 Node.js 的
ws库)收到后,event.data是Buffer;FastAPI 中需检查data.get("bytes")字段
前端生成代码别用 protobuf.js,选 protobuf-ts
protobuf.js 包体积大(压缩后仍 >20KB),且运行时需动态加载、反射解析,移动端首屏加载和低端机解析都慢。而 protobuf-ts 是编译期生成纯静态类,无运行时依赖,TS 类型安全,打包后通常
实操要点:
- 定义
player.proto后,用命令:npx protoc --plugin=protoc-gen-ts --ts_out=. player.proto - 发送时:
new Player({x: 10, y: 20}).encode().finish()→ 返回Uint8Array - 接收时:
Player.decode(new Uint8Array(event.data))→ 直接得 JS 对象 - 开发期加
enableReflection = false,避免意外触发反射逻辑
Protobuf 协议设计不省流,光靠工具没用
很多团队以为“用了 Protobuf 就自动省流量”,其实错在协议层。比如字段全用 int32、嵌套三层 message、每条包带完整 timestamp 字符串——这些都会抵消二进制优势。
真正省流的关键设计:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 高频字段(如位置更新)必须加
[packed=true],连续数值编码成 varint 流,比一个个字段头+值小 40%+ - 整型优先用
sint32:-1 编码为 1 字节(0x01),int32则要 5 字节 - 禁用
int64和uint64:JS 无法精确表示,序列化后变字符串,反而更大 - 每条消息开头塞 1 字节类型 ID(如
0x01表示心跳,0x02表示指令),避免服务端用反射查类型 - 不传图片、音频、base64 字符串——这些该走 CDN 或分片上传,Protobuf 只负责控制信令和状态摘要
弱网下二进制丢包/截断,不校验就崩溃
JSON 出错顶多 JSON.parse error 报个错,Protobuf 二进制一旦少几个字节,decode() 直接抛 InvalidProtocolBufferError,而且错误信息极难定位是哪一帧、哪个字段出问题。
必须加三道防线:
- 客户端解码前先检查长度:
if (event.data.byteLength (最小合法包长,含类型ID + 至少1字段) - 服务端返回错误时,统一用固定格式二进制包:
new Uint8Array([0xFF, errorCode]),前端onmessage里单独判断data[0] === 0xFF - 单条消息超 50KB 自动分片:每片加 2 字节序号头(大端),客户端拼完再 decode,避免 TCP 重传放大延迟
最容易被忽略的是分片重组逻辑——它不在 Protobuf 或 WebSocket 规范里,得自己实现,且必须带超时和去重,否则弱网下会卡死或错乱。

















