资讯动态

直播IM聊天开发实战:从消息送达到高可靠投递的两步进阶

发布时间:2026/10/22 7:11:23 来源:尧图企业网站定制
直播系统里的IM聊天看起来只是发消息三个字真动手做才知道坑有多密。我做过几个带互动聊天模块的直播项目从早期用轮询硬扛到后来上长连接、做消息可靠投递中间踩过的坑足够写一本小册子。这篇就把IM聊天部分的开发拆成两步来讲第一步是把消息送得到第二步是把消息送得对、送得稳。前者解决连通性问题后者解决体验和可靠性问题。如果你正在做直播系统的互动模块或者负责后端消息通道又或者只是好奇直播间里那串飞速滚动的弹幕到底怎么跑起来的这篇内容应该能帮你少走不少弯路。需要先说明一点直播场景下的IM和普通社交IM完全不是一回事。普通聊天是一对一或者小群消息量可控用户对延迟的容忍度也高直播间是一对多广播高并发写入瞬时峰值一场热门直播可能几万人同时在线消息发送频率是每秒几十到几百条而且用户对我发的消息有没有出现在屏幕上这件事极其敏感。这个差异决定了架构选型和技术方案后面所有讨论都建立在这个前提上。1. 先搞清楚直播IM到底难在哪很多人一上来就问用哪个框架要不要上某某中间件我觉得这是本末倒置。先把问题定义清楚方案自然就浮出来了。直播IM的难点可以拆成四个维度来看理解这四个维度你才知道后面每一步为什么这么设计。1.1 消息的扇出规模决定了架构上限普通IM里一条消息的接收方通常是个位数服务端只要找到对应连接推过去就行。直播间不一样一条弹幕可能要推给房间里所有在线用户。假设房间有5万人在线一条消息就要扇出5万次。如果每秒有100条消息那就是每秒500万次推送。这个量级下任何逐条遍历用户连接的写法都会瞬间把CPU打满。所以直播IM的第一个核心命题是如何把一条消息推给N个人这件事做得足够便宜。常见思路有三种一是把消息广播到多个网关节点每个网关只负责自己持有的那部分连接二是用发布订阅模型让网关订阅房间频道消息来了自动分发三是分级推送热门房间单独处理冷门房间合并处理。这三种思路后面会详细展开但你现在要记住的是扇出规模是选型的第一约束。1.2 瞬时峰值比平均负载更致命直播有个特点流量不是均匀的。主播一句话点燃全场或者抽奖环节开启消息量可能在几秒内从每秒几十条飙到每秒几千条。如果你的系统是按平均负载设计的这种峰值一来直接雪崩。我见过一个项目压测时用均匀流量跑得好好的一上真实场景就挂。原因就是没考虑峰值。应对峰值有两个方向一是削峰用队列把突发流量缓冲下来慢慢处理二是限流降级超过阈值就丢弃部分非关键消息比如普通弹幕可以丢但礼物、系统公告不能丢。这两个方向不是二选一而是配合使用。1.3 消息的可见性和顺序性要求不一致直播间里不同类型的消息要求完全不同。普通弹幕丢几条用户基本无感但礼物消息、进场特效、系统公告一条都不能少而且顺序不能乱。这就意味着你不能用一套逻辑处理所有消息必须做消息分级。我一般把消息分成三类可丢弃消息普通聊天、点赞飘心、必达消息礼物、打赏、系统通知、有序必达消息进场顺序、榜单变化。不同级别走不同的通道和策略这是后面第二步要重点讲的。1.4 连接保持本身就是一场持久战直播用户可能一看看两小时也可能进来三秒就划走。连接要能长时间稳定保持还要能快速建立和释放。移动端网络切换WiFi转4G、App切后台、锁屏都会导致连接断开。如果重连逻辑做得不好用户回来发现消息断了体验直接崩。这块的坑我踩得最多后面会专门讲重连和心跳的设计。现在你只需要建立一个认知直播IM的复杂度不在发消息这个动作本身而在高并发、峰值、分级、长连接这四个约束的叠加。2. 第一步把消息通道打通让消息送得到第一步的目标很明确建立一条从客户端到服务端、再从服务端到其他客户端的可靠通道让消息能稳定地双向流动。这一步不追求极致的性能和可靠性先跑通链路把基础打牢。2.1 传输协议选型为什么长连接是必选项早期我做第一个直播项目时图省事用了HTTP轮询客户端每隔两秒问一次服务端有没有新消息。小房间里勉强能用一旦人多就废了请求量随在线人数线性增长服务端连接数爆炸延迟还高最坏情况要等两秒才收到消息。后来换成WebSocket世界清净了。WebSocket在单个TCP连接上做全双工通信服务端可以主动推消息延迟降到毫秒级连接开销也小。对于直播IM这种服务端推送为主的场景长连接几乎是唯一合理的选择。不过WebSocket也不是银弹它有几个现实问题要处理连接建立成本每个连接都要经历握手、鉴权、注册高并发下握手本身就可能成为瓶颈需要做连接复用和快速鉴权。中间层兼容某些网络环境对长连接不友好需要准备降级方案比如自动切到长轮询。心跳维持长连接会被中间设备路由器、运营商网关静默断开必须靠心跳探测。提示协议选型不要只看技术先进性要看你的客户端环境。如果是纯AppWebSocket基本没问题如果还要兼容Web页面要考虑部分环境对WebSocket的限制准备好降级通道。2.2 连接层架构网关节点怎么设计单机扛不住几十万连接必须做分布式。我采用的架构是接入网关 逻辑服务分离接入网关只负责维护连接、收发原始消息、做协议解析。它是无状态的连接状态除外可以水平扩展。每个网关持有自己那部分用户的连接。逻辑服务负责业务逻辑比如消息校验、敏感词过滤、消息分级、路由决策。它不直接持有连接通过内部通道和网关通信。这样拆分的好处是网关可以专注做连接管理逻辑服务可以专注做业务两者独立扩容。用户连到哪个网关是随机的或者按用户ID哈希网关把用户加入的房间信息上报给逻辑服务逻辑服务维护房间-网关的映射关系。这里有个关键设计房间和网关的映射关系。当一条消息要广播到某个房间时逻辑服务需要知道这个房间的用户分布在哪些网关上然后把消息推给这些网关网关再推给自己持有的连接。这个映射关系要实时维护用户进房、退房、断线都要更新。2.3 消息通道打通的最小可用实现先给一个能跑通的最小实现思路不追求性能只求链路通。服务端用WebSocket客户端连上后先鉴权鉴权通过后加入房间然后就能收发消息了。服务端伪代码大致是这样# 简化的WebSocket网关逻辑 class Gateway: def __init__(self): self.connections {} # conn_id - connection self.room_map {} # room_id - set(conn_id) async def on_connect(self, conn): conn_id generate_id() self.connections[conn_id] conn # 等待鉴权消息 auth_msg await conn.recv() user verify_token(auth_msg.token) if not user: await conn.close() return conn.user user async def on_message(self, conn, msg): if msg.type join_room: room_id msg.room_id self.room_map.setdefault(room_id, set()).add(conn.id) # 上报给逻辑服务更新房间-网关映射 await report_room_mapping(room_id, self.node_id) elif msg.type chat: # 转发给逻辑服务处理 await forward_to_logic(conn.user, msg) async def push_to_room(self, room_id, message): for conn_id in self.room_map.get(room_id, []): conn self.connections.get(conn_id) if conn: await conn.send(message)客户端侧要处理的核心逻辑是连接、鉴权、进房、收消息、断线重连。重连这块后面单独讲先保证正常流程能跑。2.4 打通阶段最容易忽略的三个细节链路跑通不代表没问题这一步有几个细节如果没处理好后面会反复返工。第一个是鉴权时机。很多人把鉴权放在连接建立之后异步做结果连接已经建立但用户还没鉴权这期间的消息怎么处理我的做法是连接建立后进入待鉴权状态只接受鉴权消息其他消息一律拒绝鉴权超时比如5秒直接断开。这样能防止未授权连接占用资源。第二个是房间容量和连接数限制。单个网关能持有的连接数是有上限的受文件描述符、内存限制要设置合理的上限并做负载均衡。同时单个房间的在线人数也要有上限超过就拒绝新用户进房或者引导到新房间。第三个是消息大小限制。WebSocket单帧消息不能无限大要设置上限比如64KB超过就拒绝。直播弹幕一般很短但礼物消息可能带特效参数要提前约定好格式和大小。注意打通阶段不要急着优化性能先把连接-鉴权-进房-收发-断开这条完整链路跑通用少量用户验证正确性。性能问题留到第二步解决。3. 第二步把消息送得对、送得稳链路通了之后真正的挑战才开始。这一步要解决的是高并发下怎么保证性能消息分级怎么落地可靠性怎么保障异常情况怎么处理。这是直播IM从能用到好用的关键。3.1 消息分级不同消息走不同通道前面提过直播间的消息不能一视同仁。我一般按可靠性和实时性两个维度分级消息类型可靠性要求实时性要求处理策略普通弹幕低可丢高直接广播超载时丢弃点赞飘心低可合并中批量合并后推送礼物消息高必达高独立通道确认重传系统公告高必达高独立通道优先级最高进场消息中可合并中合并推送避免刷屏分级之后不同级别的消息走不同的处理路径。普通弹幕走尽力而为通道用UDP式的思路丢了就丢了礼物和系统消息走可靠通道要有确认和重传机制。这样既能保证关键消息不丢又能在高负载时通过丢弃非关键消息来保护系统。实现上可以在消息头里加一个priority字段网关和逻辑服务根据优先级决定处理策略。高优先级消息进独立队列低优先级消息进普通队列队列满了就丢弃低优先级的。3.2 广播扇出优化别让一条消息压垮网关回到最开始说的扇出问题。一条消息要推给房间里所有人如果逐个连接发送5万连接就是5万次系统调用肯定扛不住。优化思路有几个层次第一层是批量发送。把要发送的消息先攒起来用批量接口一次性发给多个连接。很多WebSocket库支持批量写能显著减少系统调用次数。第二层是网关内广播。消息到达网关后网关在本地内存里遍历自己持有的该房间连接直接推送不经过逻辑服务。这样一条消息只需要在网关间传递一次网关内部完成扇出。第三层是分级广播。热门房间在线人数超过阈值单独用一组网关处理冷门房间可以多个房间共享网关资源。这样避免热门房间的流量影响冷门房间。第四层是消息合并。对于点赞、飘心这种高频低价值消息不要每条都推而是攒一段时间比如200毫秒合并成一条新增N个赞推给客户端。这能大幅降低消息量。我实测下来这四层优化叠加单网关能支撑的并发推送量能提升一个数量级。3.3 可靠投递礼物消息一条都不能丢礼物消息是直播系统的命脉丢一条就是真金白银的损失。可靠投递的核心是确认重传去重三件套。确认机制客户端收到礼物消息后要回一个ACK服务端收到ACK才认为投递成功。如果超时没收到ACK就重传。这里要注意ACK本身也可能丢所以要有超时重传但重传次数要有限制避免无限重传。重传策略重传要有退避不能一直高频重传。我一般用指数退避第一次等200毫秒第二次400毫秒第三次800毫秒最多重传3次。超过次数就记录到补偿队列由后台任务异步补偿。去重机制重传会导致客户端收到重复消息所以每条消息要有唯一ID客户端根据ID去重。服务端也要记录已投递的消息ID避免重复处理。# 可靠投递的简化逻辑 class ReliableDelivery: def __init__(self): self.pending {} # msg_id - (message, retry_count, next_retry_time) async def send_reliable(self, conn, message): msg_id message.id self.pending[msg_id] (message, 0, now() 200) await conn.send(message) async def on_ack(self, msg_id): self.pending.pop(msg_id, None) async def retry_loop(self): while True: for msg_id, (msg, count, next_time) in list(self.pending.items()): if now() next_time: if count 3: await move_to_compensation(msg) self.pending.pop(msg_id) else: await resend(msg) self.pending[msg_id] (msg, count1, now() 200 * (2 ** count)) await sleep(50)提示可靠投递会带来额外的存储和计算开销不要对所有消息都用。只对礼物、打赏、系统公告这类关键消息启用普通弹幕用尽力而为即可。3.4 顺序性保障进场顺序不能乱有些消息是有顺序要求的比如用户进场顺序、榜单变化。如果顺序乱了用户会看到张三进场在李四进场后面但实际是反的体验很怪。保障顺序的核心是同一房间的消息在服务端串行处理在客户端按序消费。具体做法是给每个房间分配一个逻辑上的消息序列号服务端处理消息时按序列号递增客户端收到后按序列号排序再展示。但这里有个矛盾为了性能我们做了并行处理和批量发送这天然会打乱顺序。所以顺序性保障要单独处理不能和性能优化混在一起。我的做法是对顺序敏感的消息单独走一条串行通道用房间ID做分区同一房间的消息进同一个处理队列保证串行。3.5 断线重连用户回来要能接上移动端断线是常态重连逻辑做得好不好直接决定用户体验。重连要解决三个问题怎么发现断了、重连后怎么恢复状态、怎么避免消息丢失。发现断线靠心跳。客户端定时比如30秒发心跳包服务端收到后回pong。如果客户端连续几个心跳周期没收到pong就认为断了主动重连。服务端侧也要检测如果超过一定时间没收到客户端心跳就主动断开释放资源。恢复状态重连后要重新鉴权、重新进房。这里的关键是消息补拉——客户端记录自己收到的最后一条消息的序列号重连后带上这个序列号服务端把之后的消息补发给客户端。这样能保证断线期间的消息不丢。避免消息丢失补拉机制要配合消息存储。服务端要把房间的最近N条消息存起来内存或Redis客户端补拉时从存储里取。N的大小要权衡太大占内存太小可能补不全。// 客户端重连逻辑示意 class IMClient { constructor() { this.lastSeq 0; this.reconnectDelay 1000; } connect() { this.ws new WebSocket(url); this.ws.onopen () { this.reconnectDelay 1000; // 重置退避 this.auth(); this.joinRoom(this.roomId, this.lastSeq); // 带上最后序列号 }; this.ws.onmessage (e) { const msg JSON.parse(e.data); if (msg.seq this.lastSeq) { this.lastSeq msg.seq; this.display(msg); } }; this.ws.onclose () { this.scheduleReconnect(); }; } scheduleReconnect() { setTimeout(() this.connect(), this.reconnectDelay); this.reconnectDelay Math.min(this.reconnectDelay * 2, 30000); // 指数退避上限30秒 } }重连退避很重要不能断了就立刻重连否则服务端一抖动所有客户端同时重连直接把服务端打垮。指数退避加上随机抖动jitter是标准做法。3.6 限流与降级保护系统不被打垮再好的系统也有上限限流和降级是最后一道防线。限流要分层做连接层限流单个IP、单个用户的连接数限制防止恶意连接。消息层限流单个用户每秒发送消息数限制比如每秒最多5条超过就拒绝或静默丢弃。房间层限流单个房间每秒消息总量限制超过就丢弃低优先级消息。降级策略要提前设计好不能等出事了临时想。我一般准备三档降级轻度降级关闭点赞飘心等非核心消息保留弹幕和礼物。中度降级弹幕改为抽样推送比如每10条推1条礼物正常。重度降级只保留礼物和系统公告其他全部关闭。降级要能自动触发基于负载指标也能手动触发运营干预并且要能快速恢复。4. 那些文档里不会写的实战经验前面讲的是方法论和架构这一节讲点实在的都是我在实际项目里踩出来的经验文档和教程里基本不会提。4.1 压测要模拟真实场景别用均匀流量我第一次压测直播IM用的是均匀流量每秒固定发100条消息跑了两小时没问题信心满满上线。结果真实场景一开播主播喊了一句扣1瞬间每秒3000条消息系统直接挂了。后来我改了压测方案用真实的历史消息回放把峰值场景复现出来。具体做法是录制一场真实直播的消息流包括时间戳压测时按原时间轴回放。这样能真实还原峰值。另外还要做尖峰测试人为制造瞬时高峰看系统的恢复能力。4.2 消息ID的生成有讲究消息ID看起来是个小问题其实很关键。它要满足几个要求全局唯一、趋势递增方便排序和补拉、生成性能高。我试过几种方案UUID唯一但无序补拉时不好用数据库自增性能不够最后用的是时间戳机器ID序列号的雪花算法变种既唯一又趋势递增单机每秒能生成几十万个够用。要注意的是消息ID在客户端和服务端要一致不能服务端一套客户端一套否则去重和补拉都会出问题。4.3 敏感词过滤别放在主链路上敏感词过滤是必须的但别放在消息发送的主链路上同步做。我早期就是同步过滤结果敏感词库一大每条消息过滤要几毫秒高并发下直接成为瓶颈。正确做法是异步过滤消息先发出去同时丢给过滤服务异步处理发现违规再撤回或屏蔽。对于直播这种实时性要求高的场景稍微延迟一点过滤是可以接受的。当然对于明显违规的内容可以在网关层做一层快速拦截比如简单的关键词匹配把大部分问题挡在前面复杂的语义过滤放异步。4.4 客户端要处理消息乱序和消息重复服务端做了很多保障但客户端不能假设消息一定有序、一定不重复。网络传输、重传、补拉都可能导致乱序和重复。客户端必须自己做排序和去重。我的做法是客户端维护一个消息缓冲区收到消息先按序列号排序再按ID去重然后才展示。缓冲区要有上限太老的消息直接丢弃避免内存无限增长。4.5 监控指标要盯住这几个直播IM的监控别只看CPU和内存要盯住这几个业务指标消息端到端延迟从发送到接收的时间P99要控制在几百毫秒内。消息丢失率关键消息的丢失率要接近0普通消息可以放宽。连接成功率连接建立的成功率低于99%就要排查。重连频率频繁重连说明连接不稳定要查网络或心跳配置。各网关负载要均衡不能有的网关忙死有的闲着。这些指标要能实时告警出问题第一时间知道。4.6 灰度发布和回滚要能秒级完成IM是直播的核心功能一旦出问题影响巨大。所以任何改动都要灰度发布先放量1%用户观察没问题再逐步扩大。回滚要能秒级完成发现异常立刻切回旧版本。我一般用配置开关来控制新功能的启用这样不用重新部署就能开关功能。关键路径的代码要有降级分支出问题能快速切到简单逻辑。5. 两步之间的关系和落地节奏最后说说这两步怎么衔接以及实际项目里怎么安排节奏。第一步和第二步不是割裂的而是递进的。第一步打通链路验证核心流程第二步在链路基础上做性能、可靠性和体验优化。实际项目里我建议这样安排第一周完成第一步用WebSocket打通连接、鉴权、进房、收发消息的完整链路用几十个用户验证正确性。这一步的目标是能跑通不追求性能。第二周开始第二步先做消息分级和广播扇出优化把单网关的并发能力提上去。同时把可靠投递机制加上保证礼物消息不丢。第三周完善断线重连、限流降级、监控告警。这一步是打磨把各种异常情况都覆盖到。第四周压测和调优。用真实场景回放压测找出瓶颈针对性优化。然后灰度上线逐步放量。这个节奏不是死的根据团队规模和项目复杂度调整。但核心思路是先通后优先正确后性能先核心后边缘。不要一上来就追求完美架构那样容易过度设计反而拖慢进度。我个人在实际操作中的体会是直播IM最难的不是某个技术点而是各种约束的平衡。性能、可靠性、实时性、成本这四个东西互相拉扯没有完美方案只有适合当前阶段的方案。早期用户少简单方案就够用用户涨上来了再逐步优化。别为了想象中的百万并发提前做一堆复杂设计那往往是浪费。另外分享一个小技巧把IM的核心逻辑和业务逻辑解耦IM只负责把消息送到业务逻辑负责消息是什么、怎么处理。这样IM模块可以独立测试、独立优化、独立扩容业务变化也不影响IM的稳定性。这个解耦做得好后面维护会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