简介:基于QT C++实现的即时通讯系统课程设计资源包,面向计算机、软件工程、通信工程等专业本科生,适合用于计算机网络课程设计或毕业设计参考。资源内含完整源码工程、课程设计报告与演示视频,覆盖TCP网络通信、多线程Socket处理、MySQL数据库存储、在线用户管理等核心模块,并提供UI界面与交互逻辑,便于快速理解C++与Qt的协作方式。整个压缩包共160个文件,约66.58MB,主要包含Qt动态库(dll)、翻译资源(qm)、C++源文件(cpp/h)、界面设计文件(ui)、可执行程序(exe)以及课程设计报告(docx)和演示视频(mp4)等,目录结构清晰,既有可直接运行的Demo,也有便于修改的源码与文档。目前已有444人学习下载,适合需要完整项目作为参考、希望对照文档和视频快速上手计算机网络课程设计的学生。
1. 基于QT C++的即时通讯系统:课程设计的完整落地资源
这套资源是我大三时写的即时通讯系统课程设计,用QT C++实现,包含完整源码、课程设计报告和演示视频。当时计算机网络课要求做一个基于TCP协议的通信程序,我直接选了QT作为界面框架,把服务端、客户端、数据库全部串了起来。它不是一个空壳demo,而是能跑通的完整系统——登录注册、用户列表、私聊消息、服务端转发,链路是通的。如果你正在做计算机网络课程设计、软件工程课设,或者想拿一个能讲的毕业设计参考项目,这套资源可以直接照抄框架再改自己的功能点。我不推荐你原样交,但强烈建议你下载后拿着源码和报告,把每个模块的调用关系走一遍,再决定怎么改自己的版本。下面我拆给你看。
2. 先把骨架搭起来:模块划分与文件职责
拿到压缩包先别急着双击打开,先看文件列表,它的命名习惯已经把架构写在脸上了。mytcpserver.cpp、mytcpsocketthread.cpp、chat.cpp、login.cpp、onlineuserlist.cpp、mysql.cpp、mainwindow.cpp,这些文件名的职责基本上是一一对应的。
2.1 服务端三件套:监听、连接、线程处理
服务端的核心逻辑在mytcpserver.cpp和mytcpsocketthread.cpp里。前者负责监听端口、接受连接,后者负责处理每个已建立的TCP连接。这种一个连接一个线程的模式在课程设计里是最常见、也最容易讲清楚的做法。
// mytcpserver.cpp 片段,监听与连接接入 void MyTcpServer::startServer(quint16 port) { if (!this->listen(QHostAddress::Any, port)) { qDebug() << "监听失败:" << this->errorString(); return; } qDebug() << "服务端已启动,端口:" << port; } void MyTcpServer::incomingConnection(qintptr socketDescriptor) { MyTcpSocketThread *thread = new MyTcpSocketThread(socketDescriptor, this); connect(thread, &MyTcpSocketThread::clientDisconnected, this, &MyTcpServer::onClientDisconnected); thread->start(); }这里有个细节值得注意:incomingConnection里直接把socketDescriptor转交给新线程,而不是在服务端主线程保留QTcpSocket对象。原因在于QTcpSocket的生命周期必须和它的线程亲和性一致,默认情况下它属于创建它的线程,如果你在主线程创建但不在这里处理读写,消息槽函数根本不会被触发。所以常见做法是把描述符传给子线程,在子线程里new出QTcpSocket。
参数说明方面,listen(QHostAddress::Any, port)表示监听所有网卡接口,适合演示场景。如果你想只允许本机连接做调试,改成QHostAddress::LocalHost。端口号你自己定,课件里常用的是6666、8888这种,避免用9100以下的常见服务端口,免得和系统服务冲突。
2.2 线程内部:socket描述符的亲和性陷阱
mytcpsocketthread.cpp是整个服务端最核心的业务处理文件。它做的事情是:在子线程里创建QTcpSocket、绑定readyRead信号、解析JSON消息、查数据库、写回响应。
// mytcpsocketthread.cpp 片段,子线程中初始化socket并连接信号 void MyTcpSocketThread::run() { socket = new QTcpSocket(); if (!socket->setSocketDescriptor(socketDescriptor)) { qDebug() << "socket描述符绑定失败"; return; } connect(socket, &QTcpSocket::readyRead, this, &MyTcpSocketThread::onReadyRead); connect(socket, &QTcpSocket::disconnected, this, &MyTcpSocketThread::onDisconnected); exec(); // 进入事件循环 }这里有一个很多人第一次写会翻车的地方:setSocketDescriptor而不是setSocketState。传入的socketDescriptor是int类型,来自incomingConnection的参数。它本质上是操作系统socket句柄的数字表示,QT封装成qintptr,你在Windows和Linux下都可以这样用。绑定成功后,这个QTcpSocket才能正常读写。
exec()这行是关键。QThread的run()默认实现就是调用exec()进入事件循环,这里手动写出来是因为前面做了自定义初始化,事件循环必须显式开启,否则readyRead永远不会触发。这是线程模型里最容易漏的一步,漏了之后表现就是客户端连上了但什么都收不到。
2.3 客户端与界面:登录、聊天主窗口、在线用户列表
客户端的三个文件分工很清晰。login.cpp负责登录和注册请求的发送与响应解析,chat.cpp是聊天主窗口的交互逻辑,onlineuserlist.cpp维护在线用户列表的刷新。这三者之间的调用关系是:登录窗口校验通过后,把当前用户名传给主窗口;主窗口首次显示时请求在线用户列表;用户选中列表中的某个用户,发起聊天。
如果只按默认文件名来理解,容易忽略一件事:onlineuserlist不只是显示一个QListWidget,它还要在每次用户上线或下线时向服务端发送查询请求,然后刷新列表。这里的数据一致性是重点,客户端必须区分“服务端推送的用户状态变化”和“本地主动刷新”,不然列表会闪烁或者重复。
在线用户列表的刷新策略,我建议用定时主动拉取而不是服务端全量推送。课程设计里定时器3秒拉一次就够,写报告的时候把这个决策讲清楚,反而比全量推送更能体现你对网络负担的考虑。
3. 服务端线程模型:为什么一个连接一个线程,以及这样设计的代价
这个系统最值得讲清楚的部分就是线程模型。很多课程设计的服务端是单线程的,用QTcpSocket的waitForReadyRead阻塞在循环里,但那样写有很明显的问题:一个客户端卡住就拖死全部。这个项目用的是每连接一线程,属于网络编程课里“并发服务器模型”中的线程模型。
3.1 线程模型的选型理由
一个连接一个线程的模型,代码写起来最直观,而且能在课程设计答辩时讲出新东西:线程的创建、调度、销毁和资源回收都在你的掌控之内。对课程设计而言,这种模型的缺点是线程数量受系统限制,不过在几十个客户端以内完全够用。
如果你在报告的“性能分析”一章里写清楚:每线程约占用栈空间8MB(默认),最大并发连接数受OS线程数限制,在Windows上实测稳定并发50路没有问题——这段话会显得你确实懂底层。相比之下,select和epoll模型的对比在计算机网络课上讲烂了,你反而没什么可讲的新意。
3.2 子线程生命周期与信号槽跨线程通信
子线程和主线程之间靠信号槽通信。客户端上线、下线、收到消息,子线程需要通知主线程更新在线列表;主线程想要主动断开某个客户端,也要跨线程告知子线程。这里有个必须注意的事:QT的信号槽跨线程默认是队列连接,如果槽函数的对象已经销毁,信号发过去会直接丢失,而且不报错。
// mytcpsocketthread.cpp 中定义的消息转发 void MyTcpSocketThread::onReadyRead() { QByteArray data = socket->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject obj = doc.object(); QString type = obj.value("type").toString(); if (type == "chat") { QString from = obj.value("from").toString(); QString to = obj.value("to").toString(); QString msg = obj.value("msg").toString(); // 通过信号让主线程执行转发 emit forwardMessage(from, to, msg); } }信号转发而不是在子线程里直接找到目标socket发送,是为了避免同时对同一个QTcpSocket进行写操作。两个线程同时write同一个socket,在QT里不会崩溃但会乱序。把发送集中到主线程,所有的写操作就线性化了。这是多线程网络程序里最容易忽略的竞态条件,我写这个项目的时候也是在线程模型改版后踩到这个坑,之后强制约定:一个socket的所有写操作必须在同一条线程内完成。
3.3 线程退出与资源回收的规范写法
客户端断开时,子线程不能什么都不做就退出。需要先清理socket对象,再退出事件循环,最后让系统回收线程资源。
void MyTcpSocketThread::onDisconnected() { if (socket) { socket->deleteLater(); } this->quit(); // 退出事件循环 this->wait(); // 等待线程结束 emit clientDisconnected(userName); }这里的deleteLater和quit的顺序有讲究。先deleteLater让socket在事件循环的下一轮删除,然后quit退出事件循环,确保不会在socket还在处理消息时就把线程停掉。如果顺序反过来,会出现QTcpSocket已经被销毁但pending信号还在队列里的情况,程序会在随机时刻崩溃,表现是退出时偶尔报段错误。
在线程里使用wait()要特别小心。如果子线程的槽函数在主线程中被调用,wait()会阻塞主线程,如果等待的子线程需要主线程处理某个事件才能结束,就必然死锁。我的做法是:客户端断开连接时的清理在子线程内部完成,emit clientDisconnected只是通知主线程做UI刷新,主线程不等待子线程结束。这样最稳妥。
4. 用户数据落库:MySQL接入与连接生命周期管理
mysql.cpp是这个项目里比较容易被低估的文件,实际上它在登录、注册、在线用户查询几处都参与运行。课程设计如果只把用户信息写在内存里,答辩时被问一句“服务端重启后用户还在吗”就会哑火。有了MySQL,这就变成加分项。
4.1 连接初始化的标准写法
// mysql.cpp 片段,数据库连接初始化 QSqlDatabase MySql::initDatabase() { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); db.setPort(3306); db.setDatabaseName("chat_db"); db.setUserName("root"); db.setPassword("123456"); if (!db.open()) { qDebug() << "数据库连接失败:" << db.lastError().text(); } return db; }这里的每个参数都可以展开讲。QSqlDatabase::addDatabase的驱动名"QMYSQL",需要你在编译QT时勾选MySQL插件,或者下载对应版本的QT MySQL驱动。如果没装驱动,open时会报“Driver not loaded”,而且这个错误不是运行时异常,是直接返回false,所以调试时很容易忽略。
setHostName里用的127.0.0.1而不是localhost,是为了避开一个常见的坑:某些系统上localhost会被解析成IPv6的::1,而MySQL服务端只监听了IPv4,于是连接失败。用127.0.0.1直接指定IPv4地址,干净利落。
4.2 数据库驱动与QT版本匹配
QT连接MySQL最折磨人的就是驱动版本不匹配。QT 5.14.2默认配套的QMYSQL驱动是基于MySQL 5.7编译的,假如你的MySQL是8.0以上,连接时可能会报caching_sha2_password认证插件不支持。解决有两种:一是把MySQL用户改成mysql_native_password认证,二是重新编译QMySQL驱动。对课程设计来说,改认证最省时间:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;如果连接还是失败,先查一下驱动是否真的加载成功:
qDebug() << QSqlDatabase::drivers(); // 应该包含 "QMYSQL"如果列表里没有QMYSQL,说明QT安装时没带MySQL插件,去QT安装目录的plugins/sqldrivers里看有没有qsqlmysql.dll或libqsqlmysql.so。这也是我最常替别人排查的问题,很多时候不是代码问题,是环境问题。
4.3 登录与注册的查询逻辑
登录的逻辑基础但有几个细节值得拿出来说。先看代码:
bool MySql::verifyUser(const QString &username, const QString &password) { QSqlQuery query; query.prepare("SELECT password FROM users WHERE username = :user"); query.bindValue(":user", username); if (!query.exec()) { qDebug() << "查询失败:" << query.lastError().text(); return false; } if (query.next()) { return query.value(0).toString() == password; } return false; }prepare加bindValue的写法,目的是防止SQL注入。这里要说明:计算机网络课程设计里很多同学会直接字符串拼接SQL,交上去也没事,但答辩老师如果问一句“你的SQL安全吗”,这个提前量就很重要。你可以在报告里明确写一句“使用预处理语句防止SQL注入”,这是一个低成本的加分点。
密码存储其实应该加密,明文存储是课程设计的常规操作,但你在报告里最好提一句“生产环境应加盐哈希”,老师听了会认为你想过这个问题。我当年图省事用了明文,被老师问了一把,补了一句MD5加盐的改进方案才过关。
4.4 连接复用与泄漏避免
mysql.cpp里还有一个值得注意的细节:数据库连接不要每次请求都新建。QSqlDatabase的addDatabase是按连接名管理的,同一个连接名重复添加会覆盖之前的连接,而且旧连接不关闭会造成句柄泄漏。我一般这样处理:
QSqlDatabase MySql::getConnection() { if (QSqlDatabase::contains("chat_conn")) { return QSqlDatabase::database("chat_conn"); } QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", "chat_conn"); // 设置参数... return db; }这里的contains检查加命名连接是关键。如果直接无参调用addDatabase,QT会把它加入默认连接,多次调用会触发“connection already exists”警告。用命名连接可以很干净地做到全局复用一条连接。实际运行中,这个系统只有一个服务端进程,几十个线程共享一条MySQL连接是有问题的,因为QSqlQuery不是线程安全的,多个线程同时查询会互相干扰。正确的做法是用一个QMutex包住查询操作,或者让所有数据库操作都通过信号槽回到主线程执行。
两个方案我推荐后者。QMutex简单但会让查询变成串行的,锁竞争高;信号槽回主线程不仅线程安全,还能自然排队。这个项目因为查询量小,用信号槽性能完全够。
5. 排查与避坑清单:四个最容易翻车的现场
这个系统我前前后后改过三轮,踩过的坑比写过的功能还多。挑四个最典型的写在这里,按现象、原因、解决的格式来,你照着对号入座就行。
5.1 现象:客户端连不上服务端,报Connection Refused
原因很可能是服务端没启动,或者端口被防火墙挡了。排查看两个地方:先确认服务端listen成功,日志里有“服务端已启动”的输出;再确认防火墙没有拦截该端口。Windows下开发时,有时候服务端崩了但进程没退出,端口还占着,再启动就监听失败。
解决方法是启动前先查端口占用,被占用就杀掉旧进程。我习惯在服务端写一行启动日志,每次都看一眼,确认是真正监听成功而不是静默失败。后来还发现一个问题:如果用了QHostAddress::Any,IPv6和IPv4会同时监听,客户端连接的具体IP地址要看实际网络环境,调试时优先用127.0.0.1来排除局域网因素。
5.2 现象:能连上但收不到任何消息,服务端界面无反应
这个坑我在前面已经预告过,就是setSocketDescriptor和exec()的顺序问题。还有一个常见变体:readyRead信号已经触发了,但是socket->readAll()读到的数据是空的。原因是消息还没到,你读早了。TCP是流协议,数据可能分片到达,QT的readyRead是一次性的,触发时读到的只是缓冲区里已有的部分。
解决方法是定义一个QByteArray缓存,每次readAll后追加到缓存尾部,再从缓存里试着按完整JSON消息解析。解析失败就等下次readyRead再拼。这个按包拆包的逻辑在聊天工具里是必须的,否则发长消息或者连续发两条消息时,就会出现半包和粘包现象。我在mytcpsocketthread.cpp里用了一个简单方案:每条消息以换行符结尾,按行拆分消息,简单但不失严谨。
5.3 现象:中文乱码显示为问号
原因百分之百是字符集不一致。MySQL表是utf8mb4,客户端界面是QString的UTF-16内部编码,QTcpSocket传输默认是UTF-8字节流,这三者任何一个环节断开都会乱码。其中最容易漏的环节是MySQL连接后的字符集设置。
QSqlQuery query(db); query.exec("SET NAMES utf8mb4");这个必须在连接成功后立即执行一次。另外检查一下连接字符串里的charset参数,有的写法在DSN里指定,有的是SQL执行,两种都没问题,但必须有一种生效。还有编码不一致的提示也很容易出现在从Qt Creator控制台复制输出内容的时候,控制台的本地代码页和程序输出的UTF-8混淆,看起来像乱码但程序本身没问题。排查时先看服务端日志,再看数据库里的原始数据,逐层定位。
5.4 现象:程序退出时崩溃,报错指向QTcpSocket相关代码
原因基本是socket的生命周期管理问题,线程退了,socket对象还没delete,或者反过来。最典型的场景是窗口关了但线程还在跑,socket还在收发数据,QT在销毁对象时发现信号槽连接还挂着未处理事件,于是崩溃。
解决方法是关窗口前先断开发送端,再等待线程退出,最后清理资源。我现在写QT都会统一加一行设置:主窗口关闭时,先调用服务端的stopServer(),把所有的activeThread都发一个quit信号,等结束后再退出。这行必须在主程序返回前完成,宁可多等几毫秒,也比随机崩溃强。
5.5 现象:在线用户列表刷新时UI卡顿
原因不在网络,在UI线程做了太多耗时操作。如果你在槽函数里直接通过socket去查数据库再刷新列表,那么UI线程卡住是必然的。特别是当在线用户的查询要在列表刷新时重复执行时,几十次查询都堆在UI线程,界面就明显掉帧。
解决方法是把耗时操作全部放到子线程里完成,完成后通过信号槽把结果传回主线程,主线程只负责更新QListWidget。如果你日后再接手更大的项目,数据量上去之后考虑用QTableView加QAbstractTableModel替代QListWidget,只渲染可见行,空数据时再加载。每次刷新前做一次去重,把没变化的行跳过更新,也能明显降低卡顿感。这种性能问题不会让程序崩溃,但它会让演示效果打折扣,答辩现场尤其明显。
6. 验证链路:把演示跑通的三步检查法
拿到这套资源,最应该做的不是急着改代码,而是先按原样跑通一遍,确认环境、依赖和功能链路都正常。我整理了一个三步检查法,顺序是从底层往上层,哪一步不通就先排查哪一步。
6.1 第一步:数据库环境自检
先打开MySQL,执行建库建表的SQL。表结构里,users表最少有id、username、password三个字段,message表有from_user、to_user、content、time四个字段。把表建好后插入一条测试用户:
CREATE DATABASE IF NOT EXISTS chat_db DEFAULT CHARSET utf8mb4; USE chat_db; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL ); INSERT INTO users(username, password) VALUES('test', '123456');然后单独测试一下mysql.cpp里getConnection和verifyUser两个函数能不能跑通,用一个纯控制台程序调用。这一步没过就不用往下走了,QT界面做再好也登录不进去。
6.2 第二步:服务端启动与连接测试
服务端启动后,日志里应该显示监听成功。这时用命令行工具做连接测试,排除QT客户端代码的干扰。Windows自带Test-NetConnection可以用,Linux直接用nc或者telnet都可以。如果这一步失败,问题定位在服务端socket链路,和QT界面代码无关。
不妨在服务端多打印一条日志:一旦有客户端接入,incomingConnection里输出对端的IP和端口。这样你能确认连接是否真的到达了服务端,而不仅仅是客户端的socket认为连上了。这个信息在排查局域网环境时尤其宝贵,因为很多时候是IP配置错误,而不是代码逻辑错误。
6.3 第三步:协议测试与端到端验证
连接通了之后开始测协议。手动构造一条JSON格式登录取包,用网络调试助手发送,观察服务端响应。我第一次调试时就是用这种方式发现JSON字段名大小写不一致的问题——服务端解析用"UserName",客户端发送用的是"username",从逻辑上两个都是对的,但服务器就是查不到数据。
验证链路跑通后,建议做一次断线重连测试:服务端运行中把客户端关掉再打开,看在线用户列表能不能正确把原用户下线再上线。这个点很多课程设计的实现是不过关的,因为在线状态存在本地而没有服务端统一管理。如果这套资源里这块做得完整,那就本身就是加分项。如果发现列表里旧用户没有移除,原因一般是服务端的disconnected信号没有被正确触发或者没有广播给所有在线客户端,这时候往这个方向排查。
我拿到别人的QT项目代码,习惯性第一件事就是全局搜索qDebug() <<,把日志打点梳理一遍,再搜索connect(,把信号槽连接关系画成一张关系图。这两个动作做完,这个项目的架构就基本在脑子里了。希望这个习惯对你有用,也希望你能从这套资源里真正学到QT和网络编程之间的黏合点,而不仅仅是为了交一个课设。希望帮到你。
本文还有配套的精品资源,点击获取