简介这是一套面向JavaWeb初学者与课程设计学习者的网页一对一聊天系统源码基于JSP、JavaScript、Ajax、Servlet、Tomcat与MySQL实现重点演示前后端异步通信的完整链路。聊天页面由JSP负责消息显示框、输入框及参数获取JS与Ajax承担向Servlet发送请求的核心职责既处理自身消息提交也每隔一秒轮询获取新消息后端TalkServlet处理发送请求TalkFromServlet处理消息更新数据库采用MySQL并配有c3p0连接配置。资源包共54个文件包含12个java源文件与12个class、8个jsp页面、7个jar依赖及若干xml、prefs等工程配置压缩包约2.88MB可直接导入Eclipse运行调试。已有348人学习下载适合用来理解Ajax轮询机制、Servlet请求分发与JSP页面交互也可作为课程设计或毕业设计的功能原型参考需注意UI尚未设计仅完成功能实现。1. 一对一网页聊天系统从“能跑”到“敢上线”的落地拆解很多人第一次接触 javaweb 一对一网页聊天都是被“网页上能实时收发消息”这个效果吸引的。但真把源码包拉下来跑起来才会发现能跑和敢上线之间隔着一条沟消息发出去对方收不到、刷新页面会话就断、两个用户同时在线就串号。这套资源解决的不是“聊天”这个泛概念而是把一对一场景下的会话绑定、消息投递、连接保活这几件事用 javaweb 技术栈串成一条可复现的链路。适合正在做课程设计、想补一个完整前后端交互案例的开发者也适合已经会写 CRUD、但没碰过实时通信的熟手拿来对照自己的实现边界。2. 拆开源码包一对一聊天的技术骨架与选型理由2.1 为什么一对一场景不适合直接套用群聊广播模型拿到一个网页聊天系统第一件事不是急着跑而是看它的消息投递模型。群聊的典型做法是“一个人发广播给房间里所有人”逻辑简单一个集合遍历就完事。但一对一聊天有个本质差异消息必须精确落到唯一的目标会话上发错人比发不出去更严重。这套资源在服务端维护的是“用户标识 → 会话通道”的映射而不是“房间 → 成员列表”。这个选择直接决定了后面所有代码的写法。常见做法是用一个 ConcurrentHashMap 存在线用户key 是用户唯一标识value 是对应的会话对象。发送消息时先按目标标识查通道查到才投递查不到就走离线落库。这个模型的好处是天然支持点对点不需要房间概念也不需要处理“广播范围”这种容易出错的逻辑。代价是服务端要自己管连接生命周期用户下线必须及时从 map 里摘掉否则会往一个死通道发消息表现为“对方明明退了这边还显示发送成功”。我一般会先确认三件事在线表用什么数据结构、下线时有没有清理钩子、消息有没有落库。这三点决定了这套代码是玩具还是能改造成可用的底子。如果在线表是普通 HashMap多线程下必然出问题如果没有下线清理跑一会儿就会内存里堆一堆僵尸会话如果消息不落库刷新页面历史全丢一对一场景基本没法用。2.2 前端连接层轮询、长连接与 WebSocket 的取舍网页聊天绕不开“服务端怎么把消息推给浏览器”这个问题。这套 javaweb 实现里前端连接层是理解整个系统的钥匙。三种常见方案摆在这短轮询、长轮询、WebSocket。短轮询是前端定时发请求问“有没有新消息”实现最简单但延迟高、请求量大一对一场景下体验很差发一条消息要等一个轮询周期才显示。长轮询是请求挂起直到有消息或超时才返回延迟降下来了但服务端要维持大量挂起请求对容器线程池压力不小。WebSocket 是双向的连接建立后服务端可以主动推延迟最低也是一对一聊天最合理的选择。这套资源如果用的是 WebSocket前端会有一个连接建立、消息收发、断线重连的完整流程。看代码时重点盯三处连接建立后有没有把用户标识注册到服务端、onmessage 里怎么区分“自己发的”和“对方发的”、onclose 里有没有重连逻辑。这三处任何一处缺失实际用起来都会翻车。// 前端连接与消息处理的核心骨架 const ws new WebSocket(ws:// location.host /chat/ currentUserId); ws.onopen function () { // 连接建立后先发一条上线通知让服务端登记会话 ws.send(JSON.stringify({ type: online, userId: currentUserId })); }; ws.onmessage function (event) { const msg JSON.parse(event.data); // 按消息类型分流普通消息、系统通知、历史回填 if (msg.type chat) { appendMessage(msg.from, msg.content, msg.from currentUserId); } else if (msg.type system) { showSystemTip(msg.content); } }; ws.onclose function () { // 断线后延迟重连避免服务端重启时前端直接躺平 setTimeout(connect, 3000); };这段代码的逻辑说明连接地址里带上当前用户标识是为了让服务端在握手阶段就能把连接和用户绑定省掉额外的注册请求。onopen 里再发一次上线通知是双保险防止某些容器在握手阶段拿不到路径参数。onmessage 按 type 分流是因为一对一系统里除了聊天内容还会有系统提示和历史消息混在一起处理后期很难维护。onclose 里的重连是血泪经验服务端一重启前端就白屏用户以为系统坏了加个延迟重连能挡掉大部分投诉。参数上重连间隔别设太短3 秒左右比较稳太短会在服务端没起来时疯狂重试。2.3 服务端会话管理在线表、消息路由与落库服务端是一对一聊天的中枢。这套 javaweb 实现的服务端通常分三层连接层负责握手和会话注册路由层负责按目标标识找通道并投递持久层负责消息落库和历史查询。三层里最容易写错的是路由层因为它要处理“目标在线”和“目标离线”两条分支。在线时直接投递离线时落库并标记未读等对方上线再拉取。这个分支逻辑看着简单但边界很多目标用户刚下线、会话还没从在线表摘除消息投进去石沉大海目标用户重复登录在线表里旧会话被覆盖旧连接还挂着消息落库成功但投递失败两边状态不一致。这些都是一对一场景特有的坑群聊里反而不会遇到。// 服务端消息路由的核心逻辑 public void routeMessage(ChatMessage msg) { String targetId msg.getToUserId(); Session targetSession onlineUsers.get(targetId); if (targetSession ! null targetSession.isOpen()) { // 目标在线且通道有效直接投递 targetSession.getBasicRemote().sendText(toJson(msg)); messageDao.save(msg, 1); // 1 表示已送达 } else { // 目标离线或通道已失效落库待拉取 messageDao.save(msg, 0); // 0 表示未送达 // 顺手清理失效会话避免在线表里留僵尸 onlineUsers.remove(targetId); } }逻辑说明先查在线表再判断通道是否真的打开这一步不能省。很多实现只判断 map 里有没有 key结果通道已经断了还在往里发抛异常不说消息也丢了。投递成功和落库的顺序也有讲究先投递再落库落库失败最多是历史缺一条先落库再投递投递失败会导致对方以为收到了但实际没收到。参数上送达状态用 0/1 标记查询历史时按这个字段区分“已读未读”的雏形后面要做未读数统计直接基于它扩展。3. 把系统跑起来环境、配置与双端联调步骤3.1 环境准备与依赖版本对齐这套 javaweb 项目跑起来的第一道坎往往不是代码是环境。常见组合是 JDK 8 或 11、Tomcat 8.5 以上、MySQL 5.7 或 8.0前端是原生页面加少量脚本。版本对不齐的典型表现是编译过了但启动报类找不到、连数据库报时区错误、WebSocket 握手 404。我一般按“先定 JDK再定容器最后定数据库”的顺序排因为容器版本对 JDK 有要求数据库驱动版本又跟 JDK 和数据库版本都相关。组件推荐版本关键注意点JDK8 或 11与容器版本匹配别混用高版本编译低版本跑Web 容器Tomcat 8.5WebSocket 支持需要 7.0.47 以上8.5 更稳数据库MySQL 5.7 / 8.08.0 注意驱动类名和时区参数构建工具Maven 3.6依赖冲突先用 dependency:tree 看依赖对齐之后先别急着改业务代码把项目原样跑通一次确认登录、发消息、收消息这条主链路是通的。这一步的意义是建立一个“已知可用”的基线后面改出问题能快速判断是自己改坏的还是环境问题。3.2 数据库初始化与连接配置数据库这块源码包里一般会带建表脚本重点看三张表用户表、会话表、消息表。消息表的设计直接决定历史查询好不好写常见字段包括消息标识、发送方、接收方、内容、发送时间、送达状态。会话表如果存在通常用来存“谁和谁之间有过对话”方便做会话列表。初始化时按脚本顺序执行别跳步外键依赖顺序错了会报错。-- 消息表的核心结构字段名按实际脚本调整 CREATE TABLE chat_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, from_user_id VARCHAR(64) NOT NULL, to_user_id VARCHAR(64) NOT NULL, content TEXT NOT NULL, send_time DATETIME DEFAULT CURRENT_TIMESTAMP, deliver_status TINYINT DEFAULT 0, INDEX idx_to_user (to_user_id, deliver_status) );这段建表语句的关键在最后那个联合索引。一对一聊天最高频的查询是“拉取某个用户的所有未送达消息”索引建在 to_user_id 和 deliver_status 上查询能直接走索引不用全表扫。content 用 TEXT 是为了容纳长消息如果确定只发短文本VARCHAR 更省空间。send_time 给默认值省得应用层每次手动塞时间少一个出错点。连接配置在配置文件里重点盯四个参数驱动类名、连接地址、用户名密码、连接池大小。MySQL 8.0 的驱动类名和 5.x 不一样地址里要带时区和是否用 SSL 的参数否则启动就报错。连接池大小别照抄示例里的几十一对一场景并发没那么高十个左右够用设太大反而占资源。3.3 双端联调两个浏览器窗口怎么验证消息真的通了联调是验证这套系统是否真的可用的关键一步。最直接的办法是开两个浏览器窗口或者一个正常窗口加一个隐身窗口分别登录两个不同账号然后互发消息。验证点按顺序来先确认两个账号都能登录成功再确认 A 发消息 B 能实时收到然后确认 B 回复 A 也能收到最后刷新页面看历史消息还在不在。这一步最容易翻车的地方是“同一个浏览器登录两个账号”。很多系统的会话标识存在 Cookie 里同一浏览器两个标签页共享 Cookie后登录的会把先登录的顶掉表现为一个窗口发消息另一个窗口收不到。用隐身窗口或者不同浏览器能绕开这个问题但这也暴露了系统在会话隔离上的设计边界如果要做多端同时在线得在服务端按连接维度区分而不是只靠 Cookie。联调时还要专门测一次“对方离线”的场景B 退出登录A 发一条消息然后 B 重新登录看这条消息能不能补收到。这个场景测的是离线消息落库和上线拉取的链路很多源码包在这里是断的消息落了库但上线时没有拉取逻辑用户以为消息丢了。测出来不通就顺着“落库 → 上线查询 → 前端渲染”三段排查定位很快。4. 避坑与排查一对一聊天最容易翻车的五个地方4.1 消息发出去对方收不到但发送方显示成功现象是 A 点发送界面显示已发送B 那边毫无反应。原因通常有两个一是服务端路由时按目标标识查在线表没查到走了离线落库分支但前端没区分“已投递”和“已落库”统一显示成功二是目标标识对不上比如前端传的是用户名服务端在线表存的是用户 ID查不到就静默落库。解决方法是把发送结果分状态返回前端按状态显示“已送达”或“对方离线”同时在服务端路由处加日志打印目标标识和在线表 key 的对比一眼就能看出是不是标识不一致。4.2 刷新页面后历史消息全丢现象是聊天界面一刷新就空白之前的对话不见了。原因是前端只做了实时消息渲染没有在页面加载时拉取历史消息或者服务端有历史接口但前端没调。解决方法是页面初始化时先请求历史消息接口按时间正序渲染再建立连接接收新消息。这里有个顺序坑如果先建连接再拉历史可能出现历史和新消息交错、顺序错乱常见做法是先拉历史渲染完再建连接新消息追加到末尾。4.3 两个用户同时在线时消息串号现象是 A 发的消息跑到了 C 的窗口里或者 B 收到了本该发给别人的消息。原因基本锁定在服务端在线表的设计上如果用用户名做 key 但存在重名或者用会话对象做 key 但路由时取错了目标都会串。更隐蔽的一种是前端渲染时没按会话区分所有消息往同一个列表里塞。解决方法是服务端在线表 key 必须全局唯一用户 ID 最稳路由时严格按消息里的目标标识查前端渲染按会话标识分列表别图省事共用一个容器。4.4 服务端重启后前端一直转圈现象是服务端一重启前端页面就卡住发消息没反应也不报错。原因是 WebSocket 连接断了但前端没有重连逻辑或者有重连但重连后没重新注册会话。解决方法是 onclose 里加重连重连成功后重新发上线通知服务端收到后更新在线表里的会话引用。注意重连要有退避别固定间隔猛冲服务端刚起来扛不住一波集中重连。另外重连次数要设上限无限重连在服务端长时间不可用时只是徒增日志。4.5 消息内容里的特殊字符导致解析失败现象是发普通文字正常一发带引号、换行或者表情的内容就报错或者显示乱码。原因是消息用 JSON 传输但拼接时没做转义或者前端解析时直接字符串截取。解决方法是统一走 JSON 序列化别手拼字符串前端用 JSON.parse 解析服务端用成熟的 JSON 库序列化。数据库字段的字符集也要确认utf8mb4 才能存表情utf8 存四字节字符会截断。这个坑不显眼但用户一发表情就翻车排查时容易往网络层想其实是编码问题。5. 从能跑到好用消息可靠性与未读数的一个实用改法把这套系统跑通之后真正决定它好不好用的是消息可靠性。我一般会在原基础上加一个“未读数”机制因为它同时验证了落库、查询和前端渲染三条链路是检验系统完整度的试金石。做法不复杂消息表里已经有送达状态字段把“未送达”当作未读的近似用户上线时查一次未读总数渲染到会话入口上点进对话后把对应消息状态更新为已送达。// 上线时拉取未读数并回填历史 public int loadUnreadAndHistory(String userId, ListChatMessage history) { // 先查未读总数用于角标展示 int unread messageDao.countByToUserAndStatus(userId, 0); // 再查最近若干条历史用于对话渲染 history.addAll(messageDao.findRecentByToUser(userId, 50)); // 拉取后统一标记为已送达避免重复计数 messageDao.markDelivered(userId); return unread; }这段逻辑的关键在最后一步拉取后立刻标记已送达否则用户刷新一次未读数就翻倍。参数上历史条数别一次拉太多50 条够首屏用多了拖慢加载要做分页就再加一个按时间游标的查询。标记已送达用批量更新别一条条 update消息多了性能差距很明显。验证这个改法是否生效按三步走A 给离线的 B 发三条消息B 登录后角标显示 3B 点进对话角标清零B 刷新页面角标仍然是 0 而不是又变回 3。第三步最能暴露问题很多实现在拉取时没做标记刷新就重复计数。这套验证流程我每次改消息相关逻辑都会走一遍因为它把“落库、查询、状态更新、前端渲染”四段全串起来了任何一段有毛病都会在这一步现形。从那以后我每次拿到这类聊天系统源码都强制先跑一遍离线消息加未读数的完整链路再谈改界面加功能。底子不稳界面做得再花也是空中楼阁。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?