在传统的 Web 开发模式中,浏览器和服务器之间的通信基本都靠 HTTP 请求完成,而 HTTP 是一种请求响应模型,通信只能由客户端发起。如果服务器有新数据需要推送给浏览器,比如聊天消息、订单状态变更、监控告警等场景,客户端就只能靠轮询不断发请求,既浪费带宽又有延迟。Socket.IO 正是为解决这类问题而生,它基于 WebSocket 协议建立持久连接,服务端可以随时主动推送消息。这篇文章就来详细讲讲如何在 Spring Boot 项目中整合 Socket.IO,搭建一套完整的双向通信服务。

为什么选择 Socket.IO 而不是原生 WebSocket
Spring 本身提供了 @ServerEndpoint 注解和 spring-websocket 模块来支持原生 WebSocket,那为什么还要引入 Socket.IO?主要原因在于 Socket.IO 在 WebSocket 之上做了很多工程化封装。首先是自动降级机制,当浏览器不支持 WebSocket 时,它会自动退回到长轮询等传输方式,保证连接始终可用。其次是内置心跳检测,客户端和服务端会定期发送 ping、pong 报文,及时发现断线并触发重连。
另外,Socket.IO 提供了命名空间和房间(Room)的概念。命名空间可以把不同业务的消息隔离,比如一个命名空间处理聊天,另一个处理系统通知;房间则可以做分组广播,把消息只推送给加入某个房间的用户,这在多人协作、直播弹幕等场景中非常实用。
在 Java 生态中,Socket.IO 的服务端实现是 netty-socketio,它基于 Netty 网络框架构建,性能表现不错,API 也和 Node.js 版本的 Socket.IO 保持高度一致,前端可以直接使用官方的 socket.io.js 客户端连接,兼容性很好。
搭建 Socket.IO 服务端
第一步是在 pom.xml 中引入 netty-socketio 依赖。注意这个库不归属 Spring 官方管理,版本号需要显式指定:
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.23</version>
</dependency>
</dependency>
接下来编写服务端配置。netty-socketio 自带 Netty 服务器,会独占一个端口,不要和 Spring Boot 的 HTTP 端口冲突。这里建议通过 Configuration 类来创建 SocketIOServer 的 Bean,交给 Spring 容器管理:
@Configuration
public class SocketIOConfig {
@Bean
public SocketIOServer socketIOServer() {
com.corundumstudio.socketio.Configuration config =
new com.corundumstudio.socketio.Configuration();
// Socket.IO 服务监听端口,独立于 Spring Boot 的 8080
config.setPort(9092);
// 升级 WebSocket 连接时允许的最大帧长度,按需调整
config.setMaxFramePayloadLength(1024 * 1024);
config.setPingInterval(25000);
config.setPingTimeout(60000);
return new SocketIOServer(config);
}
}
配置完成后,需要写一个监听器在 Spring 容器启动时开启服务,在容器销毁时关闭服务。然后就可以注册事件监听了。下面的代码演示了连接建立、断开、消息接收的完整处理逻辑:
@Component
public class SocketIOListener {
@Autowired
private SocketIOServer server;
@PostConstruct
public void start() {
// 监听客户端连接
server.addConnectListener(client -> {
String userId = client.getHandshakeData().getSingleUrlParam("userId");
client.set("userId", userId);
System.out.println("用户 " + userId + " 已连接,sessionId=" + client.getSessionId());
});
// 监听客户端断开
server.addDisconnectListener(client -> {
System.out.println("客户端断开:" + client.getSessionId());
});
// 监听自定义事件 chatMessage
server.addEventListener("chatMessage", ChatMessage.class, (client, data, ackRequest) -> {
System.out.println("收到消息:" + data.getContent());
// 广播给所有已连接的客户端
server.getBroadcastOperations().sendEvent("chatMessage", data);
});
server.start();
System.out.println("Socket.IO 服务已启动");
}
@PreDestroy
public void stop() {
server.stop();
}
}
代码中有几个关键点值得说明。addConnectListener 里可以通过 getHandshakeData().getSingleUrlParam() 拿到客户端连接时附带的查询参数,通常用它来标识用户身份,然后调用 client.set() 把用户信息绑定到当前会话,后续推送时就能取出来用。addEventListener 的第二个参数是消息体对应的实体类,netty-socketio 会自动完成 JSON 反序列化,直接以对象形式操作数据即可。
服务端主动推送与定向消息
双向通信的核心价值在于服务端可以主动推送。推送方式主要分三类:广播、定向推送和房间推送。广播用 server.getBroadcastOperations().sendEvent(),所有在线客户端都能收到。定向推送则需要遍历 server.getAllClients(),根据会话中绑定的用户 ID 筛选出目标客户端。下面是一个消息推送服务的示例:
@Service
public class PushService {
@Autowired
private SocketIOServer server;
// 定向推送给指定用户
public void pushToUser(String userId, String event, Object data) {
for (SocketIOClient client : server.getAllClients()) {
String uid = client.get("userId");
if (userId.equals(uid)) {
client.sendEvent(event, data);
}
}
}
// 推送给某个房间内的所有客户端
public void pushToRoom(String room, String event, Object data) {
server.getRoomOperations(room).sendEvent(event, data);
}
}
房间的使用分两步,客户端先通过 join 事件申请加入房间,服务端调用 client.joinRoom(room) 完成登记,之后 getRoomOperations(room) 拿到的操作对象就只会向该房间的成员发送消息。这种方式比遍历所有连接再过滤高效得多,适合按群组、按页面维度推送的场景。
前端客户端连接与常见问题处理
前端直接引入 socket.io.js,注意客户端版本要和协议兼容,建议使用 2.x 版本配合 netty-socketio 1.7.x。连接时代码如下:
<script src="https://cdn.socket.io/socket.io-2.5.0.js"></script>
<script>
// 连接时把 userId 放在查询参数里传给服务端
var socket = io('http://127.0.0.1:9092', {
query: 'userId=10001',
transports: ['websocket']
});
// 监听服务端推送的 chatMessage 事件
socket.on('chatMessage', function (data) {
console.log('收到推送:', data);
});
// 向服务端发送消息
function send() {
socket.emit('chatMessage', { content: '你好,服务器' });
}
</script>
整合过程中有几个高频问题需要注意。第一是跨域,前端页面运行在 8080 端口而 Socket.IO 服务在 9092 端口,属于跨域请求,需要在 Configuration 中调用 config.setOrigin(null) 允许所有来源,生产环境则建议指定具体域名白名单。第二是客户端反复断开重连,多半是 transports 参数导致的握手问题,可以先去掉该参数让客户端自动协商,或者显式指定 websocket 方式排查。
第三是会话丢失问题。netty-socketio 默认把会话保存在内存里,服务重启后所有连接都会断开,客户端需要依赖 socket.io 自带的重连机制恢复连接。如果业务要求重启后仍能识别用户,就要在连接建立时根据前端传来的 token 重新绑定用户身份。另外在集群部署时,多个 Socket.IO 节点之间无法共享客户端会话,需要借助 Redis 的发布订阅机制转发消息,由各节点再推送给自己的客户端,这是生产环境必须提前设计的一环。
总结
Spring Boot 整合 Socket.IO 的整体思路并不复杂:通过 netty-socketio 暴露独立的 Netty 端口,注册连接监听和事件监听,配合 Bean 的生命周期方法完成服务的启停。相比原生 WebSocket,它带来的自动重连、心跳保活、房间分组等特性,能显著减少实时推送场景下的开发工作量。只要处理好跨域、会话管理和集群扩展这几个关键点,这套方案完全可以支撑聊天、告警、协同编辑等实时性要求较高的业务场景。
Spring BootSocket.IO双向通信修改时间:2026-09-07 12:12:42