当前位置: 首页 > news >正文

多端即时通讯源码技术实践,Redis路由、MySQL持久化与Socket接入

即时通讯系统看起来只是“发送一条消息,再推送给接收方”,但真正进入多端、集群和高并发环境后,消息链路会迅速变得复杂。

同一名用户可能同时登录安卓、iOS、H5和PC端;消息类型也不再局限于文字,还包括图片、文件、自定义表情、红包、名片、引用回复、合并转发、音视频邀请等内容。

此时,系统需要处理的不只是实时推送,还包括:

消息是否重复、顺序是否一致、离线后能否补回、多个终端状态是否同步,以及节点扩容后如何找到用户连接。

基于Java Spring Boot、Redis、MySQL和WebSocket,构建多端即时通讯系统中的可靠消息链路。

01 从一条消息开始拆解IM系统

先看一个普通的群聊场景。

用户通过安卓端向群聊发送一条消息:

@张明 请查看刚刚上传的项目文件

这条消息背后可能同时包含:

  • 文本内容;
  • @群成员;
  • 文件引用;
  • 发送人信息;
  • 群会话标识;
  • 客户端消息编号;
  • 服务端消息编号;
  • 会话序列号;
  • 发送时间;
  • 消息状态。

如果接收方正在使用PC端,消息需要立即推送到Vue2页面;如果另一名成员处于离线状态,消息还要进入离线同步范围。

发送者本人如果同时登录H5端,H5也需要同步显示刚发送的内容。

因此,即时通讯系统中的一条消息,至少要完成四件事:

接收、存储、路由、同步。

任何一个环节处理不完整,都会出现消息丢失、重复、乱序或多端状态不一致。

02 多端统一的消息结构

安卓、苹果和H5端可以通过uni-app开发,PC端使用Vue2,后端采用Spring Boot。

不同客户端虽然界面实现不同,但必须使用统一的消息模型。

一个基础消息体可以设计为:

{ "clientMessageId": "android_10086_1720589100123", "messageId": "20260710000192384", "conversationId": "group_30001", "senderId": "user_10086", "receiverId": "group_30001", "messageType": 1, "sequence": 98231, "sendTime": 1783645200123, "content": { "text": "请查看刚刚上传的项目文件", "atUserIds": ["user_20001"], "replyMessageId": "20260710000191001" } }

其中几个字段承担着不同职责。

字段主要用途
clientMessageId客户端生成,用于消息幂等
messageId服务端生成的全局消息编号
conversationId标识单聊或群聊会话
messageType区分文本、文件、红包、名片等类型
sequence维护会话内消息顺序
atUserIds记录群聊中被@的成员
replyMessageId关联被引用回复的消息

这套结构不只适用于文本消息。

文件、红包、名片分享、自定义表情、语音通话邀请和多人会议通知,都可以通过扩展messageTypecontent完成。

例如,文件消息可以保存:

文件名称 文件大小 文件格式 资源地址 缩略图地址 上传状态

消息表中只保留文件元数据,不直接保存文件二进制内容,从而避免数据库记录过大。

03 WebSocket负责实时,但不负责全部可靠性

WebSocket能够建立长连接,适合单聊、群聊、已读回执、消息撤回等实时事件。

但WebSocket连接成功,并不意味着消息一定可靠。

一次消息发送过程中,可能出现以下异常:

情况一:客户端已发送,服务端未落库

网络请求已经到达服务端,但数据库写入失败。此时如果直接向客户端返回成功,消息会永久丢失。

情况二:服务端已经保存,确认包没有返回

客户端没有收到发送成功通知,于是重新发送,服务端可能插入两条重复消息。

情况三:接收端突然掉线

消息已经生成并持久化,但接收方连接中断,实时推送失败,需要等待重新登录后补拉。

情况四:多个终端状态不一致

手机端已经阅读消息,PC端却仍显示未读数量;或者手机端撤回消息后,H5端仍显示原内容。

所以,WebSocket只是消息传输通道,可靠性还需要持久化、幂等、序列号和状态同步共同完成。

04 一条可靠消息的完整处理链路

可以把消息发送过程拆成三个阶段。

第一阶段:客户端提交

客户端生成临时消息编号,在本地先显示“发送中”状态,再通过Socket或WebSocket发送到服务端。

第二阶段:服务端确认

服务端依次执行:

身份认证 ↓ 会话权限校验 ↓ 重复消息校验 ↓ 生成服务端消息ID ↓ 分配会话序列号 ↓ 写入持久化存储 ↓ 返回发送确认

客户端只有收到服务端确认后,才能把消息状态调整为“已发送”。

