easy-socketio 使用指南
easy-socketio 是基于 netty-socketio 2.0.14 的 Spring Boot Socket.IO 服务端组件:自动配置 + 生命周期托管 + 握手鉴权 SPI + 房间协议 + 可选 Redis 集群模式。本文覆盖从引入到集群部署的全部使用场景,设计细节见 design.md。
- 1. 引入依赖
- 2. 最小配置启用
- 3. 实现握手鉴权 SPI
- 4. 编写业务事件处理器
- 5. 服务端推送
- 6. 房间协议
- 7. 集群部署(分布式房间)
- 8. 客户端能力(SocketClient)
- 9. 配置项
- 10. 常见问题 FAQ
1. 引入依赖
<dependency>
<groupId>io.matrix</groupId>
<artifactId>easy-socketio</artifactId>
<version>${revision}</version>
</dependency>
注意:easy-socketio 暂未纳入 kernel-dependencies BOM,当前需显式指定版本(待补 BOM 条目后可省略)。
前端使用官方 socket.io-client v4(EIO4 协议):
<script src="https://cdn.socket.io/4.x/socket.io.min.js"></script>
2. 最小配置启用
组件默认关闭,socketio.enabled=true 才会装配并启动端口:
socketio:
enabled: true
port: 9092
# hostname: 0.0.0.0 # 监听地址,默认全网卡
# path: /socket.io # 握手路径,需与前端 io(url, { path }) 一致
# namespace: / # 命名空间
# authRequired: false # 是否强制握手鉴权
启用后 SocketIOServer 会作为 Spring Bean 暴露,随容器启停(SmartLifecycle),无需手工 start/stop。
3. 实现握手鉴权 SPI
实现 AuthListener 并注册为 Bean:
@Component
public class AppAuthListener implements AuthListener {
@Override
public String authenticate(String token) {
// 校验 token,通过返回 userCode,失败返回 null
return authorizeService.verify(token);
}
@Override
public boolean canSubscribe(String userCode, String room) {
// 房间订阅权限校验,按业务房间语义判定;默认放行
return true;
}
}
前端连接时携带 token,两种方式任选(auth 载荷优先):
// 方式一:auth 载荷(client v4 推荐)
const socket = io("http://localhost:9092", { auth: { token: "xxx" } });
// 方式二:URL 参数
const socket = io("http://localhost:9092?token=xxx");
鉴权结果:
| 情形 | 结果 |
|---|---|
authenticate 返回 userCode | 自动加入 user:{userCode} 个人房间 |
返回 null 且 authRequired=true | 连接被拒绝(unauthorized) |
返回 null 且 authRequired=false | 以 anonymous 身份连接,只进 broadcast 房间 |
4. 编写业务事件处理器
一个 BaseHandler 子类对应一个客户端可调用的事件(topic),注册为 Bean 即自动挂载:
// 1. 定义事件载荷(实现 Event 标记接口)
@Data
public class TraceEvent implements Event {
private String orderNo;
}
// 2. 编写处理器
@Component
public class TraceHandler extends BaseHandler<TraceEvent> {
public TraceHandler() {
super("trace", TraceEvent.class); // topic 与事件类型
}
@Override
protected void handle(SocketIOServer server, SocketIOClient client, TraceEvent msg) {
// 客户端 emit("trace", {...}) 触发
}
}
客户端调用:
socket.emit("trace", { orderNo: "SF123456" });
5. 服务端推送
直接注入 SocketIOServer 推送。命名空间必须经 ServerAdapter.resolveNamespace 获取(默认 / 在 netty-socketio 内部名为空串,直接 getNamespace("/") 会新建命名空间导致监听与投递错位):
@Service
public class PushService {
private final SocketIOServer server;
public PushService(SocketIOServer server) {
this.server = server;
}
/** 点对点:推送到用户个人房间 */
public void pushToUser(String userCode, String topic, Object msg) {
ServerAdapter.resolveNamespace(server, "/")
.getRoomOperations(RoomAdmin.roomUser(userCode))
.sendEvent(topic, msg);
}
/** 全员广播 */
public void broadcast(String topic, Object msg) {
ServerAdapter.resolveNamespace(server, "/")
.getRoomOperations(RoomAdmin.ROOM_BROADCAST)
.sendEvent(topic, msg);
}
}
客户端接收:
socket.on("notice", (msg) => { /* ... */ });
定向投递载荷 Packet(rooms/clients/sender/message)是业务侧到推送侧的传输约定,供上层推送服务(如 easy-notification)解析后调用 sendEvent。
6. 房间协议
三级房间,前两级自动加入,业务对象房间由客户端显式订阅:
| 房间 | 加入方式 | 用途 |
|---|---|---|
user:{userCode} | 鉴权通过自动加入 | 点对点推送 |
broadcast | 连接成功自动加入 | 全员广播 |
product:{code} | 客户端显式订阅(经 canSubscribe 校验) | 业务对象级订阅(如单据动态) |
客户端订阅/退订:
socket.emit("subscribe", { room: "product:PRD-001" });
socket.emit("unsubscribe", { room: "product:PRD-001" });
服务端推送业务房间:
ServerAdapter.resolveNamespace(server, "/")
.getRoomOperations(RoomAdmin.roomProduct("PRD-001"))
.sendEvent("notice", msg);
7. 集群部署(分布式房间)
无需改业务代码。应用已有 Redis 时(容器中存在 RedisMessageListenerContainer + RedisTemplate Bean),组件自动切换为分布式模式:
spring:
data:
redis:
host: 127.0.0.1
port: 6379
| 模式 | 触发条件 | 行为 |
|---|---|---|
| 单机(默认) | 无 RedisMessageListenerContainer | netty-socketio 内存房间 |
| 集群 | 存在 RedisMessageListenerContainer | RedisStoreFactory:会话存 Redis Hash(Store_{sessionId}),跨节点广播走 Redis Pub/Sub,nodeId 过滤抑制回环 |
多节点部署注意:
- RedisTemplate value 序列化器必须保留类型信息(JDK 序列化,或带 defaultTyping 的 Jackson),Pub/Sub 消息按
PubSubMessage类型反序列化,普通 JSON 序列化器会失败 - 负载均衡建议对
/socket.io配置 sticky session(主要影响 polling 降级场景;WebSocket 升级后连接本身固定节点) - 组件不缓存离线消息,可靠投递/离线补偿由上层业务设计
8. 客户端能力(SocketClient)
服务端到服务端通信(如业务服务连接独立部署的 easy-notification、连通性探测)。依赖 optional,使用方需显式引入:
<dependency>
<groupId>io.socket</groupId>
<artifactId>socket.io-client</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
// 连接(token 经 auth 载荷传递,对应服务端 AuthListener.authenticate;匿名传 null)
SocketClient client = SocketClient.connect("http://host:9092", token);
client.emit("chat", Map.of("room", "product:PRD-001", "content", "hello")); // POJO/Map 自动 JSON 序列化
client.on("order.paid", OrderPaidPayload.class, payload -> ...); // 订阅并反序列化
client.on(Socket.EVENT_CONNECT, args -> ...); // 连接生命周期
client.close(); // 断开(自动重连内建,默认开启)
9. 配置项
| 配置 | 默认值 | 说明 |
|---|---|---|
socketio.enabled | false | 是否启用(唯一开关,未启用零装配) |
socketio.hostname | 0.0.0.0 | 监听地址 |
socketio.port | 9092 | 监听端口(独立于应用 HTTP 端口) |
socketio.path | /socket.io | 握手路径,需与前端 io(url, { path }) 一致 |
socketio.namespace | / | 命名空间 |
socketio.authRequired | false | 握手强制鉴权;false 时 anonymous 只进 broadcast |
10. 常见问题 FAQ
Q1:为什么调试时 connect 回调先于 auth 执行? EIO4 协议时序问题(netty-socketio 2.0.14 实测)。组件已处理:user 房间在 auth 回调内立即加入,connect 回调仅做幂等兜底,业务无感。
Q2:推送发出去了但客户端收不到?
按序排查:① 命名空间是否经 resolveNamespace 获取(直接 getNamespace("/") 会错位);② 房间名是否一致(user: / product: 前缀拼写);③ topic 是否一致;④ 集群模式下确认 Redis 配置生效(见 Q3)。
Q3:集群下收不到其它节点的广播?
① 确认容器中存在 RedisMessageListenerContainer Bean(分布式模式的唯一开关);② 检查 RedisTemplate value 序列化器是否保留类型信息——普通 JSON 序列化器反序列化 PubSubMessage 会失败,且异常可能被监听线程吞掉,建议先用 JDK 序列化验证。
Q4:socket.io 独立端口 9092 会撞应用端口吗? 不会。netty-socketio 启动独立 HTTP 服务,不经过 Spring MVC,与应用 HTTP 端口互不干扰;跨域由 socket.io 协议握手处理,不受应用 CORS 配置影响。
Q5:如何对连接做更细的房间权限控制?
实现 AuthListener.canSubscribe(userCode, room),按房间语义(如 product: 前缀解析业务码)判定,返回 false 即拒绝订阅并记录日志。