尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Java WebSocket的即时通讯系统:从数据库到部署完整指南

基于Java WebSocket的即时通讯系统:从数据库到部署完整指南 简介面向Java Web方向的毕业设计学习者这是一份基于Java技术实现的网站即时通讯系统完整项目包。项目围绕实时在线通信平台展开覆盖用户注册登录、消息收发、好友管理等核心模块可帮助理解前后端交互、实时通信原理与数据库设计适合毕业设计参考或想系统掌握WebSocket、Spring Boot、MySQL等技术的读者。资源包大小约103.33MB内含项目报告、答辩PPT、源代码、数据库SQL文件、系统运行截图及部署操作视频。项目报告详尽呈现了背景、需求分析、系统设计与测试结果答辩PPT提炼了技术亮点并适合汇报使用源代码覆盖服务端与客户端核心逻辑SQL文件展示了用户表、聊天记录等表结构部署视频则可用于快速搭建运行环境即便缺少完整开发经验也能按步骤跑通项目。目前已有792人学习下载资料关联性强从开发到演示形成完整闭环对完成课程设计与实际项目落地均有参考价值。1. 为什么这个标题值得你按 WebSocket 路径重做一遍每年毕业季都会冒出一批「基于 Java 的网站即时通讯系统」资源包命名格式和这个标题几乎一样项目报告、答辩 PPT、源代码、数据库、截图、部署视频一样不少。但真正拿到手的人很快会发现多数压缩包里的代码停留在 JSP Servlet 轮询的阶段浏览器每隔几秒发一次 Ajax 请求去查数据库新消息。这种实现能跑通演示却经不起面试官追问两个问题消息实时性怎么保证在线状态是真实推送还是定时刷新如果你正在做这个题目或者想把这个压缩包当成课设、毕设的底子建议先换一条更可靠的技术路径用 WebSocket 做消息推送服务端用 JavaServlet 原生 API 或 Spring 均可数据落 MySQL离线消息靠数据库兜底。这套方案代码量不大但覆盖了即时通讯系统的核心链路连接管理、消息协议、心跳保活、在线状态、离线补偿。下文会给出可直接复现的建表语句、WebSocket 端点和关键参数并解释为什么有些「源码里的常见写法」不值得抄。配合本文你可以独立完成从数据库设计到部署演示的全过程也能在项目报告里写清楚「为什么不用轮询而用长连接」这一类必问题。答辩 PPT 和部署视频属于锦上添花前提是代码逻辑你能讲明白。2. 技术选型与系统架构先定 WebSocket再定 Java 容器2.1 为什么这一题首选 Java WebSocket 而不是轮询或 SSE即时通讯系统毕业设计最常见的错误实现是轮询前端setInterval每 2 秒调一次查询接口后端查message表返回新数据。它的优点只有实现简单缺点有两个——第一消息延迟受人眼感知不到但接口压力看得见第二面试官一定会问「如果 1000 人在线每 2 秒一次请求服务器 QPS 是多少」。算完这个账轮询方案基本站不住。对比三种服务端推送方案方案连接方式实时性服务端压力实现成本轮询HTTP 短连接差取决于间隔高大量无效请求最低SSEHTTP 长连接单向推送中适合服务端到客户端单向通知低WebSocketTCP 长连接双向实时低连接复用中WebSocket 是全双工长连接客户端和服务端都能随时发数据而 HTTP 语义里服务端不能主动「推」给浏览器SSE 是单向的变通。Java 侧实现 WebSocket 并不复杂Servlet 3.1 容器Tomcat 8.5 及以上自带javax.websocket实现Spring Boot 可用spring-boot-starter-websocket写起来更像注解开发不依赖第三方消息中间件单机即可支撑毕业设计演示。2.2 架构分层这个压缩包里最该有的四个模块把整个系统拆成四层对应源码目录和项目报告里的逻辑结构答辩时按这个讲最顺接入层WebSocket 端点Endpoint负责握手、连接建立、断开业务层消息处理逻辑包括消息类型判断、会话校验、入库落库数据层MySQL 数据库 JDBC 或 MyBatis存用户、关系、消息前端页面JSP 或静态 HTML JavaScript WebSocket 客户端。im-system ├── src/main/java/com/example/im │ ├── ws/ // WebSocket 端点 │ ├── servlet/ // 登录、好友管理等 HTTP 接口 │ ├── service/ // 业务逻辑 │ └── dao/ // JDBC/MyBatis 数据访问 ├── src/main/webapp │ ├── WEB-INF/web.xml │ ├── login.jsp │ ├── chat.jsp │ └── static/js/ws.js └── sql/im.sql提示如果压缩包源码里找不到ws相关目录说明它还是老式轮询实现。重构成 WebSocket 只需要保留用户和消息模块不用推翻全部业务代码。2.3 会话管理的一个关键取舍用 Session 还是 Token浏览器登录后HTTP 接口和 WebSocket 连接都需要识别用户身份。常见做法有两种HttpSession Cookie适合纯 JSP 项目后端用request.getSession().getAttribute(userId)取登录态。WebSocket 握手时HttpSession可以通过HttpSessionHandshakeInterceptor拿到。Token 方案登录接口返回一个随机字符串UUID 即可客户端存localStorage每次请求包括 WebSocket 握手带在 header 或 query 里。对毕设来说 Token 加密用不用 JWT 无所谓UUID 足够重点是能让服务端维护一份token - userId的映射。我一般会选 Session因为压缩包配套的项目报告里大部分是这么写的答辩时能和原有代码上下文对得上。但你若用 Spring Boot 重构Token 更顺手HandshakeInterceptor里也能直接读 header。3. 数据库设计先行消息表、好友关系表和未读消息存储3.1 五张核心表从用户到消息回执即时通讯系统数据库设计不需要太复杂毕业设计阶段五张表足够user、friend、group、group_member、message。下面给出建表语句CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 建议存 MD5 或加盐哈希, nickname VARCHAR(32) DEFAULT , avatar VARCHAR(255) DEFAULT , status TINYINT DEFAULT 0 COMMENT 0离线 1在线, last_login_time DATETIME ); CREATE TABLE friend ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, friend_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_friend (user_id, friend_id) ); CREATE TABLE group ( id INT PRIMARY KEY AUTO_INCREMENT, group_name VARCHAR(64) NOT NULL, owner_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE group_member ( id INT PRIMARY KEY AUTO_INCREMENT, group_id INT NOT NULL, user_id INT NOT NULL, UNIQUE KEY uk_group_user (group_id, user_id) ); CREATE TABLE message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, from_id INT NOT NULL, to_type TINYINT NOT NULL COMMENT 1单人 2群聊, to_id INT NOT NULL, content TEXT NOT NULL, msg_type TINYINT DEFAULT 1 COMMENT 1文本 2图片 3文件, status TINYINT DEFAULT 0 COMMENT 0未读 1已读, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_conversation (to_type, to_id, status) );关键设计点有三个message表用to_type to_id区分「发给单人」还是「发给群」。群聊消息投递到每个成员时都插一条记录还是只存一条、群成员各维护一个已读位置对毕设而言插多条最简单直观代码不用维护复杂的已读游标但要注意群聊大消息量时表膨胀演示数据量小不是问题。status字段做「已读/未读」。这是未读消息红点的基础。常见误区是表里不加状态前端靠本地记录已读消息 ID刷新页面就丢了服务端也没有可靠数据支撑「有多少未读」查询。friend表加唯一键uk_user_friend防止重复加好友。删除好友时删对应行即可不做双向级联代码里判断两次userId-friendId和friendId-userId。3.2 获取在线好友列表的 SQL 怎么写WebSocket 建立后前端要拉取好友列表。接口逻辑是「查所有好友 查每个好友当前在线状态」。用一条 SQL 关联两张表SELECT f.friend_id, u.nickname, u.avatar, u.status FROM friend f JOIN user u ON f.friend_id u.id WHERE f.user_id ? ORDER BY u.status DESC, u.nickname ASC;逻辑说明WHERE f.user_id ?是把当前登录用户替换进预编译占位符避免拼接字符串引发注入ORDER BY u.status DESC让在线的排在前面演示效果直观。但u.status字段有一个坑用户直接关掉浏览器时服务端不一定能立即感知连接断开TCP FIN 没发出来status可能一直是 1造成「显示在线但消息没人回」。应对方法是引入心跳超时见 3.3 节。3.3 在线状态不能只靠数据库字段要配合心跳超时数据库里的status是快照真正的在线状态应该由 WebSocket 连接状态推导。我做的时候维护了一个ConcurrentHashMapInteger, Session onlineUserskey 是用户 IDvalue 是 WebSocket Session连接建立onOpenonlineUsers.put(userId, session)更新user.status 1连接关闭onCloseonlineUsers.remove(userId)更新user.status 0心跳每 30 秒一次超过 90 秒没收到心跳主动关闭 Session触发onClose。提示ConcurrentHashMap在这里比HashMap更安全多个 WebSocket 线程会并发读写它HashMap在 put 时可能产生链表环导致 CPU 100%。4. 用 WebSocket 实现一对一聊天连接、消息协议与心跳4.1 定义消息协议type 字段决定逻辑分支WebSocket 消息格式推荐 JSON至少包含四个字段{ type: chat, toType: 1, toId: 2, content: 你好, fromId: 1, fromName: 张三, timestamp: 2025-01-15 10:00:00 }type取值建议做成常量type 值含义数据流向chat聊天消息客户端 - 服务端 - 客户端read已读回执客户端 - 服务端 - 客户端heartbeat心跳包客户端 - 服务端服务端原样返回online好友上线通知服务端 - 客户端history历史消息请求客户端 - 服务端 - 客户端注意区分「消息类型」和「消息内容」这里的type是信令类型msgType字段才表示内容是文本还是图片。4.2 服务端 WebSocket 端点完整实现下面是一个不依赖 Spring、直接用javax.websocket的端点实现Tomcat 8.5 可直接部署ServerEndpoint(value /im/{userId}) public class IMWebSocket { private static final ConcurrentHashMapInteger, Session ONLINE_USERS new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(userId) Integer userId) throws IOException { // 同一用户重复登录时关掉旧连接新连接覆盖 Session old ONLINE_USERS.put(userId, session); if (old ! null old.isOpen()) { old.close(); } session.setMaxIdleTimeout(120000); // 2分钟无读写自动断开 updateUserStatus(userId, true); // 广播好友上线通知 pushOnlineNotice(userId); } OnMessage public void onMessage(String message, Session session) { JsonObject json JsonParser.parseString(message).getAsJsonObject(); String type json.get(type).getAsString(); switch (type) { case chat: handleChat(json); break; case heartbeat: session.getAsyncRemote().sendText({\type\:\heartbeat\}); break; case read: handleRead(json); break; } } OnClose public void onClose(Session session, PathParam(userId) Integer userId) { Session cur ONLINE_USERS.get(userId); if (cur ! null cur session) { // 只清当前连接 ONLINE_USERS.remove(userId); updateUserStatus(userId, false); } } private void handleChat(JsonObject json) { Integer fromId json.get(fromId).getAsInt(); Integer toId json.get(toId).getAsInt(); String content json.get(content).getAsString(); // 入库 saveMessage(fromId, toId, 1, content); // 在线则实时推送 Session target ONLINE_USERS.get(toId); if (target ! null target.isOpen()) { target.getAsyncRemote().sendText(json.toString()); } } }逻辑说明ServerEndpoint(/im/{userId})把用户 ID 直接拼进 URL前端new WebSocket(ws://ip:8080/im/ userId)就能建立映射关系不需要额外传握手参数ONLINE_USERS.put返回旧连接用于处理「同一账号多端登录」——演示时经常开着两个标签页测试不关旧连接会出现消息只发到上一个 Session页面上没反应非常迷惑。参数说明setMaxIdleTimeout(120000)是容器级空闲超时单位为毫秒设置 120 秒超过没有收发就断开。这个值要和心跳间隔配合心跳 30 秒一次120 秒超时留了余量不会误杀。getAsyncRemote().sendText()是异步发送不阻塞 WebSocket 处理线程。如果用getBasicRemote().sendText()同步发送目标客户端慢的话会阻塞后续消息处理并发一高就出问题。OnClose里用cur session做引用比较判断当前关闭的 Session 是否还是 map 里存的那个避免旧连接关闭时误删新连接。4.3 前端 ws.js连接、心跳和重连前端核心逻辑就三部分连接、心跳、重连。以下代码可直接用于chat.jsplet ws null; let heartBeatTimer null; let reconnectTimer null; const userId document.getElementById(currentUserId).value; function connectWS() { ws new WebSocket(ws:// location.host /im/ userId); ws.onopen function () { console.log(WebSocket connected); // 连接建立后启动心跳 heartBeatTimer setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: heartbeat })); } }, 30000); }; ws.onmessage function (event) { const msg JSON.parse(event.data); switch (msg.type) { case chat: appendMessage(msg); // 追加到聊天面板 sendReadReceipt(msg.fromId); // 自动回已读 break; case online: updateFriendStatus(msg.fromId, true); break; case heartbeat: // 心跳响应 无需处理 break; } }; ws.onclose function () { clearInterval(heartBeatTimer); // 3 秒后重连重试用例在演示时最容易碰见 reconnectTimer setTimeout(connectWS, 3000); }; ws.onerror function (err) { console.error(WebSocket error:, err); }; } window.onload connectWS;逻辑说明心跳setInterval每 30 秒发一次服务端收到后返回同类型消息作用是让连接链路有持续流量防止 NAT 设备和容器超时把空闲连接踢掉。onclose里 3 秒重连是为了解决「后端 Tomcat 重启后前端所有连接断开」的场景——不重连的话刷新页面才能恢复演示时很尴尬。参数说明心跳间隔 30 秒和服务端setMaxIdleTimeout120 秒匹配。如果你把服务器超时改小了比如改成 60 秒心跳间隔必须小于 60 秒否则连接会被误断开。这里 120 vs 30 有 4 倍余量稳妥。4.4 常见崩溃点Tomcat 默认缓冲区太小导致中文消息截断Tomcat 的 WebSocket 消息默认 buffer 是 8192 字节如果单条消息超过这个值比如发了一段长文本onMessage会被多次调用每次只拿到一部分。最简单的规避方式服务端把默认 buffer 调大。在ServerEndpoint注解上加maxTextMessageBufferSizeServerEndpoint(value /im/{userId}, maxTextMessageBufferSize 1024 * 1024) public class IMWebSocket { ... }同理前端发送大消息前先做长度校验超过 1MB 提示改用文件发送。这部分在项目报告的「系统限制」里写一句面试官会觉得你考虑过边界。5. 群聊、未读消息与历史记录三个必须讲清楚的周边模块5.1 群聊实现遍历群成员批量投递群聊和单聊的差异只在toType2时的投递逻辑。收到群聊消息后private void handleGroupChat(JsonObject json) { Integer fromId json.get(fromId).getAsInt(); Integer groupId json.get(toId).getAsInt(); String content json.get(content).getAsString(); ListInteger memberIds getGroupMemberIds(groupId); for (Integer memberId : memberIds) { // 每人存一条消息status0 未读 saveMessage(fromId, 2, groupId, content, memberId); Session target ONLINE_USERS.get(memberId); if (target ! null target.isOpen()) { target.getAsyncRemote().sendText(json.toString()); } } }saveMessage相比单聊多了一个收件人字段消息表需要加一个receiver_id字段。这里设计上有个注意点消息表里to_type2时to_id是群 ID但每个成员各有一条记录查询「我收到的群消息」用WHERE to_type2 AND receiver_id ?过滤。5.2 未读消息数一条 SQL 搞定但别 N1 查询未读消息红点在好友列表上显示核心是「统计我跟每个好友之间的未读消息数」。正确做法是GROUP BY聚合SELECT from_id, COUNT(*) AS unread_count FROM message WHERE to_type 1 AND to_id ? AND status 0 GROUP BY from_id;逻辑说明to_id ?是当前用户 IDstatus 0过滤未读最后按from_id分组算出每个好友发来的未读条数。常见误写是循环查每个好友的未读数好友有 50 个就发 50 条 SQL。必须在项目报告里写明「一次聚合查询替代 N 次单查」的优化答辩有这个意识很加分。同理群聊未读数SELECT group_id, COUNT(*) AS unread_count FROM message WHERE to_type 2 AND receiver_id ? AND status 0 GROUP BY group_id;5.3 历史消息加载分页 时间倒序刷新页面后聊天记录为空是常见翻车现场。需要单独写一个历史消息接口进入会话时拉最近的消息SELECT * FROM ( SELECT id, from_id, to_type, to_id, content, create_time FROM message WHERE to_type 1 AND to_id ? AND from_id ? OR to_type 1 AND from_id ? AND to_id ? ORDER BY id DESC LIMIT 20 ) t ORDER BY t.id ASC;参数说明LIMIT 20取最近 20 条内层倒序取出外层反转成正序展示这样聊天面板的时间顺序是自然的。?位的顺序依次是我个人 ID、对方 ID、对方 ID、我个人 ID——因为一条会话里我可能是发送方也可能是接收方。分页的下拉加载用id 最小id实现避免 OFFSET 在大表上变慢。6. 部署演示与答辩素材从 war 包到 PPT 大纲的一次性清单6.1 用 Maven 打包并在 Tomcat 上部署开发环境用 IDEA 启动 Tomcat 很容易但答辩现场通常要换一台电脑演示。最稳妥的做法是打 war 包部署到独立 Tomcat# 项目根目录执行保证先跑过测试 mvn clean package -DskipTests # 将 war 包复制到 Tomcat webapps 目录 cp target/im-system.war /opt/tomcat/webapps/ # 启动 Tomcat /opt/tomcat/bin/startup.sh # 查看日志确认启动成功 tail -f /opt/tomcat/logs/catalina.out部署成功后访问http://localhost:8080/im-system/。注意三个易错点MySQL 连接串如果写的是localhost在非本机部署时要改成 IP项目使用 Java 8 编译的话Tomcat 10 会因javax.websocket换成jakarta.websocket而无法识别务必用 Tomcat 9 或 8.5防火墙要放行8080端口浏览器用的是http://而不是ws://入口。6.2 答辩 PPT 大纲按技术决策讲不按代码行数讲压缩包里附带的答辩 PPT 未必适合你重构后的代码。如果代码换成了 WebSocket 实现PPT 至少要改三页架构图、核心功能、技术难点。推荐大纲是课题背景与需求分析为什么要即时通讯目标用户技术选型对比轮询 vs WebSocket选型理由系统架构图浏览器、WebSocket 端点、业务层、MySQL 四层数据库设计5 张表 未读消息查询 SQL核心功能演示登录 - 加好友 - 单聊 - 群聊 - 未读消息关键技术难点心跳保活、重复登录踢下线、离线消息补偿测试与部署用 Navicat 演示建库、Tomcat 部署过程。6.3 演示前必做的 5 项验证临场演示翻车大多不是代码 bug而是环境问题。照这个清单过一遍检查项操作方法预期结果MySQL 服务命令行mysql -uroot -p连接建表成功message 表能查询Tomcat 启动浏览器访问首页登录页正常渲染双用户登录Chrome 普通窗口 无痕窗口各登一个账号互相能看到上线状态变化心跳不断链打开 F12 Network 面板观察 WS 帧每隔 30 秒有心跳发出离线消息用户 B 退出用户 A 发消息B 重新登录B 收到未读红点点击后显示消息最后提醒一个细节答辩现场往往只有一份 PDF 或一张截图来展示「数据库设计」建议在 Navicat 里提前把 ER 图导出成图片放进 PPT现场不要临时连库操作省去等数据库驱动加载的尴尬时间。同样部署视频可以在虚拟机里录制好把「从 war 包到聊天成功」的三分钟过程存成 mp4现场网络不稳时直接播视频比干等界面响应体面得多。本文还有配套的精品资源点击获取
返回列表