简介这是一套基于JavaWeb技术栈实现的一对一网页聊天系统面向Java初学者与Web开发入门者帮助其掌握JSP、Servlet、AJAX异步通信及MySQL数据库集成等核心技能适用于课程设计、毕业设计或Web实时交互功能实践。资源包共54个文件包含12个Java源码如TalkServlet、TalkFromServlet、8个JSP页面含login.jsp、chat.jsp等完整会话流程视图、12个编译后class文件、7个运行依赖jar包含c3p0连接池配置以及web.xml、sql.txt等关键配置与建表脚本整体压缩包仅2.88MB轻量易部署。已有347人学习下载适合快速上手调试与二次开发。读者可直接导入Tomcat运行获得完整的前后端交互逻辑前端通过JSAJAX每秒轮询获取新消息后端双Servlet分工处理发信与拉取虽UI未美化但功能闭环、结构清晰是理解Web即时通信底层机制的优质教学案例。1. 这不是又一个“在线客服弹窗”JavaWeb 一对一网页聊天系统是能跑在 Tomcat 里、带真实会话隔离、消息不串楼、断线能续传的生产级最小闭环你肯定见过那种“在线客服”按钮点开后弹个 iframe背后其实是第三方 SaaS 嵌入——它不归你管日志看不到用户状态摸不清连换个头像都要提工单。而这篇要拆的是一个完完全全由 Java Servlet JSP WebSocket或轮询降级 MySQL 构成的、可独立部署、无外部依赖的原生 JavaWeb 一对一网页聊天系统。它不卖云服务不推 SDK就一个 WAR 包丢进 Tomcat 8.5 启动注册两个用户A 给 B 发消息B 的浏览器实时收到且 C 完全看不见——这才是“一对一”的物理意义。它适合某高校课程设计收尾阶段需要交源码演示视频的学生也适合某公司内部运维系统想加个轻量级工单沟通入口的工程师。别被“网页聊天”四个字骗了它没用 Vue/React没接 Socket.IO没调阿里云信令服务它的心跳逻辑写在ChatHeartbeatFilter里会话绑定靠HttpSession 内存 Map 双保险离线消息存进chat_message表并带is_read0标记——这些不是概念是代码里真能 grep 到的字段名。接下来我们就从源码结构开始一层层剥开这个看似简单、实则暗藏三处关键状态同步陷阱的系统。2. 从 WAR 包解压开始看清目录结构、核心类职责与通信链路的真实走向拿到资源包第一件事不是跑起来而是先解压看骨架。这个 JavaWeb 聊天系统典型结构如下路径以/为根/WEB-INF/ ├── web.xml ← 部署描述符servlet 映射、filter 配置、session 超时 ├── classes/ │ ├── com/example/chat/ │ │ ├── servlet/ │ │ │ ├── LoginServlet.class ← 处理登录 POST校验账号密码写 session │ │ │ ├── ChatMessageServlet.class ← 接收发消息 POST存 DB推 WebSocket │ │ │ └── GetHistoryServlet.class ← 拉取指定会话历史含分页参数 │ │ ├── filter/ │ │ │ └── ChatHeartbeatFilter.class ← 每 30s 拦截 /heartbeat 请求更新用户在线状态 │ │ ├── model/ │ │ │ ├── User.java ← id, username, password, last_active_time │ │ │ └── ChatMessage.java ← id, sender_id, receiver_id, content, send_time, is_read │ │ └── util/ │ │ └── DatabaseUtil.java ← 单例 DataSource getConnection() ├── lib/ │ ├── mysql-connector-java-8.0.28.jar ← JDBC 驱动注意版本8.0 需 useSSLfalse │ └── gson-2.10.1.jar ← JSON 序列化WebSocket 消息体用 / ← 静态资源根目录 ├── index.jsp ← 登录页含表单 action/login ├── chat.jsp ← 主聊天界面含 JS 初始化 WebSocket ├── js/ │ └── chat.js ← 核心前端逻辑连接 ws://localhost:8080/chatws └── css/ └── style.css提示web.xml是整个系统的“宪法”。重点看三处①servlet-mapping中/login是否映射到LoginServlet②filter-mapping中ChatHeartbeatFilter是否拦截/heartbeat③session-config的session-timeout值——默认 30 分钟但聊天场景建议设为60单位分钟否则用户切屏回来 session 就丢了。2.1 WebSocket 端点不是魔法ChatWebSocketEndpoint如何绑定用户与会话系统采用标准 Java EE WebSocket APIjavax.websocket.*而非 Spring WebSocket。核心端点类ChatWebSocketEndpoint位于com.example.chat.websocket包下部分版本放在servlet同级。它不是靠 URL 路径区分用户而是靠OnOpen时从HttpSession中提取登录态ServerEndpoint(value /chatws, configurator HttpSessionConfigurator.class) public class ChatWebSocketEndpoint { private static final MapString, Session onlineUsers new ConcurrentHashMap(); OnOpen public void onOpen(Session session, EndpointConfig config) { // 从 HttpSession 获取当前用户 IDLoginServlet 已 setAttribute(userId, id) HttpSession httpSession (HttpSession) config.getUserProperties().get(HttpSession.class.getName()); String userId (String) httpSession.getAttribute(userId); if (userId ! null !userId.trim().isEmpty()) { onlineUsers.put(userId, session); // 关键用 userId 作 key不是 session.getId() System.out.println(User userId connected, total: onlineUsers.size()); } } }这段代码决定了“一对一”的底层能力onlineUsersMap 的 key 是业务用户 ID如u1001value 是其 WebSocketSession对象。当 A 给 B 发消息时后端查出 B 的userId再从onlineUsers中取出 B 的Session调用session.getBasicRemote().sendText(jsonStr)—— 这就是消息精准投递的全部秘密。注意HttpSessionConfigurator是自定义类必须存在它负责把 HTTP 会话注入 WebSocket 配置中否则config.getUserProperties().get(HttpSession.class.getName())永远为 null。2.2 消息发送链路从表单提交到浏览器刷新五步不能少用户在chat.jsp输入文字点击发送完整链路如下以 A 发给 B 为例前端触发chat.js监听表单 submit阻止默认行为收集senderIdA,receiverIdB,content你好构造 JSONHTTP 提交AJAX POST 到/sendmessage由ChatMessageServlet处理后端存库ChatMessageServlet解析参数插入chat_message表SQL 类似INSERT INTO chat_message (sender_id, receiver_id, content, send_time, is_read) VALUES (u1001, u1002, 你好, NOW(), 0);实时推送检查onlineUsers中是否存在u1002B 的 userId若存在则通过其 WebSocketSession推送 JSON 消息体含type:msg,from:u1001,content:你好前端渲染chat.js的websocket.onmessage收到后解析 JSON追加到聊天记录 DOM并播放提示音如有。关键参数说明is_read0是离线消息标记send_time必须用数据库NOW()而非 Javanew Date()避免服务器时区不一致导致时间错乱receiver_id必须严格校验存在性查user表否则非法用户可伪造 ID 发送垃圾消息。3. 数据库设计与初始化三张表撑起会话状态字段命名直白但有深意系统依赖 MySQL建库脚本通常名为init_db.sql或嵌在DatabaseUtil.java注释中。核心三张表结构如下已脱敏字段名保留原始风格表名字段类型说明是否为空索引useridVARCHAR(32) PK用户唯一标识非自增数字常用 UUID 或业务编码如u1001NOT NULLPRIMARYusernameVARCHAR(50)登录名NOT NULLUNIQUEpasswordVARCHAR(100)BCrypt 加密后的密码不是明文NOT NULL—last_active_timeDATETIME最后活跃时间ChatHeartbeatFilter每次心跳更新NULL—chat_messageidBIGINT PK AUTO_INCREMENT消息主键NOT NULLPRIMARYsender_idVARCHAR(32)发送方 user.idNOT NULLINDEXreceiver_idVARCHAR(32)接收方 user.idNOT NULLINDEXcontentTEXT消息内容需防 XSS后端入库前应 HTML 转义NOT NULL—send_timeDATETIME发送时间数据库生成NOT NULLINDEXis_readTINYINT(1) DEFAULT 00未读1已读这是实现“已读回执”的唯一依据NOT NULL—chat_sessionidVARCHAR(64) PK会话 ID格式u1001_u1002小ID_大ID保证唯一NOT NULLPRIMARYuser1_idVARCHAR(32)会话成员1NOT NULLINDEXuser2_idVARCHAR(32)会话成员2NOT NULLINDEXlast_message_timeDATETIME该会话最后一条消息时间NULL—注意chat_session表不是所有版本都有。若缺失系统将无法在首页展示“最近联系人”列表——因为每次拉取历史消息都需SELECT * FROM chat_message WHERE sender_id? OR receiver_id? ORDER BY send_time DESC LIMIT 10性能极差。有此表的版本首页只需SELECT * FROM chat_session WHERE user1_id? OR user2_id? ORDER BY last_message_time DESC快一个数量级。3.1 初始化数据两条用户 一条测试消息三行 SQL 足够启动不要试图导入大型测试数据集。最简启动只需-- 插入两个测试用户密码 123456 经 BCrypt 编码后为 $2a$10$... INSERT INTO user (id, username, password, last_active_time) VALUES (u1001, alice, $2a$10$abc123def456..., NOW()), (u1002, bob, $2a$10$xyz789uvw012..., NOW()); -- 插入一条预置消息让首次打开 chat.jsp 时能看到历史 INSERT INTO chat_message (sender_id, receiver_id, content, send_time, is_read) VALUES (u1001, u1002, 欢迎使用 JavaWeb 聊天系统, NOW(), 1);提示BCrypt 密码生成不能手写。DatabaseUtil.java通常自带工具方法BCrypt.hashpw(123456, BCrypt.gensalt())运行一次即可获取哈希值。严禁在 SQL 脚本里写明文密码这是安全红线。3.2 查询历史消息分页 SQL 必须带ORDER BY send_time DESC否则时间线错乱GetHistoryServlet的核心逻辑是执行分页查询。常见错误写法// ❌ 错误没加 ORDER BY数据库返回顺序不确定翻页时消息重复或丢失 String sql SELECT * FROM chat_message WHERE (sender_id? AND receiver_id?) OR (sender_id? AND receiver_id?) LIMIT ?,?; // ✅ 正确强制按时间倒序确保第1页是最新消息 String sql SELECT * FROM chat_message WHERE (sender_id? AND receiver_id?) OR (sender_id? AND receiver_id?) ORDER BY send_time DESC LIMIT ?,?;参数顺序ps.setString(1, userId); ps.setString(2, friendId); ps.setString(3, friendId); ps.setString(4, userId);—— 因为条件是(A→B) OR (B→A)必须覆盖双向。4. 避坑五个血泪经验总结每一条都来自真实翻车现场这个系统看着简单但部署和调试时极易在以下环节翻车。以下是某开发者在某实验室部署时踩过的坑已验证复现并定位根因4.1 现象登录成功跳转chat.jsp但页面显示“未登录”F12 查看 Network/chatws连接失败原因ChatWebSocketEndpoint的ServerEndpoint路径/chatws与web.xml中servlet-mapping的 URL 模式冲突或 Tomcat 版本低于 7.0.47WebSocket 支持不完整。解决① 确认 Tomcat 版本 ≥ 8.5② 检查web.xml中是否误将ChatWebSocketEndpoint当作普通 Servlet 配置它不需要servlet块③ 浏览器访问http://localhost:8080/yourapp/chatws应返回 404WebSocket 不走 HTTP若返回 404 之外的状态码说明配置有误。4.2 现象A 给 B 发消息B 页面无反应但数据库chat_message表已新增记录原因onlineUsersMap 中 B 的userId对应的Session已失效浏览器关闭、网络中断但后端未及时清理onlineUsers.get(u1002)返回 null推送逻辑静默跳过。解决在OnClose方法中添加清理逻辑OnClose public void onClose(Session session, CloseReason reason) { // 遍历 onlineUsers找到 value session 的 entryremove onlineUsers.entrySet().removeIf(entry - entry.getValue().equals(session)); }4.3 现象消息中文乱码数据库存的是????浏览器显示方块原因MySQL 连接 URL 缺少characterEncodingutf8mb4参数且数据库、表、字段未统一设为utf8mb4_unicode_ci。解决① 修改DatabaseUtil.java中的 JDBC URLjdbc:mysql://localhost:3306/chatdb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4② 执行 SQLALTER DATABASE chatdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;③ 对chat_message.content字段执行ALTER TABLE chat_message CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4.4 现象两个用户同时在线A 发消息给 BB 收到后A 自己的聊天窗口也刷出一条“自己发给自己的消息”原因前端chat.js在onmessage回调中未判断消息from字段是否等于当前用户 ID直接渲染所有收到的消息。解决在chat.js的消息接收处理逻辑中加入过滤websocket.onmessage function(event) { var data JSON.parse(event.data); if (data.from ! currentUserId) { // currentUserId 从页面 hidden input 或 JS 变量获取 appendMessageToChat(data.from, data.content, data.send_time); } };4.5 现象重启 Tomcat 后所有用户显示“离线”last_active_time未更新原因ChatHeartbeatFilter的心跳请求/heartbeat未被正确拦截或web.xml中filter-mapping的url-pattern写成了/heartbeat/*多了/*导致实际请求/heartbeat不匹配。解决检查web.xml中 filter-mappingfilter-mapping filter-nameChatHeartbeatFilter/filter-name url-pattern/heartbeat/url-pattern !-- ✅ 必须是精确匹配 -- /filter-mapping并在ChatHeartbeatFilter.doFilter()开头加日志System.out.println(Heartbeat received for userId);确认日志是否打印。5. 进阶技巧用“双写 时间戳”实现离线消息可靠投递绕过 WebSocket 不可用的黑匣子WebSocket 是理想状态但真实网络中移动端切后台、浏览器休眠、弱网环境都会导致连接中断。系统原生只做内存级在线状态一旦断连消息就石沉大海。要真正落地必须补上离线消息兜底。这不是加个定时任务那么简单而是要解决三个关键问题谁来判定离线消息存哪怎么唤醒我的方案是放弃“实时判定”改用“延迟确认” “双写保障”。5.1 离线判定不依赖心跳而用“最后活跃时间窗口”ChatHeartbeatFilter每 30 秒更新user.last_active_time但这只是“心跳时间”不是“真实活跃”。更鲁棒的做法是在ChatMessageServlet处理每条发信时同步更新发送方的last_active_time在GetHistoryServlet被拉取历史时同步更新接收方的last_active_time。这样last_active_time始终反映用户最近一次“主动交互”时间。离线判定逻辑改为// 判定用户是否离线用于决定是否走 WebSocket 推送 public boolean isUserOffline(String userId) { User user DatabaseUtil.findUserById(userId); if (user null) return true; // 超过 2 分钟无任何交互视为离线比心跳周期长留容错 long offlineThreshold 2 * 60 * 1000; // 2 minutes return System.currentTimeMillis() - user.getLastActiveTime().getTime() offlineThreshold; }5.2 消息存储必须“双写”内存 Map 数据库且顺序不可逆当 A 发消息给 B先写数据库chat_message保证持久化再查isUserOffline(B)若为false则尝试 WebSocket 推送无论推送成功与否都必须在数据库中标记is_read0若 WebSocket 推送失败onlineUsers.get(u1002) null立即触发“离线消息队列”——这里不用 Kafka就用一个简单的ConcurrentLinkedQueueOfflineMessage内存队列存senderId,receiverId,messageId启动一个守护线程每 5 秒扫描队列对每个OfflineMessage再次检查isUserOffline(receiverId)若变为false则重试推送并从队列移除。关键点数据库写入必须在 WebSocket 推送之前。否则若先推再存库推送成功但存库失败消息就丢了。这是分布式事务的朴素实践。5.3 唤醒机制前端轮询 后端轻量通知拒绝长连接绑架既然 WebSocket 不可靠就用 HTTP 轮询保底。在chat.js中添加一个checkOfflineMessages()函数function checkOfflineMessages() { fetch(/checkoffline?userId currentUserId) .then(r r.json()) .then(data { if (data.unreadCount 0) { // 触发一次全量拉取或增量拉取 lastId loadNewMessages(); playNotificationSound(); } }); } // 每 30 秒执行一次 setInterval(checkOfflineMessages, 30000);对应的CheckOfflineServlet很简单protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String userId req.getParameter(userId); int unreadCount DatabaseUtil.countUnreadMessagesForUser(userId); resp.setContentType(application/json); resp.getWriter().write({\unreadCount\: unreadCount }); }这个方案没有用 Server-Sent EventsSSE因为 Tomcat 8.5 对 SSE 支持不完善也没有用 Ajax 长轮询long polling因为会耗尽 Tomcat 线程。30 秒短轮询对现代浏览器和服务器压力极小且逻辑清晰可控。从那以后我每次部署 JavaWeb 聊天系统都强制走一遍这三步① 先用telnet localhost 3306确认数据库通再SELECT NOW()看时区② 启动后立刻开两个隐身窗口分别登录 A 和 B发三条消息关掉 B 的标签页再开确认离线消息能拉到③ 最后用 Chrome DevTools 的 Network 标签过滤ws和xhr盯着每条请求的状态码和响应时间。这三步走完心里才真正踏实。希望帮到你。本文还有配套的精品资源点击获取