
简介WebSocket聊天室项目是一份融合JavaScript、jQuery与Java的实时通讯应用源码面向想掌握WebSocket双向通信、前后端协作及聊天业务逻辑的初中级开发者适合用于学习典型实时互动应用的实现路径。项目覆盖多人聊天、私人对话与在线客服模块后端使用Java处理连接与消息定向前端借助jQuery简化DOM操作并兼顾登录验证、在线用户维护及断线重连等细节。压缩包共23个文件含5个XML配置文件、2个Java源码、2个HTML页面及class编译产物另有project、classpath等Eclipse工程描述文件整体仅43KB便于导入Tomcat直接使用。已有116人浏览学习通过实际工程可理解WebSocket基于TCP的Upgrade握手、低延迟全双工数据帧传输以及服务端广播、私聊定向、在线状态同步等核心机制。参考该项目还能练习前端交互、后端并发处理、登录与隐私安全设计并可沿其Eclipse目录扩展性能优化与可扩展性方案是一份紧凑而完整的Web开发练手素材。1. WebSocket聊天室没你想的那么简单真正的难点在连接存活“WebSocket聊天室”这个标题看起来像个入门demo但真去写的时候才会发现消息广播只是最表层的事连接怎么建立、怎么保活、怎么在断线后自愈才是线上真正消耗精力的部分。聊天室这类实时交互场景HTTP那套“请求-响应”模型天然不匹配服务端要主动推消息客户端又不可能每秒都来轮询一次。WebSocket在一条TCP连接上做全双工通信让浏览器和Java服务端可以互推数据实时推送才有了底子。这篇按Java后端 原生WebSocket API jQuery前端的组合把最小聊天室跑通再补上心跳、重连这些生产要素。适合要在Java项目里落地WebSocket实时消息的开发者后端或前端方向都可以跟着走一遍。2. 选型先把账算清为什么是WebSocket、Java后端和jQuery前端这三个组合2.1 HTTP轮询撑不起聊天室对比WebSocket的实时性和开销聊天室的第一直觉是“定期刷新消息列表”也就是HTTP短轮询。每3秒拉一次在线100人就是每秒33个请求其中绝大多数是空响应。请求本身带着Cookie、Header服务端还要走一遍Servlet管线连接反复建立和释放这个账在人数上来之后根本没法看。更关键的是实时性做不到“秒级”轮询间隔3秒消息就延迟最多3秒间隔缩到1秒请求量又翻三倍。长轮询稍微聪明一点客户端发请求服务端hold住不返回等有新消息再响应一个然后客户端立刻发下一个请求。实时性变好了但服务端必须为每个客户端挂着一个挂起的请求和线程连接数一多就吃内存和线程池。而且中间任意一层代理超时长轮询就断实现复杂度并不比WebSocket低收益却只有“假装全双工”。WebSocket的做法是把手握在一次完成浏览器发一个带Upgrade: websocket的GET请求服务端返回101 Switching Protocols之后这条TCP连接就是双向通道服务端随时可以推数据客户端也可以随时发数据不再有请求和响应的绑定关系。连接数等于在线人数没有空轮询没有挂起线程实时性回到真正的“事件驱动”。一个常见的误解是“通过websocket发送post请求”。WebSocket协议里没有HTTP方法这个概念消息以文本帧或二进制帧存在Java端OnMessage拿到的就是字符串或字节数组。想区分消息类型靠的是自己在JSON里写type字段而不是沿用HTTP那套动词。这个认知不掰过来后面设计消息协议时很容易跑偏。方案实时性服务端主动推双向通信连接开销实现复杂度短轮询差否否高低长轮询中半否高中SSE好是否低低WebSocket好是是低中2.2 Java端的两个流派注解式ServerEndpoint还是Spring封装Java里做WebSocket服务端主流就两条路。一条是JSR 356标准定义的javax.websocket用ServerEndpoint把一个普通Java类变成WebSocket端点OnOpen、OnMessage、OnClose、OnError四个注解对应连接生命周期。Tomcat、Jetty都内置了这套实现加个依赖、写一个类不碰Spring也能跑。另一条是Spring WebSocket核心是WebSocketHandler接口加HandshakeInterceptor再往上还有STOMP子协议和消息代理。它跟Spring生态结合得好Service可以直接注入convertAndSendToUser可以做点对点推送配合Spring Security做鉴权也顺。代价是概念多了一层Handler、拦截器、订阅路径、消息代理最小的demo也要好几个类。这套方案选注解式理由很直接聊天室业务里连接生命周期就是最核心的模型四个注解方法把“连接进来、消息到达、连接断开、异常发生”完整映射出来了看着代码就能理解连接的状态流转。广播范围自己维护一个Session集合就够。而Spring封装更适合要做房间订阅、消息路由到指定用户、跟Security无缝集成的项目等这套最小闭环跑通再往那上面迁也不迟。进生产前有个信号要留意业务代码里开始频繁写SpringContextUtils.getBean(...)去拿ServerEndpoint里的Service时就该换Spring WebSocket了。ServerEndpoint的实例是WebSocket容器创建的不在Spring管理范围内想要注入Service得绕路这个别扭是选型没选对的典型信号。2.3 jQuery在WebSocket项目里负责什么连接归API渲染归jQuery要先把边界划清楚创建连接、收发消息、监听事件这些全是浏览器原生WebSocket对象的能力jQuery不参与。new WebSocket(url)、socket.send()、onmessage回调都是标准API不需要也不应该让jQuery封装。jQuery在这个项目里的位置是DOM操作往消息列表里追加li、更新在线人数、切发送按钮的禁用状态、处理输入框内容。很多老项目的聊天室前端长这样后端拼好HTML字符串通过WebSocket发过来前端拿$(#list).append(html)直接挂上去。这种做法快是快但用户输入的内容会以HTML形式被解析一条script消息炸掉整个页面。现在前端框架盛行jQuery的新项目越来越少但存量聊天室代码里jQuery还是主力。有同行问“jquery还有必要学吗”如果你要接手维护这种老项目答案是有必要至少看得懂、改得动。从零新写的话原生querySelector加textContent确实也能完成同样的事。前端JavaScript函数这块没什么玄学onmessage本身就是一个函数回调WebSocket每收到一帧消息浏览器就调用一次。你需要在这个函数里完成三件事——解析JSON、判断消息类型、把字段渲染到DOM。类型判断做扎实了系统消息、普通聊天、在线人数变动才不至于混成一锅。3. 服务端落地用Java原生的javax.websocket跑通消息广播和心跳3.1 第一步先定消息协议JSON里需要哪些字段和类型服务端和前端不是两个独立个体它们通过一串字符串沟通。这串字符串的格式不定好后面前端解析、服务端广播都会乱。聊天室最简协议用JSON字段就四个type区分消息类型sender记录发送人content放正文timestamp做展示用的时间戳。{ type: chat, sender: nickname, content: hello, timestamp: 1710000000000 }type至少要有chat、system、online、offline、ping、pong这几个值。system用来广播“xx上线了/xx离开了”这类消息没有具体发送人前端碰到它要渲染成居中灰字而不是普通聊天气泡。online和offline用来告诉前端在线人数变化前端拿到后更新顶部计数。ping和pong是后面心跳机制用的服务端收到ping不广播只回一个pong给同一个连接。这里有一个设计原则服务端只做消息的转发者和类型标注者不做HTML渲染。内容里带什么标签、气泡长什么样应该是前端的事。后端一旦把div classchat-item拼进content前端就失去了样式控制权还引入了XSS风险。协议里只放纯数据展示逻辑完全交给前端这是聊天室协议设计的第一条规矩。3.2 用ServerEndpoint写服务端一个类的三个注解方法先给一个完整的服务端类。这个类不需要继承任何基类加上ServerEndpoint注解后放到容器里WebSocket请求进来时容器自动创建实例并调用对应方法。import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; ServerEndpoint(/chat) public class ChatEndpoint { /** 所有在线会话注意是静态的因为每个连接一个实例 */ private static final SetSession SESSIONS new CopyOnWriteArraySet(); private Session session; OnOpen public void onOpen(Session session) { this.session session; SESSIONS.add(session); broadcast({\type\:\system\,\content\:\新用户上线\}); } OnMessage public void onMessage(String message, Session session) throws IOException { // 心跳消息只回给当前连接不广播 if (message.contains(\type\:\ping\)) { session.getBasicRemote().sendText({\type\:\pong\}); return; } broadcast(message); } OnClose public void onClose(Session session) { SESSIONS.remove(session); broadcast({\type\:\system\,\content\:\用户离线\}); } OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } private void broadcast(String message) { for (Session s : SESSIONS) { try { if (s.isOpen()) { s.getBasicRemote().sendText(message); } } catch (IOException e) { // 单个连接发送失败不影响其他会话 e.printStackTrace(); } } } }逻辑说明分四点。第一SESSIONS必须是staticServerEndpoint的实例是针对每个连接创建的不是单例如果把会话集合放在实例字段里每个连接看到的集合都不完整广播就只能发给自己。第二选CopyOnWriteArraySet而不是ArrayList因为WebSocket的onOpen和onClose可能由不同线程触发并发读多写少这个集合在写时复制遍历时不抛ConcurrentModificationException。第三broadcast方法内部对每个session单独try-catch一个连接断了不能把整个广播循环拖死。第四心跳消息用contains判断不是最严谨的写法正式项目里建议用Jackson或Gson解析后再判断这里为了减少依赖用了字符串匹配后面会看到更规范的改法。3.3 在线用户管理用并发Map存Session重复登录要踢旧连接SetSession能广播但回答不了“这个用户在哪”的问题。要支持用户维度管理得把Set升级成ConcurrentHashMapString, Sessionkey是用户标识value是WebSocket会话。登录时从握手参数或者第一条消息里拿到用户名存入Map同时广播在线人数。private static final MapString, Session USERS new ConcurrentHashMap(); OnMessage public void onMessage(String message, Session session) throws IOException { // 简化处理第一条消息带上用户名做注册 if (message.contains(\type\:\register\)) { String nickname extractNickname(message); Session old USERS.put(nickname, session); if (old ! null old.isOpen()) { // 同一账号重复登录踢掉旧连接 old.close(); } return; } broadcast(message); }重复登录是聊天室必踩的场景。用户在两个标签页打开同一个聊天室后登录的连接会把先登录的顶掉旧连接不关的话同一个用户会收到两遍消息自己发的消息还能看到两份。解决方式就是上面代码里的put返回值判断ConcurrentHashMap.put在key已存在时返回旧值拿回来直接close()。这里顺手把USERS.size()作为在线人数每次变化后随online/offline类型消息广播出去前端就能实时展示在线用户数。3.4 心跳参数怎么定前端多少秒ping、服务端多少秒判死WebSocket连接建立后中间任何一层网络设备、代理都可能把空闲连接悄悄断开。TCP本身没有应用层的“我还活着”信号所以聊天室必须靠应用层心跳来维持连接存活。常见节奏是前端每20秒发一个{type:ping}服务端收到后回pong同时更新这个会话的最后活跃时间服务端单独跑一个定时任务每30秒扫一次发现某会话超过60秒没有活跃记录就主动close()把半死不活的连接清理掉。private static final MapSession, Long LAST_ACTIVE new ConcurrentHashMap(); private static final ScheduledExecutorService CLEANER Executors.newSingleThreadScheduledExecutor(); static { CLEANER.scheduleAtFixedRate(() - { long now System.currentTimeMillis(); for (Session s : LAST_ACTIVE.keySet()) { if (now - LAST_ACTIVE.get(s) 60_000) { try { s.close(); } catch (IOException e) { // 会话已失效忽略 } LAST_ACTIVE.remove(s); } } }, 30, 30, TimeUnit.SECONDS); }三个参数值要配套调前端心跳间隔20秒服务端活跃记录更新频率就是每次收到任何消息时刷新LAST_ACTIVE清扫任务每30秒跑一次超过60秒不活跃判定掉线。间隔太短增加无谓流量太长又发现不了死链。另外注意容器层面的maxIdleTimeoutTomcat默认对WebSocket空闲连接有超时限制要么在容器配置里调大要么让心跳消息足够频繁使它始终处于“非空闲”状态。生产环境这两层要一起看否则前端心跳正常容器照样把连接掐掉再多的应用层心跳也救不回来。4. 前端落地用原生WebSocket API jQuery渲染消息列表和在线状态4.1 建立连接的代码模板构造函数和四个事件回调前端连接代码没有任何魔法new WebSocket(url)一行就建立连接关键在于URL的写法。不能用写死的ws://localhost:8080否则部署到生产换域名就得改前端代码。常规做法是拿location.protocol和location.host动态拼HTTP协议配ws://HTTPS配wss://。let ws connect(); function connect() { const protocol location.protocol https: ? wss:// : ws://; const url protocol location.host /chat; const socket new WebSocket(url); socket.onopen handleOpen; socket.onmessage handleMessage; socket.onerror handleError; socket.onclose handleClose; return socket; }四个事件回调是JavaScript函数的标准用法。onopen表示握手完成连接真正可用这时候把发送按钮的disabled状态去掉。onmessage是核心后面单独讲。onerror和onclose经常成对出现网络异常时先触发error接着触发close。有一个容易踩的细节是不要在onerror里做重连因为随后onclose一定会来在onclose里统一处理断线重连避免两个回调触发两次连接逻辑。readyState这个属性也值得提一下它有CONNECTING(0)、OPEN(1)、CLOSING(2)、CLOSED(3)四个值。发送消息前判断ws.readyState WebSocket.OPEN能避免连接还没建好就调send()导致的异常。4.2 消息分发与jQuery渲染JSON.parse之后按type做不同处理服务端广播过来的原始数据是一个字符串前端第一步永远是JSON.parse。解析之后不要一股脑往页面上堆先按type分路。这是一个典型的JavaScript分发函数每个分支做一件事职责清楚。function handleMessage(event) { let msg; try { msg JSON.parse(event.data); } catch (e) { console.error(消息格式错误, event.data); return; } switch (msg.type) { case chat: renderMessage(msg.sender, msg.content); break; case system: renderSystemMessage(msg.content); break; case online: case offline: updateOnlineCount(msg.count); break; default: // 心跳pong等消息不用处理 break; } }渲染消息时jQuery就派上用场了。直接拼接HTML是最快的写法但也是最危险的写法用户输入里带img onerror或者script都会被浏览器执行。安全做法是用text()方法输出文本让jQuery帮你做HTML转义。这里给出两条路线的对比一眼就能看出差别。function renderMessage(sender, content) { // 危险写法content 是用户输入可能带 HTML 标签 // $(#messageList).append(li sender : content /li); // 安全写法text() 会自动转义 HTML 特殊字符 const $li $(li); $li.text(sender : content); $(#messageList).append($li); // 让列表滚动到最底部 const $box $(#messageBox); $box.scrollTop($box[0].scrollHeight); }消息列表滚动到底部也是体验的关键细节新消息追加后如果列表不在最底部应该自动滑下去。每次append之后取一次scrollHeight赋给scrollTop就行不用做平滑动画否则消息一多滚动会变得卡顿。4.3 断线重连怎么实现指数退避和页面可见性优化网络抖动、代理超时、服务端重启都会导致WebSocket连接断开。生产环境里断线重连不是可选项是保命项。重连的经典策略是指数退避第一次断开等1秒第二次等2秒第三次等4秒最大等到30秒就不再涨。这么做是避免服务端故障恢复瞬间几百个客户端同时发起重连把服务端打垮。let retryCount 0; function connect() { const socket new WebSocket(url); socket.onopen function () { retryCount 0; startHeartbeat(socket); }; socket.onclose function () { stopHeartbeat(); const delay Math.min(1000 * Math.pow(2, retryCount), 30000); retryCount; setTimeout(connect, delay); }; return socket; }心跳和重连要配合起来。心跳的作用是让连接保持活跃也是探测连接是否真正死亡的探针如果send之后一段时间连onclose都没触发说明连接已经处于假死状态前端需要手动close()触发重连逻辑。这里的startHeartbeat和stopHeartbeat是两个配套函数一个负责定时发ping一个负责清理定时器。let heartbeatTimer null; function startHeartbeat(socket) { if (heartbeatTimer) clearInterval(heartbeatTimer); heartbeatTimer setInterval(function () { if (socket.readyState WebSocket.OPEN) { socket.send(JSON.stringify({ type: ping })); } }, 20000); } function stopHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer null; } }有个容易被忽略的优化浏览器标签页切到后台时定时器会被浏览器降频甚至暂停心跳自然不发了这没问题但回到前台时定时器恢复发现连接可能已经断开onclose会立刻触发重连。如果担心后台期间连接被断可以在visibilitychange事件里监听页面重新可见时主动检查readyState不是OPEN就直接重连把恢复时间从几十秒缩短到秒级。4.4 发送消息与UI交互按钮点击、回车键和消息清空发送消息的逻辑在jQuery里就是两个事件绑定点击发送按钮、在输入框按回车。这里唯一的注意点是发送前校验连接状态和内容非空发送成功后清空输入框并保持焦点。$(#sendBtn).on(click, sendMessage); $(#messageInput).on(keydown, function (e) { if (e.key Enter) { e.preventDefault(); sendMessage(); } }); function sendMessage() { const $input $(#messageInput); const content $input.val().trim(); if (!content) return; if (!ws || ws.readyState ! WebSocket.OPEN) { alert(连接已断开正在重连中); return; } ws.send(JSON.stringify({ type: chat, sender: currentNickname, content: content })); $input.val(); $input.focus(); }e.preventDefault()在回车事件里是必须的否则在form里会触发表单提交导致页面刷新。trim()去掉首尾空格空消息直接丢弃。发送按钮的禁用状态最好跟onopen和onclose联动连接断开时按钮置灰重连成功再恢复比弹窗提示体感好得多。currentNickname这个变量在页面加载时由用户输入或者从登录态获取这里不展开登录流程把它当做一个全局变量即可。5. 避坑指南WebSocket聊天室最容易踩的5个坑及排查路径5.1 握手403或404代理层没转发Upgrade头现象浏览器控制台报WebSocket connection failedNetwork面板里请求返回403或者404服务端日志里可能根本没有请求到达。本地开发直连Tomcat没问题一放到服务器上就挂。原因WebSocket握手是HTTP Upgrade请求Nginx这类反向代理默认不会把Upgrade: websocket和Connection: Upgrade这些头转发给后端。代理层只把这当普通GET请求处理转发到后端时升级头已经丢了后端没法响应101。解决在Nginx对应的location里显式配置。location /chat { proxy_pass http://java-backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }proxy_http_version 1.1也很关键HTTP/1.0不支持Upgrade机制。配置完记得nginx -s reload然后重新测试握手。排查这类问题先看浏览器Network里的请求头有没有Upgrade再看Nginx配置里有没有对应转发规则多了一层代理就多看一层这基本是一条直线路径。5.2 连接一到空闲时间就断容器和前端心跳配合缺失现象连接能建立但不是稳定运行而是过几十秒或者一两分钟就自动断断之前服务端没有任何异常日志前端onclose安静地触发。重连之后又断循环往复。原因这通常是两层超时叠加。Nginx层的proxy_read_timeout默认60秒这段期间内如果没有任何数据从后端流向客户端代理就关闭连接。Tomcat对WebSocket也有maxIdleTimeout默认每个会话有超时限制。空闲时间一到任意一层都会主动断开。解决这套组合拳要一起打。前端按前面第4.3节的方案每20秒发一次心跳让连接始终不空闲服务端在OnMessage里收到任意消息都更新活跃时间同时把Nginx的proxy_read_timeout调大到至少120秒或者3600秒。只调Nginx不写心跳连接照样断只写心跳不调Nginx短时间没问题但Nginx的默认超时依然是个隐患。心跳频率和超时时间这两个参数写进项目文档里部署时跟着环境一起检查。5.3 广播只发给自己或丢消息Session集合并发修改现象自己发的消息自己能收到别人的消息偶发收不到或者多人同时发言时某些会话的消息串了。服务端日志偶尔出现ConcurrentModificationException。原因SESSIONS用了ArrayList。一个线程正在遍历SESSIONS做广播另一个线程在onOpen或者onClose里同时add/removeiter遍历到一半结构变了异常直接抛出来。就算用了Collections.synchronizedList同步锁只保护单次操作遍历期间其他线程依然可以改这个list异常照旧。解决用CopyOnWriteArraySet它每次修改都生成新的底层数组遍历不受影响不抛并发修改异常。另一个并发隐患是多个线程同时往同一个Session调用getBasicRemote().sendText()文本帧会交错。需要同步单会话发送的话给每个Session配一个锁对象或者直接用getAsyncRemote()让发送异步化。广播很简单但并发的广播就不简单了这个坑在压测时一定会暴露。5.4 连接状态OPEN却收不到数据OnMessage签名和buffer上限现象浏览器控制台显示连接是OPEN服务端日志也确认收到了消息并且调用了broadcast但客户端的onmessage就是一直不触发。网络没有断开握手是成功的。原因一是OnMessage方法签名不对。javax.websocket对方法参数顺序有约定文本消息对应String二进制对应byte[]最后的Session参数是可选参数。如果方法写成了onMessage(Session session, String message)参数顺序反了容器会认为方法签名不合法直接不认为这是一个消息处理器连接建立了但消息永远进不来。二是消息体太大超过了容器的maxTextMessageBufferSize消息被容器丢弃。三是编码问题前端发过来的是UTF-8服务端按其他编码读取自然乱码或者解析失败。解决检查OnMessage的参数顺序String message必须在Session前面。调整容器buffer大小Tomcat场景在Connector层面配置maxTextMessageBufferSize。前后端统一用UTF-8编码前端send字符串时浏览器默认用UTF-8服务端不要手动做new String(bytes, 其他编码)。这类问题不好定位建议在OnError里把异常打全比在onMessage里try-catch吞掉有用得多。5.5 用jQuery直接拼HTML引发的XSS用户输入必须转义现象用户发一条带script标签的消息所有在线的聊天室页面都执行了一段脚本轻则弹窗重则窃取登录态的Cookie。样式也可能被一条/li破坏消息列表整个错乱。原因前端用了$(#messageList).append(li content /li)content是用户输入浏览器把它当HTML解析。聊天室是多人实时交互场景每一帧消息都经过所有在线客户端渲染XSS可以在几秒内传播给全部在线用户这是聊天室最需要重视的安全问题之一。解决所有用户输入的内容一律走text()方法或者先做HTML转义再拼进HTML。jQuery里$(li).text(content)会帮我们处理转义如果用模板字符串拼HTML就自己写一个escapeHtml函数把、、、引号全部转换成实体。服务端广播时只转发纯文本不要帮忙包一层HTML这个前面协议设计时说过。聊天室的每条消息都来自不可信源前端渲染时一律当攻击代码处理这个意识比任何安全扫描工具都重要。6. 生产级的进阶动作连通性验证、多房间和鉴权三个必做项6.1 浏览器Console里做一个简单的连通性验证调试连接问题不要急着打开复杂的测试工具浏览器Console就是现成的调试环境。页面开着F12打开Console直接跑几行原生JavaScript就能完成最基本的功能验证。const testWs new WebSocket(ws://localhost:8080/chat); testWs.onopen function () { console.log(握手成功); testWs.send(JSON.stringify({ type: chat, sender: tester, content: hello })); }; testWs.onmessage function (e) { console.log(收到服务端消息, e.data); };在Console里执行这段代码观察onopen是否触发、onmessage能否收到广播能在五分钟内区分出问题是握手环节还是消息转发环节。这一步能省下大量在Nginx、防火墙、容器配置之间猜来猜去的时间。6.2 从单聊天室改造成多房间嵌套Map隔离会话要支持多房间核心是把单一会话集合改成“房间 → 用户 → 会话”的嵌套结构。用ConcurrentHashMapString, ConcurrentHashMapString, Session外层key是roomId内层存这个房间里的用户会话。消息协议里加一个roomId字段服务端收到消息后先按roomId找到对应的内层Map只在这个Map里广播。改造时注意两点用户离开房间要同时从内层Map和静态用户表里移除别让session对象残留造成内存泄漏房间不存在时要不要自动创建取决于产品定位临时聊天室可以直接computeIfAbsent创建固定房间则需要先校验房间是否在配置里。这一步做完聊天室就从“一个大厅”进化成“多个独立空间”代码结构上没有本质变化但会话管理的边界要清晰得多。6.3 握手参数带token鉴权在连接建立前完成WebSocket握手本质是一次HTTP GET请求参数可以直接放在URL上。连接地址从/chat扩展成/chat?tokenxxx服务端在握手阶段拿到token并校验比连接建立后再做身份识别更安全因为WebSocket协议本身没有请求头这个概念把token拼在URL里虽然能被Nginx日志记下来但总比明文放在聊天消息里强。服务端做法是在ServerEndpoint里注入HandshakeRequest在OnOpen之前先做一次校验ServerEndpoint(value /chat, configurator ChatConfigurator.class)ChatConfigurator继承ServerEndpointConfig.Configurator重写modifyHandshake方法从HandshakeRequest.getParameterMap()里取出token校验失败直接抛异常拒绝握手。配合前面4.3节的断线重连前端重连时带上同一个token服务端不上线已有的token可以在校验逻辑里直接拒绝避免一个用户反复重连刷出大量僵尸连接。回头看我维护聊天室那段时间最深刻的教训是心跳和超时这些不可见的部分决定了线上体验的生死。功能上线第一天大家用的都是好网络看不出差别等到用户在地铁、电梯、弱网环境里用起来连接频繁断开又重连跟浏览器网络状态栏一样一跳一跳的才知道当初把心跳间隔调成20秒、把超时定成60秒这些参数才是真正扛住生产环境的底气。记住一个原则聊天室的价值不在那一次握手而在握完手之后连接能活多久。希望帮到你。本文还有配套的精品资源点击获取