news 2026/10/3 4:06:29

OpenLiberty与WebSocket构建轻量级实时聊天室实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenLiberty与WebSocket构建轻量级实时聊天室实践

1. OpenShell 项目核心解读与应用场景分析

做这个项目之前,我先说清楚一件事:OpenShell 并不是我凭空拍脑袋想出来的名字,它背后其实有着明确的工程语义。Open 对应的是 OpenLiberty 这套轻量级 Java 运行时容器,Shell 则是 WebSocket 通信的那层“外壳”。一句话总结这个项目的定位:在 OpenLiberty 容器里,用原生 WebSocket 构建一个支持多端实时互通的轻量级聊天室。

1.1 为什么选 OpenLiberty 而不是 Spring Boot 那套

很多朋友一听到 Java 后端实时通信,第一反应就是 Spring Boot 加 WebSocket 再加一堆起步依赖。这当然能用,但如果你只是想要一个低延迟、轻部署、甚至能直接跑在低配开发机上的实时通信服务,Spring Boot 那套组合拳反而有点重。

OpenLiberty 最吸引我的地方在于“快”和“干净”。它不像 Tomcat 或者 Undertow 那样需要单独配置容器,也不像传统 Java EE 那样要装一个几十 GB 的完整应用服务器。OpenLiberty 本身就是一套可组合的运行时,你在 Maven 里声明需要哪些特性,它就只加载哪些特性,连启动时间都被压得非常低。我在自己的笔记本上实测过,从零启动一个带 WebSocket 特性的 Liberty 服务器,基本在两三秒内就绪。这对于本地开发、联调、甚至在边缘设备上做轻量服务,体验都是碾压级的。

再一个原因是标准。OpenLiberty 对 Jakarta EE 规范的支持非常彻底,其中就包括 WebSocket 的完整实现。也就是说,我不需要引入额外的第三方 WebSocket 库,直接用 JDK 自带的javax.websocketAPI(或者 Jakarta 命名空间)就能把服务端端点写出来。这种“标准到骨子里”的干净感,是我最终确定这个方案的关键。

1.2 项目需求拆解

OpenShell 这个项目表面上看是个聊天室,但拆开之后,核心技术点其实集中在以下三个层面:

  • 连接层:如何让多个客户端同时连上服务器,并且保持长连接状态不被意外断开。
  • 消息层:服务器收到一条消息后,如何高效地分发给其他客户端,涉及广播、过滤、并发安全。
  • 状态层:如何感知客户端的上线与下线。不仅仅是日志里输出一行提示,而是要能在业务逻辑里动态维护一份在线列表,并在广播时剔除已经失效的连接。

这三个层面几乎覆盖了所有 WebSocket 业务的基础模型。不管你是做推送服务、协同编辑、在线客服还是大屏实时看板,核心逻辑都跑不出这个框架。这也是我推荐用 OpenShell 作为入门或复现对象的原因:把连接、消息、状态这三件事吃透,后面换什么业务场景都是顺手的事。

2. 开发环境搭建与 OpenLiberty 容器配置

2.1 工具链版本组合

写这个项目之前,我把工具链卡在了一个比较舒服的版本组合上。因为 OpenLiberty 对 JDK 的兼容性跨度很大,但如果你用的是比较新的 Liberty 版本,建议直接用 JDK 17 起步,遇到 Java 17 的模块限制少,配合 Maven 编译器插件也省心。

实测推荐组合如下:

  • JDK 17(偏底层,OpenLiberty 23.0.0.x 之后对 JDK17 支持很稳)
  • Maven 3.8+
  • 最新稳定版 OpenLiberty(我写的时候是 23.0.0.12 左右,直接拉最新的就行)
  • IDE 方面我用的是 IntelliJ IDEA,装上 liberty-dev 插件,调试体验会好很多

注意一个细节:OpenLiberty 有两个命名空间版本。旧的是javax.websocket,新的是jakarta.websocket。如果你的 Liberty 版本是 23.0.0.x 及以上,默认走 Jakarta EE 10 那一套 API,也就是jakarta.websocket.*。如果你以前写过javax.websocket的老代码,直接粘贴过来编译会报错,需要全量替换 import。

2.2 Maven 工程结构与 OpenLiberty 插件集成

工程结构其实很简单,不需要微服务那套多模块拆分的麻烦事,单模块项目即可:

OpenShell/ ├── pom.xml ├── src/main/java/com/openshell/ │ ├── ChatEndpoint.java │ └── OnlineUsers.java ├── src/main/webapp/ │ └── index.html └── src/main/liberty/config/ └── server.xml

