ARTICLE DETAIL

资讯详情

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

Java仿QQ聊天系统源码解析:Swing+Socket+MySQL一条链路实战

Java仿QQ聊天系统源码解析:Swing+Socket+MySQL一条链路实战 简介这是基于Java Swing与Socket网络编程的仿QQ聊天软件完整源码压缩包内同时提供服务端、客户端以及MySQL数据库脚本主要面向Java进阶学习者、课程设计及期末大作业开发者。项目采用MVC分层架构借助JDBC与Druid连接池完成用户信息、好友关系的持久化管理支持多客户端并行登录并实时互聊综合体现图形界面、多线程通信与数据访问层的整合运用。压缩包共490个文件大小7.17MB其中53个Java源文件、90个class编译产物、42个XML配置、281张PNG界面图片以及SQL脚本覆盖从IDEA工程元数据到界面素材、数据库建表语句的完整资源便于直接导入开发环境理解运行。已有994人学习下载适合需要快速上手Swing界面搭建、Socket多线程交互和连接池配置的读者可作为完成即时通讯类项目的参考蓝本与答辩演示基础。1. 这套 Java 仿 QQ 聊天源码能不能用Swing、Socket、MySQL 一条链路全都有这段时间做课程设计选型我把手头几套 Java 仿 QQ 聊天软件源码都拉出来跑了一遍多数包解压后不是缺数据库脚本就是服务端起不来。这套相对完整桌面端用 Swing 画界面网络层走 Socket数据访问用 JDBC 配 Druid 连接池用户信息和好友关系都落在 MySQL 的 qq_for_java 库里。换句话说登录校验、好友列表、多个用户实时互聊这些最常卡人的环节它都覆盖到了。适合谁一是 Java 基础还凑合、想在短时间内跑通一个完整 C/S 项目的人二是要交 Java 进阶课程设计或期末大作业的学生三是想拿可运行代码当脚手架再往上面加群聊、文件传输等功能的开发者。如果你是纯零基础建议先把 IO、多线程、JDBC 过一遍再碰它。2. 资源包结构与运行链路ServerMain、ClientMain、数据库三方怎么对齐拿到压缩包后别急着解压双击先做一次文件首检。这个包的文件名对第一次接触的人不太友好解压后你能看到 AaClient.apk、resources.ap_、proguard.cfg、SystemPortraitActivity.class、R$id.class 这些产物它们看起来像安卓工程其实是打包时把编译中间产物一起塞了进来。真正的桌面端源码入口只有两个ServerMain.java 和 ClientMain.java后面所有搭环境步骤都以这两个文件为圆心展开。2.1 文件首检哪些参与编译哪些直接忽略我一般会把压缩包里的文件分成“参与编译”和“不参与编译”两堆。参与编译的只有 .java 源码以及你后续要引入的 jar 依赖其余 .class、.apk、.ap_ 都是构建过程的副产物复制到新工程里只会干扰编译器和 IDE 的判断。文件判断处理建议ServerMain.java服务端入口保留启动服务器ClientMain.java客户端入口保留可多开模拟多用户UserTableDao.java用户数据访问保留依赖 Druid 和 MySQL各种 Model/View/Controller 类工程主体保留按包结构还原AaClient.apk / resources.ap_ / proguard.cfgAndroid 或构建残留不参与桌面端编译忽略SystemPortraitActivity.class / R$id.class / *.class编译产物无法作为源码阅读忽略为什么会出现这些文件一种可能是原作者在同一个工作空间里开过 Android 端打包时把整个工程目录拉了进去另一种可能是导出项目时 IDE 顺手把 build 目录打进压缩包。无论哪种情况结论一致桌面端只认 .java。在这个步骤里我还会顺带看一眼有没有 README 或 SQL 脚本。如果有直接按作者给的顺序执行如果没有就按下面这套建库建表语句自己补。2.2 建库建表qq_for_java 与最简好友关系模型项目的数据库名是 qq_for_java这是摘要里明确写的不要顺手改成别的名字否则你后面要么改代码里的 URL要么改配置白白多踩一个坑。建库和建表脚本如下-- 创建数据库 CREATE DATABASE IF NOT EXISTS qq_for_java DEFAULT CHARACTER SET utf8mb4; USE qq_for_java; -- 用户表 CREATE TABLE IF NOT EXISTS user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50) DEFAULT , status TINYINT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 好友关系表 CREATE TABLE IF NOT EXISTS friend ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, friend_id INT NOT NULL, remark VARCHAR(50) DEFAULT , UNIQUE KEY uk_user_friend (user_id, friend_id) ); -- 演示账号 INSERT INTO user (username, password, nickname) VALUES (demo1, 123456, 路人甲), (demo2, 123456, 路人乙);字段说明username 加了 UNIQUE 约束避免重复账号password 这里存的是明文字符串期末项目可以接受真实项目必须加盐哈希。status 字段用来表达在线/离线很多相似源码里这个字段只是摆设因为在线状态一般由服务端内存维护数据库里的 status 不一定能实时更新。friend 表通过 user_id 和 friend_id 两个外键组成好友关系remark 是备注名类似 QQ 里的“好友昵称”。UNIQUE KEY 保证重复加好友时会直接报错而不是插入两条脏数据。2.3 IDEA 导入与启动顺序先跑服务端再双开客户端这个项目开发环境是 IntelliJ IDEA工程没有用 Maven 或 Gradle 管理原样导入时需要注意手动加依赖。完整步骤走一遍在 IDEA 里新建一个普通 Java 项目把 .java 源码整个复制到 src 目录下保持原有包结构。准备两个 jarmysql-connector-java数据库驱动和 druid连接池。放到项目根目录的 lib 文件夹里右键 Add as Library。把 druid.properties 放到 classpath 根目录也就是 src 目录下确保代码里能通过类加载器读到它。先在 MySQL 客户端执行上面的 SQL确认 user 表和 friend 表都创建成功。运行 ServerMain.java看到监听端口日志后再运行 ClientMain.java 登录 demo1。再次运行 ClientMain.java 登录 demo2此时两个客户端同时在线可以互发消息。我特意强调“先启动服务端再启动客户端”这个顺序是因为客户端登录时会立刻 new Socket(127.0.0.1, 8888)服务端没起的话客户端会直接弹“无法连接服务器”。如果你把服务端和客户端都配置成随项目启动一定要在运行配置里给服务端留出建立监听的时间否则第一次连接大概率失败。3. 核心链路代码走读Socket 连接、登录校验与消息路由跑通只是第一步真正要应付验收和面试你至少得能说清楚三个问题服务端怎么接收多个客户端、客户端登录后怎么维持长连接、消息从 A 到 B 到底经过哪几行代码。这一章把这三段逻辑拆开讲。3.1 服务端ServerSocket 监听与在线用户管理服务端的骨架通常是一个 ServerSocket 加一个无限循环每 accept 到一个客户端就交给独立线程处理。类似的完整代码长这样public class ServerMain { private ServerSocket serverSocket; // 在线用户表用户名 - Socket private static final MapString, Socket onlineUsers new ConcurrentHashMap(); public static void main(String[] args) throws IOException { ServerMain server new ServerMain(); server.start(8888); } public void start(int port) throws IOException { serverSocket new ServerSocket(port); System.out.println(服务端已启动监听端口: port); while (true) { Socket socket serverSocket.accept(); // 阻塞等待客户端连接 // 每个连接单独用线程处理避免一个客户端阻塞整个服务端 new Thread(new ClientHandler(socket)).start(); } } }这里有几个点需要注意。第一while(true) 必不可少少了它服务端只能 accept 到第一个连接然后 main 方法直接就结束了第二onlineUsers 用了 ConcurrentHashMap因为多线程环境下普通 HashMap 在并发写入时可能丢数据甚至产生环第三new Thread 是最简单的做法如果想更严谨一点可以换成 ExecutorService 线程池限制最大线程数。ClientHandler 内部要做的核心事情是先从 Socket 里读取用户登录信息把用户名和 Socket 放入 onlineUsers然后进入一个 read 循环持续读取客户端发来的 Message 对象再根据 Message.type 决定是转发聊天内容还是处理下线。3.2 客户端登录校验与会话建立客户端的登录按钮事件一般会做两件事先查数据库验证账号密码再建立 Socket 连接。这两步的顺序是有讲究的先校验后连接能避免无效连接堆积在服务端。public void loginAction() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); // 校验本地数据库账号合法才继续 User user userTableDao.login(username, password); if (user null) { JOptionPane.showMessageDialog(this, 用户名或密码错误); return; } // 与服务器建立连接并保持 try { Socket socket new Socket(127.0.0.1, 8888); ClientSession session new ClientSession(socket); session.send(new Message(Message.TYPE_LOGIN, username)); startChatWindow(user, session); } catch (IOException e) { JOptionPane.showMessageDialog(this, 无法连接服务器); } }登录按钮这段代码把 UI、业务、网络三件事都压在一个方法里期末项目这样写没问题结构上稍微偏“重”。真正启动聊天窗口后客户端还需要单独开一个读线程不断地从 socket 的输入流里读 Message这样才能做到不点按钮也能收到对方消息。这个读线程与 Swing 界面线程必须是分离的否则窗口会卡死这个坑在第 5 章会单独展开。3.3 消息协议Message 对象的字段与序列化约束A 发给 B 的消息不是裸字符串而是一个可序列化的 Message 对象。字段设计非常朴素但够用public class Message implements Serializable { private static final long serialVersionUID 1L; public static final int TYPE_LOGIN 1; public static final int TYPE_CHAT 2; public static final int TYPE_LOGOUT 3; private int type; // 消息类型登录/聊天/退出 private String from; // 发送方用户名 private String to; // 接收方用户名 private String content; // 消息正文 private String time; // 客户端生成的时间戳 // 构造器、getter/setter 省略 }客户端和服务端之间通过 ObjectOutputStream 和 ObjectInputStream 传输这个对象。这里最大的隐患是 serialVersionUID两端只要有一方改动过 Message 类的字段结构序列化就会抛 InvalidClassException。你从压缩包解压源码后客户端和服务端用的是同一个 Message.java通常没问题但如果你后来只改了服务端的 Message 类而忘记同步客户端就会出现“服务端日志正常、客户端收不到消息”的诡异现象。路由逻辑也很简单服务端从 Message.from 拿到发送方从 Message.to 拿到目标用户名然后从 onlineUsers 里取出目标 Socket把 Message 原样写出去。如果 to 是特殊值如“BROADCAST”就遍历 Map 给所有人发这种设计后续扩展群聊非常方便。4. 数据层实现Druid 连接池、MySQL 用户表与 DAO 增删改查聊天软件的数据层绕不开三件事用户认证、好友关系、在线状态。这套资源的亮点在数据库访问部分没有用笨重的框架而是自己写 DAO 配合 Druid 连接池很适合作为 java 数据库连接池的入门范本。4.1 Druid 配置druid.properties 与参数选择项目运行时会先读一份 druid.properties再创建 DataSource。典型的配置如下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/qq_for_java?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password123456 initialSize3 maxActive10 minIdle2 maxWait60000这里每个参数都不是摆设。initialSize3 表示连接池启动时预建 3 个连接避免第一个请求过去现场建连maxActive10 限制最大连接数防止高峰期把 MySQL 拖垮minIdle2 保持至少 2 个空闲连接应对突发请求maxWait60000 是拿连接的最大等待时间单位毫秒超过 60 秒拿不到连接就抛异常而不是无限阻塞下去。我经常看到有人把 maxActive 调到 100觉得越大越好。实际上连接池大小要结合 MySQL 的 max_connections 一起看客户端也就开两个窗口并发量极低10 个已经是绰绰有余。这个项目能体现 Druid 的价值不在高并发而在连接复用登录时 getConnection用完 return下一次登录再 getConnection拿的还是那条连接省掉了每次 TCP 握手和 MySQL 认证的开销。4.2 UserTableDao登录、好友列表与状态更新DAO 层代码通常是整个项目里最“套路”的部分。登录方法和好友查询写在同一类里public class UserTableDao { private static final DataSource dataSource DruidDataSourceFactory.createDataSource( readProperties(druid.properties) ); public User login(String username, String password) throws Exception { String sql SELECT * 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()) { return mapRow(rs); } } } return null; } public ListUser findFriends(int userId) { String sql SELECT u.* FROM user u INNER JOIN friend f ON u.id f.friend_id WHERE f.user_id ?; // 省略执行和映射 } }登录方法的意义不只是“查出来就返回”它同时承担了密码校验和会话初始化的职责。PreparedStatement 的 ? 占位符能防 SQL 注入不要在 DAO 里写成拼接字符串方式尤其是这种要交作业的代码字符串拼接被老师看到就是送分题变送命题。try-with-resources 写法保证了 Connection、PreparedStatement、ResultSet 都能自动关闭很多人学了 JDBC 之后还是写 finally 手动 close逻辑没错但代码冗余。好友列表查询用 INNER JOIN 把 friend 表和 user 表关联起来查出该用户所有好友的昵称和头像。这段 SQL 建议背下来课程设计答辩时老师大概率会问“你的好友列表数据怎么来的”。4.3 MVC 分层View、Controller、Model 的边界这套资源采用 MVC 架构这是摘要里明确提到的代码层面也能看出划分意图层代表类职责范围ViewClientMain、ChatWindowSwing 界面渲染按钮点击事件只做转发ControllerClientController处理事件调用 Service/DAO调度 SocketModelUser、Message、UserTableDao业务数据、DTO、数据库访问为什么要单独讲分层因为我拆过太多课程设计源码很多同学把所有代码堆在 JFrame 子类里一个类三四千行功能也能跑但答辩时连自己都说不清方法在哪。MVC 的意义不是代码变多而是把“界面刷新”“业务处理”“数据访问”三个变化频率不同的部分隔离开界面改样式不动业务加一个好友字段不动界面层换数据库驱动也只动 DAO。这个项目里最典型的分层体现是登录流程View 层拿到用户名密码后交给 ControllerController 调 UserTableDao 完成校验拿到 User 对象后再把结果传回 View 层渲染聊天窗口。你读源码时只要看到类似的三层调用链就能确定它的结构是标准 MVC。5. 避坑与排查把这套仿 QQ 源码跑起来之前先看这几个坎我拆过的“Java 课程设计源码包”不少能一次跑起来的比大家想得少。下面这几条是我复现项目时实际踩过的加上远程帮人看工程时见到的共性问题按“现象、原因、解决”三段式记录下来。5.1 服务端只能服务第一个客户端第二个连接就没反应现象第一个客户端登录成功第二个客户端卡在登录界面提示无法连接服务器。原因ServerMain 的 accept 没有放在 while 循环里或者 accept 虽然循环了但保存 Socket 的时候用了普通变量导致对象被回收。还有一种写法是 accept 之后没有启动新线程直接在同一个线程里 sleep 等待读消息导致第二个 accept 根本执行不到。解决确认 accept 写在 while(true) 里每个 Socket 必须交给独立线程处理服务端用一个 MapString, Socket 保存在线用户既方便路由消息也方便在客户端退出时 remove。注意 Map 要用 ConcurrentHashMap我在 3.1 里的代码就是标准写法。5.2 A 发消息 B 收不到但两边都显示在线现象两个客户端都登录成功A 发消息后界面提示发送成功B 那边毫无反应。原因十有八九是消息对象里的 to 字段填错了。常见情况是直接把 to 设为自己的用户名或者从联系人列表取值时取到了空值。另一个高频原因是服务端转发时从 onlineUsers 里取 Socket 取到了 null因为目标用户压根没被存进去。解决在服务端转发代码前后各加一行 System.out.println把 from、to、是否找到目标 Socket 打出来。我上次帮人排障日志一打出来立刻发现 A 把自己的用户名同时填进了 from 和 to消息发出去又回到自己身上。5.3 客户端窗口卡死、拖动无响应现象登录成功后聊天窗口能显示但几秒后变白拖动窗口卡顿甚至整个程序无响应。原因Socket 的输入流 read 操作被放在了 Swing 的事件派发线程里。Swing 是单线程模型所有界面刷新都要在 EDT 上执行你在按钮事件里直接写 while((msg in.readObject()) ! null)这个循环会一直霸占 EDT界面自然就冻结了。解决把读消息的循环放到独立工作线程收到消息后通过 SwingUtilities.invokeLater 切回 EDT 更新聊天面板。这是我处理这类源码时最常写的补丁基本改完立刻解决卡顿。5.4 压缩包里混着 .apk 和 .class误以为源码不完整现象解压后看到 AaClient.apk、resources.ap_、proguard.cfg、SystemPortraitActivity.class、R$id.class以为拿到的不是 Java 桌面项目代码打不开人也懵了。原因压缩包打包时把构建产物甚至另一个 Android 工程的产物一起收了进来。这类文件既不能直接阅读也不能参与桌面端编译它们的出现会严重干扰新手判断。解决做一次文件首检只保留 .java 源码和必要的配置文件其余一律剔除。用 IDEA 打开工程时只看 src 目录不要被根目录的 .class 文件带偏。5.5 MySQL 连接报错Communications link failure 或驱动类找不到现象服务端启动即抛异常或客户端登录时弹“无法连接服务器”控制台显示 ClassNotFoundException 或 Communications link failure。原因常见原因四个MySQL 服务没启动数据库根本没创建mysql-connector-java 版本和 driverClassName 不匹配URL 缺少 serverTimezone 参数。Java 8 和 MySQL 8 的组合下驱动类必须是 com.mysql.cj.jdbc.Driver老版的 com.mysql.jdbc.Driver 在部分环境会直接失败。解决先手动用命令行或 MySQL Workbench 执行一遍建库脚本确认数据库能通再回过来检查代码。URL 里务必带上 serverTimezoneAsia/Shanghai并且把 useSSL 设为 false否则本机 MySQL 也会因为 SSL 握手报错。6. 进阶改造在线状态广播与双客户端自测的落地手法如果你想让这个项目在答辩或面试时显得不那么“作业”最划算的改造是给好友列表加上在线状态。原本数据库里的 status 字段只是摆设现在我们可以用心跳包把这个状态真正用起来客户端每隔几秒向服务端发送一个 TYPE_PING 消息服务端收到后更新对应用户的时间戳并把在线用户列表广播给所有在线客户端。这样好友列表里的头像就能区分灰色和彩色效果很直观。自测环节我习惯用两个账号直接跑完整链路。用一个表格列出五件事第一步用错误密码登录预期弹窗提示用户名或密码错误第二步登录 demo1再登录 demo2回到 demo1 刷新好友列表预期能看到 demo2 在线第三步demo1 发消息给 demo2预期 demo2 能收到且时间戳正确第四步直接关闭 demo2 窗口预期 demo1 的好友列表里 demo2 变灰第五步服务端日志里能看到 demo2 的连接被移除再登录新客户端不报错。这套自测流程是我从一次答辩翻车里换来的经验。当时我把全部精力放在聊天窗口上老师随口一句“你客户端退出后服务端在线列表里会不会残留记录”我半天没答上来因为根本没测过异常退出路径。从那以后我每次拿到源码包都强制先建库、再起服务端、最后双开客户端把正常路径和异常路径全部点一遍才敢说这个项目我吃透了。希望帮到你。本文还有配套的精品资源点击获取
返回列表