当前位置: 首页 > news >正文

我为什么在 WebSocket 上坚持用二进制帧 + Protobuf,而不是直接传 JSON

我为什么在 WebSocket 上坚持用二进制帧 + Protobuf,而不是直接传 JSON

前段时间我在翻aq-chat-server的消息链路时,又一次确认了一件事:做实时聊天这类高频交互场景,协议层真的不该图省事。很多项目一上来就选 WebSocket + JSON,因为调试方便、前后端都熟。但这个项目没有这么做,而是在aq-chat-im/src/main/java/com/howcode/aqchat/codec/MessageDecoder.javaaq-chat-im/src/main/java/com/howcode/aqchat/codec/MessageEncoder.java里,明确用了[消息体长度 4 字节][指令号 2 字节][消息体 Protobuf 二进制]这套格式。

我现在反而越来越认同这种“看起来麻烦一点”的设计,因为它把消息边界、消息类型和业务负载拆得很干净,后面的 Netty 分发链路也因此简单很多。

先把协议边界固定住

MessageDecoder的处理方式很直接:先检查可读字节数够不够 6,不够就继续等;够了以后先读长度,再读指令号;如果当前帧里的消息体还没到齐,就resetReaderIndex()回退,等下次继续。这一段很关键,因为它实际上把“半包怎么办”这个问题封在了解码器里,而不是让后面的业务处理器感知一个“不完整消息”。

如果这里走 JSON,常见写法往往会变成“拿到一段字符串再想办法判断是不是完整对象”。这在 HTTP 请求里问题不大,但在长连接的高频双向通信里,其实很容易把协议边界和业务格式搅在一起。长度头的价值就在这里:消息有没有收完整,不靠猜,不靠扫括号,不靠找分隔符,直接按字节数判断。

指令号让分发成本保持稳定

这个项目还有一个我很喜欢的点,是它没有把“消息类型”塞到正文里,而是单独给了 2 字节的commandMessageRecognizer会在启动时扫描AQChatMsgProtocol里的内部类,把 Protobuf 消息类和枚举里的指令号关联起来,位置在aq-chat-im/src/main/java/com/howcode/aqchat/message/MessageRecognizer.java。这样一来,解码器拿到command之后,可以直接找到对应的 Builder,完成反序列化。

这一步看似只是少写几个if-else,但工程意义其实挺大。因为到了aq-chat-im/src/main/java/com/howcode/aqchat/handler/AQChatCommandHandler.java,入站对象已经是明确的GeneratedMessageV3子类了,后面只需要根据消息类去找ICmdHandler对应实现,不需要先 parse 一遍 JSON,再读type字段,再做一次路由。协议层、识别层、业务层的职责边界是清楚的。

我做这类服务时特别怕一种写法:网络层收到一坨 JSON,业务层自己判断action,再从 Map 里掏字段,最后每个 handler 都偷偷做一遍校验和转换。这样前期写起来快,后期加命令、查线上问题、做兼容时都会越来越乱。这个项目至少在消息入口这件事上,是把地基先打稳了。

二进制协议不只是“更快”,而是更可控

很多人提到 Protobuf,第一反应都是性能。这个当然没错,二进制更省流量,序列化反序列化也更直接。但如果只把它理解成“比 JSON 快”,我觉得有点低估它了。

对一个 IM 服务来说,更重要的是可控性。比如指令号固定之后,协议演进的入口就很明确;比如消息体是强类型的,服务端不会在运行时才发现某个字段名拼错;再比如MessageEncoder出站时也是同一套格式,意味着客户端和服务端在协议层天然对齐,而不是各写各的字符串约定。

这套设计当然不是没有代价。调试门槛会高一点,肉眼抓包不像 JSON 那么友好;新增命令也要同步改 proto、生成类、注册映射、补 handler,流程比“加个字段”重一些。但如果业务本身就是聊天、广播、房间同步、离线消息这些高频场景,这种代价我认为值得。因为它买来的不是某次 benchmark 上的几毫秒,而是协议不会随着功能变多逐渐失控。

我现在更在意的是链路稳定性

回头看aq-chat-server这条链路,其实最有价值的不是单个类写得多花,而是几个点串起来以后很顺:MessageDecoder负责切包和识别入口,MessageRecognizer负责把指令号映射到具体消息类型,AQChatCommandHandler负责把强类型消息交给真正的命令处理器。每一层都只做一件事。

这也是我现在做实时系统时越来越看重的一个标准:协议层是否能把复杂度挡在前面。只要入口是稳定的,后面的房间、离线、MQ、AI 扩展都还有整理空间;但如果入口本身就是松散文本协议,后面基本只会越来越难收拾。

所以这篇文章如果只留一个结论,那就是:我坚持二进制帧 + Protobuf,不是因为它“更高级”,而是因为它让长连接消息系统在规模变大之后,依然能维持比较低的认知成本。对聊天系统来说,这比一开始省下来的那点开发时间重要得多。

http://www.jsqmd.com/news/699983/

相关文章:

  • XGBoost多线程优化实战与性能调优指南
  • 如何用Seraphine实现终极英雄联盟BP自动化:告别手忙脚乱的对局准备
  • 解码器专用Transformer模型构建与Llama系列优化实践
  • 机器学习评估指标全解析:从原理到Python实战
  • Day02-02.张量和Numpy之间相互转换
  • Hermes-Agent:修复dingtalk不支持上传文件的问题
  • 百度网盘Mac版破解SVIP:3分钟实现下载速度70倍提升的终极方案
  • HunyuanVideo-FoleyAPI可观测性:Prometheus指标采集与Grafana看板
  • C语言基础-基本数据类型(2)
  • 2026网站建设需要多少钱?不同阵营网站制作报价区间
  • WPF转换与特效
  • GreaterWMS:5分钟部署完整的开源仓库管理系统终极指南
  • Unity WebCamTexture实战:从权限申请到区域截图,一个完整AR证件照项目的避坑实录
  • Java学习15
  • 随机森林在房地产价格预测中的实战应用
  • 计算机图像处理会议征稿中|2026年图像处理 、机器学习与模式识别国际学术会议
  • 从零开始:如何利用Kohya_ss轻松训练你的专属AI绘画模型
  • OpenClaw智能体的涌现与异化——复杂系统演化、知识权力重构与文明纪元跃迁(第五篇)
  • Phi-4-mini-flash-reasoning行业落地:半导体设计文档逻辑一致性校验
  • C++26反射能否取代Boost.Hana?性能对比实测:编译耗时↓47%,AST遍历速度↑3.2×
  • Windows系统管理神器:5分钟掌握WinUtil的一键优化与批量安装
  • 【Docker WASM边缘部署终极指南】:20年架构师亲授源码级调优与生产避坑清单
  • 别再只盯着SIFT和ORB了!用R2D2在Python里实现更鲁棒的特征点匹配(附完整代码)
  • 技术解密:Beyond Compare 5.x 注册密钥生成器完整实现指南
  • 理解 JS 事件循环:同步代码、微任务、异步任务 Vue computed/watch/nextTick 执行时机
  • FanControl深度技术解析:基于插件架构的Windows散热控制系统优化方案
  • 7种配色+百变空间+全系ADS 4.1:问界M6的“新锐”不止一面
  • 2026年3月市场上粉盒商家,办公用纸/色带/办公耗材/彩色打印机墨盒/碳粉/墨盒/彩色墨盒,粉盒服务商口碑推荐 - 品牌推荐师
  • Phi-3.5-mini-instruct快速上手:无需root权限,在普通用户目录完成全部部署
  • AI代理模型在CAE仿真中的革命性应用