对应的 pom.xml 关键部分如下:

<properties> <liberty.version>23.0.0.12</liberty.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <!-- 这里不需要额外引入 websocket 依赖,Liberty 容器本身已实现规范 --> </dependencies> <build> <plugins> <plugin> <groupId>io.openliberty.tools</groupId> <artifactId>liberty-maven-plugin</artifactId> <version>3.8.3</version> </plugin> </plugins> </build>

这里有个值得展开的细节:为什么不需要在 pom 里显式声明 websocket 库?因为 OpenLiberty 的插件会在启动时读取server.xml里的 feature 配置,动态按需加载功能模块。你在server.xml里声明了webSocket-1.1(老版本)或websocket-2.1(新版本)特性,Liberty 就能把对应实现挂载到运行时里。这种“按需加载”的模式,在依赖管理上省了一大堆事。

2.3 server.xml 核心配置与特性加载

server.xml是整个 OpenLiberty 启动的中枢,配置错了服务起不来,配置对了很多东西都不用管了。我贴一份本项目的完整配置:

<?xml version="1.0" encoding="UTF-8"?> <server description="OpenShell Server"> <!-- 启用 WebSocket 特性和 Servlet 特性 --> <featureManager> <feature>websocket-2.1</feature> <feature>servlet-6.0</feature> </featureManager> <!-- HTTP 端点配置 --> <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080" httpsPort="9443" /> <!-- 默认应用根路径配置 --> <webContainer> <applicationMonitor updateTrigger="mbean"/> </webContainer> <!-- 开发模式热部署配置 --> <applicationManager autoExpand="true"/> </server>

几个容易踩坑的点,我单独拎出来说:

第一,host="*"表示监听所有网卡。如果你只想在本机测试,可以改成localhost,这样别人在局域网内是连不上你的 WebSocket 服务的,从安全角度也更保险。

第二,httpPort可以按需改。如果你本机 9080 被别的服务占了,直接在这里换个端口,比如 8082。但注意,改完之后前端页面里ws://localhost:9080/chat这个连接地址也要跟着改,否则页面会一直报连接失败。

第三,feature 版本不要写错。现在 OpenLiberty 新版本里 WebSocket 特性已经叫websocket-2.1,不是老旧的webSocket-1.1。如果你混用了,启动时会报找不到特性或版本冲突的错误。如果你不确定当前 Liberty 版本支持哪个版本的特性,可以启动时加--verbose参数,日志里会列出所有可用特性。

3. 核心实现:服务端 WebSocket 端点与消息广播

3.1 @ServerEndpoint 端点的生命周期逻辑

服务端逻辑是整个 OpenShell 的核心,我用一个类ChatEndpoint承载所有连接逻辑。这个类的写法其实特别简单,就是几个注解加回调方法,但里面藏着 WebSocket 生命周期的全部内容。

