尧图精选

Java WebSocket聊天系统课程设计:从选型到避坑全指南

🕒 发布时间:2026/10/1 20:22:05 📁 来源:尧图网络
简介本资源是面向高校网络编程课程设计与Java毕业设计场景的完整项目包围绕基于WebSocket的多人聊天系统展开适合正在准备课程设计、需要可运行源码与配套报告的学生及自学者。项目实现了用户名密码登录、多人同时在线、在线用户实时同步、群聊与一对一私聊、管理员禁言与解除禁言、历史记录缓存读取以及数据库保存用户信息和聊天记录等核心功能覆盖网络编程中长连接、消息推送与并发在线管理等关键知识点。压缩包共74个文件约7.3MB包含14个Java源文件、3个HTML页面、4个CSS样式、3个JavaScript脚本、1个SQL建库脚本、2个properties配置、1个pom.xml及1份课程设计报告docx另附png与jpg截图、README说明和Maven包装脚本结构完整便于直接导入运行与二次开发。目前已有238人学习可作为课程设计参考、答辩演示与功能扩展的实践基础。1. Java 网络编程课程设计选 WebSocket 聊天系统为什么它比 Socket 多线程方案更值得做如果你正在为网络编程课程设计选题发愁大概率会在这两条路里纠结一条是经典的 Java Socket 多线程 自定义协议另一条是 Java 基于 WebSocket 的聊天系统。我当年也纠结过后来两个都写过一遍血泪经验是——Socket 方案能让你把 TCP 三次握手、线程池、粘包拆包全踩一遍但答辩时老师最爱问的「实时性怎么保证」「浏览器能不能直接连」「消息怎么广播」Socket 方案答起来很累。而 WebSocket 聊天系统天然带 HTTP 握手升级、全双工、服务端主动推送这几个特性正好把网络编程里「应用层协议设计」和「长连接管理」两个核心考点都覆盖了。这篇笔记面向的是要交课程设计报告、还要附源代码的本科生也适合想拿一个能跑起来的小项目练手 Java 网络编程的初学者。我会把「Java 基于 WebSocket 的聊天系统」从选型理由、环境搭建、服务端和客户端实现、心跳保活、到报告怎么写按能复现的粒度讲清楚。你照着做能拿到一个支持多人在线、消息广播、私聊、在线列表、断线重连的聊天系统报告里的架构图、时序图、测试用例也都有素材。中间我会把 websocket 心跳机制实现、websocket 实时推送数据这些热搜里高频出现的点单独拆开讲因为这几个地方翻车的人最多。2. WebSocket 握手、帧结构与 Java 服务端选型先把协议底子打牢2.1 从 HTTP 升级到 WebSocket 的那一次握手到底发生了什么很多人写 WebSocket 只会在前端new WebSocket(ws://...)后端加个ServerEndpoint跑通了就完事。但课程设计答辩一定会问「它和 HTTP 什么关系」。你得能说清楚WebSocket 的连接建立阶段复用了 HTTP 的 GET 请求客户端发过来的请求头里带Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key一个 16 字节随机数的 Base64服务端如果同意升级就返回101 Switching Protocols并把Sec-WebSocket-Key拼上固定 GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11做一次 SHA-1 再 Base64作为Sec-WebSocket-Accept返回。这一步做完底层 TCP 连接不变但双方约定后面不再用 HTTP 报文格式改用 WebSocket 帧。帧结构是第二个必考点。一个 WebSocket 帧开头是 FIN、RSV、opcode接着是 MASK 位和 payload 长度7 位、716 位、764 位三种客户端发往服务端的帧必须掩码服务端发往客户端的帧不能掩码。opcode 里 0x1 是文本帧、0x2 是二进制帧、0x8 是关闭帧、0x9 是 Ping、0xA 是 Pong。心跳机制就是靠 0x9 和 0xA 这两个控制帧实现的后面第 5 章会展开。你在报告里把这张帧结构表画出来比贴十页代码都管用。2.2 Java 侧三种实现路线怎么选Java 做 WebSocket 服务端主流有三条路课程设计里选哪条直接决定你后面写代码的痛苦程度。方案依赖上手难度适合场景课程设计推荐度Jakarta WebSocket原 Java EETomcat 自带ServerEndpoint注解低部署在 Servlet 容器里和 JSP/Servlet 课程衔接高最省事Spring Boot spring-websocketSpring 生态中想顺便秀 Spring 技能中配置多Netty单独引入高想秀 NIO、EventLoop低容易跑偏我一般会推荐第一条。原因很实在网络编程课程设计通常已经学过 ServletTomcat 你本来就装了ServerEndpoint一个注解就能把普通类变成 WebSocket 端点onOpen、onMessage、onClose、onError四个回调对应连接生命周期代码量最少报告里也好画时序图。Spring Boot 方案虽然时髦但自动配置把很多细节藏起来了答辩时老师问「握手在哪处理的」你答不上来反而扣分。Netty 更适合作为进阶除非你标题里明确写了 Netty否则别给自己加难度。选 Jakarta WebSocket 还有一个隐藏好处它内置了Session.getAsyncRemote()和Session.getBasicRemote()两套发送接口同步发送会阻塞当前线程异步发送不会。广播消息时必须用异步否则一个慢客户端能把整个广播线程拖死这是新手最常翻的车之一。2.3 环境与依赖一份能直接抄的 pom 配置课程设计最怕环境跑不起来。下面这份pom.xml片段是我验证过能跑的最小依赖集Servlet 容器用 Tomcat 9对应 Jakarta EE 8包名还是javax.websocket如果你用 Tomcat 10包名变成jakarta.websocket改一下 import 即可。dependencies !-- WebSocket API作用域 provided因为 Tomcat 自带实现 -- dependency groupIdjavax.websocket/groupId artifactIdjavax.websocket-api/artifactId version1.1/version scopeprovided/scope /dependency !-- JSON 序列化用于消息协议编解码 -- dependency groupIdcom.google.code.gson/groupId artifactIdgson/artifactId version2.10.1/version /dependency /dependenciesscope设成provided是关键如果你写成默认的compile打 war 包时会把 API 一起打进去和 Tomcat 自带的实现冲突启动时报ClassCastException或者端点注册不上。Gson 用来把消息对象转成 JSON 字符串比手拼字符串靠谱得多也方便前端JSON.parse。版本号用 2.10.1 是我本地验证过的你换成 2.8 以上一般也没问题但别用太老的版本老版本对泛型支持有坑。3. 服务端核心实现会话管理、消息广播与私聊路由3.1 用 ConcurrentHashMap 管理在线会话WebSocket 服务端最核心的数据结构就是「谁在线」。每个连接对应一个javax.websocket.Session对象你需要一个线程安全的容器把它存起来。为什么强调线程安全因为onOpen、onMessage、onClose可能在不同线程里被调用普通HashMap在并发 put/remove 时会死循环JDK 7 的老问题JDK 8 虽然改成红黑树但依然不安全。ServerEndpoint(/chat/{username}) public class ChatEndpoint { // key 是用户名value 是该用户的会话 private static final MapString, Session ONLINE_SESSIONS new ConcurrentHashMap(); // 记录每个 Session 对应的用户名onClose 时要用 private static final MapSession, String SESSION_USER new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(username) String username) { // 如果同名用户已在线先踢掉旧连接避免消息发错人 Session old ONLINE_SESSIONS.get(username); if (old ! null old.isOpen()) { try { old.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, 重复登录)); } catch (IOException ignored) {} } ONLINE_SESSIONS.put(username, session); SESSION_USER.put(session, username); // 广播最新在线列表 broadcast(buildSystemMsg(username 上线了, ONLINE_SESSIONS.keySet())); } OnClose public void onClose(Session session) { String username SESSION_USER.remove(session); if (username ! null) { ONLINE_SESSIONS.remove(username); broadcast(buildSystemMsg(username 下线了, ONLINE_SESSIONS.keySet())); } } }这段代码有三个设计点值得在报告里写。第一用PathParam把用户名放在 URL 路径里ws://localhost:8080/chat/张三比在消息体里传更直观也方便服务端在onOpen阶段就完成身份登记。第二同名用户踢旧连接是聊天系统的常见需求否则一个账号开两个标签页消息会随机发到其中一个用户会觉得「消息丢了」。第三SESSION_USER这个反向映射不能省onClose回调只给你Session不给你用户名没有它你就不知道该从ONLINE_SESSIONS里删哪个 key。3.2 消息协议设计一条 JSON 走天下课程设计里消息格式别搞太复杂用一条统一的 JSON 就够字段包括type、from、to、content、time、onlineUsers。type用枚举值区分CHAT群聊、PRIVATE私聊、SYSTEM系统通知、ONLINE_LIST在线列表、PING/PONG心跳。这样前端拿到消息先看type再决定怎么渲染逻辑清晰报告里画一张消息字段表就能讲一页。public class Message { private String type; // CHAT / PRIVATE / SYSTEM / ONLINE_LIST / PING / PONG private String from; private String to; // 私聊时的目标用户群聊为 null private String content; private long time; private ListString onlineUsers; // getter/setter 省略 }time用System.currentTimeMillis()前端自己格式化成HH:mm:ss别在服务端格式化成字符串否则时区问题能让你调半天。onlineUsers只在type为ONLINE_LIST或SYSTEM时有值其他类型为 nullGson 序列化时会自动忽略 null 字段默认行为前端判断msg.onlineUsers是否存在即可。3.3 广播与私聊异步发送是保命符广播的实现看着简单坑最多。下面这段是核心private void broadcast(Message msg) { String json new Gson().toJson(msg); for (Session s : ONLINE_SESSIONS.values()) { if (s.isOpen()) { // 必须用 getAsyncRemote同步发送会阻塞 s.getAsyncRemote().sendText(json, result - { if (!result.isOK()) { // 发送失败记录日志不要在这里删 session交给 onClose System.err.println(发送失败: result.getException().getMessage()); } }); } } } private void sendTo(String username, Message msg) { Session s ONLINE_SESSIONS.get(username); if (s ! null s.isOpen()) { s.getAsyncRemote().sendText(new Gson().toJson(msg)); } }getAsyncRemote().sendText()的第二个参数是SendHandler回调发送结果通过result.isOK()判断。为什么强调异步因为getBasicRemote().sendText()是阻塞的如果某个客户端网络卡了TCP 发送缓冲区满了这个调用会一直阻塞广播循环就卡在那里后面所有用户都收不到消息。我当年第一次写就用了同步发送本地测试两个人聊天没问题一放到实验室局域网十个人同时在线就出现「有人发消息别人半天收不到」排查了一下午才定位到这儿典型的血泪经验。私聊路由就是sendTo从ONLINE_SESSIONS里按用户名取 Session 单独发。注意私聊消息也要回显给发送者自己否则发送方看不到自己发出去的内容体验很怪。前端可以在发送后本地先渲染一条也可以等服务端回推我一般让服务端统一回推保证消息顺序一致。4. 客户端与前端联调从 HTML 页面到断线重连4.1 一个能跑的最小前端页面课程设计不要求前端多漂亮但至少要能演示。下面这个 HTML 页面包含连接、发送、接收、在线列表四个功能直接丢到webapp目录下就能用。!DOCTYPE html html headmeta charsetUTF-8titleWebSocket 聊天室/title/head body div idlogin 用户名input idusername valueuser1 button onclickconnect()连接/button /div div idonline/div div idmessages styleheight:300px;overflow-y:auto;border:1px solid #ccc/div input idinput stylewidth:70% placeholder输入消息用户名 可私聊 button onclicksend()发送/button script let ws null; let reconnectTimer null; function connect() { const username document.getElementById(username).value.trim(); if (!username) return alert(请输入用户名); // 用 ws 协议端口和 Tomcat 一致 ws new WebSocket(ws://${location.host}/chat/${encodeURIComponent(username)}); ws.onopen () { console.log(已连接); // 连接成功后启动心跳 startHeartbeat(); }; ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type PONG) return; // 心跳响应不渲染 if (msg.type ONLINE_LIST || msg.type SYSTEM) { renderOnline(msg.onlineUsers); } renderMessage(msg); }; ws.onclose () { console.log(连接关闭3 秒后重连); stopHeartbeat(); // 断线重连避免用户手动刷新 reconnectTimer setTimeout(connect, 3000); }; ws.onerror (e) console.error(WebSocket 错误, e); } function send() { const content document.getElementById(input).value.trim(); if (!content || !ws || ws.readyState ! WebSocket.OPEN) return; // 用户名 开头视为私聊 const match content.match(/^(\S)\s(.)$/); const msg match ? { type: PRIVATE, to: match[1], content: match[2] } : { type: CHAT, content: content }; ws.send(JSON.stringify(msg)); document.getElementById(input).value ; } /script /body /htmllocation.host会自动取当前页面的域名和端口避免你硬编码localhost:8080后换台机器就失效。encodeURIComponent处理中文用户名否则 URL 里的中文可能被截断。断线重连用setTimeout递归调用connect注意重连前要stopHeartbeat否则旧的定时器还在跑会往一个已关闭的 socket 发心跳控制台报一堆错。4.2 心跳机制实现为什么你的连接总是「假死」websocket 心跳机制实现是热搜里高频词也是实际项目里最容易翻车的地方。现象是客户端和服务端都显示连接着但消息发不出去或者过几分钟连接自动断了。原因通常是中间有 Nginx、防火墙或者云负载均衡它们对空闲连接有超时限制常见 60 秒或 300 秒TCP 连接被悄悄回收但两端应用层都不知道这就是「假死」。解决办法就是应用层心跳。服务端用OnMessage收到PING就回PONG客户端每 30 秒发一次PING如果连续两次没收到PONG就主动ws.close()触发重连。let heartbeatTimer null; let pongTimeout null; function startHeartbeat() { stopHeartbeat(); heartbeatTimer setInterval(() { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: PING })); // 5 秒内没收到 PONG 就认为连接已死 pongTimeout setTimeout(() { console.warn(心跳超时主动关闭连接); ws.close(); }, 5000); } }, 30000); } function stopHeartbeat() { if (heartbeatTimer) clearInterval(heartbeatTimer); if (pongTimeout) clearTimeout(pongTimeout); heartbeatTimer null; pongTimeout null; }服务端对应处理OnMessage public void onMessage(String text, Session session) { Message msg new Gson().fromJson(text, Message.class); if (PING.equals(msg.getType())) { // 心跳直接回 PONG不走广播 Message pong new Message(); pong.setType(PONG); session.getAsyncRemote().sendText(new Gson().toJson(pong)); return; } // ... 处理 CHAT / PRIVATE }心跳间隔设 30 秒是个经验值太短浪费流量太长比如 120 秒可能中间设备 60 秒就断了你还没发心跳。pongTimeout设 5 秒是因为局域网内 PONG 往返通常几十毫秒5 秒足够超过说明连接确实有问题。注意onMessage里收到 PING 要return别让它继续走后面的广播逻辑否则心跳消息会被当成聊天内容广播出去前端满屏 PING。4.3 用 websocket test client 做接口验证写完后端别急着开浏览器先用websocket test client类工具比如浏览器插件或者 Postman 的 WebSocket 功能单独测服务端。连接ws://localhost:8080/chat/testuser手动发{type:CHAT,content:hello}看能不能收到广播。这样能把「前端问题」和「后端问题」分开不然浏览器控制台一堆报错你根本不知道是 JS 写错了还是 Java 端点没注册上。我一般会先用测试工具把onOpen、onMessage、onClose三个回调都验证一遍再联调前端。5. 避坑与排查课程设计里最容易翻车的 5 个点5.1 现象连接建立后立刻断开控制台报 404原因ServerEndpoint的路径和前端请求路径不一致或者web.xml里metadata-completetrue导致注解没被扫描。Tomcat 9 默认支持注解扫描但如果你从老项目拷的web.xml很可能带着metadata-completetrue。解决检查ServerEndpoint(/chat/{username})和前端ws://host/chat/xxx是否一致注意应用上下文路径如果 war 包名是chat.ws完整路径是/chat.ws/chat/xxx。把web.xml里的metadata-complete改成false或直接删掉这个属性。5.2 现象广播时部分用户收不到消息或者服务端线程卡死原因用了getBasicRemote().sendText()同步发送某个客户端网络慢导致阻塞。或者广播时直接遍历ONLINE_SESSIONS.values()遍历过程中有用户上下线ConcurrentHashMap虽然不会抛ConcurrentModificationException但弱一致性迭代器可能漏掉刚加入的用户。解决统一改用getAsyncRemote()。如果对实时性要求高可以先把values()拷贝一份再遍历new ArrayList(ONLINE_SESSIONS.values())避免迭代期间集合变化。5.3 现象中文消息乱码前端显示问号原因Tomcat 的 WebSocket 默认用 UTF-8但如果你在onMessage里手动处理字节数组或者前端send时没编码就可能乱码。另外OnMessage如果写成onMessage(byte[] data)而不是onMessage(String text)需要自己new String(data, StandardCharsets.UTF_8)。解决服务端用String参数接收文本消息前端JSON.stringify后send两端都是 UTF-8。检查 Tomcat 的server.xml里 Connector 有没有URIEncodingUTF-8虽然 WebSocket 不走 URI 编码但 HTTP 握手阶段会用到。5.4 现象心跳发了但服务端不回 PONG或者回了前端收不到原因服务端onMessage里判断type时用了比较字符串应该用equals。或者前端onmessage里把 PONG 也渲染成消息了看起来像「服务端没回」。解决字符串比较一律PING.equals(msg.getType())把常量放前面避免 NPE。前端onmessage里先判断if (msg.type PONG) return;再走渲染逻辑。5.5 现象部署到服务器后连不上本地却正常原因服务器防火墙没放行端口或者 Nginx 反代没配置 WebSocket 的Upgrade头。Nginx 默认不转发Upgrade和Connection头需要显式配置。解决防火墙放行 Tomcat 端口。Nginx 配置里加location /chat/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 300s; # 要大于心跳间隔 }proxy_read_timeout必须大于心跳间隔否则 Nginx 会在心跳到达前主动断开连接你又回到「假死」问题。6. 课程设计报告怎么写才不被扣分架构图、时序图与测试用例报告是课程设计的另一半分数代码跑通只完成了一半。我见过太多人代码写得不错报告里全是代码截图最后分数不高。老师要看的是你的设计思路和验证过程不是代码复读机。架构图我一般画三层浏览器层多个客户端、网络传输层HTTP 握手升级 WebSocket 帧、服务端层ChatEndpoint Session 管理 消息路由。用 draw.io 或者 Visio 画别用截图。时序图重点画两条一条是「客户端 A 发群聊消息到服务端广播给 B、C」另一条是「心跳保活与断线重连」。时序图里把onOpen、onMessage、broadcast、onClose这几个方法名标上和代码对应老师一看就知道你真写了。测试用例表至少覆盖五种场景单用户连接、多用户群聊、私聊、用户上下线通知、心跳超时重连。每种场景写清楚「前置条件、操作步骤、预期结果、实际结果」。比如心跳超时重连这条前置条件是客户端已连接操作是手动关闭服务端或断网 35 秒预期结果是客户端 3 秒后自动重连并重新出现在在线列表实际结果填「通过」。这张表比任何文字描述都有说服力。最后说个我自己的习惯报告里专门留一节写「遇到的问题与解决」把第 5 章那五个坑挑两三个写进去配上你的排查过程。老师特别喜欢看这个因为它证明你是真动手了而不是网上抄的。我当年把「同步发送导致广播阻塞」这个坑写进去答辩时老师还追问了异步和同步的区别正好是我准备过的直接加分。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →