@ServerEndpoint路径必须带参数(如/client/{uid})才能支持单聊,因Session.getId()随机且重连即变,需握手时通过URL路径传递uid并绑定到ConcurrentHashMap,否则只能群聊;须避免查询参数、注意URL编码、处理发送异常、集群下用Redis PUB/SUB同步。

为什么@ServerEndpoint路径带参数才支持单聊
因为WebSocket连接本身不携带业务身份,服务端靠Session维持长连接,但Session.getId()是随机字符串,每次重连都变,没法对应真实用户。必须让客户端在握手时就把uid塞进URL路径(比如/api/websocket/client/{uid}),服务端才能在@OnOpen里提取并绑定到内存Map中。
- 不带参数的
@ServerEndpoint("/chat")只能做群聊,所有消息往一个广播池里扔 - 路径参数必须用
@PathParam接收,不能靠查询参数(?uid=123)——WebSocket握手阶段Servlet容器不解析Query String - 注意URL编码:如果
uid含特殊字符(如@、/),客户端得encodeURIComponent(),服务端不用额外解码
webSocketClientMap存引用但没加锁,真没问题?
问题很大。Java WebSocket容器(如Tomcat)默认为每个连接分配独立线程处理@OnMessage,多个客户端同时发消息时,HashMap并发put可能引发死循环或数据丢失——这是典型的“内存可见性+线程安全”双坑。
- 必须换成
ConcurrentHashMap,别信“只是存个引用不会出事”的直觉 -
AtomicInteger计数在线人数是对的,但记得在@OnClose里decrementAndGet(),否则断网重连后人数虚高 - 更稳妥的做法是把
ConcurrentHashMap声明为static且加@Component交由Spring管理生命周期,避免类加载器隔离导致Map不共享
广播消息时,session.getBasicRemote().sendText()抛IOException怎么处理
这是最常被忽略的“假稳定”:连接看似建着,其实网络已中断,但TCP层面还没触发RST,sendText()会卡住几秒后直接抛异常。不捕获会导致整个广播流程中断,其他正常用户收不到消息。
- 必须对每个
session的发送做try-catch,捕获IOException和IllegalStateException(session已关闭) - 捕获后立即从
ConcurrentHashMap里remove()该uid,否则下次发消息还会失败 - 别用
session.isOpen()提前判断——它返回true不代表能发成功,本质是伪防御
本地跑通了,上线就丢消息?检查Redis同步逻辑
单机版聊天室靠ConcurrentHashMap就能广播,但生产环境必集群。此时A用户连在服务器1,B用户连在服务器2,光靠各自Map无法互通——必须引入外部中间件做消息中转。
立即学习“Java免费学习笔记(深入)”;
- 选Redis的
PUB/SUB而非Stream:延迟低,适合实时通知;但注意订阅关系需在应用启动时建立,别放在@OnOpen里(否则扩容时新节点收不到旧消息) - 消息体建议只传
uid和msgId,具体内容走数据库查——避免Redis消息体过大拖慢吞吐 - 别漏掉
@PreDestroy里取消订阅,否则重启应用后Redis连接堆积,最终触发maxclients限制










