WebSocket 知道吗?如果让你设计一个 IM 协议,你会考虑什么?
- 标签:后端、场景题
🏛 业务架构师
WebSocket 和 IM 协议设计,我先点破 IM 的业务本质——海量长连接下的消息必达、有序、低延迟,再展开协议要素,最后落到产品设计需要的业务能力。
连接层设计
- 长连接网关集群(百万级连接需要独立接入层,与应用层分离):连接管理、心跳保活(30~60 秒,超时摘除)、断线重连(指数退避 + 重连后增量同步)
- 连接层只做转发不做业务逻辑,无状态化方便水平扩展;用户与网关节点的映射存 Redis,跨节点投递靠路由
协议设计要点(这题的核心)
- 二进制紧凑协议(对齐 Protobuf/自定义头):header 定长(魔数、版本、指令类型、序列化方式、包长度)+ body,比 JSON 省数倍带宽——百万连接场景,每条消息省 100 字节就是省真金白银的带宽
- 消息序列号(seq):会话内服务端单调递增,客户端用 seq 去重、排序、检测丢消息(seq 不连续触发拉取补齐)——必达和有序全靠它
- ACK 机制:客户端收到回 ACK,超时未 ACK 服务端重推(投递语义 = 至少一次 + 客户端去重 = 恰好一次效果)
- 指令分级:心跳/握手/单聊/群聊/已读回执/系统通知分指令号,扩展新消息类型不改协议框架(版本字段兼容)
业务视角的收尾(差异点)
- 写扩散 vs 读扩散是业务决策:私信写扩散(每人收件箱一份,读快写贵),大群读扩散(存一份共同读,读时聚合)——按群规模和消息量混合使用,微信也是这么分层做的
- 协议要为产品商业化预留:消息漫游(多端同步历史)、已读回执、撤回(撤回本质是一条"撤回指令"消息)这些会员卖点,都需要 seq 和时间线设计提前支持
- 合规要求:消息内容审核(先审后发/发后审)的钩子要留在投递链路上,IM 内容监管是红线
🏗 系统架构师
WebSocket 我很熟(全双工、基于 TCP 的一次 HTTP Upgrade 握手、头部 Upgrade: websocket + Sec-WebSocket-Key 校验),但设计 IM 协议时 WebSocket 只是传输层,我重点讲协议设计要考虑的八件事。
1. 协议格式与序列化 二进制帧优先(Protobuf),帧结构:魔数 + 版本 + 消息类型 + 序列化方式 + 长度 + 体。长度字段必须显式声明并限制上限(防恶意超大帧打爆内存)。
2. 消息类型体系 区分信令(登录、心跳、ACK、订阅)与业务消息(单聊、群聊、通知),版本号字段为协议演进留余地。
3. 可靠性:ACK + 重传 + 去重 TCP 可靠不等于消息可达(服务端转发后崩溃、接收方处理失败)。消息带唯一 ID 与 seq,客户端超时未收 ACK 重发,服务端按 ID 幂等去重。在线消息走连接推送,离线消息落库,上线后按游标拉取。
4. 心跳与连接管理 心跳间隔(如 30s 客户端 ping)检测假死;断线重连用指数退避 + 抖动防止服务重启引发重连风暴;连接恢复后基于 seq 增量同步,而不是全量。
5. 连接层与业务层分离 接入层(Connection Gateway,只管长连接与编解码)与逻辑层(消息路由、群组)拆开,接入层无状态化、可水平扩展,用户与连接的映射存路由层(Redis),跨节点消息经路由层/MQ 转发——这是支撑百万长连接的架构基础。
6. 安全:TLS(wss)、登录态 token 鉴权、消息频率限制(防刷屏)。
7. 顺序性:单聊按会话维度 seq 保证有序(同一会话路由到同一逻辑节点),全局有序不追求。
8. 群聊扩散:读扩散 vs 写扩散按群规模选,大群写推结合。
这八个点答下来,就从"知道 WebSocket"上升到"能设计 IM 系统"。
💻 全栈开发专家
WebSocket 是传输通道,设计 IM 协议我关注六件事:协议格式、心跳、ACK 可靠性、消息 ID、连接路由、离线消息。
1. 自定义二进制协议(帧结构)
| 魔数 2B | 版本 1B | 指令 1B | 序列化 1B | 保留 4B | body长度 4B | body |指令枚举:LOGIN / MSG / ACK / PING/PONG / LOGOUT。定长头 + 变长 body,Netty 用 LengthFieldBasedFrameDecoder 处理粘包拆包,自定义编解码器做对象互转。
2. 心跳保活
服务端 IdleStateHandler(60, 0, 0),读空闲超时主动关连接;客户端定时 PING,断线指数退避重连(带 jitter 防雪崩)。
3. 可靠性:消息 ID + ACK + 去重
- 每条消息全局唯一 ID(雪花),客户端收到回 ACK,服务端超时未 ACK 重推(有限次)。
- 客户端维护已收 ID 窗口去重(重推必然重复)。
4. 连接路由(多实例必须解决)
用户与连接不在同一节点:
uid -> gatewayId 映射存 Redis: SET conn:uid:{uid} gateway-03
gateway-03 收到 A->B 的消息,查 B 在 gateway-05,经 MQ/内部 RPC 转发到 05 下发5. 离线消息:为离线用户落库(或 Redis list),上线后按序列号增量拉取(syncseq > lastSeq),支持多端同步。
6. 安全与规模:登录态在握手阶段用 token 鉴权;单机 10w 连接靠 epoll + 业务线程池隔离(IO 线程绝不跑业务)。