第三阶段:消息分发

消息落库完成后,服务端查询接收方在线节点:

接收方在线 → 推送到对应连接节点 接收方离线 → 保留在离线同步范围 发送者多端在线 → 同步到其他登录设备

这套流程能够保证:即使实时推送失败,消息仍然可以依靠历史记录和离线同步恢复。

05 Redis幂等控制,避免重复消息

移动网络不稳定时,客户端通常会设置超时重试。

比如用户发送文件消息后,五秒内没有收到确认,客户端再次提交同一请求。

如果服务端没有幂等控制,聊天窗口会出现两条完全相同的消息。

解决方式是由客户端生成clientMessageId,服务端使用发送人ID和客户端消息编号组成唯一键。

@Service public class MessageIdempotentService { private final StringRedisTemplate redisTemplate; public MessageIdempotentService( StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public boolean tryAccept( String senderId, String clientMessageId) { String key = "im:message:idempotent:" + senderId + ":" + clientMessageId; Boolean success = redisTemplate.opsForValue() .setIfAbsent( key, "1", Duration.ofMinutes(10) ); return Boolean.TRUE.equals(success); } }

第一次请求进入时,Redis中不存在该键,写入成功。

客户端再次重试时,setIfAbsent返回失败,服务端便可以识别为重复消息。

这里不能使用消息正文判断重复。

因为用户连续发送两次“收到”,可能是合法操作。只有客户端消息编号才能明确区分请求是否相同。

涉及钱包和红包时,需要双重幂等

红包、钱包余额、付费礼物等业务不能只依赖Redis。

Redis缓存可能被清除,也可能在节点故障时出现异常,因此数据库层还应增加唯一约束。

ALTER TABLE im_wallet_record ADD UNIQUE KEY uk_business_no (business_no);

当同一业务流水重复写入时,MySQL唯一索引能够阻止二次入账。

可以将处理逻辑分为两层:

Redis:快速拦截重复请求 MySQL:保证最终业务唯一

这种方式同样适用于重复领取红包、重复提交转账和重复确认支付。

06 用会话序列号处理乱序和断线补偿

消息时间不能完全依赖客户端生成。

原因很简单:不同设备的系统时间可能不一致,网络延迟也可能导致后发送的消息先到达服务端。

因此,每个会话都应维护独立、连续递增的序列号。

例如:

group_30001 消息A sequence = 98231 消息B sequence = 98232 消息C sequence = 98233

客户端按sequence排序,而不是只依赖本地时间。

Redis中的原子自增适合生成序列号:

public long nextSequence(String conversationId) { String key = "im:conversation:sequence:" + conversationId; Long sequence = redisTemplate.opsForValue() .increment(key); if (sequence == null) { throw new IllegalStateException( "会话序列号生成失败" ); } return sequence; }

序列号还可以解决断线后的消息恢复。

假设客户端最后保存的序列号是98220,服务端当前序列号是98233,那么客户端缺少的消息范围就是:

98221—98233

重新连接后,只需要按序列区间拉取缺失记录,不必重新同步整个会话。

序列号还能支撑哪些功能

序列号不只是排序字段,还能参与多个模块:

  • 离线消息补拉;
  • 历史消息漫游;
  • 已读未读计算;
  • 跨端同步;
  • 撤回消息定位;
  • 引用回复定位;
  • 消息缺口检测。

当用户在PC端登录新设备时,也可以根据服务端会话游标逐步恢复聊天记录。

07 Redis如何完成集群节点路由

单机部署时,所有用户连接都存在同一个Java进程中。

服务端收到消息后,可以直接从内存连接表中找到接收方。

但进入多机部署后,情况会变成:

用户A → gateway-01 用户B → gateway-03 用户C → gateway-05

用户A向用户B发送消息时,gateway-01并没有用户B的本地连接。

因此,需要在Redis中记录用户和节点之间的映射关系。

im:online:user_1001:android → gateway-01 im:online:user_1001:pc → gateway-02 im:online:user_2001:ios → gateway-03

消息服务收到请求后,先查询接收方位于哪个节点,再将消息推送给对应网关。

如果用户同时登录多个终端,则需要返回多个节点和设备记录。

多端同步不能只推送给接收方

用户在安卓端发送消息时,自己的PC端和H5端也应同步显示。

所以消息路由对象通常包括两部分:

接收方所有在线终端 发送方除当前设备外的其他在线终端

这样才能保证手机、网页和桌面端聊天记录一致。

类似的跨端同步事件还包括:

