尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

仿QQ聊天软件MyQQ源代码解析:Socket通信与离线消息实现

发布时间:2026/9/29 17:56:14

资讯中心
01
ARTICLE

仿QQ聊天软件MyQQ源代码解析:Socket通信与离线消息实现

仿QQ聊天软件MyQQ源代码解析:Socket通信与离线消息实现
简介这份仿QQ聊天软件MyQQ源代码来自北大青鸟完整版面向具备一定C#基础、希望从课堂语法练习迈向真实项目开发的进阶学习者可用于课程设计、毕业设计或即时通讯原理的动手实践。资源包为rar压缩格式整体约11.42MB上游未提供文件总数与类型明细但按项目性质应包含解决方案文件、窗体与类源码、资源文件及数据库脚本等便于直接编译运行与二次修改。目前已有247人学习关注属于小众但垂直的实战型素材。代码覆盖网络通信、多线程、数据序列化、数据库交互、WinForms或WPF界面设计、安全性校验、错误日志与async/await异步编程等关键知识点读者可借此理解用户注册登录、消息收发、群聊与文件传输等核心功能的实现思路并对照真实项目结构积累排错与架构经验提升C#工程化开发能力。1. 仿QQ聊天软件MyQQ源代码一套老派Java网络编程的活体标本如果你手里正躺着一份「仿QQ聊天软件MyQQ源代码(北大青鸟完整版)」大概率是三种人之一正在做课程设计的学生、想拿它练手网络编程的转行者、或者被要求「三天交一个能演示的聊天系统」的倒霉蛋。这套代码的价值不在于它多先进而在于它是一份完整闭环的C/S架构实现——登录、好友列表、私聊、群聊、文件传输、消息转发一个都不少而且用的是最朴素的Socket多线程JDBC没有Spring、没有Netty、没有Redis所有「魔法」都摊在你面前。我见过太多人拿到这份源代码后第一反应是「太老了换Netty重写吧」结果三天后连登录都没跑通。老代码的坑不在技术栈而在环境依赖、编码格式、数据库字段和线程模型这四件事上。这篇文章不打算教你从零写一个IM而是带你把这套MyQQ源代码真正跑起来、读懂它的通信协议、改掉它的致命缺陷最后能自己加一个「离线消息」功能。适合有Java基础、能看懂Socket和JDBC、但没做过完整网络项目的从业者。读完你至少能回答为什么客户端收不到消息、为什么中文变问号、为什么好友列表刷新不出来。2. MyQQ源代码的通信骨架从登录到消息转发的完整链路2.1 先看清C/S两端各自在干什么这套代码的结构非常典型服务端一个Server类启动ServerSocket监听端口每来一个客户端就开一条线程线程里用ObjectInputStream和ObjectOutputStream收发对象。客户端则是LoginFrame→MainFrame→ChatFrame三层界面每个界面持有一个Socket连接或者从全局连接池里取。消息不是字符串而是自定义的Message对象里面封装了类型登录、私聊、群聊、下线、发送者、接收者、内容、时间戳。关键点在于服务端维护了一个HashMapString, ClientHandlerkey是用户名value是对应的线程处理器。当A给B发消息时服务端从map里找到B的handler把消息对象写进B的输出流。如果B不在线这条消息直接丢弃——这就是为什么你测试时「对方没登录就收不到」。整套逻辑没有消息队列、没有持久化、没有ACK确认简单到粗暴但也正因为简单你才能在一周内把它彻底吃透。常见做法是先用telnet或者自己写一个最小客户端去连服务端端口确认服务端能接受连接并返回登录结果再去调界面。很多人一上来就启动完整客户端结果界面卡死、报错信息被Swing吞掉根本不知道是网络问题还是UI问题。2.2 用最小服务端最小客户端验证通信链路不要急着跑完整项目。先写一个30行的服务端和20行的客户端确认ObjectInputStream和ObjectOutputStream的配对顺序正确。这是MyQQ源代码里最容易翻车的地方——构造ObjectInputStream会阻塞等待对方先写header如果两端都先构造输入流再构造输出流直接死锁。// 最小服务端监听8888接收一个对象并回写 import java.io.*; import java.net.*; public class MiniServer { public static void main(String[] args) throws Exception { ServerSocket server new ServerSocket(8888); System.out.println(服务端启动等待连接...); Socket socket server.accept(); // 注意先构造输出流再构造输入流避免阻塞 ObjectOutputStream out new ObjectOutputStream(socket.getOutputStream()); out.flush(); // 必须flush否则header不发送 ObjectInputStream in new ObjectInputStream(socket.getInputStream()); Object msg in.readObject(); System.out.println(收到: msg); out.writeObject(服务端已收到: msg); out.flush(); socket.close(); server.close(); } }// 最小客户端连接8888发送一个字符串 import java.io.*; import java.net.*; public class MiniClient { public static void main(String[] args) throws Exception { Socket socket new Socket(127.0.0.1, 8888); ObjectOutputStream out new ObjectOutputStream(socket.getOutputStream()); out.flush(); ObjectInputStream in new ObjectInputStream(socket.getInputStream()); out.writeObject(hello myqq); out.flush(); Object reply in.readObject(); System.out.println(服务端回复: reply); socket.close(); } }逻辑说明服务端和客户端都遵循「先out.flush()再构造in」的顺序。ObjectOutputStream的构造函数会立即写入序列化header如果两端都先构造输入流输入流的构造函数会阻塞等待header而header又因为输出流没构造而发不出来形成死锁。参数上端口8888可以改成任意未被占用的端口但客户端和服务端必须一致。flush()不能省否则对象留在缓冲区里对面永远读不到。跑通这一步之后再把MyQQ源代码里的Message类拿过来替换字符串确认自定义对象能正常序列化。Message类必须实现Serializable且serialVersionUID要显式声明否则两端编译版本不一致时直接抛InvalidClassException。2.3 消息对象的字段设计与序列化陷阱MyQQ源代码里的Message类通常长这样typeint、senderString、receiverString、contentString、timeDate。看起来没问题但有几个隐藏坑。第一Date是可序列化的但如果你在两端用了不同的时区显示时间会差8小时。第二content如果包含换行符某些老版本JDK的序列化会有问题——实际上不会但如果你把content拆成多行再拼接容易漏掉转义。第三type用int枚举没问题但如果你后来加了新类型而客户端没更新服务端发过来的未知type会让客户端的switch走到default分支如果default里没处理消息就静默丢失。我一般会建议把type改成String常量比如LOGIN、PRIVATE_CHAT、GROUP_CHAT、LOGOUT这样日志里一眼能看懂调试时不用查数字对照表。另外receiver字段在群聊场景下可以填群ID但MyQQ源代码里群聊是广播给所有在线用户没有群成员概念——这是它的功能边界别指望它支持多群组。提示如果你在Message里加了新字段比如avatar一定要同步更新服务端和客户端的class文件并且保持serialVersionUID不变否则反序列化会失败。最稳妥的做法是每次改完都重新编译两端。3. 把MyQQ源代码跑起来数据库、编码、端口三件事3.1 数据库建表与JDBC连接参数这套代码默认用MySQL库名通常是myqq表至少有三张user用户名、密码、昵称、在线状态、friend用户、好友、分组、message离线消息但很多版本没实现。建表语句在源代码的sql文件夹或者db.sql里如果没有就按下面这个最小结构建CREATE DATABASE IF NOT EXISTS myqq DEFAULT CHARSET utf8mb4; USE myqq; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, nickname VARCHAR(32), online TINYINT DEFAULT 0 ); CREATE TABLE friend ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, friend_id INT NOT NULL, group_name VARCHAR(32) DEFAULT 我的好友 );JDBC连接串要特别注意jdbc:mysql://localhost:3306/myqq?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。useSSLfalse在老版本MySQL驱动里必须加否则控制台会刷警告serverTimezone不加的话Date字段存进去会报时区错误。驱动版本建议用mysql-connector-java-5.1.49和这套老代码兼容性最好用8.x驱动会遇到com.mysql.cj.jdbc.Driver类名变化和allowPublicKeyRetrieval问题。参数说明characterEncodingutf8要和数据库的utf8mb4配合否则emoji存不进去——虽然MyQQ源代码本来也不支持emoji但至少中文不能乱码。online字段在登录时置1下线时置0服务端启动时应该把所有用户置0否则上次异常退出后所有人都是「在线」状态。3.2 中文乱码的三种表现和对应修法中文乱码是这套代码最高频的翻车点表现有三种界面按钮文字变方块、聊天内容变问号、数据库里存进去就是乱码。第一种是Swing字体问题在main方法开头加UIManager.setLookAndFeel并指定支持中文的字体比如new Font(微软雅黑, Font.PLAIN, 14)。第二种是Socket传输编码问题ObjectOutputStream默认用UTF-8写字符串但如果你的IDE编译时用了GBKclass文件里的字符串常量就已经是乱码了。统一在IDE里把项目编码设为UTF-8并且编译参数加-encoding UTF-8。第三种是数据库连接编码上面JDBC串里已经带了characterEncodingutf8但还要确认MySQL服务端的character_set_server是utf8mb4。用SHOW VARIABLES LIKE character%;查如果character_set_database是latin1那就得改my.ini或者建库时显式指定DEFAULT CHARSET utf8mb4。# 检查MySQL当前编码 mysql -u root -p -e SHOW VARIABLES LIKE character%; # 如果不对临时修改重启失效 mysql -u root -p -e SET GLOBAL character_set_server utf8mb4;逻辑说明SET GLOBAL只对新建连接生效已经建立的连接不受影响所以改完要重启服务端和客户端。永久修改要改配置文件但不同系统路径不同这里不展开。验证方法是往user表插一条中文昵称再用SELECT查出来如果显示正常说明数据库层没问题。3.3 端口占用与防火墙的排查顺序服务端启动时报BindException: Address already in use说明端口被占了。Windows上用netstat -ano | findstr 8888找到PID再taskkill /PID xxx /FLinux上用lsof -i:8888或ss -lntp | grep 8888。如果端口没被占但客户端连不上先确认服务端绑定的IP是0.0.0.0还是127.0.0.1——如果绑了127.0.0.1局域网内其他机器连不上。MyQQ源代码里通常写的是new ServerSocket(8888)默认绑所有网卡没问题。防火墙方面Windows Defender会拦截入站连接第一次启动服务端时会弹窗询问如果点了「取消」后面就静默拦截。去「高级安全Windows Defender防火墙」里加一条入站规则放行TCP 8888。Linux上iptables -L -n看有没有DROP规则测试时可以先systemctl stop firewalld临时关闭。注意不要用ping测试端口通不通ping走的是ICMP和TCP端口无关。用telnet 127.0.0.1 8888或者nc -zv 127.0.0.1 8888。4. 避坑与排查MyQQ源代码里那些让人摔键盘的瞬间4.1 客户端登录后好友列表空白现象输入正确用户名密码登录成功主窗口打开但好友列表是空的数据库里明明有friend记录。原因通常是查询语句写错了——MyQQ源代码里常见的是SELECT * FROM friend WHERE user_id ?但user_id存的是当前登录用户的id而登录时只拿到了username没有把id传过来。解决方法是登录成功后用username再查一次user表拿到id或者直接把friend表的user_id改成username字段。我一般会改成后者少一次查询而且调试时直接看username更直观。4.2 私聊消息发出去对方收不到现象A给B发消息A的聊天窗口显示已发送B的窗口毫无反应。原因有三个可能B不在线服务端map里没有B的handler、B的用户名大小写不一致map的key是区分大小写的、消息的receiver字段填的是昵称而不是用户名。排查时在服务端的sendMessage方法里加一行System.out.println(转发给: receiver , 在线用户: map.keySet());一眼就能看出问题。如果是大小写问题统一在登录时把username转成小写存map。4.3 服务端线程阻塞导致所有客户端卡死现象一个客户端执行了耗时操作比如查询大量消息其他客户端全部无响应。原因是MyQQ源代码里服务端处理消息是单线程的——while(true) { Object msg in.readObject(); handle(msg); }如果handle里做了数据库查询或者文件IO整个服务端就卡在这一条消息上。解决方法是把handle里的耗时操作丢到新线程里或者用线程池。但注意如果消息处理顺序有依赖比如登录必须先于发消息就不能简单异步得加状态标记。4.4 文件传输功能一传大文件就内存溢出现象传小文件正常传超过50MB的文件时客户端或服务端抛OutOfMemoryError。原因是MyQQ源代码把整个文件读进byte[]再序列化发送文件多大就占多大内存。解决方法是改成分块传输每块4KB或8KB接收端追加写入FileOutputStream。但这就涉及协议改造——需要在Message里加fileId、chunkIndex、totalChunks字段服务端要维护一个MapString, FileOutputStream来跟踪每个传输中的文件。这是进阶改造后面第5章会展开。4.5 重复登录导致map覆盖现象同一个账号在两台机器上登录后登录的把先登录的挤下线但先登录的客户端不知道继续发消息服务端找不到handler消息丢失。原因是map.put(username, handler)直接覆盖了旧handler。解决方法是put之前先检查如果已存在给旧handler发一条「您的账号在其他地方登录」的消息然后关闭旧连接。但关闭旧连接时要注意旧handler的线程可能正在读socket直接close会抛SocketException需要用一个volatile boolean closed标记来优雅退出。5. 给MyQQ源代码加一个离线消息功能从表结构到线程安全5.1 离线消息的表设计和触发时机离线消息的核心逻辑是当服务端发现receiver不在线时不丢弃消息而是写入数据库当用户登录时查询offline_message表把未读消息推送给客户端然后标记为已读或删除。表结构如下CREATE TABLE offline_message ( id INT PRIMARY KEY AUTO_INCREMENT, receiver VARCHAR(32) NOT NULL, sender VARCHAR(32) NOT NULL, content TEXT NOT NULL, send_time DATETIME DEFAULT CURRENT_TIMESTAMP, is_read TINYINT DEFAULT 0, INDEX idx_receiver_read (receiver, is_read) );触发时机有两个发送时和登录时。发送时在服务端的sendMessage方法里判断map.containsKey(receiver)如果false执行INSERT INTO offline_message ...。登录时在登录成功的处理逻辑里执行SELECT * FROM offline_message WHERE receiver ? AND is_read 0遍历结果逐条构造Message对象写回客户端然后UPDATE offline_message SET is_read 1 WHERE id ?。参数说明content用TEXT而不是VARCHAR(255)因为聊天内容可能很长。send_time用DATETIME而不是TIMESTAMP避免2038年问题。索引idx_receiver_read是为了登录时查询快如果数据量不大可以不加但养成习惯没坏处。5.2 服务端推送离线消息的代码实现// 在ClientHandler的登录成功分支里调用 private void pushOfflineMessages(String username, ObjectOutputStream out) throws Exception { String sql SELECT id, sender, content, send_time FROM offline_message WHERE receiver ? AND is_read 0 ORDER BY send_time ASC; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ResultSet rs ps.executeQuery(); ListInteger ids new ArrayList(); while (rs.next()) { Message msg new Message(); msg.setType(OFFLINE_CHAT); msg.setSender(rs.getString(sender)); msg.setReceiver(username); msg.setContent(rs.getString(content)); msg.setTime(rs.getTimestamp(send_time)); out.writeObject(msg); out.flush(); ids.add(rs.getInt(id)); } // 批量标记已读 if (!ids.isEmpty()) { String update UPDATE offline_message SET is_read 1 WHERE id ?; try (PreparedStatement ups conn.prepareStatement(update)) { for (int id : ids) { ups.setInt(1, id); ups.addBatch(); } ups.executeBatch(); } } } }逻辑说明先查再推再标记顺序不能乱。如果先标记再推送推送过程中客户端断线消息就永久丢失了。ORDER BY send_time ASC保证消息按时间顺序到达。out.flush()每条都刷避免缓冲区堆积。批量更新用addBatch和executeBatch比逐条update快一个数量级。参数上conn是当前线程持有的数据库连接注意不要用单例连接——多线程共用一个Connection会出问题。每个ClientHandler在构造时从连接池拿一个连接或者每次操作时新建。MyQQ源代码里通常是每次操作新建简单但性能差测试够用。5.3 客户端如何区分在线消息和离线消息客户端收到type为OFFLINE_CHAT的消息时不应该弹新窗口而是追加到对应好友的聊天记录里并在好友列表里给该好友加一个红点或者「有离线消息」的标记。实现上在MainFrame里维护一个MapString, ListMessage offlineCache收到离线消息就存进去用户双击好友时再从缓存里加载。如果好友不在好友列表里陌生人发来的离线消息可以弹一个系统通知。提示离线消息的send_time是服务端时间客户端显示时不要用new Date()直接用消息里的时间否则用户会看到「刚刚」但实际上消息是昨天的。5.4 验证离线消息是否真的可靠测试步骤用A账号登录给B发三条消息然后关闭A。用B账号登录应该看到三条离线消息。再关闭B用A登录给B发一条然后B登录应该只看到一条新的之前三条不再重复。最后测试边界A给B发消息时B正好在登录过程中map里还没put这条消息应该走离线通道不能丢。我一般会在服务端加一个简单的日志每次写离线消息和读离线消息都打一行System.out.println跑一遍测试用例看日志是否成对出现。如果写入了但没读出来检查is_read字段是不是被其他逻辑提前置1了如果读出来了但客户端没显示检查客户端的switch里有没有处理OFFLINE_CHAT这个type。这套MyQQ源代码我前后跑过不下十遍每次带新人都会让他们先跑通最小通信链路再改数据库编码最后加离线消息。最深的教训是不要试图用现代框架的思维去重构它先让它跑起来再在它的逻辑上打补丁。老代码的每一行都有它存在的理由哪怕那个理由只是「当年老师就这么教的」。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。