首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >IM分布式架构系列(09) 为什么不用 HTTP or WebSocket | 自研二进制协议的必要性

IM分布式架构系列(09) 为什么不用 HTTP or WebSocket | 自研二进制协议的必要性

原创
作者头像
拉丁解牛说技术
发布2026-06-02 20:04:17
发布2026-06-02 20:04:17
790
举报

读书笔记:

以前我们担心机器抢走体力活,现在AI的出现,轮到担心脑力活也被抢走。

AI让知道变得廉价,但是“判断力”和提问变得昂贵。

机器人越来也像人不可怕,可怕的是人越来越像机器。

一、为什么不直接用 HTTP / WebSocket

二、自研二进制协议的几个考虑要素

三、大厂如何设计

四、如何优化提升


一、为什么不直接用 HTTP / WebSocket

HTTP/2、WebSocket、gRPC 这些成熟轮子摆在那儿,IM 接入层为什么还有人费劲去自研一套二进制协议,这不是重复造轮子吗?

但在移动端 IM 一线待过就会发现,轮子是好轮子,只是装在移动端这台车上有些地方就是不合脚。我们一个一个来说说。

1.1 接入层这层到底在传什么

IM 接入层(长连接网关)干的活本质就一件事:在客户端和服务端之间维持一条长期打开的双向通道,让消息随时能上行、也能下行

1
1

图 1. 一个常见格局:移动端和桌面 / Web 接的是两套不同的接入网关,跑两套不同的协议,最后汇到同一个消息核心。协议选型是分端的,不是全局一刀切。

注意一个反直觉的点:两端的接入协议是分开选的。移动端跑自研二进制,桌面 / Web 跑 WebSocket(实践中常用 Socket.IO,WebSocket 优先、Long-Polling 降级)。后面会说这个分端逻辑。

1.2 HTTP 撑不起服务端主动推

第一个被排除的是 HTTP,原因很直接:它是请求-响应模型,天生由客户端发起,服务端没法主动找客户端说话

可 IM 最核心的能力恰恰是"服务端主动推"——别人给你发消息得即时弹到屏幕上,不能等你自己去问。用 HTTP 硬凑只有轮询、长轮询两条别扭的路:要么实时性差、大量空跑费流量电,要么连接反复起落开销不小。更要命的是头部开销——每个请求都背着一坨 header,一条几十字节的消息能裹上几百字节的头,在移动网络下就是实打实的流量电量负担。

所以:服务端高频主动推送的实时消息IM链路,HTTP 从模型上就不对路。但它在 IM 里仍有位置——登录、拉历史、拉离线这类"客户端主动问一次"的接口,用 HTTP 反而省事。

1.3 WebSocket 够用了,为什么移动端还要再往下走

WebSocket 正是为解决 HTTP 不能双向而生——一次握手后连接保持打开,两边随时发数据。对桌面和 Web,它基本就是标准答案:浏览器原生支持、开发简单、生态成熟。但移动端有几个痛点必须考虑:

  • 握手开销:建连要先走一次带完整 HTTP 头的 Upgrade 握手。这个看起来开销不大,但问题是:移动端频繁切网、断线重连是非常常见的,每次重连都付一次握手成本。
  • 帧头开销:每个数据帧有自己的帧头,浏览器侧发出的帧还强制做掩码。对几字节的心跳、ACK 小包,帧头占比偏高。
  • 协议"太宽":WebSocket 是通用协议,包类型、压缩、加密这些想精细控制的东西都得在它上面再叠一层。
  • 流量电量极度敏感:这是最根本的一条。手机流量花钱买、电量有限、后台保活受系统管制,每省一字节、每避免一次唤醒都有意义;插电连 WiFi 的的桌面端(windows、mac这种)就没这么要命。

总的来说:不是 WebSocket 不行,而是移动端约束太极端,值得为它榨干每个字节、掌控每个细节;桌面 / Web 没这么极端,WebSocket 就是更优解。

二、自研二进制协议的几个考虑要素

2.1 核心约束

自研协议要扛的硬指标:

  • (包尽量小,省流量也省电,这是头号动机);
  • 可切包(从 TCP 字节流里准确切出独立消息);
  • 可分类(消息、心跳、ACK、登录能一眼区分);
  • 可加密(内容不能裸奔)
  • 可演进(新老版本客户端要能共存一段时间);

2.2 怎么从字节流里切出一条完整的包

