
最喜欢苏东坡的这两句话:竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生,回首向来萧瑟处,归去,也无风雨也无晴。
人是可以有"定力"的。不是麻木,不是逃避,是在看清生活所有的残酷之后,依然选择好好活、有滋有味地活。
这种豁达,不是天生的乐观。是被生活反复暴击之后,长出来的本事。
一、消息上行最开始的那N毫秒发生了什么
二、上行那一秒走过的几道关
三、大厂如何做
今天讲讲从用户点"发送"到消息抵达接入层第一个微服务这段路,平均耗时大概10-200ms,这期间:客户端 SDK 怎么拼包、走哪个通道、怎么解析域名、TLS 怎么握、鉴权怎么过、Channel 怎么"落位"。每道关里都有一两件事值得我们去盘算。

图 1. 上行那一秒走过的六道关
上行的最开始这块,有个问题要留意:就是弱网、切网、后台、被劫持、被限流的状况下,要么上行成功,要么把失败告知用户。决不允许发生"消息丢了但用户不知道",这是 IM 最大的负面体验,比"没发出去"还糟糕。
这一秒的关键指标有两个:上行成功率和上行延迟。
我们做这一块第一反应几乎都是"加重试、加超时",最容易忽略的是 DNS 解析失败、TLS 握手失败、token 过期、端口被防火墙拦掉——这几类在弱网用户里频率比"网络丢包"高得多,靠重试是修不好的,只是把同一个失败重复一遍。
用户点"发送"那一刻,SDK 做的第一件事不是发包,而是先把消息按本地序号、消息 ID、时间戳记录到本地数据库。没有这一步,失败就没法重发、断网就没法续传、UI 也立不起"发送中"气泡。
接着是拼包。一条上行消息大致是:协议头(magic + version + length + checksum) + 鉴权字段(token / session) + 业务体。业务体序列化格式有取舍:
格式 | 优势 | 代价 |
|---|---|---|
JSON | 可读、调试方便、跨语言无痛 | 体积大、字段名重复占用、解析慢 |
Protobuf | 体积小(可省 30%~50%)、解析快、强类型 | 调试需要 .proto、跨版本兼容要小心字段编号 |
自定义二进制 | 体积最小、贴合业务 | 维护成本高、跨平台对齐麻烦、扩展难 |
我们的经验是:初创期直接用Protobuf。自定义二进制只在某信那种规模、要榨干每字节的项目里才值得做——中等规模 toB IM 用 Protobuf 已经够,加压缩(gzip / zstd)能再拿一截。
接下来是上行队列。SDK 内部 FIFO 队列,本地落盘消息按序进队,网络可用时按序出队。两个细节容易被忽略:断网时不阻塞用户(状态走"发送中",不卡输入框)、重启不丢消息(队列持久化到本地数据库)。
on_user_send(content):
msg = build_message(content)
msg.local_id = generate_local_id()// 客户端本地 ID,服务端拿到后映射成 msgSeq
msg.client_ts = now()
db.save(msg, status=PENDING)// 关键:先落盘,后发送
show_ui(msg, status=SENDING)
upload_queue.enqueue(msg)最容易踩的坑:消息 ID 用服务端生成,客户端只用 local_id 做去重映射。这条错过了,网络抖动重传时服务端拿不到稳定客户端 ID,就只能靠业务字段去重,代价巨大。
消息拼好、本地落盘了,SDK 要决定走哪条通道。接入通道设计为三级降级结构:

图 2. 接入通道三级降级:长连优先,HTTP 备用,本地兜底。
降级判定:
通道选定,SDK 要知道接入层在哪里——也就是域名解析。这一关是失败率最高的一关。
运营商 LocalDNS 在移动场景下几个老问题:劫持(返回错误 IP 把流量导走)、调度不准(华南用户路由到华北节点)、缓存过期不及时、跨网解析慢。这些影响叠加,弱网场景下上行失败率会显著高于稳态网络(具体数字依网络环境差异较大)。
业界主流是 HTTPDNS——用 HTTP 协议直接向自家 DNS 服务请求 IP,绕开 LocalDNS。机制简单,工程化有几个层次:
层次 | 做法 | 说明 |
|---|---|---|
基础版 | 客户端 HTTP → 自家 DNS 拿 IP → 本地缓存 → 发起业务连接 | 解决劫持,不解决调度精度 |
进阶版 | 缓存按"城市 + 运营商"粒度,服务端按地理位置返回最近节点 | 某字火山 HTTPDNS 2.0 之前的主流做法 |
精细版 | 缓存粒度细化到"网段"——同城同运营商内不同网段走不同节点 | 某字火山 HTTPDNS 2.0,解决"城市级调度污染" |
HTTPDNS 的代价是首次解析多 1 个 RTT(向自家 DNS 服务也得发 HTTP),所以几乎所有实现都做"启动时预解析"——App 一启动就把 IP 拉回来缓存,用户真正发消息时直接走缓存。
IP 拿到了,SDK 要和接入层建连。任何一条 IM 上行通道都要加密——明文传输在移动场景下等于把消息内容曝光给运营商、企业代理、公共 Wi-Fi。问题在于,标准 TLS 握手本身就要 1~2 个 RTT,在弱网下这一段可能占到整个上行延迟的一半。

