资讯详情

Java仿QQ聊天源码拆解:Swing+Socket+JDBC+MySQL实战

📅 2026/10/18 13:30:20 | 华诺云谱 👁 阅读
Java仿QQ聊天源码拆解:Swing+Socket+JDBC+MySQL实战
简介这是一份基于Java语言的仿QQ聊天软件完整源码工程使用图形界面Swing、网络套接字编程、数据库访问JDBC及Druid连接池技术自带服务端、客户端与数据库设计并采用MVC分层架构用IntelliJ IDEA开发。面向Java进阶学习者可作为期末大作业、课程设计或项目实践参考也适合希望系统练习界面开发、网络通信、多线程与数据持久化的开发者。资源包共490个文件大小约7.17MB主要包含53个Java源文件、90个编译后的类文件、281张界面图片另有依赖库、配置描述、数据库建表脚本等辅助内容目录划分清晰便于按模块阅读和二次开发。目前已有994人学习或下载内容覆盖登录注册、好友管理、消息收发、多客户端在线聊天等关键模块。压缩包内还提供可直接安装的安卓客户端、工程配置文件及完整数据库脚本可帮助读者从客户端界面、服务端消息转发到数据存储完整理解开发链路也可通过多客户端同时运行直观模拟多个用户在线聊天的完整过程并用于排查网络连接异常、数据库访问异常等实际问题。整体源码结构完整启动入口清晰适合边运行边调试能快速积累实战经验。1. 拆这份 Java 仿 QQ 聊天源码先把 Swing、Socket 和 JDBC 这条链路捋清楚拆包第一反应是闹心resources.ap_、AaClient.apk、proguard.cfg 和一堆 .class 混在一起乍看像个 Android 工程残留。但翻完项目说明就明白了——这份 Java 仿 QQ 聊天源码核心是三条技术线Swing 管界面Socket 管通信JDBC 配 Druid 连接池访问 MySQL 存用户与好友关系。服务端和客户端是分开的两个入口支持多客户端同时在线、互相聊天不是只有界面没有逻辑的教学 Demo。它解决的问题很具体一个 Java 进阶课程设计该有的东西都有——服务端、客户端、数据库、好友关系、在线状态外加 MVC 分层。适合三类人期末要交大作业、想省掉搭骨架时间的学生想补 Swing 加 Socket 实战的初级开发想拿现成代码改成局域网小工具的技术爱好者。接下来按架构 → 数据库 → 通信 → 联调的顺序拆包括我试跑时踩过的坑。2. 整体架构先立住MVC 分层与 Socket 通信模型的职责边界2.1 ServerMain 与 ClientMain两端各自管什么聊天程序最容易写成一锅粥就是因为两端的职责没先划清。服务端管三件事监听端口接连接、按用户名维护在线线程表、转发消息并读写数据库客户端管另外三件登录界面与用户输入、好友列表展示、消息收发和 UI 刷新。两端中间只过一个东西——消息对象。谁都不直接碰对方的内部状态这就是通信能稳定的前提。// ServerMain.java 核心骨架 public class ServerMain { private static final int PORT 8888; // 保存所有在线客户端userName - ServerThread private static final MapString, ServerThread ONLINE new ConcurrentHashMap(); public static void main(String[] args) throws Exception { // 1. 初始化 Druid 连接池交给 DAO 层使用 UserTableDao dao new UserTableDao(initDataSource()); // 2. 绑定端口启动监听循环 ServerSocket server new ServerSocket(PORT); System.out.println(聊天服务器已启动端口 PORT); while (true) { Socket socket server.accept(); // 每个客户端独立线程避免一个慢客户端阻塞所有人 new Thread(new ServerThread(socket, dao)).start(); } } }逻辑说明PORT 定在 8888服务端启动时只做两件初始化——连接池和 ServerSocket。ONLINE 为什么用 ConcurrentHashMap 而不是普通 HashMap因为多个 ServerThread 会同时读写在线表普通 Map 在高并发下可能丢记录甚至死循环这是 Socket 服务端的老坑之一。accept() 拿到 Socket 后立刻 new Thread是一客户端一线程模型的标准写法能把多客户端在线跑起来但单机撑几百连接就到头了别拿它跟 Netty 比。再看客户端入口它极简得让有些人怀疑是不是少发了个文件// ClientMain.java 核心骨架 public class ClientMain { public static void main(String[] args) { // 先启动登录窗口真正的连接动作在按钮事件里触发 LoginFrame login new LoginFrame(); login.setVisible(true); } }参数说明客户端 main 里只弹窗口是有意的。Swing 的组件创建和操作必须在事件分发线程EDT里做main 里 new JFrame 只是把窗口创建工作交给了 Swing 的事件队列如果在这里抢着连 Socket、读数据库窗口没出来线程就先堵住了。我见过不少人把连接逻辑堆在 main 里结果登录窗口卡死十几秒才弹出来——这份源码的入口拆分方式是教科书级的别改。2.2 MVC 在 Swing 项目里的落地方式这门课的大作业要求 MVC但很多同学的MVC只是把文件分成三个包包之间照样互相 new。真正的关键是搞清楚每个类属于哪一层、能依赖谁不能依赖谁。这份源码里分层是跟着职责走的层次落地位置职责ModelUserTableDao、User、Message数据库读写、用户状态、消息协议ViewLoginFrame、ChatFrame、好友列表只管显示和收集输入不写业务逻辑Controller按钮 ActionListener、消息接收线程把界面事件翻译成 Message驱动 Model 更新为什么聊天程序尤其需要 MVC因为逻辑分散在三个完全不同的驱动源里按钮点击是用户事件消息到达是网络事件登录校验是数据库查询。不隔离的话按钮监听器里直接写 SQL、接收线程里直接操作 JTextArea短期能跑改一次需求全崩。这份代码的 Controller 层虽然薄但边界很清楚——界面代码里看不到 JDBC 调用DAO 里也看不到任何 Swing 组件。src ├─ server │ ├─ ServerMain.java // 服务端入口监听、初始化连接池 │ └─ ServerThread.java // 单客户端处理线程登录/转发/下线 ├─ client │ ├─ ClientMain.java // 客户端入口启动登录窗口 │ ├─ view // LoginFrame、ChatFrame 等界面类 │ └─ net // 连接管理、消息接收线程 ├─ common │ ├─ Message.java // 两端共用的消息协议对象 │ └─ User.java // 用户实体 └─ dao └─ UserTableDao.java // 所有数据库访问集中在这里这个包结构把 MVC 直接映射到了目录上答辩时老师问分层在哪指着目录说一遍就够。另外一个容易被问到的点是为什么 Message 要单独提到 common 包因为服务端和客户端都只认 Message 类型服务端转发消息时不需要知道客户端界面长什么样界面代码也不关心消息在网络上怎么走——这叫依赖倒过来。3. 数据库与连接池qq_for_java 建库、建表与 DAO 封装3.1 建库建表脚本与字段设计项目说明里写死了两个要求数据库名叫 qq_for_java剩下的表结构要自己建。我这边拆包后按常见方案补了一套读者可以直接抄。设计上只保留能支撑聊天的最小集一张用户表、一张好友关系表离线消息表属于进阶改造放到最后一章。-- 建库utf8mb4 避免中文乱码也兼容昵称里的特殊符号 CREATE DATABASE IF NOT EXISTS qq_for_java DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE qq_for_java; -- 用户表username 唯一登录时按名字直接定位 CREATE TABLE IF NOT EXISTS user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(32), status TINYINT DEFAULT 0, -- 0 离线 1 在线 2 离开 avatar VARCHAR(128) ); -- 好友关系表user_id 是主friend_id 指向 user.id CREATE TABLE IF NOT EXISTS friend ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, friend_id INT NOT NULL, group_name VARCHAR(32) DEFAULT 我的好友, UNIQUE KEY uk_user_friend (user_id, friend_id) );参数说明username 加 UNIQUE 约束是登录校验的物理保证靠程序判重永远有并发缝隙friend 表的联合唯一键 uk_user_friend 防止同一对好友被重复插入——课程设计里这是加好友接口的底线。status 用 TINYINT 而不是 VARCHAR原因是好友列表要按在线状态排序和过滤整数比较比字符串快而且不容易拼错。逻辑说明密码列宽度给到 64 是有意的。源码里大概率是明文比对课程设计能跑就行但表结构预留了换哈希的空间将来想改成 MD5 或 BCrypt 不用 ALTER TABLE。avatar 字段存的是头像资源路径不是二进制图片——把图片塞进数据库会让连接池和内存都很难受正确做法是图片放本地或资源目录库里只存路径字符串。提示建表前先确认 MySQL 版本。5.7 用 utf8 没问题8.0 默认字符集就是 utf8mb4如果驱动版本太旧连接串里不写 serverTimezone你会发现下一节第一个报错就是它。3.2 Druid 连接池参数与 UserTableDao 封装Druid 在这里干的事只有一个管理数据库连接别让每次登录都现建现关。现建连接的成本很高TCP 握手加认证加会话初始化一套下来几十毫秒连接池预先把连接建好放池子里用的时候借、用完还这才是服务端该有的姿势。// 连接池初始化ServerMain 启动时执行一次全局共享 public static DruidDataSource initDataSource() { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://127.0.0.1:3306/qq_for_java ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); ds.setUsername(root); ds.setPassword(换成你自己的密码); ds.setDriverClassName(com.mysql.cj.jdbc.Driver); ds.setInitialSize(5); // 启动时预建 5 个连接 ds.setMinIdle(2); // 最小空闲连接数 ds.setMaxActive(20); // 最大活跃连接数超过则排队等待 ds.setMaxWait(60000); // 取连接最多等 60 秒超时抛异常 return ds; }参数说明serverTimezoneAsia/Shanghai 是 MySQL 8 驱动的新规矩不加直接抛The server time zone value的异常这是新手第一个翻车点useSSLfalse 是本地开发为了省去证书配置公网部署绝不能照抄。initialSize5 意味着服务端刚启动就去 MySQL 建了 5 条连接第一个用户登录时不用等连接创建体感上会快很多。MaxActive 20 对课程设计绰绰有余一个在线用户一般只占用一条连接连查询都很少。连接池有了访问数据库的动作全收进 UserTableDao。这个类只干 CRUD不干别的界面层和通信层都不允许直接碰 JDBCpublic class UserTableDao { private final DruidDataSource dataSource; public UserTableDao(DruidDataSource dataSource) { this.dataSource dataSource; } // 登录校验查不到记录返回 null调用方据此判断登录失败 public User login(String username, String password) { String sql SELECT id, username, nickname, avatar FROM user WHERE username ? AND password ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setNickname(rs.getString(nickname)); u.setAvatar(rs.getString(avatar)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }逻辑说明这里必须用 PreparedStatement 而不是字符串拼接 SQL答辩时老师盯的就是这个。字符串拼接能登录但用户名里塞一个 OR 11就直接绕过密码校验了这不是理论风险是真实存在几十年的注入手法。try-with-resources 保证 Connection 和 PreparedStatement 不管成功失败都会自动关闭连接用完归还连接池——忘记关连接是这类项目最隐蔽的内存泄漏跑一天服务端就卡成 PPT。参数说明DAO 构造时只收 DruidDataSource不自己 new 连接池。这样设计的好处是 ServerMain 只初始化一次所有 ServerThread 线程共享同一批连接要是每个线程各建一套连接池在线用户一多MySQL 的 max_connections 直接被打爆报错信息是Too many connections第一次见会一头雾水。4. 登录与在线聊天链路Socket 消息协议与多客户端并发处理4.1 服务端线程模型一客户端一线程 消息转发两端通信靠的是一个自定义协议对象。课程设计级别不需要引入 JSON 或 ProtobufJava 自带的序列化足够缺点是你抓包看到的是一堆二进制流不是可读文本——调试时只能靠两端打日志。这个取舍对作业来说划算。// 两端共用的消息协议实现 Serializable 才能走 ObjectOutputStream public class Message implements Serializable { private String type; // LOGIN / LOGIN_SUCCESS / LOGIN_FAIL / CHAT / FRIEND_LIST / LOGOUT private String from; // 发送方用户名 private String to; // 接收方用户名 private String content; // 消息正文或登录时携带密码 private long time; // 客户端本地时间戳 // 省略 getter/setter }参数说明type 字段是路由核心服务端拿到消息先看 type 再决定走哪条分支。一个类承载所有消息类型的缺点是字段复用导致语义偶尔含糊——比如 FRIEND_LIST 的 content 里塞的是好友列表的序列化文本但这在课程设计里完全够用。加新功能只需要加一个 type 常量不用动通信链路。服务端的核心处理线程是这个模板几乎所有 Java Socket 服务端都长这样public class ServerThread implements Runnable { private final Socket socket; private final UserTableDao dao; private ObjectInputStream in; private ObjectOutputStream out; private String currentUser; // 登录成功后记录用户名 Override public void run() { try { // 顺序要固定先输出流后输入流客户端必须保持一致 out new ObjectOutputStream(socket.getOutputStream()); in new ObjectInputStream(socket.getInputStream()); while (true) { Message msg (Message) in.readObject(); if (LOGIN.equals(msg.getType())) { handleLogin(msg); } else if (CHAT.equals(msg.getType())) { handleChat(msg); } else if (LOGOUT.equals(msg.getType())) { break; } } } catch (IOException | ClassNotFoundException e) { // 客户端断开或网络异常统一走清理分支 } finally { ServerMain.removeOnlineUser(currentUser); closeAll(); } } }逻辑说明readObject 是阻塞调用某个客户端 Socket 断开时这里会抛 IOException整个线程退出并进入 finally 清理在线表。这套 try-catch-finally 几乎是所有 Socket 服务端线程的骨架。注意 ObjectOutputStream 要先于 ObjectInputStream 创建两端顺序必须一致否则会出现两端互相等对方流的死锁——这是网络编程里最玄学的握手问题之一症状是程序不报错也不动。消息转发的核心藏在 handleChat 里private void handleChat(Message msg) throws IOException { // 从在线表里找目标用户的处理线程 ServerThread target ServerMain.getOnlineThread(msg.getTo()); if (target ! null) { // 在线直接把消息对象转发给目标线程的输出流 target.getOutputStream().writeObject(msg); } else { // 离线课程设计阶段就打印日志不做存储 System.out.println([离线消息暂不处理] msg.getFrom() - msg.getTo()); } }参数说明在线表的 key 是用户名而不是用户 ID简化了查找逻辑但代价是用户名不能改。转发是直接把 Message 对象从一个线程的输出流写出去目标端阻塞在 readObject 上会立刻收到。不做离线存储是这份源码的明确边界进阶改造的切入点也在这里——把 else 分支换成 INSERT 语句就是一个离线消息模块。4.2 客户端事件分发登录、刷新好友、收发消息客户端这边的逻辑比服务端分散因为要跟 Swing 事件模型纠缠。登录按钮的监听器是第一个关键点// 登录按钮的 ActionListener loginButton.addActionListener(e - { String username usernameField.getText(); String password new String(passwordField.getPassword()); try { Socket socket new Socket(127.0.0.1, 8888); ObjectOutputStream out new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in new ObjectInputStream(socket.getInputStream()); Message loginMsg new Message(); loginMsg.setType(LOGIN); loginMsg.setFrom(username); loginMsg.setContent(password); out.writeObject(loginMsg); // 阻塞等服务端返回登录结果 Message ack (Message) in.readObject(); if (LOGIN_SUCCESS.equals(ack.getType())) { JFrame mainFrame buildMainFrame(); mainFrame.setVisible(true); // 登录成功才启动收消息线程 new Thread(new MessageRecvThread(in, mainFrame)).start(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } catch (Exception ex) { ex.printStackTrace(); } });逻辑说明这段代码的关键决策是登录成功后才启动收消息线程。新手常犯的错是一连上 Socket 就开线程结果登录失败时那个线程还在空转等消息。另一个隐蔽问题是顺序——客户端必须先写登录消息再读响应服务端那边是登录分支里先读再写两边的顺序刚好配对。谁也别提换一提就两端互相等这就是我常说的握手黑匣子。收消息线程拿到数据后有个 Swing 专属陷阱必须处理子线程不能直接操作界面组件否则轻则抢锁卡死重则抛异常。标准解法是把界面更新丢回事件分发线程// 收消息线程内部 SwingUtilities.invokeLater(() - { chatArea.append(ack.getFrom() ack.getContent() \n); onlineListModel.addElement(ack.getFrom()); });参数说明invokeLater 会把 Runnable 排到 Swing 事件队列里等 EDT 空闲时执行这样既安全又不阻塞网络线程。有人图省事直接在子线程里调 setText本地跑偶尔能过一到消息频繁时界面就闪退这个坑下面联调章节还会再提一次。5. 联调跑通与避坑指南IDEA 双客户端全流程与五个常见坑5.1 从导入到双客户端联调的完整步骤这个项目跑通的路径我按顺序捋一遍每步都给了验证点卡住就知道停在哪。第一步准备环境。JDK 8 以上、MySQL 5.7 或 8.0、IntelliJ IDEA。解压资源包后别被里面那堆 .apk 和 .class 文件带偏真正要看的只有 .java 文件ServerMain、ClientMain、UserTableDao、Message外加 views 包。压缩包是从工程目录直接打的Android 编译产物是历史残留直接忽略。第二步建库。用 Navicat 或命令行执行第三章的建表脚本。执行完验证一次确保 user 和 friend 两张表真实存在再进行下一步。mysql -u root -p mysql use qq_for_java; mysql show tables; -------------------- | Tables_in_qq_for_java | -------------------- | user | | friend | -------------------- 2 rows in set (0.00 sec)第三步改连接参数。打开 ServerMain 顶部的 initDataSource把用户名密码换成你本机的确认 3306 端口没被改过。这里如果填错启动不会报错第一个用户登录时才炸。第四步启动服务端。运行 ServerMain控制台出现聊天服务器已启动端口 8888就是成功了。如果报 BindException说明 8888 被其他程序占了换个端口同时改客户端连接代码。第五步启动两个客户端。IDEA 里 Run 一次 ClientMain 后会弹登录窗口再点一次 Run 会开第二个进程——Swing 程序多开就是这么验证的。如果源码里有注册逻辑就走注册没有的话直接往 user 表插两条测试数据password 列按明文存因为登录校验大概率是明文比对。第六步互发消息。A 登录后给 B 发一条B 的聊天窗口实时弹出来就通了。注意好友列表要想显示出来friend 表里要先插好关系数据否则列表是空的这不是 bug是数据没准备。5.2 踩坑记录坑 1ClassNotFoundException: com.mysql.cj.jdbc.Driver 现象ServerMain 一启动就抛 ClassNotFoundException指向 MySQL 驱动类。 原因两个 jar——mysql-connector-java 和 druid——没进运行时 classpath。IDEA 里只把 jar 拖进窗口不代表加入了依赖。 解决Project Structure → Libraries把两个 jar 的路径加进去勾选传递给模块导出 Runnable JAR 时选extract required libraries选后者经常漏依赖这是血泪经验。坑 2The server time zone value 乱码 is unrecognized 现象getConnection 抛 SQLException报错信息里是乱码时区名。 原因MySQL 8 驱动强制要求连接串带 serverTimezone而系统默认时区名在 GBK 控制台里解析成了乱码。 解决连接串里加serverTimezoneAsia/Shanghai一步到位同时给 IDEA 的 vmoptions 加-Dfile.encodingUTF-8否则日志乱码会干扰后续判断。坑 3SocketException: Connection reset服务端在线表留幽灵记录 现象客户端直接关窗口服务端日志抛 Connection reset再查在线表发现记录还在。 原因客户端关窗口时 Socket 没走正常关闭流程服务端 readObject 读到流异常清理逻辑只处理了正常 LOGOUT 分支。 解决JFrame 窗口关闭事件里先发 LOGOUT 消息再 close 连接服务端 finally 里做在线表清理时必须判空和加锁用 synchronized 包住 remove 操作防止两个线程同时清理同一用户。坑 4点登录按钮界面卡死转圈圈 现象窗口失去响应鼠标转圈过一会儿恢复或直接闪退。 原因登录时的 Socket 连接、数据库校验、读响应全在 EDT 线程里同步执行。本地回环网络快看不出来一旦放在跨机器环境就彻底翻车。 解决所有网络 IO 挪到工作线程登录校验完成后用 invokeLater 跳转主界面收消息线程同步也保持只接收不操作 UI界面更新一律交给 SwingUtilities。坑 5解压后以为是 Android 项目找不到入口 现象Debug 文件列表里全是 .class 和 apk翻半天没有 MainActivity。 原因压缩包打包时把工程目录里的 Android 编译产物和历史残留一并收进来了主干其实是纯 Java 的 Swing 项目。 解决只保留 .java 源码忽略 proguard.cfg、AaClient.apk、resources.ap_ 和所有 .class 文件。导入 IDEA 时新建 Java 项目把 src 里的源码拷过去比直接 Open 整个混乱目录省事得多。6. 进阶把单聊扩成群聊与离线消息再验证一次这套源码你吃透了拆这份源码的价值一半在跑通另一半在改。我建议的进阶方向是两个离线消息和群聊。两个都动到协议层能逼你把消息流转完整走一遍比多看十篇教程都管用。先扩协议。在 Message 的 type 常量里加GROUP_CHAT和离线消息推送服务端 handleChat 的 else 分支从打日志改成落库// 离线消息表客户端上线时按 to_user 拉取未读消息 CREATE TABLE IF NOT EXISTS message ( id INT PRIMARY KEY AUTO_INCREMENT, from_user VARCHAR(32) NOT NULL, to_user VARCHAR(32) NOT NULL, content TEXT NOT NULL, send_at DATETIME DEFAULT CURRENT_TIMESTAMP, is_read TINYINT DEFAULT 0 );// handleChat 的离线分支改造 private void handleChat(Message msg) { ServerThread target ServerMain.getOnlineThread(msg.getTo()); if (target ! null) { target.getOutputStream().writeObject(msg); } else { // 离线消息落库等对方上线再推送 dao.saveOfflineMessage(msg.getFrom(), msg.getTo(), msg.getContent()); } }然后改登录流程登录验证通过、返回 LOGIN_SUCCESS 之前先查离线消息逐条推给客户端再标记已读ListMessage offline dao.getOfflineMessages(user.getUsername()); for (Message m : offline) { out.writeObject(m); // 逐条推给刚上线的客户端 } dao.markMessagesRead(user.getUsername());改完怎么验证我的验证清单是三条第一条A 发消息给不在线的 B服务端日志不出现离线暂不处理而是一条 INSERT 记录第二条B 登录聊天窗口立刻弹出离线消息顺序和发送顺序一致第三条B 退出重登同一批消息不重复推送。三条都过离线消息模块就算闭环了。群聊更简单把 CHAT 的转发目标从单个用户改成遍历在线表即可但要注意把 from 拼进 content 里否则接收方不知道谁说的。从那以后我再接手这类教学型源码都强制自己走一遍协议类型 → 在线表 → 落库点的三角验证改完第一件事是砍掉服务端重启模拟离线场景测推送确认没有幽灵数据和重复消息。这套复习动作用不了二十分钟但能把一份能跑的作业变成吃透的网络通信小项目——希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