ARTICLE DETAIL

资讯详情

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

Java Web聊天系统实战:WebSocket与Spring Boot落地指南

Java Web聊天系统实战:WebSocket与Spring Boot落地指南 简介面向Java Web课程设计的大作业聊天系统完整项目适合需要完成类似选题的在校学生或入门开发者参考。内含项目文档说明并按照config、controller、dao、dto、entity、service、utils、vo等包进行模块划分后端使用JPA操作数据库service层遵循接口实现规则processor包中集成了过滤器、拦截器与监听器目录结构清晰便于理解分层开发与前后端交互设计。压缩包共138个文件主体为66个Java源码文件同时包含12个Vue组件、11个SCSS样式以及JS、HTML、配置文件等前端工程化配置齐全整体仅2.08MB轻量易用。该资源已有1152人学习浏览附有项目总文档与前端入口页面能够帮助读者快速搭建运行环境并完成聊天系统核心功能。1. Java Web大作业做聊天系统先搞懂它到底在考什么每年课程设计和毕业设计里Java Web大作业选聊天系统的人特别多。你以为老师在考WebSocket不是。聊天只是壳真正考的是你能否把「注册登录、好友管理、消息存储、实时推送、前端渲染、部署演示」整条链路走通。一个能跑的聊天系统意味着你同时碰过HTTP请求-响应和WebSocket长连接两套模型这两者边界能用清楚你才有底气在简历里写“熟悉Java Web开发”。这篇笔记适合正在赶大作业的在读学生也适合准备把课程项目写进简历的求职者。我不打算给你整理一份完整源码而是把最落地的技术方案、关键代码和踩坑现场讲透照着搭三到五个晚上能跑出可演示的版本。2. 技术选型别急着写代码先把这套组合定下来2.1 为什么我不建议用纯JSPServlet写聊天如果你在网上搜“Java Web聊天系统”会看到大量用JSPServlet实现的老项目它们大多用Ajax轮询模拟实时聊天前端每隔一两秒发一次HTTP请求问服务器“有没有新消息”。这种方案能跑但有一个根本性硬伤HTTP是无状态请求-响应模型每次请求都要重新建连、携带Cookie、走完Servlet生命周期服务器根本没法“主动”告诉对方你发了消息。轮询有多延迟、数据库压力多大、消息乱序时前端怎么渲染这些问题在答辩时都会变成老师追问的靶子。聊天系统的核心场景是“消息主动推送”这恰恰是WebSocket的强项通过一次HTTP握手101 Switching Protocols把连接升级为全双工长连接之后服务器可以随时往客户端写数据。Servlet 3.1的javax.websocket也能做但大作业里你还要同时管登录、好友关系、消息记录和页面渲染纯Servlet会让代码散落成片改一个功能牵扯三四个文件越写越没耐心。现在课程设计基本默认用Spring Boot你打开IDEA 2024版本创建web项目时选Spring Initializr已经成了常规操作没必要逆着趋势走。2.2 推荐技术栈与版本选型Spring Boot 2.7.x MyBatis-Plus MySQL技术层选型理由后端框架Spring Boot 2.7.x内置Tomcatjavax.websocket包网上教程最多踩坑资料最好搜ORMMyBatis-Plus单表CRUD不用写SQL腾出时间做聊天核心逻辑数据库MySQL 5.7或8.0老师机器上大概率都有字符集和事务支持稳定前端原生HTMLJSWebSocket API不引Vue全家桶答辩时少背一层框架概念构建Maven默认选项不要换成Gradle避免环境不一致为什么特意强调Spring Boot 2.7.x因为Spring Boot 3.x把javax.websocket整体迁移到jakarta.websocket网上能搜到的大作业资料绝大部分基于javax。你如果上3.x就得自己处理命名空间差异和内置Tomcat版本匹配对赶作业来说不划算。MyBatis-Plus不是必须但能少写大量样板代码。网上有“mybatisplus根据java实体类生成创建表的sql语句”这类用法我建议建表SQL还是手写聊天系统就三张表手写可控性最好代码生成器反而会给你塞一堆看不懂的冗余字段。Spring Boot 2.7.x的内置Tomcat同时支持HTTP和WebSocket共用8080端口前端不需要额外配置网关路径。3. 从建表到WebSocket推送搭一个最小可跑的聊天系统3.1 数据库设计用户表、好友表、消息表三张就够CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, nickname VARCHAR(50) DEFAULT , avatar VARCHAR(255) DEFAULT , PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE friend ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, friend_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_friend (user_id, friend_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE message ( id INT NOT NULL AUTO_INCREMENT, from_user_id INT NOT NULL, to_user_id INT NOT NULL, content TEXT, is_read TINYINT(1) DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_users_created (from_user_id, to_user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;user表存账号friend表存好友关系message表存聊天记录。is_read这个字段用来做未读数批量标记已读在答辩时是个很好的展示点。索引设计上历史消息最常用的查询是“我和某人之间的记录”所以建了复合索引(from_user_id, to_user_id, created_at)数据量到十万级也能走索引。注意用utf8mb4而不是utf8否则Emoji表情和生僻字在MySQL里直接变成问号这个问题出现频率非常高属于交作业前一晚才能发现的类型。3.2 后端WebSocket端点在线用户表、私聊消息转发与离线落库ServerEndpoint(/chat/{userId}) Component public class ChatEndpoint { private static final ConcurrentHashMapInteger, Session ONLINE_USERS new ConcurrentHashMap(); OnOpen public void onOpen(PathParam(userId) Integer userId, Session session) { // 新连接先踢掉旧连接避免刷新页面时同一用户存在两个session ONLINE_USERS.remove(userId); ONLINE_USERS.put(userId, session); System.out.println(用户[ userId ]上线当前在线 ONLINE_USERS.size()); } OnMessage public void onMessage(String message, Session session, PathParam(userId) Integer senderId) { JSONObject json JSONObject.parseObject(message); // 心跳消息不落库直接回pong if (ping.equals(json.getString(type))) { session.getBasicRemote().sendText({\type\:\pong\}); return; } int receiverId json.getIntValue(receiverId); String content json.getString(content); // 先落库再推送保证历史消息可查 messageService.insert(senderId, receiverId, content); Session targetSession ONLINE_USERS.get(receiverId); if (targetSession ! null) { try { targetSession.getBasicRemote().sendText( {\fromUserId\: senderId ,\content\:\ content \}); } catch (IOException e) { // 推送失败说明对方连接已死移除在线表 ONLINE_USERS.remove(receiverId); } } else { // 对方离线消息已经落库is_read保持0登录后拉取未读 System.out.println(用户[ receiverId ]离线消息已保存); } } OnClose public void onClose(PathParam(userId) Integer userId) { ONLINE_USERS.remove(userId); System.out.println(用户[ userId ]下线); } }ONLINE_USERS必须用ConcurrentHashMap不能用普通HashMap。WebSocket的onOpen、onMessage、onClose由Tomcat线程池中不同线程触发HashMap在多线程并发put、remove时可能造成CPU 100%甚至死循环这个坑在Java面试题里也经常出现。ServerEndpoint(/chat/{userId})的路径参数由前端连接时传入用来建立“用户ID到连接”的映射这是聊天系统最常见的设计。消息先落库再推送的顺序不能反过来否则推送时网络闪断消息在数据库里查不到对方永远收不到。3.3 前端连接与消息渲染原生JS实现的最小聊天页面const ws new WebSocket(ws:// location.host /chat/ currentUserId); ws.onopen function() { console.log(WebSocket连接已建立); }; ws.onmessage function(event) { const data JSON.parse(event.data); // data: { fromUserId: 1, content: 你好 } appendMessage(data.fromUserId, data.content); }; ws.onclose function() { console.log(连接已断开); // 在这里做重连后面章节专门讲 }; function sendMessage(receiverId, content) { if (ws.readyState ! WebSocket.OPEN) { alert(连接未就绪请稍后重试); return; } ws.send(JSON.stringify({ senderId: currentUserId, receiverId: receiverId, content: content })); }连接地址用ws://而不是http://端口自动沿用当前页面端口因为Spring Boot内置Tomcat的HTTP和WebSocket共用8080不需要单独配置。currentUserId从登录接口返回后存到全局变量不要在URL里拼接密码等敏感参数。WebSocket握手时不能像HTTP那样自定义Header所以鉴权信息要么放在URL查询参数里要么放在子协议里大作业场景下URL里带userId已经足够。4. WebSocket连接管理心跳、重连与三个必调参数4.1 聊天为什么放着不动就静默掉线很多人大作业本地跑通了但页面放着几分钟不动再发消息对方收不到自己这边也没任何报错。这是WebSocket最典型的“静默掉线”浏览器和服务器之间的连接经过NAT设备、运营商路由器、公司或学校防火墙空闲连接会被中间设备回收。问题是回收时TCP层未必发RST包WebSocket的onclose事件根本不会触发两端都以为连接活着实际上数据已经发不出去。这不是代码逻辑错是网络链路现实你必须用心跳机制兜底。4.2 心跳与重连前后端配合的完整实现// 前端30秒发一次ping服务器收到后回pong let heartbeatTimer setInterval(function() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping })); } }, 30000); ws.onclose function() { clearInterval(heartbeatTimer); // 关键先清定时器避免重连后叠加多个心跳 setTimeout(connectWebSocket, 3000); // 3秒后重连 };// 后端收到ping更新lastHeartbeat60秒内没收到心跳就主动关闭 OnMessage public void onMessage(String message, Session session, PathParam(userId) Integer userId) { JSONObject json JSONObject.parseObject(message); if (ping.equals(json.getString(type))) { session.getBasicRemote().sendText({\type\:\pong\}); return; } // 正常业务消息处理 }心跳间隔要小于中间设备的空闲回收时间30秒是一个经过大量实践的安全值太短会浪费资源太长则起不到保活作用。你还需要一个后台定时任务扫描ONLINE_USERS里超过60秒没收到心跳的session主动调用session.close()并从在线表移除这能解决用户直接关浏览器导致“永远在线”的假象。大作业不要求做分布式集群单机单应用这个方案足够。4.3 三个必调参数超时时间、心跳间隔、历史消息分页大小参数推荐值说明WebSocket会话超时60000~120000毫秒服务端主动回收死连接的兜底前端心跳间隔30秒必须小于中间设备空闲回收时间历史消息分页大小20条/页一次拉太多会拖慢首屏渲染关于会话超时Spring Boot内置Tomcat里可以通过WebSocketContainer配置。大作业没必要改得太深但你要知道默认的超时行为是什么。前端在onclose里重连时必须先clearInterval旧的心跳定时器否则每重连一次就多一个心跳线程在跑网络收养了无数僵尸定时器内存和带宽一起遭殃。重连间隔用3秒合适太短会造成服务端连接风暴太长影响体验。4.4 HttpSession和WebSocket Session两个Session别混为一谈登录状态存在HttpSession里但用户在线状态应该看WebSocket的Session是否在ONLINE_USERS里。HttpSession只要Cookie没过期就存在用户关掉浏览器再开HttpSession可能还在但WebSocket连接已经断了。如果你把在线状态存在HttpSession里就会出现“联系人列表显示在线发消息却没人回”的假象。正确做法是登录时写HttpSession建立WebSocket时以onOpen为准维护在线表前端断线后自动重连不要刷新整个页面去重新发HTTP请求。5. 避坑聊天系统大作业里最常翻车的五个现场5.1 启动失败端口被占用和JAVA_HOME指错版本现象Spring Boot启动直接报“Port 8080 was already in use”或者Tomcat启动到一半抛出ClassNotFoundException。原因上一次程序没完全退出或者本机装了两个JDK导致java环境配置指错版本。解决Windows下用netstat -ano | findstr 8080查到占用端口的PID到任务管理器结束进程再用java -version确认当前JDK是8或11Spring Boot 2.7.x配JDK 8到17都能跑。IDEA里点红色方块停止后内置Tomcat偶尔还会在后台挂着这是端口占用最高频的来源。5.2 中文乱码过滤器顺序比过滤器本身更关键现象聊天消息里的中文在数据库里存成问号页面上显示的也是乱码。原因MySQL连接串没指定编码或请求体读取时用了错误的字符集。解决JDBC连接串加上useUnicodetruecharacterEncodingutf-8建表用utf8mb4。Spring Boot的CharacterEncodingFilter要确保在业务过滤器之前执行如果请求体已经被其他过滤器读取过一遍再设置编码就晚了。这个顺序问题非常隐蔽你只调整过滤器位置就正常会让人以为是玄学实际是Servlet请求体只能读取一次导致的。5.3 刷新页面就掉线重连后又收到重复消息现象前端一刷新WebSocket断开又重连对方发来的消息偶尔出现两条。原因onclose触发重连时旧连接还没完全关闭新连接已经建立在线表里同一userId对应两个session消息被两台“分身”各推一次。解决在onOpen里put之前先ONLINE_USERS.remove(userId)再重新put让新连接顶掉旧连接。前端也要等ws.readyState变成CLOSED后再初始化新连接避免并发建连的竞态。5.4 在线状态不准用户关掉浏览器还显示在线现象用户直接关掉标签页没走onclose服务端在线表一直保留着他的session。原因浏览器进程被系统杀掉时TCP连接可能没有正常发FIN包服务端感知不到断开。解决靠心跳兜底后端定时任务扫描ONLINE_USERS把超过60秒没收到心跳的session主动close并移除。注意在session对象上取不到“最后心跳时间”你需要自己维护一个MapInteger, Long记录每个userId的lastHeartbeat。5.5 换台电脑就连不上地址写死localhost的坑现象自己电脑上访问localhost一切正常发给同学同学通过你的IP加端口访问页面打不开。原因前端WebSocket连接地址写死了ws://localhost:8080别人电脑上的localhost指向他自己。解决前端统一用location.host拼WebSocket地址这样不管用户从哪个IP访问WebSocket都连到同一台主机。演示时如果必须用笔记本现场跑让手机或同学电脑和你的笔记本连同一个WiFi访问笔记本的局域网IP加端口这是最稳妥的局域网演示方式。6. 答辩前最后做两件事自测路径和简历能用的加分点6.1 五条自测路径亲手跑一遍再交交作业之前别只看功能能点通按下五条路径完整走一遍第一两个不同账号互相登录A发消息B实时收到B回复A也实时收到第二A给离线状态的B发消息B登录后能看到未读消息且is_read标记从0变1第三连续发50条消息页面不卡顿滚动定位不跳帧第四关掉浏览器重新打开重新连接WebSocket后能拉到最近20条历史消息第五用手机浏览器通过局域网IP访问同一页面能正常登录并收发消息。这五条全过大作业的功能分基本拿到手。6.2 加分项历史消息分页和未读数可以讲成你的亮点答辩时老师大概率会问“消息量变大了怎么办”。你不用背高深理论只要把历史消息分页和未读数设计讲清楚就比照着Demo轮询的版本高一个台阶。历史消息用“滚动加载”代替一次拉全部前端滚动到聊天区域顶部时触发下一页请求后端用lastId和size两个参数向下翻页SQL只查小于lastId的记录并按时间倒序取固定条数。未读数则在登录成功时执行UPDATE message SET is_read1 WHERE to_user_id当前用户 AND is_read0顺手用SELECT COUNT(*)统计未读数就能在好友列表上画小红点。这个项目打磨完完全能当“java面试题”的项目经历讲。你只要把“HTTP轮询和WebSocket的选型区别”“消息为什么先落库再推送”“在线表为什么用ConcurrentHashMap”三句话讲清楚面试官就不会把它当普通课程设计看待。我现在的习惯是每做完一个功能顺手把当时的报错和解决方式记在项目根目录的NOTE.md里答辩前翻一翻很多被问住的问题其实都踩过只是当时没留痕。你的聊天系统不一定要做得多大但每个技术选型都得接得住一句“为什么”把这层做透了这次大作业换来的就不只是一个分数希望帮到你。本文还有配套的精品资源点击获取
返回列表