
读书笔记:
以前我们担心机器抢走体力活,现在AI的出现,轮到担心脑力活也被抢走。
AI让知道变得廉价,但是“判断力”和提问变得昂贵。
机器人越来也像人不可怕,可怕的是人越来越像机器。
一、为什么不直接用 HTTP / WebSocket
二、自研二进制协议的几个考虑要素
三、大厂如何设计
四、如何优化提升
HTTP/2、WebSocket、gRPC 这些成熟轮子摆在那儿,IM 接入层为什么还有人费劲去自研一套二进制协议,这不是重复造轮子吗?
但在移动端 IM 一线待过就会发现,轮子是好轮子,只是装在移动端这台车上有些地方就是不合脚。我们一个一个来说说。
IM 接入层(长连接网关)干的活本质就一件事:在客户端和服务端之间维持一条长期打开的双向通道,让消息随时能上行、也能下行。

图 1. 一个常见格局:移动端和桌面 / Web 接的是两套不同的接入网关,跑两套不同的协议,最后汇到同一个消息核心。协议选型是分端的,不是全局一刀切。
注意一个反直觉的点:两端的接入协议是分开选的。移动端跑自研二进制,桌面 / Web 跑 WebSocket(实践中常用 Socket.IO,WebSocket 优先、Long-Polling 降级)。后面会说这个分端逻辑。
第一个被排除的是 HTTP,原因很直接:它是请求-响应模型,天生由客户端发起,服务端没法主动找客户端说话。
可 IM 最核心的能力恰恰是"服务端主动推"——别人给你发消息得即时弹到屏幕上,不能等你自己去问。用 HTTP 硬凑只有轮询、长轮询两条别扭的路:要么实时性差、大量空跑费流量电,要么连接反复起落开销不小。更要命的是头部开销——每个请求都背着一坨 header,一条几十字节的消息能裹上几百字节的头,在移动网络下就是实打实的流量电量负担。
所以:服务端高频主动推送的实时消息IM链路,HTTP 从模型上就不对路。但它在 IM 里仍有位置——登录、拉历史、拉离线这类"客户端主动问一次"的接口,用 HTTP 反而省事。
WebSocket 正是为解决 HTTP 不能双向而生——一次握手后连接保持打开,两边随时发数据。对桌面和 Web,它基本就是标准答案:浏览器原生支持、开发简单、生态成熟。但移动端有几个痛点必须考虑:
总的来说:不是 WebSocket 不行,而是移动端约束太极端,值得为它榨干每个字节、掌控每个细节;桌面 / Web 没这么极端,WebSocket 就是更优解。
自研协议要扛的硬指标:
这是网络开发绕不开的坎:粘包 / 拆包问题。根子在 TCP——它面向字节流,不保证你一次 read 读到的正好是一条完整消息。连发三条消息,对端可能一次读到一条半,也可能三条糊在一起。哪几个字节算"一条消息",TCP 不管,是应用层自己的事。
主流切包思路有三种:
方案 | 怎么切 | 适合场景 |
|---|---|---|
固定长度 | 每条定长,读够 N 字节算一条 | 长度固定的场景,IM 基本不适用 |
分隔符 | 用特殊字符标记边界 | 文本协议常用,二进制内容可能撞上分隔符 |
长度前缀 | 包头放一个字段写明包体长度 | IM 的主流选择 |
绝大多数 IM 自研协议走长度前缀这一路:每条消息前面带一小段固定格式的包头,里面有个字段写明"后面包体有多长"。接收端先读包头,知道还要读多少字节,读够了就是一条完整消息,多出来的留给下一条。逻辑简单、对变长消息友好——字节怎么排布各家不同,但长度前缀这个思路是经大量项目验证的共识。
切包解决了"一条消息从哪到哪",接下来是"内容怎么编码"。核心取舍是二进制 vs 文本(JSON)。
JSON 的好处人人都懂:可读、好调试、改字段方便。但它在移动端有两个硬伤:一是冗余大,字段名当字符串塞在每条消息里——{"from":"u123","to":"u456","content":"hi"} 光这几个 key 就占一半篇幅,且每条都重复;二是解析更耗 CPU、更费内存,间接更费电。
二进制正好反过来:字段按约定的位置和长度紧凑排布,不传字段名,收发两端按位置"心照不宣"地解析,同一条消息能比 JSON 省下相当可观的体积。
具体编码有两个选择:用现成序列化框架(Protobuf、Thrift 等),还是纯手写字节布局。框架跨平台、有工具链、字段增删兼容性好,是很多团队的首选;纯手写能把体积压到极致、不引外部依赖,但维护成本高、易出错。无论哪条岔路方向一致:移动端值得用可读性换体积;桌面 / Web 约束没那么紧,继续用 JSON over WebSocket 反而省心。
一条长连接上跑的不只是聊天消息。登录、心跳、ACK、服务端推送、业务通知……都挤在同一条通道里。协议必须让接收端一眼分清这是什么包,走不同逻辑。
通行做法是在包头里留一个包类型 / 命令字字段,给每类报文编号:
包头(精简示意):
┌─────────┬──────────┬──────────┬──────────┐
│ 包类型 │ 包体长度 │ 序号/会话 │ ... │
└─────────┴──────────┴──────────┴──────────┘
包类型示例:
1 = 登录 2 = 心跳
3 = 普通消息 4 = ACK 确认
5 = 服务端推送 ...包类型让接收端在读包体之前,就知道该用哪套逻辑处理这条消息。
分包类型还顺带让心跳有了便宜的载体——它是连接上最高频、信息量最小的包,做成包体为空的独立类型,接收端一看是心跳就走最轻逻辑、不解析包体,乘以海量连接全天候运行,省下的流量电量很可观。
这里有个容易被忽略的点:很多消息需要"请求-响应"配对。客户端发消息要知道服务端收没收到,服务端推消息也想知道客户端收没收到。所以光有包类型不够,通常还会带一个序号字段,让请求和它的 ACK 对上号。某8到家的协议设计分享专门提到这点——网络上只有"读"和"写",协议本身并不知道一条消息属于请求、响应还是推送,要靠显式标识标出来。
聊天内容在链路上不能裸奔。移动端自研协议通常会做链路加密——包体用对称加密(实践中多用 AES 这类)后再发送,到对端解密。
链路加密:让包体在客户端到服务端这段链路上是密文、到服务端解密,是绝大多数 IM 的标配;端到端加密(E2EE)连服务端都看不到明文,隐私更强但复杂度高一个数量级,通常作为单独的高敏场景能力。今天不说这个。
加密在协议里的位置一般是包头明文、包体密文——接收端得先靠明文包头判断包类型、读出长度。具体分组模式、IV 处理属于密码学实现,交给成熟加密库即可。然而加密增加 CPU 开销和少量体积,但对 IM 却是不可省的成本——省流量省到把安全省掉,那这不就是捡芝麻丢西瓜。
最后一个决定,也是最容易在初版偷懒、上线后追悔的——版本兼容。
协议上线后会迭代:加新字段、加新包类型、改某个行为。但移动端有个残酷现实:你没法让所有用户一夜升级 App,新老版本客户端会长期同时连着服务端。协议没给演进留口子,一改就炸老客户端。常见手法:

图 2. 一条移动端二进制包从字节流到上层的处理链路
某信把移动端的网络层封装库 Mars 开源了。据其公开资料,Mars 解决的问题很有代表性:提供长连、短连两种网络通道,做 DNS 防劫持、动态 IP 下发、就近接入、容灾恢复,并深度贴合移动终端的前后台、休眠、省电、省流量等特性。编码上公开资料明确该团队用 Protobuf 这类二进制编码并做了更精细优化。它的价值在于"经过海量用户和各种机型的真实考验"——移动端协议最难的从来不是设计多漂亮,而是能否在亿级用户、千奇百怪的网络和机型下稳住。
维度 | 详情 |
|---|---|
优势 | 长短连双通道分工明确;省电省流量、弱网保活做到工业级;经海量真实环境验证,可直接复用 |
代价 | 整体偏重、C++ 实现,中小团队完整吃下来成本高;很多能力是为超大体量定制,小项目用不满 |
某8到家平台部公开分享过他们实时消息系统的协议设计,视角特别适合中小 toB 借鉴。他们把考量总结成三条:扩展性、可调试性、异步处理。
维度 | 详情 |
|---|---|
优势 | 三原则直击痛点,尤其点出"可调试性"这个二进制协议的隐藏短板;分类清晰、扩展友好 |
代价 | 异步配对、状态维护都要额外逻辑;可调试性要靠配套工具补,不是协议本身能完全解决 |
看这个端的核心约束是"省"还是"快速交付"——目标约束条件是省流量省电就往二进制走,约束是开发效率和生态就用 WebSocket / Socket.IO。别因为"系统要统一"就强行让两端跑同一套,那是把架构洁癖凌驾于真实约束之上。两端不同不是不统一,是对各自约束的诚实回应。
版本号这里再强调一次,因为它是初版最常偷懒、上线后最招事故的点。哪怕第一版简单到用不上版本号,也务必先在包头里把版本字段留出来。它几乎不占体积,却是新老客户端长期共存时唯一的保险——等真出了不兼容才想加,老客户端已在线上,回旋余地基本没有。
协议选型没有"最优解",只有"对得起这个端约束的解"——移动端为省到极致而自研,桌面端为快而用 WebSocket,都对。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。