  • 消息已读;
  • 消息撤回;
  • 会话置顶;
  • 消息收藏;
  • 好友关系变化;
  • 群成员变化;
  • 红包领取状态;
  • 未读数更新。

08 群聊消息不能同步遍历全部成员

小群聊只有十几人时,发送消息后逐个推送似乎没有明显问题。

但当群成员数量扩大后,每发送一条消息都同步遍历成员,会产生明显压力。

问题主要集中在三个方面:

  1. 业务线程被长期占用;
  2. 成员查询次数增加;
  3. 在线推送与离线处理混在一起。

更合适的群聊消息流程是:

消息落库 ↓ 生成群消息事件 ↓ 拆分群成员或节点分片 ↓ 异步推送在线用户 ↓ 离线用户后续补拉

群聊发送接口只负责完成核心校验和消息保存,不等待全部成员推送成功才返回。

群管理权限应优先读取缓存

群聊消息发送前,需要判断:

  • 发送人是否仍在群内;
  • 是否开启全员禁言;
  • 当前成员是否被单独禁言;
  • 是否允许普通成员发文件;
  • 是否具备@全体成员权限;
  • 名片分享是否符合群权限。

群管理员、禁言状态和群成员热点信息可以缓存在Redis中。

MySQL负责保存最终数据,Redis负责高频权限读取。

当管理员移除群成员时,应同时执行:

更新MySQL群成员关系 清理Redis群成员缓存 通知成员所在在线节点 广播群系统消息

否则,被移除的成员可能因为旧缓存仍然继续发送消息。

09 MySQL保存事实数据,Redis保存实时状态

Redis速度快,但不适合永久承担全部消息存储。

比较清晰的职责划分是:

MySQL保存

  • 消息正文;
  • 会话记录;
  • 好友关系;
  • 群成员关系;
  • 红包流水;
  • 钱包流水;
  • 收藏记录;
  • 朋友圈内容;
  • 历史文件记录。

Redis保存

  • 在线状态;
  • 用户所在网关;
  • 会话序列号;
  • 幂等键;
  • 未读计数缓存;
  • 群权限热点数据;
  • 临时令牌;
  • 限流数据。

消息表可以包含:

message_id conversation_id sequence sender_id receiver_id message_type message_content send_time message_status

当消息量不断增长时,可以根据会话ID进行分表。

例如:

im_message_00 im_message_01 im_message_02 ... im_message_31

通过conversationId计算哈希值,将同一会话尽量写入固定分片。

这样查询历史记录时,可以直接定位到对应消息表。

10 已读未读不要逐条更新

很多即时通讯系统初期会为每条消息保存一个“已读”字段。

单聊消息量较少时还能运行,但群聊和多端同步场景下,逐条更新会产生大量数据库操作。

更轻量的做法是记录:

用户在某个会话中的最大已读序列号。

例如,用户当前已读到6532

{ "conversationId": "single_1001_1002", "readSequence": 6532 }

那么序列号小于或等于6532的消息,都可以视为已读。

未读范围则是:

用户最大已读序列号之后 到 会话最新序列号之间

这种方式不需要为每条消息单独执行更新。

群聊已读需要控制数据规模

群聊中如果为每条消息保存每个成员的已读记录,数据量会快速增加。

可以根据实际需求采用不同策略:

场景建议方式
普通群聊保存成员最大已读序列
大型群聊只维护未读数量
群公告保存详细已读人员
@消息单独记录提醒状态
重要通知保存送达和已读回执

手机端阅读消息后,服务端还要把新的readSequence同步给PC和H5端,使其他设备及时清除未读标识。

11 消息撤回、引用回复和合并转发

这三类功能看起来都作用于历史消息,但实现方式不同。

消息撤回

撤回操作不应直接删除数据库原记录。

更适合的方式是修改状态:

NORMAL → REVOKED

随后向所有在线终端广播撤回事件。

离线客户端重新拉取历史消息时,读取到撤回状态,再显示“该消息已被撤回”。

引用回复

引用回复可以在新消息中保存:

replyMessageId 被引用人 原消息类型 原内容摘要

即使原消息位于很早的聊天记录中,客户端也能根据消息ID快速定位。

如果原消息已撤回,引用区域可以保留位置,但不继续展示完整内容。

合并转发

合并转发不是把多条消息直接拼成长文本。

更合适的消息体是:

转发标题 消息数量 消息摘要列表 原消息ID集合 参与人信息

当内容包含文件、语音、图片或名片时,客户端根据原始消息类型组合展示。

这种设计能够避免单条消息体过大,也便于后续扩展预览页面。

12 Socket、WebSocket和HTTP如何共存

不同终端对通信协议的要求不同。

  • 安卓和苹果可以使用自定义Socket协议;
  • H5和PC浏览器适合WebSocket;
  • 文件上传、朋友圈加载、群管理接口适合HTTP。

但协议层不应该分别实现一套业务。

可以将整体结构拆分为:

Socket接入层 ─────┐ │ WebSocket接入层 ──┼→ 统一消息命令 → 消息业务服务 │ HTTP接口层 ───────┘

接入层只处理:

  • 连接建立;
  • 心跳检测;
  • 数据解码;
  • 身份认证;
  • 协议适配。

完成解码后,所有请求转换为内部统一命令,再进入Spring Boot业务服务。

这样,好友、单聊、群聊、红包、撤回、收藏等逻辑不会与某一种网络协议绑定。

一个端口识别多种协议

当一个端口支持多协议接入时,可以在连接建立阶段判断数据特征。

例如:

包含HTTP Upgrade头 → WebSocket 包含自定义协议魔数 → Socket协议 普通HTTP请求 → HTTP接口

如果后续需要分别扩容,也可以把三种协议拆成独立节点。

浏览器连接增加时,单独扩容WebSocket服务;文件上传增长时,单独扩容HTTP节点。

13 音视频通话不应传输在普通消息通道中

语音通话、视频通话和多人音视频会议会产生大量媒体流。

这些数据不适合直接通过普通聊天WebSocket传输,否则容易影响文本消息、红包通知和文件状态等实时事件。

IM系统在音视频场景中主要负责信令:

发起邀请 接听或拒绝 进入房间 成员加入 成员退出 开启或关闭麦克风 开启或关闭摄像头 主持人操作 会议结束

真正的音频和视频数据交给专门的实时音视频服务。

会议邀请仍然可以使用统一消息模型

多人音视频会议邀请可以定义为特殊消息:

{ "messageType": 3001, "conversationId": "group_30001", "content": { "roomId": "meeting_98231", "callType": "video", "status": "inviting", "memberIds": [ "user_1002", "user_1003" ] } }

成员接听后进入对应房间。

会议结束时,再发送结束事件,并记录通话时长和参与成员。

这样聊天记录中仍然能够展示会议邀请、未接听、已拒绝和已结束状态。

14 SSL/TLS加密与身份校验

聊天消息中可能包含文件、名片、红包、钱包状态和好友关系,因此传输层需要启用SSL/TLS。

不同协议对应的加密方式包括:

WebSocket → WSS HTTP → HTTPS 自定义Socket → TLS加密通道

建立连接后,客户端必须携带有效身份令牌。

服务端不能因为客户端上传了userId,就直接认定连接属于该用户。

还需要校验:

  • Token是否有效;
  • 账号是否被禁用;
  • 设备是否允许登录;
  • 会话是否具备发送权限;
  • 用户是否仍在群聊中;
  • 当前是否处于禁言状态;
  • 文件大小与格式是否合法;
  • 消息撤回是否超时。

扫一扫不能直接暴露用户ID

扫一扫添加好友或加入群聊时,二维码中不建议直接写入原始用户ID或群ID。

可以生成短期有效的随机凭证:

二维码随机Token ↓ 服务端查询对应用户或群聊 ↓ 校验有效期和使用权限 ↓ 执行添加好友或加入群聊

这样能够降低二维码长期传播和参数被修改带来的风险。

15 从单机部署扩展到多机集群

初期即时通讯系统可以使用单机部署:

客户端 ↓ Spring Boot服务 ↓ Redis + MySQL

但随着在线人数增长,连接层、业务层和存储层需要逐步拆分。

一种常见结构是:

安卓 / iOS / H5 / PC ↓ 负载均衡 ↓ Socket与WebSocket网关 ↓ 消息业务服务 ↓ Redis集群 / MySQL数据库

连接网关负责保存长连接。

消息业务服务负责:

  • 消息校验;
  • 消息持久化;
  • 好友关系;
  • 群权限;
  • 红包和钱包;
  • 收藏与朋友圈;
  • 在线路由查询。

长连接扩容与普通HTTP不同

HTTP请求执行完成后连接可以释放,而IM长连接需要长期保留。

因此,用户连接到某个网关后,必须把节点关系写入Redis。

当网关节点发生故障时,客户端执行自动重连:

连接断开 ↓ 切换其他可用网关 ↓ 重新完成身份认证 ↓ 上报本地最大序列号 ↓ 补拉缺失消息

这套流程使节点故障不会直接造成历史消息丢失。

16 高并发连接需要关注的并不只是Java代码

“单机支持多少在线连接”不能只看服务器内存。

实际连接能力还会受到以下因素影响:

  • 操作系统文件句柄;
  • 网络缓冲区;
  • 心跳频率;
  • 每条消息大小;
  • 是否使用阻塞线程;
  • TLS加密开销;
  • 消息持久化速度;
  • Redis访问频率;
  • 网关节点硬件配置。

高并发连接层通常采用事件驱动模型,而不是为每个用户创建一个独立线程。

Spring Boot可以负责业务组织、接口管理和模块编排,长连接层则使用异步网络模型处理大量Socket或WebSocket连接。

当压力继续增加时,可以按模块独立扩容:

Socket网关独立扩容 WebSocket网关独立扩容 HTTP接口独立扩容 消息服务独立扩容 文件服务独立扩容

所有节点通过Redis共享在线路由和实时状态,通过MySQL保存最终业务数据。

17 从底层链路看功能模块如何接入

当消息模型、序列号、在线路由和持久化链路稳定后,业务功能可以逐步接入,而不需要反复修改通信核心。

例如:

好友功能 → 建立单聊会话关系 群聊与群管理 → 复用群成员权限和群消息链路 红包与钱包 → 复用消息通知,增加资金流水幂等 表情与自定义表情 → 扩展消息类型和资源地址 文件发送 → 上传文件后发送文件消息 收藏 → 记录用户与原消息之间的关联 朋友圈 → 复用用户关系、评论通知和资源存储 语音视频通话 → 使用IM传输邀请和状态信令 多人会议 → 使用群成员关系生成会议邀请范围

这些模块表面上差异很大,但最终都依赖几项基础能力:

身份认证、消息编号、会话关系、状态同步和可靠存储。

底层通信链路保持统一后,安卓、苹果、H5和PC端只需根据消息类型完成不同界面展示,服务端也能在保持轻量结构的同时继续扩展集群部署、独立部署和后续业务模块。

技术实现思路的简要归纳说明宠友(IM即时通讯)app,支持语音、文件、图片、视频等多种类型消息的发送,安全可靠,交流轻松,私有化部署,快速开发,极简部署,支持群聊管理、语音视频通话...功能https://chongyou.info/1/product/im.html

http://www.cnnetsun.cn/news/3286571.html

相关文章:

  • 打印机租赁 vs 复印机租赁,隐藏套路对比
  • 阴阳师百鬼夜行AI自动化脚本:智能碎片收集的终极解决方案
  • PCUI样式定制攻略:打造符合品牌风格的Web界面
  • Unity3D低模手里剑资源包:从FBX导入到PBR材质配置全流程解析
  • X-Frame-Bypass与普通iframe对比:性能、兼容性和安全性分析
  • 完善IMX415驱动回调函数
  • Llama-3.3-70B-Instruct-MXFP4-Preview核心技术解密:AutoSmoothQuant算法如何平衡性能与精度
  • VSCode远程编码叠甲kimicc智能编码
  • 当你的Windows开始“思考“:如何用Win11Debloat重新掌控你的电脑
  • 英雄联盟对局先知:选人阶段精准识别队友实力,轻松判断谁是上等马
  • 技术视角下的Syncthing Android:构建去中心化文件同步生态
  • 基于Demucs深度学习模型的人声分离技术实践指南
  • 三模块解析:通达信缠论插件的算法实现与实战应用
  • AIGS是新范式 并非等同于AIGC
  • AI学术写作工具横向测评:沁言学术、DeepSeek、知网研学、Zotero 哪家更香?
  • 基于MKV58F1M0VLQ24与PAM8904的可编程报警系统设计
  • 从零开始掌握ppInk:5个技巧让你的Windows屏幕标注更高效
  • OpCore Simplify:三分钟完成黑苹果OpenCore EFI配置的智能工具
  • 如何用Sports实现足球比赛智能分析:完整实战指南
  • 神级 Cos 出圈!韩国 Coser 完美还原《剑星》伊芙,冷峻科幻气质拉满
  • Docker一键部署PSWD密码生成器:打造你的本地安全密码工厂
  • WaveTools:终极鸣潮游戏性能优化工具箱完整指南
  • 3个核心优化技巧:如何用Ryujinx让Switch游戏在PC上流畅运行
  • SSM框架+MySQL 8.0 网上书店系统:3层架构设计与5大核心模块实现
  • python 基础类json和csv
  • 开源项目推荐:bert-loves-chemistry
  • Windows 11系统优化终极指南:使用Win11Debloat实现专业级性能调优
  • 学习C++之命名空间
  • macOS Xbox控制器驱动深度解析:从系统内核到游戏体验的完整实战指南
  • MateCloud成本优化:微服务架构下的云资源使用成本控制策略