ARTICLE DETAIL

资讯详情

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

Java WebSocket聊天系统课程设计:从协议原理到心跳机制实战

Java WebSocket聊天系统课程设计:从协议原理到心跳机制实战 简介面向网络编程课程设计与毕业设计的 Java WebSocket 聊天系统完整资料适合需要掌握实时通信、前后端协作与在线用户管理的学习者。系统基于 WebSocket 长连接实现用户名密码登录、多人同时在线、在线用户同步、文字群聊、管理员禁言/解禁、一对一私聊和历史记录缓存读取后端通过在线用户列表维护连接状态数据库负责保存用户信息与聊天记录并配套课程设计报告辅助理解整体架构与实现思路。资源共 74 个文件以 14 个 Java 源码、SQL 数据库脚本、HTML/CSS/JS 前端页面、PNG/JPG 界面截图为主另含 Maven 配置、属性文件与 Word 版课程设计报告压缩包整体约 7.3MB目录划分明确便于直接导入运行或二次扩展。已有 238 人学习下载可作为网络编程课程设计、实训项目或毕业设计的高质量参考。1. 这个题目到底要交什么一次把WebSocket聊天系统从协议讲到课程设计报告“网络编程技术课程设计”这几个字对每个计算机专业的学生来说都是一道要真正动手的关卡交源码、交报告、现场演示一步都躲不过。我见过不少顺手拿 HTTP 轮询硬凑的版本页面一卡一卡老师随口一句“为什么不用 WebSocket”就哑火。这个题目核心就三件事理解 WebSocket 的握手和消息帧怎么工作用 Java 写出能支撑多客户端实时聊天的服务端再把设计过程整理成一份能讲清楚的课程设计报告。本文按这个顺序走适合正在做课程设计、想搞懂 WebSocket 心跳机制、或者准备 Java 开发工程师面试题时被问长连接原理的人。2. WebSocket 协议与 Java 服务端选型课程设计为什么首选 JSR 3562.1 先看懂握手一次 HTTP 升级请求是怎么变成长连接的WebSocket 的起点是一次普通 HTTP 请求只是多了三个头。客户端发GET /chat HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务端如果同意升级返回HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOoSec-WebSocket-Accept不是随便生成的它是把Sec-WebSocket-Key加上一个固定 GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接后做 SHA1再 Base64 编码。这个细节看起来是纯知识但在课程设计报告的“协议设计”章节里是加分项答辩时老师很喜欢让你现场推导一遍。握手完成之后TCP 连接不再承载普通 HTTP 请求而是直接传 WebSocket 帧。帧头里最重要的字段FIN 表示消息是否结束opcode 区分文本帧0x1、二进制帧0x2、ping 帧0x9和 pong 帧0xApayload length 决定消息体有多长。默认情况下客户端发给服务端的帧必须加掩码服务端回给客户端的帧不加掩码这是协议为了兼容老一代代理服务器设计的。只要用 Java 官方的 WebSocket API这些细节框架全帮你处理了但报告里能写出帧结构明显比只贴代码的同学高一个档次。2.2 三种实现路线对比纯 JSR 356、Spring WebSocket、NettyJava 做 WebSocket 主要有三条路我在实际开发里都见过有人用JSR 356javax.websocketJava EE 7 起纳入标准Tomcat 8/9 自带实现。只需在类上写ServerEndpoint注解依赖最少原理最透明课程设计最推荐。Spring WebSocket把 WebSocket 端点交给 Spring 容器管理能直接注入 Service、加拦截器适合想在企业分层架构里做聊天功能的人。但报告里要解释 Spring 的生命周期和拦截器概念多一层。Netty性能最好、可控性最强但代码量明显更大。课程设计一般用不上除非你的题目明确要求做高并发下的理论分析。路线依赖学习成本适合场景纯 JSR 356无额外依赖Tomcat 自带低课程设计、入门 DemoSpring WebSocketspring-websocket中已用 Spring Boot 的课程项目Nettynetty-all高生产级长连接服务选型时我一般按这个标准判断如果课程设计报告要重点讲协议和原理选 JSR 356如果项目里已经用了 Spring Boot你不想在 Tomcat 和 IDE 之间来回折腾选 Spring WebSocket 也没问题Netty 先别碰学习成本和答辩压力都会超出预期。2.3 开发环境怎么定Tomcat 9 JDK 8/11 是最顺的组合环境配置是很多课程设计翻车的起点。我建议用 Tomcat 9原因很简单Tomcat 10 开始把 Java EE 的javax命名空间全部换成了 Jakarta EE 的jakarta网上大量现有代码直接编译不过。配 JDK 8 或 11 都行JDK 8 最稳JDK 11 也没问题但不要直接上 JDK 17 配合老版本 Tomcat会有模块访问方面的隐性问题。开发流程一般是IDEA 里建一个普通 Java Web 项目写好端点类和前端页面打成 WAR 包放进 Tomcat 的 webapps 目录。访问http://localhost:8080/chat/index.html就能看到页面。这里的/chat是项目上下文路径和 WebSocket 端点的访问路径是两回事很多人在这里栽跟头第 5 章我会专门讲。3. 搭一个最小可跑的群聊系统ServerEndpoint 与前端页面的完整代码3.1 服务端端点 ChatEndpoint一个注解撑起连接生命周期先从服务端写起。新建一个类ChatEndpoint标注ServerEndpoint(/chat)表示这个类负责处理 WebSocket 路径/chat下的连接。类的核心方法由四个注解驱动OnOpen连接建立时触发OnMessage收到消息时触发OnClose连接关闭时触发OnError出错时触发。看代码package com.course.chat; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; ServerEndpoint(/chat) public class ChatEndpoint { // 在线表key 是用户名value 是对应的 WebSocket Session private static final MapString, Session onlineSessions new ConcurrentHashMap(); private String username; OnOpen public void onOpen(Session session) { // 这里不立即注册用户名等客户端发 login|用户名 再登记 System.out.println(新连接 session.getId()); } OnMessage public void onMessage(String message, Session session) { // 约定三种消息格式 // login|用户名 —— 登录并广播上线 // group|内容 —— 群聊广播给所有人 // private|目标用户|内容 —— 私聊只发给目标用户 if (message.startsWith(login|)) { this.username message.split(\\|)[1]; onlineSessions.put(this.username, session); // 把用户名挂到 Session 上后续断开时还能取到 session.getUserProperties().put(username, this.username); broadcast(system, this.username 上线了当前在线 onlineSessions.size() 人); } else if (message.startsWith(group|)) { broadcast(this.username, message.substring(6)); } else if (message.startsWith(private|)) { // 格式private|目标用户名|真正的消息内容 String[] parts message.split(\\|, 3); String target parts[1]; String content parts[2]; Session targetSession onlineSessions.get(target); if (targetSession ! null) { sendToTarget(targetSession, this.username, content); } } } OnClose public void onClose(Session session) { String name (String) session.getUserProperties().get(username); if (name ! null) { onlineSessions.remove(name); broadcast(system, name 下线了当前在线 onlineSessions.size() 人); } } OnError public void onError(Session session, Throwable error) { System.out.println(连接出错 session.getId() 错误 error.getMessage()); } // 群聊广播给在线表里每一个 Session 发消息 private void broadcast(String sender, String content) { String fullMessage buildMessage(sender, content); for (Session s : onlineSessions.values()) { try { synchronized (s) { s.getBasicRemote().sendText(fullMessage); } } catch (IOException e) { // 发送失败说明这个连接已经不可用交给心跳逻辑去清理 System.out.println(发送失败连接 ID s.getId()); } } } // 私聊只发给一个人不广播 private void sendToTarget(Session targetSession, String sender, String content) { try { synchronized (targetSession) { targetSession.getBasicRemote().sendText(buildMessage(sender, content)); } } catch (IOException e) { System.out.println(私聊发送失败 e.getMessage()); } } // 统一拼成 JSON 字符串前端直接用 JSON.parse 解析 private String buildMessage(String sender, String content) { return String.format({\sender\:\%s\,\content\:\%s\,\time\:\%s\}, sender, content, java.time.LocalTime.now().toString().substring(0, 8)); } }这段代码的逻辑说明onlineSessions用静态 Map 保存所有在线连接因为聊天服务端所有客户端共享同一份在线表。ConcurrentHashMap是必须的多线程同时上线、下线、发消息时会并发读写这个 Map用普通 HashMap 会出现死循环或数据错乱。synchronized (s)锁住单个 Session防止两个线程同时往同一条 TCP 连接里写数据导致消息帧交错。参数说明message.split(\\|, 3)里的3表示最多切三段这样私聊内容里即使带|符号也不会被截断。在线表 key 用用户名而不是 sessionId是为了私聊时能根据名字直接找到目标连接。getUserProperties()是 Session 自带的属性挂载点连接关闭时从这里取用户名来清理在线表。3.2 运行方式与最低依赖WAR 包放对位置其余交给 Tomcat这个项目不需要引入任何第三方 WebSocket 库因为javax.websocket是 Java EE 标准 APITomcat 8/9 自带实现。你需要做的只是把ChatEndpoint类放在项目的 src 目录里然后保证项目以 WAR 包形式部署。常见的目录结构src/main/java/com/course/chat/ChatEndpoint.java src/main/webapp/index.html课程设计报告里画一张这样的目录结构图评委会觉得你工程规范到位。打开 Tomcat 后访问http://localhost:8080/你的项目名/index.html页面里的 WebSocket 连接地址也要对应写成ws://localhost:8080/你的项目名/chat。这里有个很容易忽略的点/你的项目名是 WAR 包的上下文路径/chat是ServerEndpoint里的路径两者缺一不可。3.3 前端页面 index.html原生 WebSocket 对象怎么对接后端就绪后写一个能跑的前端页面。不需要任何框架浏览器原生提供WebSocket全局对象new WebSocket(url)即完成连接这是 java 基础里经常被忽略但实际很有用的点。完整页面代码如下!DOCTYPE html html langzh head meta charsetUTF-8 titleWebSocket 聊天室/title /head body div idmessages styleheight:400px;border:1px solid #ccc;overflow-y:auto;padding:8px;/div input idinputBox placeholder说点什么... stylewidth:300px; button idsendBtn发送/button button idlogoutBtn退出登录/button script const msgBox document.getElementById(messages); const inputBox document.getElementById(inputBox); // 这里的 /chat 路径对应服务端 ServerEndpoint(/chat) // 注意如果部署时项目上下文是 /websocket-chat地址要写全 const ws new WebSocket(ws://localhost:8080/chat/chat); ws.onopen function () { // 连接建立后立刻登录服务端靠 login 消息知道你是谁 const nick prompt(请输入昵称, user- Math.floor(Math.random() * 1000)); ws.send(login| nick); }; ws.onmessage function (event) { const data JSON.parse(event.data); appendLine(data.sender, data.content); }; ws.onclose function () { appendLine(系统, 连接已断开); }; ws.onerror function () { appendLine(系统, 连接出现错误); }; document.getElementById(sendBtn).onclick function () { const value inputBox.value.trim(); if (value) { ws.send(group| value); inputBox.value ; } }; document.getElementById(logoutBtn).onclick function () { ws.close(); }; function appendLine(sender, content) { const div document.createElement(div); div.textContent sender : content; msgBox.appendChild(div); msgBox.scrollTop msgBox.scrollHeight; } /script /body /html这段代码里有个细节ws.send(group| value)对应服务端group|前缀的分流逻辑。前端不做任何消息分发所有消息处理后都通过JSON.parse解析成对象再渲染。这里用textContent而不是innerHTML是刻意的内嵌 HTML 会有 XSS 风险课程设计可能不会有人攻击你但写进报告的“安全性分析”部分会很加分。onclose里我只写了提示没有做重连逻辑。这是最小可用版本第 4 章会补上重连和心跳。4. 从群聊到私聊在线列表、心跳保活与断线重连的落地写法4.1 消息协议设计一条消息里带登录、群聊、私聊三种语义当功能从群聊扩展到私聊最忌讳的做法是为私聊单独再开一个 WebSocket 端点。同一个端点里靠消息前缀区分业务语义是最直接、最好讲清楚的方案。上面代码设计的三前缀协议已经能覆盖课程设计的需求login|用户名登录把用户名和 Session 绑定。group|内容群聊所有人可见。private|目标用户名|内容私聊只发给目标用户。协议设计是课程设计报告的重要得分点。你在报告里把这三行前缀协议画成一张表格再说明为什么用|分隔而不是 JSON——因为字符串处理对课程设计来说更直观且减少一条消息里的转义负担。如果导师追问“为什么不直接发 JSON”你可以回答私聊内容里可能包含引号直接 JSON 需要转义处理用前缀分隔后在服务端切分更简单。这就是“自己讲过的话才答得上来”的典型问题准备方式。4.2 心跳检测定时 ping 为什么能救回半死连接聊天的真实场景里最常见的诡异现象是手机锁屏一段时间再打开时消息收不到了但页面显示“连接正常”。这是因为 TCP 连接在断网或休眠时并没有标准的断开通知发到服务端。连接变成半开状态服务端以为人还在客户端也以为连接还活着。解决这个问题的标准做法是 WebSocket 心跳机制。服务端定期向每个 Session 发 ping 帧客户端收到 ping 后由浏览器自动回 pong 帧不需要前端写任何处理代码。服务端如果发现 ping 发不出去或者连续收不到 pong就判定连接死亡并清理。// 在 ChatEndpoint 里加一个静态方法项目启动后调用一次即可 public static void startHeartbeatTimer() { // scheduleAtFixedRate5秒后开始每隔5秒执行一次 Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() - { long now System.currentTimeMillis(); for (Map.EntryString, Session entry : onlineSessions.entrySet()) { Session s entry.getValue(); try { // 发 ping 帧客户端浏览器会自动回 pong s.getBasicRemote().sendPing(null); // 记录最近一次 ping 成功时间挂在 Session 属性里 s.getUserProperties().put(lastPingOk, now); } catch (IOException e) { // 连续 ping 失败 2 次才关连接避免网络瞬断误杀 Integer failCount (Integer) s.getUserProperties().getOrDefault(pingFailCount, 0); failCount; s.getUserProperties().put(pingFailCount, failCount); if (failCount 2) { System.out.println(心跳超时关闭连接 entry.getKey()); try { s.close(new CloseReason(CloseCodes.GOING_AWAY, 心跳超时)); } catch (IOException ex) { ex.printStackTrace(); } } } } }, 5, 5, TimeUnit.SECONDS); }逻辑说明sendPing(null)的参数是可选的应用数据传 null 只是心跳探测不携带业务数据。连续失败计数存在 Session 的 userProperties 里而不是用全局变量这是为了避免多线程下计数混乱。为什么失败 2 次才关闭因为网络抖动可能让一次 ping 恰好丢包连续两次失败说明连接大概率真死了。参数说明间隔 5 秒是一个折中值。间隔太短1 秒会产生大量无意义网络包间隔太长30 秒以上会让人感觉“掉线之后很久才被踢”。局域网内测试 5 秒没问题如果做公网演示可以放宽到 10 秒。超时判定用“次数”而不是“时间”更简单课程设计答辩时能把这个逻辑讲清楚面试官会认为你有生产意识。4.3 客户端断线重连固定间隔最简单但要记得重新登录服务端心跳解决了服务端视角的“假连接”客户端还需要解决“意外断开后恢复”的问题。浏览器里 WebSocket 断线后onclose会触发这就是重连的切入点。let ws null; let connected false; function connect() { ws new WebSocket(ws://localhost:8080/chat/chat); ws.onopen function () { connected true; // 重新登录断线重连后服务端的在线表里已经没有你了 if (nickname) { ws.send(login| nickname); } }; ws.onmessage function (event) { const data JSON.parse(event.data); appendLine(data.sender, data.content); }; ws.onclose function () { connected false; appendLine(系统, 连接断开3 秒后重试...); // 固定间隔重连3秒后重新执行 connect() setTimeout(connect, 3000); }; ws.onerror function () { ws.close(); // 触发 onclose让重连逻辑统一接管 }; } let nickname null; // 首次登录先让用户填昵称再建立连接 nickname prompt(请输入昵称, user- Math.floor(Math.random() * 1000)); connect();这段代码的关键是onclose里调用setTimeout(connect, 3000)形成自动重连闭环。重连成功后服务端的onlineSessions里已经没有这个用户了所以必须重新发送login|昵称注册一次。如果不做重新登录会出现一个诡异现象消息能发出去但私聊收不到因为服务端在线表里根本没有你。重连间隔用固定 3 秒还是指数退避课程设计里用固定 3 秒足够。指数退避1 秒、2 秒、4 秒、8 秒是生产级做法用来避免服务端故障时大量客户端同时重连造成雪崩。报告里提一句“生产环境还会引入指数退避”就能展示你了解这个方向不需要真的实现。5. 课程设计避坑笔记五个让答辩现场翻车的常见问题5.1 握手 404ServerEndpoint 路径和项目 contextPath 没对上现象前端报WebSocket connection to ws://localhost:8080/chat failed服务端控制台没有任何日志Tomcat 访问日志里显示 404。原因WebSocket 握手请求本质上是一次 HTTP 请求路径必须满足“项目上下文路径 ServerEndpoint 路径”。如果项目部署名是websocket-chat端点注解是ServerEndpoint(/chat)完整路径应该是ws://localhost:8080/websocket-chat/chat。只写/chat时Tomcat 在根路径下找不到这个端点。解决前端连接地址统一写成ws://localhost:8080/${项目名}/chat。更稳妥的做法是在页面里动态拼接而不是写死const wsPath ws:// location.host location.pathname.replace(index.html, chat)。这样换项目名也不需要改代码。血泪经验部署完先看 Tomcat 的 webapps 目录下 WAR 解压出来的文件夹名那就是实际的上下文路径。5.2 中文消息变问号字符编码从页面到容器一路统一现象客户端发“你好”服务端广播后别人收到的是“”或乱码。原因这是一个链路问题。HTML 页面没设meta charsetUTF-8或者 Tomcat 的 URI 编码不是 UTF-8又或者OnMessage方法接收的 String 在容器解码时用了平台默认编码。Windows 中文系统上默认编码是 GBK字符串一到 Tomcat 的解码环节就乱了。解决三个位置全部统一。HTML head 加meta charsetUTF-8Tomcat 的server.xml里 Connector 加URIEncodingUTF-8代码层面不要在OnMessage里用byte[]参数自己解码直接收 String容器会按配置好的编码解析。顺序排查时用排除法先看服务端控制台打印收到的原始字符串如果服务端收到的就乱那是传输或容器问题如果服务端收到的正常但客户端显示乱那是页面渲染的问题。5.3 多客户端同时发消息崩溃普通 HashMap 的并发悲剧现象三五个客户端同时发消息服务端偶发java.util.ConcurrentModificationException再严重点整个 Tomcat 进程卡死。原因onlineSessions用了普通HashMap。WebSocket 是多线程模型每个连接的消息处理都在独立线程上执行多个线程同时 put 同一个 HashMap 时内部链表结构被破坏轻则异常重则死循环。解决在线表容器换成ConcurrentHashMap。注意广播遍历时也要用它的values()视图遍历过程中其他线程同时 put/remove 不会抛异常。另外还要注意 Session 对象的发送同步两个线程同时对同一个Session调sendText会把两条消息的帧字节混在一起所以广播时要加synchronized (s)。这两点做到了并发场景下基本不会出问题。5.4 服务端不知道客户端已死心跳超时判定是刚需现象用户手机断网或锁屏超过几分钟其他人发消息对方收不到服务端在线列表里却一直显示那个人在线。原因TCP 断开时有两种情况。主动断开会发 FIN 包服务端能感知并触发OnClose断网、断电、休眠这类异常断开不会发 FIN连接变成半开状态服务端要等下一个 TCP 层的超时重传周期可能几百秒才反应过来。解决按第 4 章的方案加上心跳检测。服务端定期发 ping连续失败两次就调用session.close()并清理在线表。这里有个细节CloseReason里用CloseCodes.GOING_AWAY表示服务端主动踢人课程设计报告里把这个枚举的值写出来评委一看就知道你不是临时抄的。5.5 Tomcat 10 编译不过javax.websocket 要换成 jakarta.websocket现象代码在 Tomcat 9 下跑得好好的换到 Tomcat 10 启动直接报ClassNotFoundException: javax.websocket.Session。原因Tomcat 10 是第一个完全基于 Jakarta EE 9 的版本所有 Java EE API 的包名从javax.*改成了jakarta.*WebSocket 的javax.websocket也被移到jakarta.websocket。项目里 import 的还是老包名自然找不到类。解决两条路。一是项目就锁死 Tomcat 9JDK 8 或 11 搭配 Tomcat 9网上所有javax.websocket示例代码都能直接用。二是如果你必须用 Tomcat 10把代码里所有javax.websocket替换成jakarta.websocket同时注意ServerEndpoint等注解的包名也要一起换换完后可以运行。我建议课程设计直接用 Tomcat 9省掉这一步把精力放在报告和演示上。6. 课程设计报告怎么写不被挑刺结构、测试用例与三个答辩追问6.1 报告结构七页写到点子上测试部分放手工测试表一份完整的课程设计报告不需要 30 页堆砌评委会重点看这几块需求分析、协议设计、系统设计、实现关键代码、测试验证。参考结构如下章节内容要点建议篇幅需求分析功能需求登录、群聊、私聊、在线状态非功能需求实时性、并发性1 页协议设计WebSocket 握手流程、三种消息前缀约定、帧结构简述1.5 页系统设计模块划分端点层、会话管理层、消息分发层在线表数据结构说明1.5 页实现说明贴 ServerEndpoint 核心代码注释讲清楚生命周期2 页测试验证手工测试用例表、截屏、测试结果分析1.5 页总结与反思遇到的问题编码、并发、半开连接和改进方向0.5 页测试用例表是报告里最容易出彩的部分。我见过太多报告只写“系统运行正常”没有任何数据支撑。就算只是手工测试也要把用例写完整。测试项操作步骤预期结果多客户端登录浏览器开两个标签页分别填不同昵称两个页面都显示上线提示在线人数为 2群聊广播A 页发送群聊消息B 页实时收到不刷新页面私聊隔离A 页给 B 页发私聊B 页收到第三页 C 收不到断线重连A 页关闭后重新打开3 秒内自动重连并恢复在线状态心跳清理模拟断网断网前打开服务端日志30 秒内服务端打印心跳超时并关闭连接6.2 答辩前必做的三件事稳定演示、断网复测、准备好被追问答辩演示比写报告更考验细节。第一件事演示前把浏览器调成无痕模式避免浏览器缓存干扰开两个窗口一个发送一个接收先跑通一遍群聊再演示私聊。第二件事现场断开服务端再重新启动服务端展示客户端自动重连恢复这一步比聊天本身更能体现系统健壮性。第三件事预演三个高频追问。追问一是“为什么用 WebSocket 而不是 HTTP 轮询”。你的子弹点应该是轮询每个客户端每秒发一次请求1000 在线就是每秒 1000 次 HTTP 请求大部分是无效的WebSocket 建立后只有一个 TCP 连接服务端可以主动推送流量和延迟都低。追问二是“浏览器断线重连后服务端怎么恢复状态”。你的应对思路是从重连后重新 login 说起聊服务端在线表的清理和重建。追问三是“如果部署到多台服务器在线表怎么办”。善用一句“可以用 Redis Pub/Sub 做跨节点消息转发课程设计里直接用本地内存表展示核心逻辑”既展示知识边界又不给自己挖坑。我做过一次用定时轮询冒充实时聊天的课程设计当时觉得能跑就行结果答辩时被问“一千人在线时每秒产生多少无效请求”那是我第一次意识到选型比 CRUD 重要。后来做生产级聊天系统才真正把心跳、重连、消息去重这些细节补上。这个题目值得好好做因为它贯穿了网络编程、并发编程和前端交互三条线你做完的不只是一份作业而是一个能讲清楚“实时通信到底怎么回事”的底气。希望帮到你。本文还有配套的精品资源点击获取
返回列表