spring+netty实现一个最小可运行的im server
开发学院2025-08-11 20:57:40
近期想做一个基于springboot+netty的im服务器,各方收集资料后,汇总如下
近期想做一个基于springboot+netty的im服务器,各方收集资料后,汇总如下。
需要支持浏览器的WebSocket和来自app端的Socket连接,结构如下:
┌──────────┐ ┌──────────┐ │ Browser │ │ App │ ← WebSocket / Socket.IO └────┬─────┘ └────┬─────┘ │ 1.长连接 │ │ │ ┌────┴─────────────────────┴────┐ │ Netty WebSocket Gateway │ ← 2.多协议编解码、心跳、黑白名单 │ (独立进程,无业务逻辑) │ └────┬─────────────────────┬────┘ │3. MQ/Redis 事件 │3. MQ/Redis 事件 ┌────┴────┐ ┌────┴────┐ │Chat-Svc│ │User-Svc │ │(Spring)│ │(Spring) │ └────┬────┘ └────┬────┘ │4. MyBatis-Plus │4. MyBatis-Plus ┌────┴─────────────────────┴────┐ │ MySQL 8.0 / Redis │ └───────────────────────────────┘
数据库设计(MySQL 8.0)
1 用户表
CREATE TABLE t_user ( id BIGINT PRIMARY KEY, username VARCHAR(32) UNIQUE NOT NULL, avatar VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );
2 单聊消息表
CREATE TABLE t_msg_single ( msg_id BIGINT PRIMARY KEY, from_uid BIGINT NOT NULL, to_uid BIGINT NOT NULL, content TEXT NOT NULL, msg_type TINYINT DEFAULT 1 COMMENT '1=文本 2=图片 3=文件 …', send_time DATETIME DEFAULT CURRENT_TIMESTAMP, deliver_tag TINYINT DEFAULT 0 COMMENT '0未送达 1已送达 2已读', INDEX idx_from_to (from_uid, to_uid), INDEX idx_time (send_time) );
3 群聊基础表
CREATE TABLE t_group ( group_id BIGINT PRIMARY KEY, group_name VARCHAR(64) NOT NULL, owner_uid BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_group_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_id BIGINT NOT NULL, uid BIGINT NOT NULL, UNIQUE KEY uk_group_user (group_id, uid) );
4 群聊消息表(只存一份)
CREATE TABLE t_msg_group ( msg_id BIGINT PRIMARY KEY, group_id BIGINT NOT NULL, from_uid BIGINT NOT NULL, content TEXT NOT NULL, msg_type TINYINT DEFAULT 1, send_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_group_time (group_id, send_time) );
5 离线/未读模型(Kafka 思路)
-- 用户-群离线位点,替代“每人存一份” CREATE TABLE t_group_offset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_id BIGINT NOT NULL, uid BIGINT NOT NULL, last_ack_msg_id BIGINT NOT NULL COMMENT '已读最大msg_id', UNIQUE KEY uk_group_user (group_id, uid) );
Netty + Spring Boot 关键代码
1 Channel 管理与路由
@Component public class WsChannelManager { // uid -> channel private static final MapONLINE = new ConcurrentHashMap<>(); public static void online(Long uid, Channel ch) { ONLINE.put(uid, ch); } public static void offline(Long uid) { ONLINE.remove(uid); } public static Channel get(Long uid) { return ONLINE.get(uid); } }
2 消息实体(JSON)
{ "type": 1, // 1=单聊 2=群聊 3=心跳… "fromUid": 1001, "toUid": 1002, // 群聊时代表 groupId "content": "hello", "msgId": 9223372036854775807 }
3 Netty 解码器 & 业务 Handler
public class ChatServerHandler extends SimpleChannelInboundHandler{ @Autowired private MsgService msgService; // Spring 注入 @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) { MsgDTO dto = JSON.parseObject(frame.text(), MsgDTO.class); switch (dto.getType()) { case 1 -> handleSingle(ctx, dto); case 2 -> handleGroup(ctx, dto); default -> ctx.writeAndFlush(new TextWebSocketFrame("bad type")); } } private void handleSingle(ChannelHandlerContext ctx, MsgDTO dto) { // 1. 落库 msgService.saveSingle(dto); // 2. 实时推送 Channel target = WsChannelManager.get(dto.getToUid()); if (target != null && target.isActive()) { target.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(dto))); } else { // 离线:可写入延迟队列/离线表,后续推送 } } private void handleGroup(ChannelHandlerContext ctx, MsgDTO dto) { // 1. 落库 msgService.saveGroup(dto); // 2. 广播给所有成员 Listmembers = groupService.listMemberUid(dto.getToUid()); members.forEach(uid -> { Channel ch = WsChannelManager.get(uid); if (ch != null && ch.isActive()) { ch.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(dto))); } }); } }
4 Service 层(事务 + 异步)
@Service public class MsgService { @Transactional public void saveSingle(MsgDTO dto) { MsgSingleEntity e = new MsgSingleEntity(); e.setMsgId(dto.getMsgId()); e.setFromUid(dto.getFromUid()); e.setToUid(dto.getToUid()); e.setContent(dto.getContent()); singleMapper.insert(e); } @Async("msgExecutor") // Spring异步线程池,避免阻塞IO线程 public void saveGroup(MsgDTO dto) { MsgGroupEntity e = new MsgGroupEntity(); e.setMsgId(dto.getMsgId()); e.setGroupId(dto.getToUid()); e.setFromUid(dto.getFromUid()); e.setContent(dto.getContent()); groupMapper.insert(e); } }
4. 离线/未读消息拉取
用户上线时,后端根据 last_ack_msg_id 查询 > 该值的所有群消息,再合并单聊 t_msg_single 中未读,一次性推送给客户端。
public ListpullOffline(Long uid) { Listlist = new ArrayList<>(); // 1. 单聊离线 list.addAll(singleMapper.selectUnread(uid)); // 2. 群聊离线 Listgroups = groupService.listGroupIdsByUid(uid); groups.forEach(gid -> { long ack = offsetMapper.selectAck(gid, uid); list.addAll(groupMapper.selectAfter(gid, ack)); }); return list; }
部署 & 扩展要点
Netty 网关 单独部署(多实例 + 无状态),通过 Redis 广播跨节点消息。
业务服务 可按模块拆分(单聊、群聊、文件、好友关系)。
MySQL 分库分表可按 from_uid / group_id 做 Sharding。
消息顺序 由客户端根据 msgId(雪花算法)排序,服务端不保证全局顺序。
消息幂等 客户端发送时携带 msgId,服务端表唯一索引保证不重复落库。
相关文章
- spring+netty实现一个最小可运行的im server
- windows修改ollama程序和模型保存位置
- UE5中使用蓝图实现对象池功能
- UE5开发2D/3D混合平台跳跃游戏优化操作体验
- UE5敌人直接放置场景ok,代码生成不执行AI
- UE5中开发HD-2D游戏的优化设置与2D角色导入技巧
- nginxSpringboot项目常见配置
- 在MacOS上部署ComfyUI的指南
- 解决UE5开发Topdown2D动作游戏的旋转问题
- UE5开发2D游戏设置排序的步骤.
- 大幅提升FPS!Unreal Engine 5 最佳 2D 设置
- Aseprite在线编译教程
- 探索Nexa AI:开源边缘智能的新纪元
- Springboot项目允许根目录txt文件被访问
- lnmp一键安装包多php环境安装
- Python虚拟环境整合包制作:一键打包与运行指南
- aws云服务器使用root登录
- nginx配置允许跨域
- nginx配置springboot反向代理,同时允许上传路径可以直接被访问
- CentOS8更换国内安装源