这是网络开发绕不开的坎:粘包 / 拆包问题。根子在 TCP——它面向字节流,不保证你一次 read 读到的正好是一条完整消息。连发三条消息,对端可能一次读到一条半,也可能三条糊在一起。哪几个字节算"一条消息",TCP 不管,是应用层自己的事。

主流切包思路有三种:

方案

怎么切

适合场景

固定长度

每条定长,读够 N 字节算一条

长度固定的场景,IM 基本不适用

分隔符

用特殊字符标记边界

文本协议常用,二进制内容可能撞上分隔符

长度前缀

包头放一个字段写明包体长度

IM 的主流选择

绝大多数 IM 自研协议走长度前缀这一路:每条消息前面带一小段固定格式的包头,里面有个字段写明"后面包体有多长"。接收端先读包头,知道还要读多少字节,读够了就是一条完整消息,多出来的留给下一条。逻辑简单、对变长消息友好——字节怎么排布各家不同,但长度前缀这个思路是经大量项目验证的共识。

2.3 为什么是二进制,不是 JSON

切包解决了"一条消息从哪到哪",接下来是"内容怎么编码"。核心取舍是二进制 vs 文本(JSON)

JSON 的好处人人都懂:可读、好调试、改字段方便。但它在移动端有两个硬伤:一是冗余大,字段名当字符串塞在每条消息里——{"from":"u123","to":"u456","content":"hi"} 光这几个 key 就占一半篇幅,且每条都重复;二是解析更耗 CPU、更费内存,间接更费电。

二进制正好反过来:字段按约定的位置和长度紧凑排布,不传字段名,收发两端按位置"心照不宣"地解析,同一条消息能比 JSON 省下相当可观的体积。

具体编码有两个选择:用现成序列化框架(Protobuf、Thrift 等),还是纯手写字节布局。框架跨平台、有工具链、字段增删兼容性好,是很多团队的首选;纯手写能把体积压到极致、不引外部依赖,但维护成本高、易出错。无论哪条岔路方向一致:移动端值得用可读性换体积;桌面 / Web 约束没那么紧,继续用 JSON over WebSocket 反而省心。

2.4 包类型与命令字怎么分

一条长连接上跑的不只是聊天消息。登录、心跳、ACK、服务端推送、业务通知……都挤在同一条通道里。协议必须让接收端一眼分清这是什么包,走不同逻辑。

通行做法是在包头里留一个包类型 / 命令字字段,给每类报文编号:

代码语言:javascript
复制
包头(精简示意):
  ┌─────────┬──────────┬──────────┬──────────┐
  │ 包类型   │ 包体长度  │ 序号/会话 │  ...      │
  └─────────┴──────────┴──────────┴──────────┘
  包类型示例:
    1 = 登录       2 = 心跳
    3 = 普通消息    4 = ACK 确认
    5 = 服务端推送  ...

包类型让接收端在读包体之前,就知道该用哪套逻辑处理这条消息。

分包类型还顺带让心跳有了便宜的载体——它是连接上最高频、信息量最小的包,做成包体为空的独立类型,接收端一看是心跳就走最轻逻辑、不解析包体,乘以海量连接全天候运行,省下的流量电量很可观。

这里有个容易被忽略的点:很多消息需要"请求-响应"配对。客户端发消息要知道服务端收没收到,服务端推消息也想知道客户端收没收到。所以光有包类型不够,通常还会带一个序号字段,让请求和它的 ACK 对上号。某8到家的协议设计分享专门提到这点——网络上只有"读"和"写",协议本身并不知道一条消息属于请求、响应还是推送,要靠显式标识标出来。

2.5 加密放在协议哪一层

聊天内容在链路上不能裸奔。移动端自研协议通常会做链路加密——包体用对称加密(实践中多用 AES 这类)后再发送,到对端解密。

链路加密:让包体在客户端到服务端这段链路上是密文、到服务端解密,是绝大多数 IM 的标配;端到端加密(E2EE)连服务端都看不到明文,隐私更强但复杂度高一个数量级,通常作为单独的高敏场景能力。今天不说这个。

加密在协议里的位置一般是包头明文、包体密文——接收端得先靠明文包头判断包类型、读出长度。具体分组模式、IV 处理属于密码学实现,交给成熟加密库即可。然而加密增加 CPU 开销和少量体积,但对 IM 却是不可省的成本——省流量省到把安全省掉,那这不就是捡芝麻丢西瓜。

