ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Tomcat8+Java7+ExtJS环境下WebSocket聊天室实现与踩坑指南

Tomcat8+Java7+ExtJS环境下WebSocket聊天室实现与踩坑指南 简介这是一份基于Tomcat8Java7ExtJS构建的WebSocket聊天室完整实现适合Java Web学习者、前端开发者及实时通信技术研究者用于掌握JSR 356标准API下的全双工通信与服务器主动推送机制。资源共1753个文件压缩包约7.01MB包含1359个gif演示/动图、158个scss样式、114个png图标、50个js客户端脚本、34个css样式、11个jar依赖库及少量java源码与class文件目录结构清晰可对照源码和界面文件快速理清前后端交互逻辑。目前已有262人学习下载。通过这份资源读者可看到ServerEndpoint注解定义的WebSocket端点、消息入站/出站处理类、连接池管理以及ExtJS对WebSocket的封装方式还有Tomcat8环境下的项目配置文件既能作为课程设计参考也能为开发实时聊天、消息推送等场景提供直接可用的实现思路。1. Tomcat8Java7ExtJS这套老组合为什么还要自己写WebSocket聊天室很多人看到“基于tomcat8java7extjs 的webscoket 聊天室实现”这个标题第一反应是“怎么还有人在用这么老的搭配”。可真做过企业内部系统的都知道Spring 3.x 锁死、JDK 不让升、前端早就用 ExtJS 写了上百个页面这时候突然要加一个“实时聊天”模块最现实的做法不是重构而是在现有工程里用 WebSocket 做增量开发。Tomcat8 原生支持 JSR 356Java7 能写标准的 ServerEndpointExtJS 封装一个连接管理器也不复杂。这套方案适合正在维护老系统、又不想引入消息中间件和全新前端框架的团队它能解决“发消息、看在线、不丢连接”三个核心需求并且可以直接嵌入现有权限体系。本文就从服务端写到前端把能跑通的代码和那些坑一并讲透。2. 先打通 WebSocket 通道Tomcat8 上的服务端实现与 Java7 语法约束2.1 用 JSR 356 注解写一个聊天室端点ServerEndpoint 的正确姿势Java7 时代还没有 Spring Boot但 WebSocket 的标准已经落地叫 JSR 356Tomcat8 从 7.0.47 开始就有支持到了 8.x 已经很稳。常见做法是不引入额外依赖直接写一个普通类用 ServerEndpoint 注解把它暴露成 ws 端点。下面这个例子是最小可用的聊天室服务端实现了广播。import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; ServerEndpoint(/chat) public class ChatEndpoint { // 用线程安全的集合保存所有在线会话 private static final CopyOnWriteArraySetSession sessions new CopyOnWriteArraySet(); OnOpen public void onOpen(Session session) { sessions.add(session); broadcast(用户 session.getId() 进入聊天室); } OnMessage public void onMessage(String message, Session session) { broadcast([ session.getId() ] message); } OnClose public void onClose(Session session) { sessions.remove(session); broadcast(用户 session.getId() 离开聊天室); } OnError public void onError(Session session, Throwable error) { System.out.println(WebSocket错误 error.getMessage()); if (session.isOpen()) { try { session.close(); } catch (IOException e) { // ignore } } sessions.remove(session); } private void broadcast(String msg) { for (Session s : sessions) { if (s.isOpen()) { try { s.getBasicRemote().sendText(msg); } catch (IOException e) { System.out.println(向 s.getId() 发送失败: e.getMessage()); } } } } }逻辑说明ServerEndpoint(/chat) 在 Tomcat 启动时注册端点客户端连接地址就是ws://ip:8080/项目名/chat。onOpen 在握手完成后执行我们把 Session 加进一个静态集合。onMessage 收到字符串后通过 broadcast 方法发给所有连接。onClose 清理 Session。注意这里用的是 getBasicRemote().sendText()它是同步发送如果某个客户端网络阻塞循环会卡住。在 Java7 下没有 CompletableFuture 那类工具后面会讲怎么用线程池去缓解。参数说明session.getId() 只是 Socket 会话的唯一标识不是用户 ID生产环境必须通过登录握手把用户信息绑定到 Session 上。另外 CopyOnWriteArraySet 的迭代器弱一致不会抛 ConcurrentModificationException适合“写少读多”的广播场景。这个最小示例能跑通但不能拿去做并发聊天室。2.2 会话管理与广播逻辑为什么不能用静态变量保存 Session如果你去搜一些老博客会看到有人直接private static SetSession sessions new HashSet();然后并发一上来就出 ConcurrentModificationException或者在广播时数组越界。原因很简单多线程同时 onOpen、onMessage、onCloseHashSet 的 modCount 被同时修改迭代时直接翻车。Java7 里没有 Lambda但有并发包。我一般会改成 ConcurrentHashMap以用户名为 key这样既能做全量广播又能做私聊定向推送。import javax.websocket.Session; import java.util.concurrent.ConcurrentHashMap; public class SessionRegistry { private static final ConcurrentHashMapString, Session onlineUsers new ConcurrentHashMap(); public static void add(String username, Session session) { onlineUsers.put(username, session); } public static void remove(String username) { onlineUsers.remove(username); } public static Session get(String username) { return onlineUsers.get(username); } public static int size() { return onlineUsers.size(); } public static void broadcast(String message) { for (Session s : onlineUsers.values()) { if (s.isOpen()) { try { s.getBasicRemote().sendText(message); } catch (Exception e) { // 瞬时断开时记录日志不打断其他人 System.err.println(广播失败 e.getMessage()); } } } } }逻辑说明ConcurrentHashMap 的 values() 返回的是一个弱一致的视图遍历过程中如果其他线程 put/remove不会抛异常但你可能读不到最新值或漏掉刚加入的会话。对聊天室广播场景这种弱一致完全可接受。私聊时只需Session s onlineUsers.get(to);然后定向 sendText。参数说明这里的 username 必须来自服务端安全校验后的结果不能直接信客户端传参。比如客户端说usernameadmin你就存 admin那任何人都能冒充管理员。常见做法是登录成功后发一个一次性 tokenWebSocket 握手时带上 token服务端校验通过后再把真实用户名写入 Session 的自定义属性session.getUserProperties().put(username, realName)。2.3 消息协议约定从入门到可扩展的消息帧格式聊天室除了广播还要做私聊、系统通知、在线人数变化。如果直接发裸中文文本前端解析全靠 split一旦消息里出现分隔符就完了。所以我从一开始就约定消息统一用 JSON。Java7 下用 Jackson 2.x 或 Gson 都行这里用 Gson 因为依赖简单Tomcat8 的 lib 里没有需要自己放到 WEB-INF/lib。import com.google.gson.Gson; public class ChatMessage { private String type; // chat 聊天, system 系统, online 在线列表, error 错误 private String from; // 发送者用户名 private String to; // 接收者用户名为空表示群聊 private String content; // 文本内容 private long timestamp; // 毫秒时间戳 // getter/setter 省略 }服务端 onMessage 收到的字符串用 Gson 解析再根据 type 分发OnMessage public void onMessage(String json, Session session) { ChatMessage msg new Gson().fromJson(json, ChatMessage.class); String username (String) session.getUserProperties().get(username); if (username null) return; msg.setFrom(username); msg.setTimestamp(System.currentTimeMillis()); String toUser msg.getTo(); if (toUser null || toUser.isEmpty()) { SessionRegistry.broadcast(new Gson().toJson(msg)); } else { Session target SessionRegistry.get(toUser); if (target ! null target.isOpen()) { target.getBasicRemote().sendText(new Gson().toJson(msg)); } } }逻辑说明type 字段让我们可以在同一个连接里传输聊天消息、系统通知、在线列表快照前端只需要在接收回调里按 type 分发。timestamp 由服务端统一生成避免客户端时钟偏差。Java7 的 switch 支持 String但这里不需要if/else 已经够用。注意不要用 String 直接拼接 JSON框架提供一个空串防护即可。参数说明Gson.fromJson 在 Java7 下运行没有问题唯一要注意的是如果客户端发来空串或畸形 JSON会抛 JsonSyntaxException需要在 onMessage 里用 try/catch 包住否则异常会进入 onError导致连接被关闭。这是很多人踩过的坑用户粘一条带特殊字符的消息服务端就报错断开。3. ExtJS 这边怎么接从连接管理到消息面板3.1 ExtJS 没有原生 WebSocket 支持自己封装一个 WsManagerExtJS 4.2、5.x 都没提供 WebSocket 的 Store 或 Panel官方推荐自己写。我通常用一个 Ext.define 定义全局单例负责创建连接、注册事件回调、自动重连、发送心跳。下面是一段可用的封装代码基于 ExtJS 4.2 的 Ext.util.Observable。Ext.define(WsManager, { extend: Ext.util.Observable, url: null, ws: null, reconnectTimes: 0, maxReconnect: 10, task: null, constructor: function (config) { this.callParent(config); this.addEvents(message, open, close, error); }, connect: function (url) { var me this; me.url url; me.ws new WebSocket(url); me.ws.onopen function () { me.reconnectTimes 0; me.fireEvent(open, me.ws); me.startHeartbeat(); }; me.ws.onmessage function (evt) { me.fireEvent(message, evt.data); }; me.ws.onclose function () { me.fireEvent(close); me.stopHeartbeat(); // 超过最大重连次数就不自动重连 if (me.reconnectTimes me.maxReconnect) { me.reconnectTimes; setTimeout(function () { me.connect(me.url); }, me.reconnectTimes * 2000); } }; me.ws.onerror function () { me.fireEvent(error); }; }, send: function (jsonString) { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(jsonString); } else { Ext.Msg.alert(提示, 连接已断开正在重连请稍候); } }, startHeartbeat: function () { var me this; me.task Ext.TaskManager.start({ run: function () { if (me.ws me.ws.readyState WebSocket.OPEN) { // 发送一个心跳包服务端收到后直接忽略 me.ws.send({type:ping}); } }, interval: 30000 }); }, stopHeartbeat: function () { if (this.task) { Ext.TaskManager.stop(this.task); this.task null; } } });逻辑说明connect 方法真正创建 WebSocket并把 onopen/onmessage/onclose 映射到 Ext 事件。断线后采用指数退避重连第一次等 2 秒第二次 4 秒最长 20 秒。注意在 onclose 里调用 connect不能直接 onopen 里调用因为连接失败也会触发 onclose。心跳用 Ext.TaskManager 定时发 ping这样如果网络中间设备把空闲连接回收客户端可以在 30 秒内感知到。服务端收到{type:ping}时不需要回复但可以刷新最近活跃时间。参数说明maxReconnect 设成 10 意味着最多重试 10 次等于 24...20 秒约 110 秒后放弃。如果你希望无限重连可以把这个值调大。心跳间隔 30 秒是经验值公司内部的防火墙 idle timeout 一般是 60 秒30 秒能保证中间态存在。如果你的网络环境更苛刻可以改成 15 秒。3.2 用 Ext.panel.Panel 做聊天窗口消息流与输入框绑定聊天面板不需要 Grid一个普通的 Ext.panel.Panel 就能搞定。我在面板中间放一个 HtmlPanel 用来显示消息流底部放输入框和发送按钮。接收消息时通过 WsManager 自定义事件更新面板。Ext.define(ChatPanel, { extend: Ext.panel.Panel, alias: widget.chatpanel, layout: fit, title: 在线聊天, width: 400, height: 500, initComponent: function () { var me this; me.items [ { xtype: panel, itemId: msgPanel, border: 0, bodyPadding: 5, autoScroll: true, html: div classchat-history/div } ]; me.dockedItems [ { xtype: toolbar, dock: top, items: [ { xtype: tbtext, itemId: userInfo }, -, { xtype: tbtext, itemId: onlineCount } ] }, { xtype: toolbar, dock: bottom, items: [ { xtype: textfield, flex: 1, itemId: msgInput, emptyText: 请输入消息 }, { xtype: button, text: 发送, itemId: sendBtn } ] } ]; me.callParent(); me.down(#sendBtn).on(click, function () { var input me.down(#msgInput); var text input.getValue(); if (text ) return; WsManager.send(Ext.JSON.encode({ type: chat, to: , content: text })); input.reset(); }); WsManager.on(message, function (data) { var msg Ext.JSON.decode(data); me.appendMessage(msg); }); WsManager.on(open, function () { me.down(#userInfo).setText(已连接); }); WsManager.on(close, function () { me.down(#userInfo).setText(连接断开重连中...); }); }, appendMessage: function (msg) { var box this.down(#msgPanel).getEl().down(.chat-history); var cls msg.type system ? system-msg : chat-msg; var timestamp Ext.util.Format.date(new Date(msg.timestamp), H:i:s); var html div class cls [ timestamp ] b msg.from /b: msg.content /div; box.insertHtml(beforeEnd, html, true); box.scroll(b, 100000, true); } });逻辑说明发送按钮直接把输入框内容封装成 JSON通过 WsManager.send 发出去。服务端会广播给所有人前台自己的消息也会从 onmessage 回调中收到。这里没有做“自己发送的消息用右对齐”这种美化但 appendMessage 里可以通过 msg.from 和当前登录用户名判断是否自己发送。参数说明scroll(b, 100000, true)是 ExtJS 里滚动到底部的偷懒写法第二个参数是目标滚动位置传一个很大的数即可。注意 insertHtml 第三个参数 true 表示加载后执行脚本这里没有任何脚本只是为了避免浏览器解析器把当作标签如果你的消息里有 HTML 内容必须提前转义否则会有 XSS 风险。3.3 处理断线重连与心跳Ext.TaskManager 的定时器用法上一节的心跳是客户端主动保活但服务端也需要知道一个连接是否还活着。如果客户端非正常关闭比如拔网线onClose 不会立刻触发Tomcat 可能要等 TCP 超时。常见做法是在服务端 onMessage 里记录每个 Session 的最后心跳时间再启动一个后台定时任务扫描超过 60 秒没有心跳的连接并关闭。在 Java7 下没有现成的调度注解可以用 java.util.Timer 或 Executors.newScheduledThreadPool。这里用 Timer因为老项目里往往已经有用 Timer 的惯例。import java.util.Timer; import java.util.TimerTask; import javax.websocket.Session; public class HeartbeatMonitor { private static final Timer timer new Timer(HeartbeatMonitor, true); static { // 每30秒扫描一次所有在线Session timer.scheduleAtFixedRate(new TimerTask() { Override public void run() { long now System.currentTimeMillis(); for (Session s : SessionRegistry.getAllSessions()) { Long lastPong (Long) s.getUserProperties().get(lastPong); if (lastPong null || now - lastPong 60000) { try { s.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, heartbeat timeout)); } catch (Exception e) { // ignore } SessionRegistry.removeBySession(s); } } } }, 30000, 30000); } }逻辑说明Timer 构造器的第二个参数 true 表示守护线程不会阻止 Tomcat 关闭。每当收到客户端 ping 消息在 onMessage 里更新session.getUserProperties().put(lastPong, System.currentTimeMillis())。如果用户长期发聊天消息即使不发 ping也应该刷新时间因为聊天消息证明连接活着。参数说明60 秒超时是个折中值大于客户端 30 秒的心跳间隔又不会让僵尸连接挂太久。注意 TimerTask 里遍历 SessionRegistry 时如果拿的是 ConcurrentHashMap.values()调用 s.close() 不会有问题但不要在遍历过程中 remove否则可能漏掉下一个。我一般先记录需要关闭的 Socket 标识再统一清理。4. 用户身份与聊天室权限把在线列表做干净4.1 登录握手通过 URL 参数传递用户名与 tokenWebSocket 握手就是在 HTTP 请求上加了 Upgrade 头所以 URL 参数可以传递。常见做法是前端构造ws://ip:8080/chat?tokenxxxx服务端在 onOpen 里用session.getRequestParameterMap()取出 token再校验登录态。OnOpen public void onOpen(Session session) { MapString, String params new HashMap(); session.getRequestParameterMap().forEach((k, v) - params.put(k, v.get(0))); String token params.get(token); String username TokenService.getUsernameByToken(token); if (username null) { // 校验失败直接关闭 try { session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, invalid token)); } catch (IOException e) { // ignore } return; } session.getUserProperties().put(username, username); session.getUserProperties().put(lastPong, System.currentTimeMillis()); SessionRegistry.add(username, session); // 发送在线列表 sendOnlineList(session); }逻辑说明Tomcat8 的session.getRequestParameterMap()返回MapString, ListString这里用 Java8 的 forEach注意我们是在 Java7 环境下Java8 的 lambda 不能用。上面的代码片段里我用了forEach实际上Map接口在 Java8 才有forEach默认方法Java7 没有。所以这是一个需要修正的坑。正确写法MapString, ListString requestParams session.getRequestParameterMap(); String token null; if (requestParams.containsKey(token)) { token requestParams.get(token).get(0); }这里专门说出来就是为了提醒写代码时容易惯性用 Java8 语法编译到 Java7 就报错。所以要养成在 Java7 里只用 Iterator 或加强 for 的习惯。4.2 在线人数与私聊服务端维护用户映射表在线列表不能靠遍历 Session 的 Id因为 Id 对用户没有意义。正确做法是维护ConcurrentHashMapString, Sessionkey 为登录用户名。当用户登录时判断 key 是否已存在如果存在说明同一个账号多端登录可以踢掉旧连接。Session old SessionRegistry.addIfAbsent(username, session); if (old ! null old.isOpen()) { ChatMessage kick new ChatMessage(); kick.setType(system); kick.setContent(您已在其他窗口登录当前连接被关闭); old.getBasicRemote().sendText(Gson.toJson(kick)); old.close(); }逻辑说明addIfAbsent 是 ConcurrentHashMap 的原子方法如果 key 已存在就返回旧值。这样可以避免“先检查再 put”的竞态同一个账号同时登录时后进的把先进的顶掉。这个规则对内部系统很实用用户发现自己被踢就知道账号泄露或被别人登录了。参数说明私聊时Session target SessionRegistry.get(to)然后向 target 发消息。注意 to 字符串需要经过白名单校验防止用户构造一个不存在的用户名导致 null再在 getBasicRemote 上 NPE。4.3 主动下线与异常退出onClose 回调里要清理什么onClose 不只是从 map 里移除用户名这么简单你还需要广播下线消息、刷新在线人数、清理与该用户相关的私聊草稿如果有。下面是一个完整的 onClose 实现。OnClose public void onClose(Session session, CloseReason reason) { String username (String) session.getUserProperties().get(username); if (username ! null) { SessionRegistry.remove(username); // 通知所有人用户离开 ChatMessage leave new ChatMessage(); leave.setType(system); leave.setContent(username 离开了聊天室); leave.setTimestamp(System.currentTimeMillis()); SessionRegistry.broadcast(Gson.toJson(leave)); // 单独广播一次在线人数变化比每次在系统消息里附带人数更清晰 ChatMessage online new ChatMessage(); online.setType(online); online.setContent(String.valueOf(SessionRegistry.size())); SessionRegistry.broadcast(Gson.toJson(online)); } }逻辑说明onClose 的触发时机可能是主动关闭、异常断开、心跳超时。如果连接已经被对端关闭onError 也可能先触发onClose 依然会调一次。所以这里用 if 判断 username 是否为 null防止重复广播。很多人踩的坑是在 onError 里已经 remove 了 usernameonClose 又 remove 一次广播了两次离开消息。要确保 onError 只记录日志真正清理动作统一在 onClose 里做。参数说明CloseReason 可以拿到关闭码如果是因为心跳超时关闭code 是 NORMAL_CLOSURE 或自定义码。你可以在经验里判断是否要写日志但不要在这里做耗时操作。5. 必踩的坑与排查路径Tomcat8Java7ExtJS 聊天室的 5 个典型翻车现场5.1 现象前端连不上 ws://报 404 或 403前端new WebSocket(ws://localhost:8080/chat)浏览器报 404后台一看根本没有请求到达。原因是 WebSocket 的端点路径不包含应用上下文。如果你的项目发布名是myoa正确地址是ws://localhost:8080/myoa/chat。这里有个坑ServerEndpoint(/chat) 注册的是相对应用上下文的路径而前端经常漏掉上下文。另一种是 403Tomcat8 默认会对 WebSocket 请求做 Origin 校验如果请求头里的 Origin 域名与服务器不一致直接拒绝。解决方法是实现ServerEndpointConfig.Configurator重写checkOrigin方法允许内部域名ServerEndpointConfig.Configurator configurator new ServerEndpointConfig.Configurator() { Override public boolean checkOrigin(String originHeaderValue) { // 只允许自己的域名 return originHeaderValue ! null originHeaderValue.startsWith(http://oa.internal.com); } };注意 Java7 里匿名内部类只能用这种写法lambda 不可用。如果原来是 Java8 的(origin) - origin.startsWith(...)编译直接失败。5.2 现象Java7 下编译失败lambda 表达式不被允许团队里其他同事用 JDK8 写了一段sessions.forEach(s - s.sendText(...))放到 JDK7 编译就直接报“lambda expressions are not supported in -source 7”。这种问题在混用 JDK 的老项目里太多了。解决方法是统一在 pom.xml 或 build.xml 里设置maven.compiler.source1.7并且 code review 时明确禁用 lambda。如果已经有别人写好的 lambda手动改回匿名内部类。顺便说一个更隐蔽的坑Java7 支持 try-with-resources支持菱形操作符但不支持多重异常 catch 后面的 final 变量更不支持_作为变量名。这些语法差异会导致你写的代码在 IDE 里高亮一堆错误。建议在 IDE 里把 Java compiler 的 compliance level 锁成 1.7不要手工切换。5.3 现象ExtJS 的 Ajax 轮询与 WebSocket 并行时的并发错乱有些老页面同时保留了每 3 秒一次的 Ajax 轮询获取在线人数又新加了 WebSocket 在线列表推送。结果发现 WebSocket 收到的人员列表和 Ajax 回来的对不上有时候 WebSocket 消息还被延迟处理。原因很简单两个通道同时更新同一个 Ext.Panel产生竞态。我见过最离谱的情况是Ajax 回调里用Ext.getCmp(userList).store.loadData(ajaxData)WebSocket 回调里又 loadData(wsData)后到的覆盖先到的。解决方法是明确数据源WebSocket 是实时事件负责在线列表和聊天消息Ajax 只做初次进入聊天室时的历史记录加载之后完全切断。在 WsManager.on(open) 里调用一次 Ajax 加载最近 50 条消息之后不再轮询。如果你必须保留 Ajax 心跳比如为了兼容老浏览器不支持 WebSocket可以用一个开关变量控制一旦 WebSocket open 成功就Ext.TaskManager.stop(ajaxTask)。5.4 现象Tomcat8 的 NIO 与 IO 模式导致连接被重置有人为了“提升性能”把 Tomcat 的 connector 从protocolHTTP/1.1改成protocolorg.apache.coyote.http11.Http11NioProtocol结果 WebSocket 连接频繁被重置。其实 Tomcat8 默认就是 NIO你改成这个是画蛇添足。Tomcat8 的 WebSocket 在 BIO connector 和 NIO connector 上都能工作但如果你用了 APR connector需要注意支持程度。常见翻车是 server.xml 里写死了protocolorg.apache.coyote.http11.Http11Protocol也就是纯 BIO在 Java7 和 Tomcat8 下连接一多线程数暴涨然后平台层误杀。解决方法是把 server.xml 里的 connector 协议保持默认即一行Connector port8080 protocolHTTP/1.1 ...Tomcat8 会自动选择 NIO。不要自作聪明去加socket.directBuffer之类的参数除非你知道每个调优参数的含义。WebSocket 长连接占的是线程等待NIO 模型下线程能复用但 JSR 356 的实现里每个 Session 在消息回调时仍可能占用一个工作线程所以不要并发开太多聊天室适当增加maxThreads即可。5.5 现象消息中文乱码与消息长度截断客户端发“中文测试”服务端收到后变成“????”或者消息很长时后半截被截断。WebSocket 协议本身是 UTF-8但前端如果用encodeURIComponent后直接塞进 JSON服务端 Gson 解析时如果编码不对就会乱。检查你的 Tomcat 连接器 URIEncoding 是不是 UTF-8。但 WebSocket 握手请求的 QueryString 不受URIEncoding影响Tomcat 会按 UTF-8 处理。真正容易出问题的是 ExtJS 的Ext.JSON.encode()它不会自动对中文做转义但会把字符串按 UTF-8 序列化。服务端接收后需要确保request.setCharacterEncoding(UTF-8)这种老逻辑不要放到对 WebSocket 消息处理的地方那里已经是字节流了。消息截断通常是服务端OnMessage方法的参数类型不对。如果参数声明为StringWebSocket 会按照文本消息解码并限制消息长度Tomcat8 默认最大消息长度 8192 字节超过会抛MaxMessageSizeException。解决方法是给ServerEndpoint设置maxMessageSizeServerEndpoint(value/chat, configuratorMyConfigurator.class)然后在端点类里用OnMessage(maxMessageSize65536)增加限制。注意不同 Tomcat 的注解写法略有不同Tomcat8 中可以直接在OnMessage(maxMessageSize 1024 * 64)或通过端点配置类ServerEndpointConfig设置。我一般把上限设为 64KB足够企业聊天里粘贴一大段代码或日志。客户端也要限制输入框长度不再多言。6. 把聊天室改成可上线的生产版本三个必须做的加固技巧6.1 用 Origin 检查加随机 token 防止跨站连接WebSocket 默认不受同源策略限制但服务器可以校验 Origin。在生产环境我会在 Configurator 里同时检查 Origin 和 token。token 放在握手 URL 中而不是 cookie因为浏览器 WebSocket API 无法手动设置请求头。token 有效期设置为 10 分钟进入页面时从接口换取避免长期有效。6.2 消息流量控制服务端限流与客户端缓冲聊天室最怕刷屏。服务端可以用 Guava 的 RateLimiter 按用户限流但 Java7 下 Guava 稳定兼容。我为每个用户保存一个令牌桶每 500 毫秒允许发一条消息超出的直接回一条 system 消息。客户端则在消息框失去焦点时清空缓冲不需要做复杂队列。6.3 与老系统集成把聊天记录持久化到 MySQL 的异步落库写法WebSocket 事件回调线程不宜做 JDBC 操作否则会占用 Tomcat 的 worker 线程。我一般用ExecutorService单线程池处理落库消息体先放入BlockingQueue后台线程逐条插入。Java7 下用Executors.newSingleThreadExecutor()注意进程退出时要shutdown()。我自己的血泪经验是先上线聊天室再补历史记录表。第一版只做内存广播消息一没人看就丢了被业务方吐槽“聊天记录呢”。后来把落库接到队列才发现BlockingQueue.offer如果满了要设置超时否则内存溢出。这些细节不踩几回真的想不到。希望这些经验和代码能让你把那套老 Tomcat 改造得顺利一点也少加几天班。希望帮到你。本文还有配套的精品资源点击获取
返回列表