news 2026/10/18 13:30:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 Map<String, 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" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"); 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; }

参数说明:serverTimezone=Asia/Shanghai 是 MySQL 8 驱动的新规矩,不加直接抛"The server time zone value"的异常,这是新手第一个翻车点;useSSL=false 是本地开发为了省去证书配置,公网部署绝不能照抄。initialSize=5 意味着服务端刚启动就去 MySQL 建了 5 条连接,第一个用户登录时不用等连接创建,体感上会快很多。MaxActive 20 对课程设计绰绰有余,一个在线用户一般只占用一条连接,连查询都很少。

连接池有了,访问数据库的动作全收进 UserTableDao。这个类只干 CRUD,不干别的,界面层和通信层都不允许直接碰 JDBC:

public 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 '1'='1就直接绕过密码校验了,这不是理论风险,是真实存在几十年的注入手法。try-with-resources 保证 Connection 和 PreparedStatement 不管成功失败都会自动关闭,连接用完归还连接池——忘记关连接是这类项目最隐蔽的内存泄漏,跑一天服务端就卡成 PPT。

参数说明:DAO 构造时只收 DruidDataSource,不自己 new 连接池。这样设计的好处是 ServerMain 只初始化一次,所有 ServerThread 线程共享同一批连接;要是每个线程各建一套连接池,在线用户一多,MySQL 的 max_connections 直接被打爆,报错信息是"Too many connections",第一次见会一头雾水。

4. 登录与在线聊天链路:Socket 消息协议与多客户端并发处理

4.1 服务端线程模型:一客户端一线程 + 消息转发

两端通信靠的是一个自定义协议对象。课程设计级别不需要引入 JSON 或 Protobuf,Java 自带的序列化足够,缺点是你抓包看到的是一堆二进制流,不是可读文本——调试时只能靠两端打日志。这个取舍对作业来说划算。

// 两端共用的消息协议,实现 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 踩坑记录

坑 1:ClassNotFoundException: 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",选后者经常漏依赖,这是血泪经验。

坑 2:The server time zone value '乱码' is unrecognized 现象:getConnection 抛 SQLException,报错信息里是乱码时区名。 原因:MySQL 8 驱动强制要求连接串带 serverTimezone,而系统默认时区名在 GBK 控制台里解析成了乱码。 解决:连接串里加serverTimezone=Asia/Shanghai一步到位;同时给 IDEA 的 vmoptions 加-Dfile.encoding=UTF-8,否则日志乱码会干扰后续判断。

坑 3:SocketException: 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 之前,先查离线消息逐条推给客户端,再标记已读:

List<Message> offline = dao.getOfflineMessages(user.getUsername()); for (Message m : offline) { out.writeObject(m); // 逐条推给刚上线的客户端 } dao.markMessagesRead(user.getUsername());

改完怎么验证?我的验证清单是三条:第一条,A 发消息给不在线的 B,服务端日志不出现"离线暂不处理"而是一条 INSERT 记录;第二条,B 登录,聊天窗口立刻弹出离线消息,顺序和发送顺序一致;第三条,B 退出重登,同一批消息不重复推送。三条都过,离线消息模块就算闭环了。

群聊更简单,把 CHAT 的转发目标从单个用户改成遍历在线表即可,但要注意把 from 拼进 content 里,否则接收方不知道谁说的。

从那以后我再接手这类教学型源码,都强制自己走一遍"协议类型 → 在线表 → 落库点"的三角验证,改完第一件事是砍掉服务端重启,模拟离线场景测推送,确认没有幽灵数据和重复消息。这套复习动作用不了二十分钟,但能把一份"能跑的作业"变成"吃透的网络通信小项目"——希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 13:27:38

WPF无人机地面站源码解析:MAVLink通信与实时数据绑定实战

简介&#xff1a;这是一套面向计算机专业毕设与无人机开发初学者的WPF地面站控制系统完整工程源码&#xff0c;基于.NET Framework与C#构建&#xff0c;通过MAVLink协议实现与无人机的通信&#xff0c;可用于飞行参数实时监控、任务规划执行与数据采集分析等场景。压缩包共243个…

作者头像 李华
网站建设 2026/10/18 13:26:36

深圳海外GEO服务商推荐与选型:制造业、SaaS如何看内容证据

摘要&#xff1a; 深圳企业寻找海外GEO服务商&#xff0c;重点是服务能否回答目标市场买家的问题。峰极彼岸、增长超人、加搜科技和瑞诺国际服务方向各有侧重&#xff0c;适合了解整合营销、多渠道协同、多语种与外贸本地化需求。制造业与SaaS采购时&#xff0c;应把产品资料、…

作者头像 李华
网站建设 2026/10/18 13:25:45

Photoshop Connection SDK 实战:C++ 外部程序控制 PS 自动化

1. 从插件开发者的视角看Photoshop Connection SDK的定位第一次接触Photoshop插件开发的人&#xff0c;往往会被Adobe那一长串SDK名字搞晕&#xff1a;Photoshop SDK、UXP、CEP、Connection SDK……它们之间到底是什么关系&#xff1f;我该从哪个入手&#xff1f;这个问题我在不…

作者头像 李华
网站建设 2026/10/18 13:20:43

QT+C++实现可调试德州扑克游戏:含AI决策与完整工程结构

简介&#xff1a;这是一份面向计算机专业本科生的C与Qt跨平台开发实践项目&#xff0c;专为课程设计与期末大作业打造&#xff0c;完整实现德州扑克核心逻辑与图形化交互界面&#xff0c;兼顾算法理解与GUI工程能力训练。资源包含97个文件&#xff0c;主体为9个cpp源文件&#…

作者头像 李华
网站建设 2026/10/18 13:19:07

Burp Suite越权检测辅助插件:自动切换身份重放请求并比对响应

平时做授权测试&#xff0c;最磨人的不是漏洞利用&#xff0c;而是越权这类逻辑漏洞的批量化验证。一个接口在低权限账号里能正常访问&#xff0c;另一个接口换了高权限账号看起来也正常&#xff0c;可只要把两个身份互换一下&#xff0c;问题就冒出来了。手动在Burp Suite里反…

作者头像 李华
网站建设 2026/10/18 13:18:55

Appium移动自动化测试实战:从环境搭建到稳定性提升

1. 为什么Appium能成为移动自动化测试的“通用语言”聊到Appium之前&#xff0c;先说一个我在某测试团队里听得最多的抱怨&#xff1a;项目里有iOS端的自动化需求&#xff0c;又有Android端的回归测试需求&#xff0c;团队里两拨人各写各的脚本&#xff0c;一碰到跨端联调就痛苦…

作者头像 李华