2.6 协议怎么留出演进的余地

最后一个决定,也是最容易在初版偷懒、上线后追悔的——版本兼容

协议上线后会迭代:加新字段、加新包类型、改某个行为。但移动端有个残酷现实:你没法让所有用户一夜升级 App,新老版本客户端会长期同时连着服务端。协议没给演进留口子,一改就炸老客户端。常见手法:

  • 包头里放版本号:服务端按版本号分别处理,老版本走老逻辑,最基础的保险。
  • 字段只增不改:新增字段往后加,老客户端读不到就忽略。Protobuf 天生支持这种向后兼容,也是它受欢迎的原因之一。
  • 包类型预留扩展空间:命令字别用太满,给未来的新包类型留号段。
2
2

图 2. 一条移动端二进制包从字节流到上层的处理链路

三、大厂如何设计

3.1 某信的 Mars 与自研协议

某信把移动端的网络层封装库 Mars 开源了。据其公开资料,Mars 解决的问题很有代表性:提供长连、短连两种网络通道,做 DNS 防劫持、动态 IP 下发、就近接入、容灾恢复,并深度贴合移动终端的前后台、休眠、省电、省流量等特性。编码上公开资料明确该团队用 Protobuf 这类二进制编码并做了更精细优化。它的价值在于"经过海量用户和各种机型的真实考验"——移动端协议最难的从来不是设计多漂亮,而是能否在亿级用户、千奇百怪的网络和机型下稳住。

维度

详情

优势

长短连双通道分工明确;省电省流量、弱网保活做到工业级;经海量真实环境验证,可直接复用

代价

整体偏重、C++ 实现,中小团队完整吃下来成本高;很多能力是为超大体量定制,小项目用不满

3.2 某8到家的协议设计

某8到家平台部公开分享过他们实时消息系统的协议设计,视角特别适合中小 toB 借鉴。他们把考量总结成三条:扩展性、可调试性、异步处理

  • 扩展性是报文要分类(登录、业务消息、服务端推送、keepalive 各一类,业务包要能往后扩字段);
  • 异步处理是网络上只有读和写、协议本身不知道一条消息是请求还是推送,要用显式标识让请求响应配对;
  • 可调试性则点出二进制的隐藏短板——天生难抓包、难肉眼看懂,设计时就要把"怎么调试"考虑进去。

维度

详情

优势

三原则直击痛点,尤其点出"可调试性"这个二进制协议的隐藏短板;分类清晰、扩展友好

代价

异步配对、状态维护都要额外逻辑;可调试性要靠配套工具补,不是协议本身能完全解决

四、如何优化提升

4.1 分端选型而非一刀切

看这个端的核心约束是"省"还是"快速交付"——目标约束条件是省流量省电就往二进制走,约束是开发效率和生态就用 WebSocket / Socket.IO。别因为"系统要统一"就强行让两端跑同一套,那是把架构洁癖凌驾于真实约束之上。两端不同不是不统一,是对各自约束的诚实回应。

4.2 协议先留好版本位

版本号这里再强调一次,因为它是初版最常偷懒、上线后最招事故的点。哪怕第一版简单到用不上版本号,也务必先在包头里把版本字段留出来。它几乎不占体积,却是新老客户端长期共存时唯一的保险——等真出了不兼容才想加,老客户端已在线上,回旋余地基本没有。


协议选型没有"最优解",只有"对得起这个端约束的解"——移动端为省到极致而自研,桌面端为快而用 WebSocket,都对。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么不直接用 HTTP / WebSocket
    • 1.1 接入层这层到底在传什么
    • 1.2 HTTP 撑不起服务端主动推
    • 1.3 WebSocket 够用了,为什么移动端还要再往下走
  • 二、自研二进制协议的几个考虑要素
    • 2.1 核心约束
    • 2.2 怎么从字节流里切出一条完整的包
    • 2.3 为什么是二进制,不是 JSON
    • 2.4 包类型与命令字怎么分
    • 2.5 加密放在协议哪一层
    • 2.6 协议怎么留出演进的余地
  • 三、大厂如何设计
    • 3.1 某信的 Mars 与自研协议
    • 3.2 某8到家的协议设计
  • 四、如何优化提升
    • 4.1 分端选型而非一刀切
    • 4.2 协议先留好版本位
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档