尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

IM即时通信架构详解:从消息链路到高并发实战与协议选型

发布时间:2026/9/29 15:37:26

资讯中心
01
ARTICLE

IM即时通信架构详解:从消息链路到高并发实战与协议选型

IM即时通信架构详解:从消息链路到高并发实战与协议选型
我到现在还记得第一次给IM项目写心跳包时踩的坑明明两个客户端都显示在线消息却延迟了好几秒才送达。后来才明白所谓“在线”只是一个状态真正让消息秒达的是一条被小心维护着的长连接。你每天打开的微信、钉钉、淘宝客服窗口甚至直播间的弹幕本质都属于IM即时通信的范畴。它们的业务形态千差万别但底层要解决的问题几乎一致建立连接、传输消息、确认状态、保证不丢。这篇文章想从技术内核讲到生活应用把一条聊天消息从发送到送达的完整链路、高并发场景下的架构拆解、常见协议选型逻辑以及我在线上真实排查过的疑难杂症一次讲清楚。不管你刚转后端的新人、负责客服系统的产品经理还是单纯好奇“消息为什么能秒达”都能从中找到对应的干货。1. 一条消息从发送到送达中间到底发生了什么IM系统表面上看起来就是“你发一句我收一句”但这句话落到工程实现上涉及的路由、存储、推送、确认环节比想象中多。先建立整条链路的整体认知后面所有架构话题都好聊了。1.1 消息链路一次发送背后牵动了哪些服务假设A在微信里给B发了一句“晚上一起吃饭”从点击发送按钮开始这条消息大致会经历四个环节客户端编码把消息内容、目标用户、消息类型等打包成约定好的二进制或JSON结构通过一条已经建立好的长连接发送到服务器网关。网关接入服务器端的第一道门负责识别这条连接属于哪个用户、消息该转发给哪个后端服务同时做基础的频率限制和流量控制。业务逻辑处理这里才是“理解消息”的地方。判断接收方是否合法、是不是好友关系、消息是否需要保存、是否命中敏感词以及决定后续怎么推送给B。存储与推送先把消息落库一般会存两份一份是会话维度一份是用户维度然后判断B当前是否在线。在线就直接通过B的长连接推过去不在线就写进离线消息表等B下次登录再拉取。绝大多数IM系统跑完这一整套流程只需要几十毫秒。但延迟低不等于可靠所以A的客户端不会盲目相信“发出去就完了”而是会等服务端返回一个确认ack。收到ack才把消息状态从“发送中”改成“已发送”如果超时没收到客户端就会自动重发。这个设计理念可以类比寄快递你把包裹交给快递员网关快递员回执确认收件ack然后快递公司内部转运业务逻辑最终由收件地址附近的网点派送推送。如果派送失败系统会先存放离线消息直到收件人出现。1.2 为什么必须保持长连接轮询的代价实在太高早期Web应用没有成熟的实时通信手段大家用的是轮询前端每隔两三秒发一个HTTP请求问服务器“有新消息吗”。这种方式在IM场景里有两个致命问题大部分请求都是“空轮询”白占带宽和服务器资源。消息到达后用户最多只能延迟两三秒感知谈不上实时。所以IM系统普遍采用长连接客户端和服务器之间建立一条TCP连接连接建立后一直保持服务器可以随时主动把消息推给客户端不需要客户端反复询问。最直观的理解就是打电话和写信的区别打电话是通路一直存在的全双工通信写信是每次都要重新投递的短连接。如今网页端最常用的是WebSocket移动端App则常见基于TCP的自定义长连接比如微信早期就是基于自研的私有TCP协议来做消息收发。长连接的核心价值在于“服务器主动推送”这是所有实时性的基础。1.3 心跳机制让服务器知道你还活着长连接虽然好用但网络环境从来不是稳定不变的。用户进了电梯、跨了基站Wi-Fi断了又连TCP连接并不会每次都被干净地断开很多情况下会进入一种“半开”状态客户端以为连着服务器也以为连着实际上数据已经不通了。如果服务器把这个僵尸连接一直留在内存里后果很严重消息推不过去、连接数被无效占用用户明明不在线却显示在线。所以IM系统需要一个心跳机制客户端每隔一段时间常见是30到60秒发一个很小的心跳包给服务器。服务器记录每个连接最近一次心跳时间。如果超过某个阈值比如90到120秒没收到就判定连接已死主动断开并清理会话。客户端发现自己长时间没收到服务器心跳响应也会主动断开重连。心跳间隔怎么定太短浪费流量和电量太长又会让“假在线”持续太久。我见过不少刚入行的同学把心跳间隔设成5秒结果手机电量哗哗掉也见过设成10分钟导致掉线后推送不达的。实践中一般建议30秒上下同时让服务端超时阈值是心跳间隔的2到3倍并允许客户端在弱网模式下自动缩短间隔。2. 高并发IM架构的核心思路把“在线”这两个字拆开如果你只是写一个给十个人用的聊天Demo一台单机就够了。但用户量一旦上来所有问题都会暴露出来。早期我做高并发IM压力测试时感触最深的一句话是永远不要把所有职责放进同一个进程。2.1 单机瓶颈连接数本身就是一笔沉重的开销先算一笔账。每一条TCP长连接在服务器上至少需要占用一个文件描述符、一块内核缓冲区以及用于超时管理的定时器。在Go这类并发友好的语言里每个连接通常还会对应一个goroutine在Java里则可能对应一个线程或者EventLoop。当单机连接数达到几万甚至十几万时CPU、内存、文件描述符都会逼近极限。而IM场景还有个特点连接多不代表每条连接时刻都在收发消息大量连接其实是“挂着”等消息的状态。所以单机架构根本撑不起大规模IM必须从架构上做横向拆分。这也是搜“高并发im”时最常见的核心诉求不是让一台机器变强而是让系统能通过加机器变强。2.2 网关层与业务层的解耦最关键的架构决策高并发IM的第一步是把“维护连接”和“处理业务”分开。服务器拆分出两层连接网关Gateway只负责接收、维护长连接以及把收到消息的二进制流解码成业务对象再转发给下游。它不做复杂逻辑所以可以轻松水平扩展。业务逻辑层Logic负责真正的消息路由、关系链校验、存储策略、推送决策。客户端发来的消息到达网关后网关会按照某种路由规则比如对用户ID取模或一致性哈希转给对应的业务逻辑节点。业务逻辑处理完后如果需要推送再调用“推送服务”找到接收方客户端当前连接在哪个网关上把消息转发过去。网关和逻辑层之间如何通信常见做法是内部再用一套RPC或消息队列。这样做的好处很直接用户量涨了只加网关节点即可业务规则复杂了只升级逻辑层即可两边互不拖累。2.3 消息队列与写扩散群聊消息的高效路由群聊是IM系统里最考验性能的场景之一。一个500人的群有人发一条消息理论上要把这条消息推送给499个成员。如果每个人单独存一份那存储成本会很可观如果要求所有人实时拉取又会有延迟。IM行业积累下来的经典方案是“写扩散”和“读扩散”结合写扩散发消息时直接给群里的每个成员生成一条会话消息记录收件箱里放一份优点是读消息很快缺点是群人数越多写入放大越严重。读扩散群里消息只存一份成员按需获取最近的消息优点是存储省缺点是每次拉取要聚合。大多数IM系统会根据群的规模动态选择策略。小群直接写扩散大群退化为读扩散并搭配“已读位置”记录。而消息从逻辑层发出之后往往会经过消息队列做异步扇出把同步阻塞的推送变成异步任务这样即使瞬间有大量群消息涌入系统也不会被拖垮。消息队列的另一个重要作用是削峰填谷。举个例子电商大促期间客服IM的流量是平时的几十倍如果所有请求都同步推给数据库数据库会直接被压垮。引入队列后写入可以排队消费推送可以批量执行系统即使短暂过载也只是增加延迟而不是直接宕机。2.4 状态与Session管理多端在线和踢人逻辑IM系统里“在线状态”本身也是一个很重要的数据。用户在线、离线、忙碌、离开这些状态分散在各个网关节点上但业务层需要一个全局视角。所以常规做法是把会话信息抽出来放到Rediskey通常是userIdvalue则包含当前登录的设备列表、每个设备连接所在的网关节点ID、最近心跳时间等。用户登录成功后业务层通过分布式锁保证同一个账号同一设备只有一个连接重复登录时老连接会被踢下线或提示“账号在其他设备登录”。状态变更还需要广播给相关联系人比如你上线了你的好友列表会显示“在线”。这个动作如果全量广播社交大V会瞬间让推送服务爆炸所以一般会加上频控和好友分群推送。不要小看这些细节高并发IM里很多线上问题都出在这种“看似简单”的状态同步上。3. 协议选型XMPP、MQTT、WebSocket和自研私有协议怎么选聊完架构下一个绕不开的问题是客户端和服务器之间用什么协议通信协议决定研发效率、兼容性、流量开销以及可维护性。没有绝对的最好只有适合场景的方案。3.1 XMPP老牌IM协议为什么渐渐边缘化XMPP可扩展消息处理协议诞生于IM领域早期核心是基于XML流的数据交换强调联邦互通和扩展性。它的优势是标准成熟、实现丰富、社区文档多早期包括Google Talk都用过。但它在移动时代的软肋太明显XML冗余大传输效率低弱网下表现差。而且XMPP虽然扩展性强但默认能力并不覆盖移动端的“消息到达确认”“离线消息拉取”“多端同步”等IM必备功能需要大量扩展协议去补等于继承了一个框架又自己造了一辆整车。今天新启动的IM项目除非有特殊兼容需求几乎没人再从零选XMPP了。它的价值更多体现在历史系统和跨组织通信场景。3.2 MQTT弱网与物联网场景的实用选择MQTT最早是为卫星通信和物联网设计的基于发布/订阅模型非常适合带宽有限、网络不稳定的设备。它自带三个QoS等级QoS 0至多一次可能丢消息。QoS 1至少一次可能重复。QoS 2恰好一次保证不重不丢代价是性能开销大。IM场景一般用QoS 1就能满足“不丢但允许重复”的需求配合客户端去重即可。很多IoT设备推送比如智能门锁报警、远程控制指令都跑在MQTT上。它有个额外的好处是Broker消息代理生态成熟EMQX、Mosquitto都是常用组件部署和维护成本比较低。如果你的IM场景主要在弱网环境比如车载、户外定位设备、低功耗传感器MQTT比自研私有协议划算得多。3.3 WebSocket网页端IM的事实标准WebSocket可以理解为跑在TCP之上、通过HTTP握手建立的全双工协议。浏览器的原生支持让它成了网页聊天、在线客服、协同办公类产品的默认选择。你不需要安装客户端打开浏览器就能用这也是网上经常有人搜“csdn盒子im网页版”“某某IM网页版”的原因——本质上就是想要一个免安装、拿来即用的网页聊天工具。WebSocket的优点是和HTTP生态天然融合鉴权可以用已有的登录态消息可以用JSON传递出错时的错误码可以借用HTTP语义。缺点是协议本身不提供消息可靠投递、回执重传等机制这些都得业务层自己补。实际项目中WebSocket通常和HTTP接口混合使用HTTP负责查询类操作拉好友列表、拉历史消息WebSocket负责实时消息收发。这个分工模式成熟且高效绝大多数中大型Web IM都是这么做的。3.4 自研私有协议大厂为什么宁可自己造轮子微信、QQ这类亿级用户产品最终都会走向自研协议。原因主要有几点包体更小用二进制或protobuf代替JSON同样的消息能省一半以上流量。控制力更强加密方案、重传策略、连接迁移、多路复用都能按自己的需求纵深优化。业务粘合度更高消息、朋友圈、支付、小程序都跑在同一条连接上统一调度更加顺畅。当然自研协议的代价也很高客户端、服务端都要维护编解码层通信格式变更要严格做兼容测试成本翻倍。所以我的建议是如果你的团队规模、用户体量还没到那个阶段不要轻易自研。这里整理一张常见协议选型对比表供参考协议传输效率实时性弱网表现典型场景XMPP低高中历史IM、联邦通信MQTT中高高强IoT、车载、弱网设备WebSocket中高中网页IM、在线客服自研TCP私有协议高高强亿级用户IM、直播弹幕4. 从社交App到客服工作台不同场景的IM落地差异很多资料讨论IM时只盯着“聊天”这一个动作但真实的IM产品是分行业的。熟人社交里的IM和客服系统里的IM优化目标完全不同。4.1 熟人社交IM消息必达和顺序是底线微信、短信这类熟人社交场景用户最不能容忍的是“消息丢了”和“顺序乱了”。所以工程重点放在每条消息有唯一的服务端序列号接收方按序列号排序避免乱序。发送方必须收到ack才算成功否则自动重传服务端做幂等去重。多端同步时各端维护一个游标cursor离线期间的消息登录后一起拉取。这个场景还有一个用户感知极强的功能已读回执。它服务端不需要把“已读”做成实时消息更常见的做法是让接收方在打开会话时汇报一下“我的已读位置”服务端再异步通知发送方。4.2 客服与营销场景会话分配和消息合并更关键客服IM面对的通常不是“对等的聊天”而是“一个用户面对一个坐席”。它有几个完全不同的核心问题会话路由用户进来后系统自动分配一个空闲坐席可能还要考虑技能组、客户等级、历史接待人。消息排队高峰时坐席不够用户消息要进队列并明确展示“前面还有几位”。机器人转人工先由机器人应答根据语义判断是否需要人工介入转移时要把上下文一并带上。客服IM的商业价值远远不止“聊天功能”。它需要和CRM、工单、知识库打通消息往往要转成可跟踪的业务记录。所以这个赛道的技术重心不是把单条消息延迟压到几十毫秒而是把路由、排队、状态机和业务系统无缝缝合起来。4.3 网页版与嵌入式IM浏览器生态的特殊问题网页版IM的需求长期存在。很多企业想在官网加一个“在线咨询”按钮想在后台管理系统里嵌入站内信模块或者做一个只有单一功能的IM网页版供内部使用。这类场景绕不开浏览器兼容和单页应用带来的特殊问题。一个很常见的现象是聊天组件用着用着控制台突然报出类似“failed to fetch dynamically im”的错误。这个报错本身信息量很少关键词是“动态获取”和“fetch”。我排查过的案例里多数都是前端在初始化聊天配置时动态拉取会话Token或者用户档案的接口出了问题比如Token过期但前端还在用旧Token发消息。跨域配置不完整浏览器拦截了接口响应。网络切换导致请求发出去了但响应超时。后端返回的数据结构里少了某个字段前端解构时直接抛错。这类问题的排查思路放在下一章详细展开。这里只想提醒一点网页版IM对于“优雅降级”的要求更高连接失败时至少要让用户能够刷新重试并给出可读的提示而不是控制台一片红页面却没有反应。4.4 IM能力的外溢IoT通知、远程协作、直播弹幕IM并不只在聊天软件里出现。万物互联的背景下IM的核心能力“长连接实时推送”被复用在大量生活化场景家里的智能门锁开锁报警、快递柜取件通知本质是一个单向IM推送通道。在线文档多人同时编辑时光标位置和评论提醒需要低延迟通道也常复用WebSocket。直播弹幕、在线答题、远程白板底层同样是消息分发。这也是为什么理解IM架构的价值不止于做聊天应用。任何需要“实时”二字的产品都离不开这套内核能力。很多时候你不是在设计一个聊天软件而是在设计一个实时协同的底座而IM就是这个底座最经典的形态。5. 线上IM故障排查那些我踩过的坑和定位套路纸上得来终觉浅。IM系统上线之后的故障排查才是真正拉开工程师差距的地方。我这里整理几个高频问题重点讲定位思路而不是直接给答案因为这些坑换成别的项目往往只是报错文案不同而已。5.1 “连接不上”先分清是网络、网关还是客户端问题用户报“消息发不出去”“一直连接中”第一件事永远不是改代码而是分层排查。我习惯按这样的顺序来确认当前用户是否完全离线。如果只有某个用户离线大概率是客户端本地状态异常或是该用户的会话被服务端踢下了线。看网关实时连接数。如果整体连接数大幅下降说明是网关或网络层出了问题可能是机房网络抖动、网关节点OOM。在客户端抓日志看连接失败的阶段是TCP建连失败还是证书校验失败还是WebSocket握手返回了非预期状态码。重点检查NAT超时引发的半开连接。很多游客网络下TCP连接被运营商/路由器静默回收但两端都不知道。用户看起来在线实际已经失联。这种情况最常见的解决方案就是前文说的心跳机制和自动重连。5.2 消息发不出去ack超时的定位链路如果连接正常但某些消息发不出去矛头就要指向消息链路的某个环节。发送方的表现通常是“消息一直转圈”或状态变成“发送失败”。定位的时候别盯着客户端猜去看服务端日志里有没有收到这条消息。实操套路是这样给每条消息一个全局唯一的客户端消息IDclientMsgId。排查时先按这个ID查网关日志、逻辑层日志、存储日志看消息到底走到了哪一步。如果网关收到了但逻辑层没处理问题大概率在路由或鉴权。如果逻辑层处理了但推送没成功要查接收方是否在线、Session信息是否过期、推送服务是否报错。有一次我们线上出现过偶发消息丢失最后发现是逻辑层在并发场景下漏判了接收方的在线状态导致消息只存不推客户端又不知道去拉离线消息。解决方式是调整状态判断逻辑同时让客户端在重连或回到前台时主动拉取一次离线消息作为兜底。5.3 “failed to fetch dynamically …”这类动态获取失败的完整排查前面提过的控制台报错“failed to fetch dynamically im”报错本身非常含糊但本质上是在说“前端页面动态获取某个IM相关资源时网络请求失败了”。我排查过最具代表性的一次场景是前端聊天插件初始化时会先向服务端请求一个动态配置内容包括当前用户绑定的客服分组、欢迎语、WebSocket地址等。某天部分用户反馈聊天窗口打不开控制台就是这个报错。我当时的排查顺序是先看浏览器Network面板找到真正失败的接口确认返回状态码。结果发现是HTTP 401。打开接口日志发现这些用户携带的会话票据已经过期。原因是页面长期未刷新前端却用旧的票据去请求新配置。解决方案分两处后端在票据过期时返回明确错误码前端捕获该错误码后静默刷新票据并重试一次而不是直接把异常抛给用户。这个案例给我的启示是动态获取类报错十有八九不是网络断了而是配套的“授权态”和“数据格式”出了问题。先看接口返回比死磕前端代码有效得多。5.4 高并发下的消息丢失如何在压力里找真相高并发导致的故障往往不像普通bug那么自然复现它更像“压力到了某个阈值后突然爆发”。消息丢失类问题的高发点主要在三个地方消息队列积压扇出消息的生产速度远大于消费速度消费者处理不过来产生超时和丢弃。Redis异常Session或离线消息依赖Redis如果Redis出现大key、慢查询或集群节点切换整个链路都会抖动。IO线程阻塞某些服务把耗时的读写操作直接放在了IO线程里一旦数据库或外部接口变慢所有消息都堵在入口。应对这类问题最重要的前置动作是日志里有消息全链路的轨迹。建议在网关收到消息、逻辑层处理完、推送成功这三个关键节点都打上同一批消息ID的日志。排查时通过消息ID把三段日志拼在一起一眼就能看出消息断在了哪里。平时压测时也要提前验证如果单台网关挂了连接会不会自动迁移消息队列堆积十万条时系统是否能迅速恢复这些演练做多了真正线上出事时才不会手忙脚乱。6. 一周做一个最小可运行的IM从Demo到基本能用的实践路讲了这么多理论最后给想亲手验证的同学一条可执行的路径。我建议不要一上来就去复刻微信而是从“网页版单聊工具”做起这个范围恰好能覆盖IM的核心环节又控制在一周业余时间内。6.1 技术栈和项目结构一个最小可运行IM技术栈不必复杂后端Node.js或Go任选一个你熟的。Go更贴近生产环境Node.js写起来更省事。通信WebSocket浏览器端天然支持省去写TCP客户端的时间。存储Redis存在线状态和离线消息顺手解决“消息过期”问题MySQL存历史消息和用户账号数据。目录结构可以这样规划im-demo/ ├── server/ # WebSocket服务端 │ ├── auth.go # 登录与Token校验 │ ├── hub.go # 连接管理、消息路由 │ └── message.go # 消息结构与存储 ├── web/ # 前端页面 │ ├── index.html # 聊天界面 │ └── chat.js # WebSocket客户端 └── data/ # Redis/MySQL初始化脚本这个项目不需要微服务不需要消息队列等跑通后再去演进也不迟。6.2 核心流程从登录到WebSocket连接整个流程很简单用户在页面输入用户名密码调用HTTP接口登录服务端校验后返回一个Token。前端拿着Token去连WebSocket服务端在握手阶段校验Token并建立会话。连接建立后服务端把连接注册进内存管理结构并往Redis写入用户状态。发消息时客户端发送JSON例如{type:chat,to:userB,content:hello,clientMsgId:uuid-001}。服务端收到后先落库再根据to字段找到接收方的WebSocket连接把消息推过去。这里有个容易忽略的点服务端推送给接收方时也要把消息ID带上接收方客户端需要做去重。因为如果推送超时发送方可能会重发同一条消息。6.3 离线消息、历史消息和已读回执在线用户直接推送离线用户的消息则不能丢。最小版本的做法是推送失败时把消息追加到Redis里以用户ID为key的List结构中。用户下次登录建立连接后服务端把List里未读消息一次性捞出来发给他然后清空List。历史消息更简单MySQL里存一份消息表字段大致是msg_id, from_user, to_user, content, created_at。打开会话时按这两个用户的组合查最近50条即可。已读回执可以放在最后做接收方打开聊天窗口时发送一个{type:read,lastMsgId:123}服务端把这个已读位置更新到Redis再异步通知对方。这个功能虽然简单但足以帮你理解“状态同步”的核心逻辑。6.4 压测与调优别让Demo只是Demo项目跑通之后建议做一次最简单的压测让“高并发”这个抽象的词落地。工具可以使用wsbench或自己写个脚本模拟一百个并发连接。重点关注三个指标最大连接数服务端能稳定挂住多少条连接。消息吞吐量单位时间内能转发多少条消息。错误率消息发送失败、重复投递的比例。我第一次做这种Demo时就发现如果每来一条消息都同步写一次MySQL消息吞吐量会被拖到惨不忍睹。后来改成异步批量写内存里攒够一定数量再落库吞吐量立刻翻了几倍。这就是一个最简单的IM性能优化案例也是从Demo走向可用的必经之路。最后说一点我自己的体会。IM这个东西入门门槛并不高任何程序员都能在一周内写出能聊天的Demo但真正难的是在复杂的网络环境、高并发流量、多端同步、消息可靠性这些层面把细节磨扎实。如果你正在做一个IM相关的项目我的建议是不要迷信任何现成框架而是亲手把消息链路走一遍把连接、存储、推送、确认这些环节逐步打通。哪怕只是抱着学习目的做一个小Demo这个过程都会让后面读任何IM的代码都清晰很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。