简介:基于QT C++实现的即时通讯系统课程设计资源包,面向计算机、软件工程、通信工程等专业大学生,适合在计算机网络课程设计或毕业设计中作为完整参考项目。包内共160个文件,涵盖17个cpp源码、15个头文件、6个ui界面设计及2个pro工程文件,配套60个dll运行库与42个qm翻译文件确保程序可直接运行,另有2个docx课程设计报告和1个mp4演示视频,整体压缩包约66.58MB。该资源完整呈现了从登录验证、在线用户列表到线程间通信与MySQL数据存储的常见实现思路,代码结构与界面模块划分清晰,便于理解客户端/服务器架构和QT网络编程流程。已有444人学习下载,适合需要快速搭建项目框架或撰写设计文档的同学参考。
1. 这个课程设计不是让你做出一个能聊天的界面
一门计算机网络课想要拿高分,通常靠的不是界面做得有多花,而是把你对 TCP、socket、应用层协议这些概念的理解,变成一台机器上真正能跑、能演示、能讲清楚的东西。基于 QT C++ 的实现即时通讯系统正好是这类课程设计里最经典的一道题:它把 socket 编程、TCP 状态迁移、应用层协议设计、并发连接处理全部串在了一个看得见摸得着的桌面程序里。很多同学一上来就四处找带课程设计报告和演示视频的成品压缩包,却忽略了答辩时老师最常追问的三个问题:你的协议是怎么定义的?粘包怎么处理?服务端同时撑多少个连接?这篇文章把从头自己做一遍的完整路线拆开,讲清楚架构怎么选、客户端和服务端各自怎么落、报告和演示视频怎么准备,以及我见过最多的翻车点。
2. 架构与协议设计:动手写代码前先画这两张图
2.1 客户端/服务端怎么拆,决定了答辩时你能讲多深
这个项目既然叫“即时通讯系统”,它的网络拓扑必须有一个中心节点。别想着让两个客户端直接建连,那叫 P2P,需要在真实网络环境里穿透 NAT,还要考虑双方所在地域名解析,课程设计里一旦涉及这个,答辩难度直接从本科毕业设计跳到研究生课题。最稳妥也最贴合计算机网络课程知识点的方案是纯 C/S 架构:一个服务端进程负责监听端口、维护在线客户端列表、转发消息、暂存离线消息;两个及以上的客户端进程负责登录、发送、接收和展示。
我习惯把模块拆成下面这张图在心里先挂起来:
- 服务端:QTcpServer 监听 + 每个连接一个封装对象(保存 socket、用户名、缓冲区、登录状态)
- 客户端网络层:QTcpSocket 封装 + 一个收包缓冲区 + 断线重连逻辑
- 客户端界面层:登录窗口 + 主聊天窗口(会话列表、消息区、输入区)
- 公共协议层:消息帧定义、消息类型枚举、JSON 序列化/反序列化
端口号建议固定在一个不常用的高位端口,比如 8600。低于 1024 的端口需要管理员权限,在实验室机器上经常因为权限不足而直接启动失败;8000 又容易被本机其他调试服务占用。选 8600 这类四位数的非标准端口,最省事。
服务端和客户端的连接管理要定一个明确的“生命周期”。客户端启动后主动 connectToHost;连接建立后第一件事是发送登录包,服务端校验用户名密码后回一个登录应答;之后客户端定期发心跳,服务端超过一定时间没有收到心跳就主动断开这个连接并广播在线列表变化。这些逻辑在协议设计阶段就要定好,不要等代码写完再补,不然客户端和服务端的协议会越改越对不上。
2.2 应用层协议:定义一个带长度头的 JSON 帧
计算机网络这门课里最常考的“应用层协议设计”,在这个项目里就是你的消息格式。TCP 是流式协议,它只保证字节按顺序到达,不保证一次 write 的数据能被对端一次 read 收到,也不保证两次 write 的数据在接收端不合并。所以你必须自己在应用层给消息切出边界。
我的推荐方案是“4 字节长度头 + JSON 包体”。
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0-3 | 4 字节 | length | 包体长度,网络字节序(大端),不含这 4 字节 |
| 4 | length | body | UTF-8 编码的 JSON 文本 |
包体 JSON 里统一带上这几个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| type | string | login / login_ack / chat / system / heartbeat / online_list |
| from | string | 发送者用户名 |
| to | string | 接收者用户名,群聊可以约定一个特殊前缀 |
| content | string | 消息正文 |
| ts | int | 时间戳,用于排序和显示 |
为什么选 JSON 而不是 Qt 自带的QDataStream直接序列化QString和QByteArray?因为答辩老师大概率会问你“如果客户端和服务端用不同语言实现,你的协议还能通用吗”。JSON 是跨语言的,抓包能看到明文,方便演示,也方便写测试脚本。QDataStream 里面带一个 Qt 版本号,收发双方如果 Qt 版本不同,反序列化会直接失败,这在后面的避坑章节里会细说。JSON 唯一的缺点是体积比二进制大,但对课程设计阶段的聊天系统来说完全够用。真要优化,可以定一个二进制协议:第一个字节是消息类型,后面按固定格式放字段,但那会牺牲可读性,我不建议在课程设计里给自己找这个麻烦。
发包函数我一般做成一个公共工具函数,客户端和服务端共用:
// protocol.h static void writePacked(QTcpSocket* socket, const QJsonObject& msg) { // 先序列化成紧凑 JSON,去掉多余空格,减少传输体积 QByteArray body = QJsonDocument(msg).toJson(QJsonDocument::Compact); // 手动拼 4 字节大端长度头 QByteArray head; head.resize(4); quint32 len = static_cast<quint32>(body.size()); head[0] = static_cast<char>((len >> 24) & 0xFF); head[1] = static_cast<char>((len >> 16) & 0xFF); head[2] = static_cast<char>((len >> 8) & 0xFF); head[3] = static_cast<char>(len & 0xFF); socket->write(head); socket->write(body); }这段代码的逻辑说明:QJsonDocument::toJson返回的是 UTF-8 编码的字节数组,Compact模式会去掉换行和空格,一个普普通通的聊天消息大概一百字节以内。长度头用手工移位而不是memcpy,是为了明确表达“网络字节序”,也给答辩时讲大小端留一个发挥点。head[0]存最高 8 位,head[3]存最低 8 位,这就是大端序。
参数说明:resolveTimeout这类参数不在这个函数里;这个函数唯一的隐式参数是QJsonDocument::Compact,如果消息里含大量换行,这个参数能显著减少包体大小。每个 socket 的写入缓冲由 Qt 管理,write 之后不需要立刻 flush,TCP 会自己合并小包,应用层不用关心。
3. 客户端实现:QTcpSocket 的收发框架与登录流程落地
3.1 用 QTcpSocket 搭连接层,别把逻辑全塞进登录按钮
客户端的网络层应该是一个独立的类,比如ChatClient,负责持有QTcpSocket、维护接收缓冲区、解析消息帧、发出信号让界面更新。不要让登录窗口的按钮槽函数里直接写 socket 读写,那样一旦界面重新开发,网络层就要重写。
先看连接建立部分:
// ChatClient.cpp 构造函数里的初始化 sessionSocket_ = new QTcpSocket(this); connect(sessionSocket_, &QTcpSocket::connected, this, &ChatClient::onConnected); connect(sessionSocket_, &QTcpSocket::readyRead, this, &ChatClient::onReadyRead); // Qt 5.14 中用 QOverload 绑定带参数的 error 信号 connect(sessionSocket_, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), this, &ChatClient::onSocketError); connect(sessionSocket_, &QTcpSocket::disconnected, this, &ChatClient::onDisconnected); void ChatClient::connectToServer(const QString& host, quint16 port) { if (sessionSocket_->state() != QAbstractSocket::UnconnectedState) { sessionSocket_->abort(); } sessionSocket_->connectToHost(host, port); }逻辑说明:connectToHost是异步的,调用后立即返回,真正连接建立成功会触发connected信号。所以不要在connectToHost后面紧接着去判断state(),那是很多新手会犯的错。abort()会把当前 socket 状态强制复位,这样重连时不会因为上一次连接还没断干净而出问题。
参数说明:host按局域网内服务端 IP 填,课程设计演示时服务端和客户端一般在同一台或同一网段内的两台电脑。如果服务端监听的是QHostAddress::Any,客户端填127.0.0.1即可。port必须和服务端listen的端口一致,这是客户端连不上的第一排查点。
登录流程在网络层里的做法是:连接成功后立刻组装登录包,不需要等用户再点一次“登录”。
void ChatClient::onConnected() { QJsonObject loginMsg; loginMsg["type"] = "login"; loginMsg["from"] = username_; loginMsg["pass"] = password_; loginMsg["ts"] = QDateTime::currentSecsSinceEpoch(); writePacked(sessionSocket_, loginMsg); }onConnected里不会立刻弹出主窗口,而是等服务端回login_ack,确认密码正确之后,客户端才进入聊天主界面。这个细节很重要:如果连接一建立就认为登录成功,那密码错误时你还要再退回去,交互逻辑会很别扭。
3.2 接收缓冲区:把 TCP 字节流一条一条切出来
客户端收到任何网络数据都会触发readyRead。最稳妥的写法是每次把readAll()追加到自己的缓冲区,然后在一个循环里不断尝试解析完整的帧。这就是处理粘包和拆包的核心。
void ChatClient::onReadyRead() { recvBuffer_.append(sessionSocket_->readAll()); // 循环解析,直到缓冲区里不足一个完整帧 while (recvBuffer_.size() >= 4) { quint32 len = 0; len |= (static_cast<quint32>(recvBuffer_[0]) & 0xFF) << 24; len |= (static_cast<quint32>(recvBuffer_[1]) & 0xFF) << 16; len |= (static_cast<quint32>(recvBuffer_[2]) & 0xFF) << 8; len |= (static_cast<quint32>(recvBuffer_[3]) & 0xFF); if (recvBuffer_.size() < static_cast<int>(len + 4)) { // 数据还没收齐,等下一次 readyRead 再处理 return; } QByteArray body = recvBuffer_.mid(4, static_cast<int>(len)); recvBuffer_.remove(0, static_cast<int>(len + 4)); handleMessage(QJsonDocument::fromJson(body).object()); } }逻辑说明:每次从缓冲区头部取 4 字节算出包体长度,如果缓冲区里已经有完整的包体,就切出来解析;如果没有,立即返回,剩下的数据保留在recvBuffer_里,等待下个readyRead信号。这个while循环能同时处理“一次收到多条消息”和“一条消息分多次到达”两种场景。
参数说明:这里没有用QDataStream去读长度头,是为了避免编译器自动做本机字节序转换。手工按位拼接时,必须用& 0xFF把char提升为无符号值,否则负数位移运算会得到错误结果。这是 C++ 里一个非常经典的有符号数坑。
handleMessage按type分发:
void ChatClient::handleMessage(const QJsonObject& msg) { if (msg["type"] == "login_ack") { if (msg["ok"].toBool()) { emit loginSucceeded(); } else { emit loginFailed(msg["reason"].toString()); } } else if (msg["type"] == "chat") { // 来自其他用户的消息 emit messageReceived(msg["from"].toString(), msg["content"].toString(), msg["ts"].toInt()); } else if (msg["type"] == "online_list") { emit onlineListChanged(msg["users"].toArray()); } }这里用emit信号把网络层和界面层解耦。主窗口只需连接messageReceived信号,把消息追加到聊天记录控件里。界面层不需要知道底层网络细节,网络层也不需要知道消息显示成什么样。
3.3 主窗口的会话区、输入区与在线列表怎么联动
主窗口我建议用QTextEdit当聊天记录显示区,只读模式;QLineEdit或QPlainTextEdit当输入区;右侧用QListWidget显示在线用户。不要一上来就用QTableWidget,在线列表就是“用户名 + 在线状态”两列信息,QListWidget轻量得多,还能用setToolTip显示更多细节。只有当你在线人数几千上万时,才需要考虑QTableView+ 自定义QAbstractTableModel来避免界面卡顿,课程设计阶段用不上。
发送消息的槽函数:
void MainWindow::onSendClicked() { QString text = inputEdit_->toPlainText().trimmed(); if (text.isEmpty()) return; QJsonObject chatMsg; chatMsg["type"] = "chat"; chatMsg["from"] = username_; chatMsg["to"] = peerName_; chatMsg["content"] = text; chatMsg["ts"] = QDateTime::currentSecsSinceEpoch(); writePacked(client_->socket(), chatMsg); // 自己的消息立刻显示在聊天区里 appendMessage(username_, text); inputEdit_->clear(); }自己的消息先显示到聊天区,是即时通讯的常见体验;等服务器转发回来再显示,会让人感觉延迟很高。这里需要注意“先发送再显示”的顺序:writePacked只是把数据写进 socket 缓冲,真正发出由事件循环控制,所以显示逻辑在 write 之后没有任何问题。服务端应答可以用来做“消息送达”标记,但课程设计里不必做这么细。
4. 避坑:网络编程与 Qt 线程里最常见的 6 个翻车点
4.1 消息串台:两条消息粘在一起,或者一条消息被切碎了
现象:聊天记录里出现“A 的消息后半截显示成了 B 的内容”,或者接收到的 JSON 每次都报解析错误,末尾多了一截。
原因:TCP 是字节流。你调用两次write发送两条消息,服务端很可能一次readyRead就把两段数据全部收完了;反过来,一条消息在网络上被分成多个包,服务端需要三次readyRead才能收齐。不按帧切分,直接拿readAll()去解析,必然出错。
解决:用第 3.2 节里的长度头 + 缓冲区循环解析。核心是把“读数据”和“解析消息”彻底分离,缓冲区里凑不齐一帧就等着,凑齐了就切出来。
4.2 程序在回收 socket 时崩溃
现象:客户端退出或关闭窗口时,程序卡住或直接崩,Qt 控制台输出QObject: Cannot create children for a parent that is in a different thread一类的提示。
原因:很常见的写法是在主窗体析构函数里delete client_,而client_内部又持有QTcpSocket,socket 断开信号触发了一个正在处理的槽函数,形成竞态。或者你让ChatClient对象的父对象在事件循环还没结束时就被销毁了。
解决:在ChatClient的析构函数里先abort()socket,再删除信号连接:
ChatClient::~ChatClient() { if (sessionSocket_) { sessionSocket_->abort(); // 立即终止,不再等待三次握手挥手 delete sessionSocket_; sessionSocket_ = nullptr; } }abort()会立刻断开并丢弃缓冲区数据,不会像disconnectFromHost()那样等对方响应,所以析构过程不会触发额外的网络事件。
4.3 主界面卡死:在槽函数里做了阻塞操作
现象:发送一条大文件消息时,整个窗口无响应,鼠标变成转圈状态,几秒后才恢复。
原因:我在代码里见过最典型的是在发送槽里调用了waitForBytesWritten(3000)或者waitForConnected(3000),这些waitFor*函数都阻塞当前线程的事件循环。Qt 的主线程还负责处理 UI 重绘和鼠标键盘事件,一被阻塞,窗口自然就“死了”。
解决:所有 socket 的读写都不调用waitFor*系列函数。用write后不立即确认,数据的实际发送由 Qt 底层完成。如果一定要知道发送完成的时机,可以用bytesWritten信号。界面只负责把数据交给 socket 再返回事件循环。
4.4 跨线程访问界面控件导致崩溃或显示错乱
现象:客户端把网络层放在一个QThread里,在线程里直接调mainWindow->appendMessage(...),结果有时界面能显示,有时崩溃,而且崩溃位置随机。
原因:Qt 的 UI 类不是线程安全的,任何对QWidget的访问必须在主线程。工作线程里直接调用槽函数,相当于把 UI 更新并发到了主线程正在绘制控件的时间点,这个冲突不可预测。
解决:工作线程只发信号,不调界面方法。Qt 信号槽默认在线程之间是队列连接,emit signal后槽函数会在目标线程的事件循环里执行:
// 在线程类中 emit receiveMessage(from, content, timestamp); // 在主窗口中 connect(worker, &NetworkWorker::receiveMessage, this, &MainWindow::appendMessage);只要 connect 的第五个参数没改,Qt 自动用Qt::QueuedConnection把调用排到主线程,安全可靠。这么做之后,网络线程里禁止再出现ui->xxx->setText(...)这类代码。
4.5 QDataStream 换台电脑就读不了
现象:代码在自己电脑上收发优先,换到教室电脑或打包给别人演示时,接收到的第一个数字就异常,整个消息解析失败,程序直接退出。
原因:QDataStream写入字符串时默认带一个 Qt 版本标记,而且默认版本跟当前编译的 Qt 版本挂钩。写端用 Qt 5.14,读端用 Qt 5.14,没问题;读端换 Qt 5.9,字节流解析就会错乱。
解决:要么用 QDataStream 时在两端强制setVersion(QDataStream::Qt_5_14),要么像我推荐的那样,干脆不用 QDataStream 做消息帧,改成手工拼字节的长度头。这样协议完全由你控制,不依赖 Qt 的序列化版本,换任何语言都能对接。
4.6 拔网线之后服务端还认为客户端在线
现象:客户端直接关机或断网,服务端客户端列表里还显示这个用户在线,广播在线列表永远带着这个名字。
原因:TCP 连接的断开检测依赖对端回包。客户端拔网线后没有正常发disconnect包,服务端收不到任何消息,除非 TCP keepalive 超时,否则它永远不会发现连接已经死了。
解决:客户端每 15 秒发一个心跳包,服务端记录每个连接的最后活跃时间,超过 45 秒没收到包就主动断开并广播下线通知。心跳包只需要一个最简单的 JSON:
QJsonObject hb; hb["type"] = "heartbeat"; hb["from"] = username_; hb["ts"] = QDateTime::currentSecsSinceEpoch(); writePacked(sessionSocket_, hb);服务端用一个 QTimer 定期遍历所有连接,检查时间差,别再依赖 TCP 自己发现死链。
5. 服务端实现:QTcpServer 接收连接与并发转发的落地方式
5.1 一个连接一个上下文对象,别裸着管理 socket
服务端我建议用一个ChatServer类持有QTcpServer,每个新连接进来时,创建一个封装了 socket、用户名、缓冲区、上次心跳时间的ClientContext对象。这样比到处存QTcpSocket*干净得多。
void ChatServer::onNewConnection() { while (server_->hasPendingConnections()) { QTcpSocket* socket = server_->nextPendingConnection(); auto* ctx = new ClientContext(socket, this); clients_.insert(socket, ctx); connect(socket, &QTcpSocket::readyRead, ctx, &ClientContext::onReadyRead); connect(socket, &QTcpSocket::disconnected, ctx, &ClientContext::onDisconnected); log(QString("client connected: %1:%2") .arg(socket->peerAddress().toString()) .arg(socket->peerPort())); } }逻辑说明:hasPendingConnections加nextPendingConnection的模式必须在newConnection信号里循环调用,因为一个信号可能对应多个排队连接。clientContext对象直接挂在this(服务端)下面,socket 挂载在 context 下,整个对象树只要服务端窗口不销毁,就不会提前释放。
参数说明:peerAddress返回的地址,IPv6 环境下可能是一长串::ffff:192.168.x.x,控制台倒无所谓,如果写日志文件,记得做一次去前缀处理。peerPort是客户端的临时端口,每次连接不固定,只能作为日志参考,不能当身份标识。
客户端断开时:
void ClientContext::onDisconnected() { if (!username_.isEmpty()) { // 登录过的连接才需要广播离线消息 emit server_->userOffline(username_); } emit server_->removeClient(this); deleteLater(); }deleteLater()是 Qt 里安全的“稍后删除”机制,它会把删除操作排到下一次事件循环里执行,不会在当前信号处理中半途销毁对象。
5.2 并发模型:课程设计用单线程事件循环就可以了
这是很多人的纠结所在:QTcpServer 是不是每个连接要起一个线程?不是。Qt 的 socket 是非阻塞的,QTcpServer监听、QTcpSocket读写都在同一个事件循环里也能正常工作。服务端只需要做到“每个 readyRead 里尽量快速处理完数据”,不要做耗时操作,比如把消息写入数据库时用 SQLite 的批量事务,避免每一条消息都触发一次磁盘刷新。
我这些年做过多个这类系统后的经验:单线程事件循环撑个 200 个在线连接毫无压力,不需要多线程。只有当服务端要处理文件传输、视频流这类持续高带宽场景,或者要同步查询外部数据库,才需要考虑把阻塞操作丢给工作线程。课程设计答辩现场,老师更关心的是“为什么这样设计是有道理的”,你能说出“IO 复用 + 事件驱动,单线程足以支撑当前并发量”这句话,已经比盲目上多线程讲不清要高分。
如果你坚持体验多线程,正确姿势是把ClientContext对象moveToThread到工作线程,然后它包含的 socket 和定时器都跟随过去:
QThread* t = new QThread(this); ctx->moveToThread(t); connect(t, &QThread::started, ctx, &ClientContext::startHeartbeatTimer); t->start();这会引入额外的复杂度:线程对象的生命周期、socket 从旧线程移出必须在其disconnected之后操作、向 UI 线程发信号时要小心排队连接。我的建议仍然是在主干先跑通单线程版本,确认全部功能稳定了再碰线程,千万不要一上来就线程套 socket。
5.3 消息转发、在线列表广播与离线消息
服务端处理chat消息的核心逻辑:
void ClientContext::handleChat(const QJsonObject& msg) { QString to = msg["to"].toString(); if (to.isEmpty() || to == username_) { // 给自己发或目标为空,直接忽略 return; } if (server_->isOnline(to)) { server_->forwardMessage(msg); } else { server_->saveOfflineMessage(msg); } } void ChatServer::forwardMessage(const QJsonObject& msg) { QString to = msg["to"].toString(); ClientContext* target = clients_by_name_.value(to, nullptr); if (target && target->isLoggedIn()) { writePacked(target->socket(), msg); } }逻辑说明:目标在线就转发,目标不在线就存离线表。clients_by_name_是一个QMap<QString, ClientContext*>,用户名和连接建立映射关系。用户登录时插入,断开时删除。整个服务端就靠两张表:clients_按 socket 指针索引,clients_by_name_按用户名索引。
参数说明:离线消息这里存内存够用,进程重启就消失;想做得严谨一点,用 QSQLite 存到本地文件,一张offline_msg表,字段from_user, to_user, content, ts。用户登录成功后,服务端查询这张表,把未读消息逐条推给客户端,然后删除已推送记录。这个功能在课程设计报告里是很好的加分项,实现成本也很低。注意在推离线消息时,每条消息的to字段必须保留原目标用户,客户端才会把它们显示在正确的会话里。
心跳检测放在服务端:
void ChatServer::onHeartbeatCheck() { qint64 now = QDateTime::currentMSecsSinceEpoch(); for (auto it = clients_.begin(); it != clients_.end();) { ClientContext* ctx = it.value(); if (ctx->isLoggedIn() && now - ctx->lastHeartbeatMs() > 45000) { ctx->socket()->abort(); it = clients_.begin(); // 列表变化之后重新遍历 } else { ++it; } } }如果列表里删除了元素,直接在循环过程对QMap的遍历要小心。上述代码用it = clients_.begin()重新从一个已经变动的容器开始遍历是不够严谨的,更稳的写法是先把超时的 socket 收集到数组,遍历结束后统一断开。这个细节出现在答辩里,老师会认为你考虑问题很周全:
QList<QTcpSocket*> expired; for (auto it = clients_.begin(); it != clients_.end(); ++it) { if (it.value()->isLoggedIn() && now - it.value()->lastHeartbeatMs() > 45000) { expired.append(it.key()); } } for (QTcpSocket* socket : expired) { socket->abort(); }6. 课程设计报告和演示视频:把亮点讲出来的一半功夫
6.1 报告章节骨架:每个部分写多少页、重点放哪
课程设计报告不需要长篇大论,但要保证每一部分都在回答“我做了什么、为什么这么做、怎么验证的”。我的习惯是六个部分,对应正文表格:
| 报告部分 | 需要写到的内容 | 建议篇幅 |
|---|---|---|
| 需求分析 | 用户角色、功能点列表(登录/私聊/群聊/离线消息/心跳) | 1-2 页 |
| 总体设计 | 客户端与服务端模块图,连接与消息流向图 | 2-3 页 |
| 协议设计 | 帧格式、字段表、每种消息的 JSON 示例、时序说明 | 3-4 页 |
| 详细实现 | 关键类和核心函数,选 2-3 段代码贴进去并逐行解释 | 4-5 页 |
| 测试过程 | 测试用例表 + 5-8 张运行截图,包含错误场景截图 | 2-3 页 |
| 遇到的问题 | 从避坑章节里挑 3 个真实的,按“现象-原因-解决”写 | 2 页 |
协议设计这一章是整个报告的题眼。让老师一眼看到你定义了长度头、处理了粘包、设计了心跳,这比你在总体设计里写十页 UML 图更要命。测试部分不要只贴“成功登录”的截图,一定要贴一个“断线重连”和“离线消息推送”的截图,证明你考虑过异常场景。
6.2 演示视频:四分半钟讲完一个完整闭环
演示视频不要录半小时的完整开发过程,那是浪费老师时间,也容易暴露你自己没准备的边角问题。按下面这个分镜来安排:
| 片段 | 内容 | 时长 |
|---|---|---|
| 片段一 | 服务端启动,监听 8600 端口,控制台输出日志 | 30 秒 |
| 片段二 | 客户端 A 登录,服务端日志出现登录信息 | 40 秒 |
| 片段三 | 客户端 B 登录,A 的在线列表即时刷新 | 40 秒 |
| 片段四 | A 给 B 发消息,两条消息内容一致、时序正确 | 1 分 30 秒 |
| 片段五 | B 退出登录,A 发消息触发离线存储,随后 B 重新登录收到离线消息 | 1 分钟 |
| 片段六 | 关闭服务端,客户端提示连接断开并自动重连 | 30 秒 |
录制时用屏幕录制工具,分辨率 1080p 就够了,关键是不要录到微信 QQ 的弹窗、不要录到文件路径里含自己中文名的敏感目录。声音可以不加,但最好在关键操作时打上简单字标注,或者用鼠标工具高亮点击位置。视频长度务必控制在四分钟到五分钟之间,答辩现场老师没有耐心看完十分钟。
我个人的收尾习惯是:视频录完后,自己不看代码,照着分镜重新操作一遍,一旦发现某个步骤与脚本不一致,回到代码里去排查,绝不抱着“反正演示时能过”的侥幸。课程设计答辩翻车,十次里有八次是演示前没有重新跑过完整流程,另外两次是换了一台没有配好 Qt 运行环境的电脑。把我上面这套“单线程事件循环 → 长度头协议 → 心跳检测 → 离线消息”的路线完整走一遍,你不仅有了一个能答辩的即时通讯系统,计算机网络的几个主要知识点也会真正落到你的肌肉记忆里。希望帮到你。
本文还有配套的精品资源,点击获取