图 3. TLS 握手的三种姿势
TLS 通了,消息加密发出去了,轮到接入层做事——接入层要回答两个问题:这是谁发的(鉴权)、这条 socket 挂到哪个用户上(连接归一)。
鉴权三种时机:
时机 | 做法 | 优势 / 代价 |
|---|---|---|
TCP 握手前 | TLS 客户端证书 / mTLS | 最严;客户端证书管理复杂,移动端实施成本高 |
TCP 握手后第一个业务包 | 首包带 token,校验通过才允许后续业务 | 主流做法,兼顾安全和实施成本 |
每包都鉴权 | 每条消息都带 token | 性能损耗大,只在高敏场景下用 |
主流是第二种:长连建立后,第一个业务包必须是"鉴权包"(常叫 Login / PushReg / Hello),包含 token / 设备指纹 / 协议版本。接入层校验 token(常委托独立鉴权服务),通过后把 Channel 标记"已激活"。
接着做连接归一——把这条 socket 和"用户 + 设备端类型"(uid#mobile / uid#desktop / uid#web)绑定。绑定信息写两个地方:本地内存 Map<userKey, Channel>(下行查 socket 用)、集中存储 im:online:<userKey> → 接入层 IP:port(其他节点知道用户挂在哪台机器)。
这是 Session 管理的入口——Session 一致性(死连接 / 多端互踢 / Pod 漂移)是独立大话题,这一秒里只需做对"绑定完成、可投递"这件事。
on_first_business_packet(channel, packet):
if not is_auth_packet(packet):
channel.close() // 关键:不是鉴权包,直接断
return
token = packet.token
user = auth_service.verify(token)
if user is None:
channel.close()
return
channel.user = user
channel.activated_at = now()
local_session_map.put(user.key, channel)
redis.set("im:online:" + user.key, this_node_ip, ttl=300)
publish_online_event(user.key)用户感知上是瞬间,但每一关都吃几十到几百毫秒——时间轴长这样:

图 4. 上行那一秒的时间轴。长连复用 + DNS 命中缓存稳态下点击到 ACK 通常 50~200ms;长连重建 + DNS 重解析的冷启动可能 500ms~2s(我们toB IM 项目里观察到的范围,具体值随网络质量和服务部署差异显著)。
冷启动和稳态差一个数量级——这是 IM SDK 优化的核心命题:让冷启动尽可能快、稳态尽可能稳。客户端预解析、长连预热、0-RTT 都是这条命题下的具体手段。
mars 是某信内部后来开源的移动端 IM 跨平台网络组件,把 IM 上行链路的工程经验沉淀到 socket 层封装。公开资料里的关键点:
维度 | 详情 |
|---|---|
优势 | 每一关都做到极致;开源后业界可参考的工程实现样本;长连 + 短连 + 弱网都覆盖 |
代价 | 工程量极大,需长期投入的底层团队;中小团队复用 mars 比自研更现实 |
接入层无状态化是他们公开资料里完整披露的话题——核心三个点:连接层无状态化(Channel 状态外移到集中存储)、多 IDC 调度(用户连接可在不同机房漂移)、连接层和业务层解耦(接入层只管连接,业务消息透传)。
无状态化直接对应这一秒"接入层鉴权与连接归一"的工程难点:接入层 Pod 可随时重启升级(状态在 Redis,新 Pod 拉起用户重连即可)、单 IDC 挂了客户端可重连到另一个 IDC、横向扩容线性加机器。
维度 | 详情 |
|---|---|
优势 | 接入层水平扩展简单;故障域小;升级 / 灰度成本低 |
代价 | 集中存储成为新瓶颈和故障点;消息下行要先查"用户挂哪台机器"多一跳; |
某字火山 HTTPDNS 服务他们自家多个核心产品 App。把缓存粒度从"城市-运营商"细化到"网段"。采用"城市 + 运营商"做缓存键——同城同运营商所有用户拿同一份 IP。但实际网络里,同城同运营商不同网段可能属不同 CDN 调度域,部分用户会被调度到"次优节点"。他们通过自研网段库 + 动态划分算法压住这个调度污染问题,工程上还配多级缓存、预取、拨测日志聚合作为网段划分数据源。
维度 | 详情 |
|---|---|
优势 | 根据某字公开数据,缓存命中率提升 15%,CPU/流量降低约 70% |
代价 | 网段库维护成本高;客户端缓存策略复杂度上升; |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。