跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

WebSocket怎么根据UserID发送消息 WebSocket消息分发器实现【技术】

单机环境下通过ConcurrentHashMap以userId为key缓存WebSocketSession,使用getAsyncRemote().sendText()异步发送;分布式时需借助Redis存储节点映射或Pub/Sub广播消息。 WebSocket 怎么根据
userId
找到并发送消息 单机环境下,
userId
到
WebSocketSession
的映射必须是内存级、线程安全的,否则并发写入会丢连接。最常用的是
ConcurrentHashMap
,key 为
userId
,value 为当前活跃的 session。 注意:一个
userId
可能对应多个 session(比如用户在手机 + 笔记本同时登录),所以严格来说应是
ConcurrentHashMap>
;但多数业务只要“推给任意一个在线端”即可,用单个 session 也能满足。
@OnOpen
方法里必须从 URL 路径、请求参数或 token 中解析出
userId
,不能依赖 session 属性后期注入(容易为空) 若使用 Spring Boot 的
@ServerEndpoint
,需确保路径含
{userId}
并用
@PathParam
正确提取 推送前务必检查
session.isOpen()
,否则调用
sendText()
会抛
IllegalStateException
不要直接用
session.getBasicRemote().sendText()
,它会阻塞线程;改用
session.getAsyncRemote().sendText()
,避免 I/O 拖垮整个连接池 为什么
ConcurrentHashMap
在分布式下会失效 当服务部署多节点(如 Node1 和 Node2),用户 A 连在 Node1,用户 B 连在 Node2,Node1 想给 B 发消息时,本地
ConcurrentHashMap
根本查不到 B 的 session —— 这就是单机缓存的天然边界。 此时必须引入外部协调机制,让所有节点知道“谁在哪”。常见做法有两种: Redis Set 存储
ws:user:{userId}
,成员为
nodeId:sessionId
,发消息前先查 Redis 获取目标用户所在节点列表 Redis Pub/Sub 订阅统一频道(如
ws:msg:all
),所有节点监听,收到后自行判断本机是否有对应
userId
的 session 前者适合精准投递(只通知目标节点),后者实现简单但网络开销略高(每条消息广播给所有节点) Kafka/RocketMQ 更适合高吞吐、需重试或审计的场景,但引入额外运维成本,小项目不推荐
sendToUser()
方法里最容易漏掉的三件事 封装一个
sendToUser(String userId, String msg)
工具方法很常见,但线上出问题往往就卡在这几个细节上: WebSocket 8.18.2 WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。 下载 没做空值校验:
userId
为空或
msg
为 null 时,直接调用
sendText(null)
会导致连接异常关闭 没处理 session 过期:
WebSocketSession
对象可能还存在,但底层 TCP 连接已断;必须配合心跳检测(如
@Scheduled
定期遍历并调用
session.isOpen()
)或依赖
@OnClose
清理 没加 try-catch:
sendText()
抛出的
IOException
或
IllegalStateException
若未捕获,会导致后续消息无法发送,且无日志提示 前端传
userId
不安全?怎么验证 前端在建立 WebSocket 连接时通过 URL(如
ws://host/ws?userId=123
)或路径参数(如
/ws/123
)传
userId
,这是明文,不可信。必须在
@OnOpen
阶段做服务端鉴权。 典型做法是:要求前端在握手请求头中携带
x-auth-token
,后端解析 JWT 或查 Redis 缓存,比对 token 中声明的
userId
是否与 URL 中的一致。不一致则拒绝连接(
session.close(CloseStatus.UNAUTHORIZED)
)。 如果用 Spring Security + WebSocket,可复用已有 filter 链,但注意:HTTP 握手阶段的 header 是可用的,而
Principal
默认不会自动注入到
@ServerEndpoint
的生命周期方法中,需手动从
HttpSession
或 request attribute 提取。 真正难缠的不是技术实现,而是连接状态和业务身份的耦合时机——
userId
必须在
onOpen
结束前确认,晚一秒,就可能把消息错推到冒名用户头上。

相关文章