单点推送和广播是同一连接管理机制下的两种消息分发策略:广播需遍历concurrenthashmap或concurrentlinkedqueue中的在线session并校验isopen()后发送;单点推送则通过用户id映射多个session,依赖连接时传入userid并维护concurrenthashmap,登出时需主动关闭该用户所有session。

单点推送和广播不是两个并列功能,而是同一套连接管理机制下的两种消息分发策略——关键在会话怎么存、消息往哪发。
如何安全地维护 WebSocket 会话集合
所有推送能力都依赖一个可读写、线程安全的活跃连接容器。用 ConcurrentHashMap 存 Session 或 WsContext 是通用解法,但必须注意三点:
- 不能用
HashSet或普通HashMap,多线程并发 add/remove 会抛ConcurrentModificationException - 连接关闭时必须显式从集合中移除,否则内存泄漏 + 广播失败(
session.isOpen()返回false但对象仍被引用) - Spring WebFlux 场景下推荐用
ConcurrentLinkedQueue,避免ConcurrentHashMap的 key 冲突风险(如多个 tab 共享同一 session id)
广播消息:遍历所有在线 Session 发送
广播本质就是「无差别转发」,但实际开发中容易忽略连接状态检查:
- 发送前必须调用
session.isOpen(),否则可能触发IllegalStateException: Session is invalid - 不要在循环中直接调用
session.getRemoteAddress(),部分嵌入式服务器(如 Jetty 9.4+)在连接断开后该方法会抛NullPointerException - 广播大量消息时建议异步执行(如
CompletableFuture.runAsync()),避免阻塞主线程导致新连接接入延迟
示例逻辑(Javalin):
connections.forEach(ctx -> { if (ctx.session.isOpen()) { ctx.send(message); }});
单点推送:按用户标识精准路由到 Session
单点不等于「只发一个」,而是「发给某用户的所有活跃终端」。难点在于关联用户 ID 和 Session:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 连接建立时,客户端需在 URL query 或首帧消息里传
userId(如ws://host/ws?uid=1001),服务端解析后存入session.getUserProperties().put("uid", uid) - 用
ConcurrentHashMap<string copyonwritearraylist>></string>管理用户-会话映射,避免查表时加锁 - 用户登出或 Token 失效时,要主动遍历该用户所有
Session并调用session.close(),不能只等心跳超时
常见错误:把 userId 当作 Session ID 使用——WebSocket 协议本身不提供用户身份,全靠应用层绑定和维护。
STOMP 协议下 /user/xxx 路径的实质
Spring Boot + STOMP 的 /user/{id}/queue 看似魔法,其实底层仍是会话路由:
- STOMP broker 会自动将带
/user/前缀的 destination 解析为「当前认证用户的会话集合」 - 它依赖 Spring Security 的
Principal,所以必须确保 WebSocket 连接走的是同一个认证上下文(比如 Cookie 或 JWT Header 一致) - 若用 SockJS 回退,需额外配置
withSockJS().setSessionCookieNeeded(true),否则匿名会话无法绑定用户
绕过 STOMP 自己实现单点,反而更可控——尤其当你要支持「同一用户多设备互踢」或「消息离线缓存」时。
真正难的从来不是发消息,而是判断「此刻该用户有几个有效连接、哪些该推、哪些该跳过、断连后怎么补」——这些细节藏在连接生命周期管理里,而不是 send() 那一行代码中。










