简介:这是一套面向高校网络编程课程设计与Java毕业设计场景的完整项目资料,围绕基于WebSocket的多人聊天系统展开,适合正在准备课程设计、实训答辩或需要Java网络编程实战案例的学生与开发者。资源包共74个文件,约7.3MB,包含14个Java源文件、3个HTML页面、4个CSS样式、3个JavaScript脚本以及SQL建表脚本、properties配置、pom.xml与mvnw构建文件,另附课程设计报告docx与多张运行截图,覆盖从后端逻辑到前端界面的完整实现。系统功能较为完整:用户名密码登录、多人同时在线、在线用户实时同步、群聊与一对一私聊、管理员禁言与解除禁言、历史记录缓存读取,用户信息与聊天记录均持久化到数据库。目前已有238人学习下载,可帮助读者快速理解WebSocket长连接、在线列表维护与消息推送机制,并对照报告梳理设计思路与排错方法。
1. 从课程设计到能跑通的聊天系统:WebSocket 到底解决了什么
做过 Java 课程设计的人大概都有过这种经历:用 Socket 写了个 C/S 聊天室,结果一到多人群聊就卡死,或者用 HTTP 轮询硬撑实时消息,服务器 CPU 直接拉满。这个标题——Java 基于 WebSocket 的聊天系统设计与实现——本质上就是在解决这个问题:如何用 Java 生态里最成熟的全双工通信方案,搭一个能支撑多人群聊、在线状态同步、消息可靠投递的聊天系统,并且把它写成一份能过答辩的课程设计报告。
它适合两类人:一是正在做网络编程课程设计的学生,需要一套能跑通、能讲清楚原理、代码量可控的方案;二是刚接触 WebSocket 的 Java 开发者,想通过一个完整项目理解长连接、心跳、消息路由这些概念。我见过太多课程设计最后变成“能连上但不敢演示”的半成品,问题往往不在代码量,而在选型和边界没想清楚。下面按实际落地顺序拆开讲。
2. 选型与架构:为什么是 WebSocket 而不是 Socket 或轮询
2.1 三种实时通信方案在课程设计场景下的真实差距
课程设计最怕的是“技术上说得通,演示时翻车”。先把三种常见方案摆出来对比,你就知道为什么 WebSocket 是当前最稳的选择。
| 方案 | 连接方式 | 服务端推送 | 实现复杂度 | 课程设计适配度 |
|---|---|---|---|---|
| 原生 Socket | TCP 长连接 | 支持 | 高,需自定义协议 | 中,适合展示底层 |
| HTTP 轮询 | 短连接 | 伪推送 | 低 | 低,实时性差 |
| WebSocket | TCP 长连接 + HTTP 握手 | 原生支持 | 中,有标准 API | 高,演示效果最好 |
原生 Socket 的问题在于你要自己定义消息边界、处理粘包拆包,课程设计报告里写这些当然加分,但调试成本高。HTTP 轮询的硬伤是延迟和服务器压力,答辩时老师问一句“消息延迟多少”就很难回答。WebSocket 在浏览器和 Java 服务端都有成熟实现,握手阶段复用 HTTP 端口,之后升级为全双工通道,既能讲清楚协议升级过程,又能快速做出可演示的效果。
我一般会建议:如果课程设计明确要求“网络编程底层”,可以在报告里用一章对比 Socket 和 WebSocket 的协议差异,但实现主体用 WebSocket,这样既有深度又有可交付性。
2.2 服务端技术栈:Spring Boot + 原生 WebSocket 还是 STOMP
Java 侧实现 WebSocket 有两条路:一是用javax.websocket或jakarta.websocket原生注解,二是用 Spring 的WebSocketHandler或 STOMP 子协议。课程设计里我推荐 Spring Boot + 原生@ServerEndpoint或TextWebSocketHandler,原因很实际。
STOMP 的好处是自带消息代理语义,适合复杂路由,但引入@MessageMapping、SimpMessagingTemplate后,代码层级变多,答辩时容易被问到“消息代理配置在哪”“为什么用 SockJS 降级”这类你未必准备充分的问题。原生 WebSocket 的 API 更直白:一个ConcurrentHashMap存会话,一个broadcast方法发消息,逻辑一目了然。
// 使用 Spring 的 TextWebSocketHandler 作为核心处理器 @Component public class ChatWebSocketHandler extends TextWebSocketHandler { // 用 ConcurrentHashMap 保证多线程下的会话安全 private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 从握手拦截器放入的属性中取用户 ID String userId = (String) session.getAttributes().get("userId"); SESSIONS.put(userId, session); // 广播上线消息,格式为 JSON broadcast(new ChatMessage("SYSTEM", userId + " 上线了", System.currentTimeMillis())); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); // 解析 JSON,这里用 Jackson 或 Gson 均可 ChatMessage chatMessage = JsonUtil.parse(payload, ChatMessage.class); // 补全发送者和时间戳,防止客户端伪造 chatMessage.setFrom((String) session.getAttributes().get("userId")); chatMessage.setTimestamp(System.currentTimeMillis()); broadcast(chatMessage); } private void broadcast(ChatMessage msg) { String json = JsonUtil.toJson(msg); TextMessage textMessage = new TextMessage(json); SESSIONS.values().forEach(session -> { if (session.isOpen()) { try { session.sendMessage(textMessage); } catch (IOException e) { // 发送失败说明连接已异常,移除会话 SESSIONS.remove(session.getAttributes().get("userId")); } } }); } }这段代码的关键点有三个。第一,ConcurrentHashMap是必须的,因为 WebSocket 的回调方法由容器线程池执行,普通HashMap在并发写入时会丢数据甚至死循环。第二,用户身份不要从消息体里取,要在握手阶段通过HandshakeInterceptor解析 token 或 session 后放入attributes,否则任何人都能伪造发送者。第三,广播时逐个检查session.isOpen(),关闭的会话要及时清理,不然内存泄漏会让服务端在演示中途崩掉。
参数方面,TextWebSocketHandler默认的消息缓冲区大小是 8KB,如果课程设计要传图片或长文本,需要在WebSocketConfig里调整setMaxTextMessageBufferSize和setMaxBinaryMessageBufferSize。我一般设成 64KB 和 1MB,足够课程设计场景使用。
2.3 前端连接与消息格式约定
前端用原生WebSocketAPI 即可,不需要引入额外库。关键是消息格式要前后端约定死,推荐用 JSON,字段固定为type、from、content、timestamp。
// 建立连接,ws 协议,端口与服务端一致 const socket = new WebSocket('ws://localhost:8080/chat?token=' + userToken); socket.onopen = () => { console.log('连接已建立'); // 连接成功后发送加入消息 socket.send(JSON.stringify({ type: 'JOIN', content: '进入聊天室' })); }; socket.onmessage = (event) => { const msg = JSON.parse(event.data); // 根据 type 区分系统消息和用户消息 renderMessage(msg); }; socket.onclose = () => { // 断线后 3 秒重连,避免频繁重试 setTimeout(connect, 3000); };这里有个容易忽略的点:token放在 URL 查询参数里,服务端在HandshakeInterceptor的beforeHandshake方法中解析。不要放在Sec-WebSocket-Protocol头里,浏览器 API 对自定义头支持不好。重连间隔设 3 秒是经验值,太短会在服务端重启时造成连接风暴,太长演示时等待明显。
3. 核心功能实现:会话管理、心跳与消息可靠投递
3.1 会话管理:从登录到断线的完整生命周期
会话管理是聊天系统的骨架。课程设计里至少要覆盖四个状态:连接建立、用户绑定、消息路由、连接关闭。用WebSocketSession的getId()作为底层标识,用业务用户 ID 作为路由键,两者用ConcurrentHashMap做映射。
// 握手拦截器:在升级协议前完成身份校验 @Component public class AuthHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 从 URL 参数中取 token String query = request.getURI().getQuery(); String token = parseToken(query); if (token == null || !TokenUtil.validate(token)) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; // 拒绝握手 } // 校验通过,把用户 ID 放入 attributes attributes.put("userId", TokenUtil.getUserId(token)); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手后无需额外操作 } }beforeHandshake返回false时,握手会被拒绝,前端收到onclose事件。这里要注意:response.setStatusCode在 WebSocket 握手场景下不一定能传到前端,更可靠的做法是返回false后前端根据onclose的code判断。常见做法是约定4001表示认证失败,前端收到后跳转登录页。
会话关闭时要做的清理包括:从SESSIONS移除、广播下线消息、更新在线用户列表。afterConnectionClosed方法里不要做耗时操作,否则会阻塞容器线程。如果课程设计需要持久化聊天记录,建议用异步线程或消息队列,不要在 WebSocket 回调里直接写数据库。
3.2 心跳机制:为什么 30 秒是课程设计的甜点值
WebSocket 连接在真实网络里会被中间设备(路由器、防火墙、NAT)静默断开,表现是前端以为还连着,服务端已经收不到消息。心跳机制就是定期发一个空包或特定消息,维持连接活跃并检测对端是否存活。
// 服务端心跳检测:每 30 秒扫描一次,60 秒无响应则关闭 @Scheduled(fixedRate = 30000) public void heartbeatCheck() { long now = System.currentTimeMillis(); SESSIONS.forEach((userId, session) -> { Long lastActive = (Long) session.getAttributes().get("lastActive"); if (lastActive != null && now - lastActive > 60000) { try { session.close(CloseStatus.SESSION_NOT_RELIABLE); } catch (IOException e) { // 关闭失败也要移除 } SESSIONS.remove(userId); } }); }前端侧对应发送心跳包:
// 每 25 秒发一次心跳,比服务端检测间隔略短 setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'PING', timestamp: Date.now() })); } }, 25000);参数选择上,服务端检测间隔 30 秒、超时阈值 60 秒、前端发送间隔 25 秒,这三个值的关系是:前端发送间隔 < 服务端检测间隔 < 超时阈值。这样即使丢一两个心跳包,也不会误判。课程设计里如果演示环境是局域网,心跳可以放宽到 60 秒,但报告里要写清楚生产环境建议值。我见过有同学把心跳设成 5 秒,结果浏览器控制台全是 PING 日志,答辩时被问“这算不算浪费带宽”就很尴尬。
3.3 消息可靠投递:ACK 确认与离线消息的简化处理
严格意义上的可靠投递需要消息队列和持久化,课程设计里可以做一个简化版:客户端收到消息后回 ACK,服务端维护一个待确认队列,超时未确认则重发。这个逻辑不需要引入额外中间件,用ConcurrentHashMap加定时任务就能实现。
// 待确认消息队列:key 为消息 ID,value 为消息内容和重试次数 private final Map<String, PendingMessage> pendingAcks = new ConcurrentHashMap<>(); public void sendWithAck(String userId, ChatMessage msg) { String msgId = UUID.randomUUID().toString(); msg.setMsgId(msgId); PendingMessage pending = new PendingMessage(msg, 0); pendingAcks.put(msgId, pending); // 发送消息 WebSocketSession session = SESSIONS.get(userId); if (session != null && session.isOpen()) { session.sendMessage(new TextMessage(JsonUtil.toJson(msg))); } // 3 秒后检查是否收到 ACK scheduler.schedule(() -> { PendingMessage p = pendingAcks.get(msgId); if (p != null && p.getRetryCount() < 3) { p.setRetryCount(p.getRetryCount() + 1); // 重发逻辑 sendWithAck(userId, msg); } else { pendingAcks.remove(msgId); } }, 3, TimeUnit.SECONDS); }客户端收到消息后回{ type: 'ACK', msgId: 'xxx' },服务端在handleTextMessage里识别并移除pendingAcks中的记录。这个方案在课程设计报告里可以写成“基于应用层 ACK 的轻量级可靠投递”,比直接说“用了 WebSocket 所以可靠”要专业得多。离线消息则更简单:用户不在线时把消息存入数据库,下次上线时拉取最近 N 条。N 取 50 还是 100 取决于报告篇幅,我一般写 50,并说明“可根据存储容量调整”。
4. 避坑与排查:课程设计演示前必须过的五道关
4.1 连接建立成功但收不到消息
现象:前端onopen触发,readyState为 1,但发送消息后服务端无响应,或服务端广播后前端无反应。
原因通常有三个:一是消息格式不匹配,前端发的是纯文本,服务端按 JSON 解析直接抛异常,异常被吞掉后连接看似正常;二是@ServerEndpoint或WebSocketHandler没有被 Spring 扫描到,实际处理请求的是另一个空实现;三是跨域配置缺失,浏览器控制台有403但被忽略。
解决:在handleTextMessage入口加日志打印原始 payload,确认消息到达;检查WebSocketConfig是否注册了 handler 和拦截器;跨域用setAllowedOrigins("*")临时排查,确认后再收紧。
4.2 多人群聊时消息乱序或丢失
现象:三个人同时发言,A 看到的消息顺序和 B 不一致,或者某条消息只发给了部分人。
原因:广播时遍历SESSIONS.values()的过程中,有会话被移除或新增,ConcurrentHashMap的弱一致性迭代器不保证看到所有元素。另外,多个线程同时调用session.sendMessage时,WebSocketSession本身不是线程安全的。
解决:广播前先复制一份会话列表new ArrayList<>(SESSIONS.values()),再遍历发送;对同一个session的发送操作加同步块,或者用ConcurrentWebSocketSessionDecorator包装。
4.3 服务端运行一段时间后内存溢出
现象:演示进行到十几分钟,服务端抛OutOfMemoryError,日志显示WebSocketSession对象大量堆积。
原因:连接关闭后没有从SESSIONS移除,或者afterConnectionClosed没有被正确触发。常见于前端直接关浏览器标签页,TCP 连接没有正常四次挥手,服务端要等心跳超时才发现。
解决:确保afterConnectionClosed和handleTransportError里都做移除;心跳检测任务必须开启;用jmap或VisualVM观察WebSocketSession实例数,正常应该等于在线用户数。
4.4 前端重连导致重复消息
现象:网络抖动后前端自动重连,用户看到自己之前发的消息又出现一遍。
原因:重连后服务端把该用户当作新连接,离线消息拉取逻辑把已读消息又推了一次;或者 ACK 机制在重连后重发了未确认消息。
解决:客户端维护一个lastReceivedMsgId,重连时带上,服务端只推比这个 ID 新的消息;ACK 队列在连接关闭时清空该用户的所有待确认记录,避免重连后重发。
4.5 答辩时被问“并发量多少”答不上来
现象:老师问“你这个能支持多少人同时在线”,只能回答“没测过”。
原因:课程设计通常不要求压测,但这是加分项。
解决:用 JMeter 或简单的多线程 Java 客户端模拟 100 个连接,每个连接每秒发一条消息,观察服务端 CPU 和内存。把结果写进报告:单机 2 核 4G 环境下,100 并发连接、每秒 100 条消息时,CPU 占用约 30%,内存增长平稳。这个数据不需要多精确,但能体现你有工程意识。
5. 从能跑到能讲:课程设计报告的写法与演示技巧
报告和代码是两回事。代码跑通只是及格线,报告写清楚“为什么这么做”才是拿高分的关键。我一般建议报告按这个结构组织:第一章讲 WebSocket 协议握手过程,画出Upgrade: websocket的请求响应头对比;第二章讲系统架构,用表格列出 HTTP 轮询、Socket、WebSocket 的对比;第三章贴核心代码,但不要全贴,只贴会话管理和心跳这两段;第四章写测试,包括功能测试用例和简单的并发测试数据;第五章写不足与改进,比如“当前 ACK 机制在弱网下重试次数固定,后续可引入指数退避”。
演示环节有个血泪经验:不要用 localhost 演示多用户,用两台设备或两个浏览器窗口,但服务端地址写本机局域网 IP。提前把防火墙关掉或者放行端口,否则演示时连不上,全场尴尬。另外,准备一个“降级方案”:如果 WebSocket 因为环境问题连不上,快速切到轮询版本,虽然实时性差,但至少能展示功能。这个降级方案不用写进报告,但自己心里要有数。
最后说一个具体技巧:在报告里加一张“消息时序图”,用文字描述也行——客户端 A 发送消息 → 服务端接收并补全时间戳 → 服务端遍历会话广播 → 客户端 B 收到并回 ACK → 服务端移除待确认记录。这张图能直观展示你对全链路理解到位,比堆代码有效得多。我每次做类似项目都会先画这张图,画清楚了再写代码,返工次数少很多。希望帮到你。
本文还有配套的精品资源,点击获取