📡 前言:为什么选 Netty-SocketIO?
Spring 官方提供了spring-boot-starter-websocket,为什么不用?
虽然官方的支持 STOMP 协议,上手简单,但在面对高并发、长连接维持、心跳检测、断线自动重连等复杂场景时,基于Netty封装的netty-socketio表现得更加稳健和高性能。
它完美适配了前端的socket.io-client库,让前后端联调变得异常简单。
🏗️ 一、 架构设计:用户如何找到彼此?
IM 系统的核心在于**“路由”**:当 UserA 发消息给 UserB 时,服务器怎么知道 UserB 的长连接是哪一个?
我们需要维护一张用户 ID <–> Socket Session的映射表。
IM 消息流转图 (Mermaid):
🛠️ 二、 后端实战:搭建 Netty-SocketIO 服务
1. 引入依赖
<dependency><groupId>com.corundumstudio.socketio</groupId><artifactId>netty-socketio</artifactId><version>1.7.22</version></dependency>2. 配置启动类 (SocketIOConfig.java)
我们不使用 Tomcat 的端口,而是另起一个 Netty 端口(如 9092)。
@ConfigurationpublicclassSocketIOConfig{@BeanpublicSocketIOServersocketIOServer(){com.corundumstudio.socketio.Configurationconfig=newcom.corundumstudio.socketio.Configuration();config.setHostname("localhost");config.setPort(9092);// 关键:设置最大帧长度,防止发大图报错config.setMaxFramePayloadLength(1024*1024);config.setMaxHttpContentLength(1024*1024);// 握手协议参数SocketConfigsocketConfig=newSocketConfig();socketConfig.setReuseAddress(true);config.setSocketConfig(socketConfig);returnnewSocketIOServer(config);}// Spring Boot 启动时同时启动 Netty 服务@BeanpublicSpringAnnotationScannerspringAnnotationScanner(SocketIOServersocketServer){returnnewSpringAnnotationScanner(socketServer);}}3. 核心业务逻辑 (MessageEventHandler.java)
这里实现了上线注册、单聊、群聊逻辑。
@ComponentpublicclassMessageEventHandler{// 线程安全的 Map,存储 UserId -> SocketClient 的映射publicstaticfinalConcurrentHashMap<String,UUID>USER_CLIENT_MAP=newConcurrentHashMap<>();@AutowiredprivateSocketIOServerserver;// --- 1. 客户端连接 (握手) ---@OnConnectpublicvoidonConnect(SocketIOClientclient){// 前端连接时带上参数:http://localhost:9092?userId=1001StringuserId=client.getHandshakeData().getSingleUrlParam("userId");if(userId!=null){USER_CLIENT_MAP.put(userId,client.getSessionId());System.out.println("用户上线: "+userId);}}// --- 2. 客户端断开 ---@OnDisconnectpublicvoidonDisconnect(SocketIOClientclient){StringuserId=client.getHandshakeData().getSingleUrlParam("userId");if(userId!=null){USER_CLIENT_MAP.remove(userId);System.out.println("用户下线: "+userId);}}// --- 3. 处理单聊消息 ---@OnEvent("send_msg")publicvoidonEvent(SocketIOClientclient,ChatMessageRequestdata){StringtoUserId=data.getToUserId();UUIDtargetSessionId=USER_CLIENT_MAP.get(toUserId);// 如果用户在线,直接推送if(targetSessionId!=null&&server.getClient(targetSessionId)!=null){server.getClient(targetSessionId).sendEvent("receive_msg",data);}else{// 用户不在线,存入数据库标记为“未读消息”saveOfflineMessage(data);}}// --- 4. 处理群聊 (加入房间) ---@OnEvent("join_group")publicvoidonJoinGroup(SocketIOClientclient,StringgroupId){client.joinRoom(groupId);// SocketIO 自带房间管理}@OnEvent("send_group_msg")publicvoidonGroupMsg(SocketIOClientclient,ChatMessageRequestdata){// 直接向房间内广播server.getRoomOperations(data.getGroupId()).sendEvent("receive_group_msg",data);}}🎨 三、 前端 Vue3 实战:Socket.io-client
前端使用socket.io-client库,代码极其简洁。
安装:
npminstallsocket.io-client连接与收发:
import{io}from"socket.io-client";// 1. 建立连接 (带上自己的 ID)constsocket=io("http://localhost:9092",{query:{userId:"1001"},transports:["websocket"]// 强制使用 WebSocket,不用轮询});// 2. 监听连接成功socket.on("connect",()=>{console.log("连接成功,SessionID:",socket.id);});// 3. 接收消息 (监听 receive_msg 事件)socket.on("receive_msg",(data)=>{console.log("收到新消息:",data);// 这里将 data push 到聊天记录数组中,页面会自动渲染messages.value.push(data);});// 4. 发送消息constsendMessage=()=>{socket.emit("send_msg",{fromUserId:"1001",toUserId:"1002",content:"你好,今晚吃什么?",type:"text"});};🚀 四、 进阶挑战:分布式集群下的 Session 共享
如果你的用户量达到 10 万,一台服务器扛不住,你需要部署两台 Netty 服务。
问题来了:
UserA 连上了 Server1,UserB 连上了 Server2。
UserA 发消息给 UserB,Server1 的内存 Map 里找不到 UserB 的 Session,怎么办?
解决方案:Redis Pub/Sub (发布订阅)
- Server1 发现 UserB 不在本地。
- Server1 将消息 Publish 到 Redis 的频道
IM_CHANNEL。 - Server2 订阅了该频道,收到消息后,发现 UserB 在自己这儿。
- Server2 将消息推送给 UserB。
Redisson 提供了很好的支持,或者直接使用 Socket.IO 官方的Redis Adapter。
🎯 总结
通过 Spring Boot + Netty-SocketIO,我们只用了几百行代码就实现了一个高实时性的 IM 系统核心。
这不仅是一个聊天工具,它还是即时通知、在线客服、游戏对战等场景的基石。
Next Step:
现在的消息只存在内存里,重启就丢了。
试着引入MongoDB来存储聊天记录(写入速度快,结构灵活),并实现“历史消息回溯”功能,你的 IM 系统就具备商业价值了!