package com.openshell; import jakarta.websocket.*; import jakarta.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @ServerEndpoint("/chat") public class ChatEndpoint { private static final Set<Session> onlineSessions = new CopyOnWriteArraySet<>(); @OnOpen public void onOpen(Session session) { onlineSessions.add(session); broadcast("用户 " + session.getId() + " 进入了聊天室,当前在线人数:" + onlineSessions.size()); } @OnMessage public void onMessage(String message, Session session) throws IOException { if (message == null || message.isBlank()) { return; } broadcast("[" + session.getId() + "] " + message); } @OnClose public void onClose(Session session) { onlineSessions.remove(session); broadcast("用户 " + session.getId() + " 离开了聊天室,当前在线人数:" + onlineSessions.size()); } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); onlineSessions.remove(session); } private void broadcast(String message) { for (Session s : onlineSessions) { try { if (s.isOpen()) { synchronized (s) { s.getBasicRemote().sendText(message); } } } catch (IOException e) { e.printStackTrace(); // 发送失败说明这个连接大概率已经断了,顺手清掉 onlineSessions.remove(s); } } } }

这段代码的核心逻辑走一遍你就明白了:

  • @ServerEndpoint("/chat")把当前类暴露成一个 WebSocket 端点,对应的网络地址就是ws://服务器IP:9080/chat。
  • Set<Session> onlineSessions用的是CopyOnWriteArraySet。这个结构的好处是“读多写少”场景下的线程安全,每次调用contains和遍历时不需要加锁,而添加和删除元素时它会内部复制一份新数组。因为 WebSocket 的广播是高频遍历、低频增删,这个选择非常合适。
  • onOpen和onClose都调用了broadcast,意思是把在线状态的变化即时同步到所有客户端。
  • onError里面不要只打个printStackTrace就完事,最好是顺手把异常连接移除掉,避免僵尸连接一直挂在集合里,后面广播发消息报错还找不到原因。

3.2 消息广播的并发与线程安全问题

这里有个细节值得展开讲,因为很多人写多人在线应用时都会忽略:WebSocket 的 Session 是否线程安全?

答案是:不完全安全。同一个 Session 的getBasicRemote().sendText()方法如果被多个线程同时调用,在极端情况下会抛出MessageBodyTooLargeException或者消息错乱。所以我在广播时专门写了synchronized (s)包裹发送逻辑,保证同一个连接在任意时刻只有一个线程在写。

这个锁是实例级别的,对性能影响几乎可以忽略,但能避免在“多线程同时调onMessage向同一会话发消息”时出现的不可预知问题。如果你做的是高吞吐量的场景,可以考虑用每个 Session 单独维护一个单线程执行器,把消息排队发送,但那是更后期的优化策略,OpenShell 项目暂时用不着。

还有一个问题是遍历时的向集合中增删元素。如果不用CopyOnWriteArraySet,在for循环遍历的同时另一个用户刚好上线(往集合里添加元素),会抛出ConcurrentModificationException。这个异常属于运行期,一旦抛出,当前消息就发送失败。用CopyOnWriteArraySet可以从根源上规避。

3.3 前端页面的完整实现

服务端写完了,前端需要一个能直接用浏览器打开的入口。我坚持不引入任何构建工具,也不用 npm 包管理器,就用原生 HTML + JavaScript 实现,这样整个项目零外部依赖,拉下来就能跑。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>OpenShell 聊天室</title> <style> body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; max-width: 700px; margin: 40px auto; padding: 20px; } #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 12px; margin-bottom: 12px; background: #fafafa; } .system { color: #888; font-style: italic; } .user { color: #333; margin-bottom: 4px; } .row { display: flex; gap: 8px; } #input { flex: 1; padding: 10px; border: 1px solid #ccc; } #sendBtn { padding: 10px 20px; cursor: pointer; } </style> </head> <body> <h2>OpenShell 聊天室</h2> <div id="messages"></div> <div class="row"> <input type="text" id="input" placeholder="输入消息,回车发送"> <button id="sendBtn">发送</button> </div> <script> const ws = new WebSocket("ws://localhost:9080/chat"); const messagesDiv = document.getElementById("messages"); const input = document.getElementById("input"); function appendMessage(msg, isSystem) { const div = document.createElement("div"); div.className = isSystem ? "system" : "user"; div.textContent = msg; messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } ws.onopen = () => appendMessage("连接已建立", true); ws.onmessage = (event) => { appendMessage(event.data, false); }; ws.onerror = (err) => { appendMessage("连接异常:" + err.message, true); }; ws.onclose = () => { appendMessage("连接已断开", true); }; function send() { const text = input.value.trim(); if (text) { ws.send(text); input.value = ""; } } document.getElementById("sendBtn").addEventListener("click", send); input.addEventListener("keypress", (e) => { if (e.key === "Enter") { send(); } }); </script> </body> </html>

这个页面的交互非常简单,但已经把 WebSocket 客户端需要处理的四类核心事件全部覆盖了:onopen连接建立、onmessage消息接收、onerror异常处理、onclose连接关闭。

前端这里还有个隐蔽的坑:如果页面放在index.html里直接在本地用file://协议打开,浏览器不会对 WebSocket 连接有特殊限制,但如果你用 HTTPS 协议访问页面,那 WebSocket 也必须用wss://,否则浏览器会直接拦截。本地开发顺手用 HTTP 就行,但是如果部署到生产环境,别忘了后端做 TLS 配置以及前端协议同步切换。

4. 部署运行与高频问题排查

4.1 从零到一运行一个聊天室

运行 OpenShell 其实就三步,不需要额外启动脚本、不用跑什么初始化 SQL:

第一步,在项目根目录执行:

mvn clean package liberty:dev

liberty:dev是 OpenLiberty 的开发模式,它会自动启动服务器并开启热部署。你在 IDEA 里改完代码保存,服务器几乎立刻就能加载到新版本,这对开发调试 WebSocket 这种需要长连接的场景非常管用,因为重启服务器会导致所有连接断掉,而热部署可以在很多场景下免去重启的麻烦。

第二步,打开浏览器,访问http://localhost:9080。因为 Liberty 默认把 Web 应用根目录映射到了请求根路径,所以直接访问默认端口就能看到index.html。

第三步,开两个浏览器窗口,都打开这个页面。在一个窗口里输入“大家好”,另一个窗口里应该能看到服务端广播出的消息。此时控制台里也能看到 Liberty 输出的 WebSocket 连接日志。

如果你想模拟真实的多客户端接入,可以用终端里的命令行工具测试连接,比如用wscat(Node.js 生态)直接连上来:

wscat -c ws://localhost:9080/chat

这样你会在服务端日志里看到这个终端型客户端也进入聊天室,浏览器端同样能收到它发来的文本消息。这种方式很适合用来验证你不依赖浏览器也能正常通信。

4.2 高频问题速查与排查实录

我在实际使用中遇到的一些问题,以及对应的排查心得,整理在下面这个表格里,按出现频率排序:

问题现象可能原因解决办法
启动时报找不到 websocket-2.1 特性Liberty 版本太老,不支持该 feature 名称用websocket-1.1或升级 Liberty 版本;检查是否把 1.1 写成 1.0
前端连接失败,控制台报Error during WebSocket handshake服务端server.xml未启用 WebSocket 特性,或端口写错确认featureManager已启用websocket-2.1,URL 端口与httpPort一致
中文消息出现乱码前端页面 charset 不对,或 WebSocket 握手阶段文本格式不一致确认 HTML 头部有<meta charset="UTF-8">;服务端不用额外配置,默认按 UTF-8 处理
用户下线后仍然收到离线消息onClose未移除 Session,僵尸连接残留在集合中检查onClose中是否调用了onlineSessions.remove(session)
多人同时发消息,偶发消息内容错乱同一个 Session 被多线程并发写入在sendText外层加synchronized (s)或改用单线程队列发送
连接频繁断线重连服务器有超时机制,或 HTTP Session 失效调整 Liberty 的httpSession超时时间,或客户端在onclose事件里做自动重连逻辑
局域网内其他设备连不上server.xml里host配置的是localhost改为host="*",并检查防火墙是否放行对应端口
WebSocket 连接正常,但页面收不到消息广播方法异常或 Session 发送失败被捕获后静默吞掉在broadcast的 catch 块里打印日志,检查有没有 IOException 被吃掉

4.3 一个我认为非常值得养成的习惯

在 WebSocket 项目里,日志比在普通 HTTP 项目里更重要。为什么?因为连接是长连接,很多问题是随时间累积出现的,比如到了半夜某个连接悄悄断开、内存里的 Session 集合越来越膨胀,这些问题如果只在控制台看,很难第一次就抓到线索。我在onOpen、onMessage、onClose里都刻意打了日志:

@OnOpen public void onOpen(Session session) { onlineSessions.add(session); System.out.println("[OpenShell] Connection opened: " + session.getId() + ", total online: " + onlineSessions.size()); }

这行日志对生产环境排查有非常大的帮助。你可以清楚地看到连接的建立和断开是否成对出现。如果发现某些连接只开了没关,那就是客户端异常退出导致onClose没有被触发或者触发失败,需要从客户端侧找原因(比如移动端在切换网络时会直接断 TCP 连接,服务端可能来不及收到关闭帧)。此时可以考虑引入心跳机制,定期从服务端session.isOpen()或者客户端发 ping 包来清理垃圾连接。

5. 项目扩展思路与进阶优化建议

5.1 加一个简单的在线用户列表

OpenShell 当前版本只广播在线人数,并没有广播具体是谁。你如果想要更完整的体验,可以维护一个类似Map<String, String>的结构,把session.getId()和昵称对应起来。用户进入时,通过 URL 参数或页面输入框传入昵称,服务端把昵称存在 Session 的 userProperties 里:

@OnOpen public void onOpen(Session session) { String nickname = "用户" + session.getId(); session.getUserProperties().put("nickname", nickname); onlineSessions.add(session); broadcast("[系统] " + nickname + " 加入了聊天室"); }

这样前端显示消息时,就可以直接使用到昵称,而不是一长串难以理解的 Session ID。

5.2 接入数据库做消息历史

WebSocket 本身不负责持久化,但如果你的聊天室需要保存历史消息,就可以引入任何关系型数据库或者 KV 存储。在onMessage里拿到消息后,先写库再广播即可。这里要注意的是:写库是 IO 操作,不要直接写在 WebSocket 回调用主线程里,因为会阻塞当前 Session 的消息读取,我建议把消息推到一个BlockingQueue,再由一个单独的消费线程批量落库。篇幅原因我不展开聊,但方向上这是正确的做法。

5.3 从开发到生产:用容器镜像封装 OpenShell

OpenLiberty 官方提供了非常成熟的容器镜像,比如icr.io/appcafe/open-liberty。生产环境部署时,用 Dockerfile 把项目打进去,完整 Dockerfile 就几行:

FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi COPY --chown=1001:1001 src/main/liberty/config/server.xml /config/server.xml COPY --chown=1001:1001 target/openshell.war /config/apps/openshell.war

构建和启动:

mvn clean package docker build -t openshell:1.0 . docker run -d -p 9080:9080 openshell:1.0

放到容器里跑的好处不只是隔离部署环境,更重要的是你的服务每次启动都是干净的状态,不会因为本地环境残留的配置引出莫名其妙的连接问题。镜像启动后,依然用浏览器打开http://localhost:9080/chat就能进聊天室,体验和本地开发完全一致。

6. 写在最后的实操心得

OpenShell 这个项目从确定技术方向到跑通完整对话流程,我前后花了一个周末的时间。如果让我重新做一遍,应该能在半个工作日内完成。但正是因为中间踩了不少坑,我才对这个技术组合有了更深的体感。

我个人最大的感受是:OpenLiberty 这套运行时比想象中还要顺手。它没有某些传统中间件那种“配置地狱”,也不像一些嵌入式容器那样把应用和运行时死死捆绑,你既能在开发时体验秒级启动,又能在生产环境用同一套配置平滑迁移。对于中大型团队而言,它可能不是首选,但对于像我这样偏好轻量、追求规范、又不想被框架绑架的开发者,这个组合非常舒服。

如果你现在正想找一个既有实时通信体验、又带点后端服务端建模感觉的小项目来练手,我认为 OpenShell 是个不错的参考。不需要庞大的工程脚手架,不需要复杂的网络拓扑,一个 Maven 项目加一个浏览器,就能完整体验从连接建立到消息广播、从状态维护到异常恢复的全链路开发流程。你可以把它继续扩展成点对点私聊、加入消息去重、引入鉴权、甚至在消息频率上做限流,这个基础骨架都能顺滑承接。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 4:06:24

RIGOL DS1102E示波器5分钟快速上手指南

1. 这台“老伙计”到底值不值得你花5分钟上手&#xff1f;RIGOL DS1102E——这个型号在电子爱好者圈子里有个外号叫“示波器界的Kindle”&#xff0c;不是因为它能看小说&#xff0c;而是因为它足够轻、足够稳、足够便宜&#xff0c;还自带一股子“你上我也上”的朴实劲儿。它不…

作者头像 李华
网站建设 2026/10/3 4:06:05

Vite+Electron构建仿微信桌面聊天系统全流程实践

我印象很深&#xff0c;半年前手上有个内部工具一直跑在浏览器里&#xff0c;每次开会演示都得先开后台、再找 URL、还得祈祷缓存别捣乱。被恶心了几次之后我下了决心&#xff1a;把它搬成一个桌面端应用。正好那段时间一直在折腾聊天类 UI&#xff0c;干脆就拿“仿微信桌面端聊…

作者头像 李华
网站建设 2026/10/3 4:04:42

Java用DoubleSummaryStatistics统计员工工资六个指标实战

做Java开发的朋友一定遇到过这种场景&#xff1a;数据库里几万条员工记录摆在那&#xff0c;领导一句话就要求统计员工数量、平均工资、最高工资、最低工资、工资总和&#xff0c;顺手还要一个最高工资人数。SQL里一条SELECT加GROUP BY就能办到&#xff0c;可问题常出在代码层—…

作者头像 李华
网站建设 2026/10/3 4:03:53

Python+SQLite实现运动会管理系统:从数据模型到排名算法全解析

简介&#xff1a;基于Python的运动会管理系统源码&#xff0c;是面向软件工程课程设计的完整项目&#xff0c;适合课设答辩、技术学习或快速搭建赛事管理平台&#xff0c;核心解决选手报名、赛程编排、成绩记录与报表生成的自动化问题。压缩包共326个文件&#xff0c;包含162个…

作者头像 李华
网站建设 2026/10/3 4:03:51

从零搭建能干活的Agent:LLM、LangChain、RAG与Agent Skills全路径

1. 这套Agent Skills课程到底在讲什么先把话说在前头&#xff1a;这不是那种“三天速成大模型专家”的营销课。我花了半个月时间&#xff0c;把市面上关于Agent Skills的零散知识重新梳理成一条从零到能上手的路径&#xff0c;核心目标只有一个——让你学完之后&#xff0c;能自…

作者头像 李华