ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Qt 简易聊天室(第二篇),单线程 改为 多线程,方便后续增加文件传输功能

2026/8/14 23:54:57 拓冰建站 浏览量
Qt 简易聊天室(第二篇),单线程 改为 多线程,方便后续增加文件传输功能 在上一篇文章的基础上本文只把 客户端的单线程 改为多线程暂不增加 文件传输。避免一次改太多。暂不改动 服务器端 单线程代码。如果 UI 绘制、socket 网络 I/O、磁盘文件读写都放在主线程传输大文件时 磁盘读写会阻塞事件循环导致界面卡顿。所以把 socket 收发和文件传输移到子线程主线程只负责 UI 和信号协调为后续文件传输打好基础。客户端代码main.cpp 文件#include mainwindow.h #include QApplication int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }mainwindow.cpp 文件#include mainwindow.h #include ui_mainwindow.h #include networkworker.h #include QHostAddress #include QThread MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); setWindowTitle(客户端); ui-port-setText(8899); ui-ip-setText(127.0.0.1); ui-disconnectServer-setEnabled(false); // 状态栏 m_statusPixmap new QLabel; m_statusPixmap-setPixmap(QPixmap(:/disconnect).scaled(20, 20)); m_statusText new QLabel(连接状态: ); ui-statusbar-addWidget(m_statusText); ui-statusbar-addWidget(m_statusPixmap); // 子线程 NetworkWorker m_netWorkThread new QThread(this); m_netWorker new NetworkWorker; m_netWorker-moveToThread(m_netWorkThread); // 线程结束时自动销毁 worker connect(m_netWorkThread, QThread::finished, m_netWorker, QObject::deleteLater); connect(this, MainWindow::connectToServer, m_netWorker, NetworkWorker::connectToServer); connect(this, MainWindow::disconnectFromServer, m_netWorker, NetworkWorker::disconnectFromServer); connect(m_netWorker, NetworkWorker::connected, this, []() { QString name ui-clientName-text().trimmed(); emit sendMessage(LOGIN: name.toUtf8() \n); // 状态栏更新 m_statusPixmap-setPixmap(QPixmap(:/connect).scaled(20, 20)); ui-record-append(已连接服务器); }); connect(m_netWorker, NetworkWorker::disconnected, this, []() { m_statusPixmap-setPixmap(QPixmap(:/disconnect).scaled(20, 20)); m_loggedIn false; ui-disconnectServer-setEnabled(false); ui-connectServer-setEnabled(true); ui-targetCombox-clear(); // 清空在线用户列表 ui-record-append(服务器断开连接...); }); connect(m_netWorker, NetworkWorker::errorOccurred, this, [](const QString err) { ui-record-append(连接失败: err); }); connect(this, MainWindow::sendMessage, m_netWorker, NetworkWorker::sendMessage); // 收到按行解析的数据 connect(m_netWorker, NetworkWorker::dataReceived, this, MainWindow::processLine); networkThread-start(); } MainWindow::~MainWindow() { // 安全退出线程 m_netWorkThread-quit(); m_netWorkThread-wait(); delete ui; } void MainWindow::processLine(const QByteArray line) { static const QByteArray REQ_LIST clientRequestList:; static const QByteArray LOGIN_OK LOGIN_OK:; static const QByteArray LOGIN_FAIL LOGIN_FAIL:; // 保存在线用户列表 if(line.startsWith(REQ_LIST)) { QString list QString(line.mid(REQ_LIST.length()).trimmed()); if (list.isEmpty()) return; QStringList users list.split(,); // 用逗号拆开字符串 ui-targetCombox-clear(); ui-targetCombox-addItem(所有人); ui-targetCombox-addItems(users); // 批量添加多项 return; } if (line.startsWith(LOGIN_OK)) { m_loggedIn true; ui-record-append(登录成功); ui-connectServer-setEnabled(false); ui-disconnectServer-setEnabled(true); return; } if (line.startsWith(LOGIN_FAIL)) { ui-record-append(登录失败: line.mid(LOGIN_FAIL.length())); emit disconnectFromServer(); return; } // 显示普通消息 ui-record-append(line); } void MainWindow::on_connectServer_clicked() { // 校验 IP 和 port QString ip ui-ip-text(); QHostAddress addr(ip); if(addr.isNull() || addr.protocol() QAbstractSocket::UnknownNetworkLayerProtocol) { ui-record-append(IP 地址无效); return; } unsigned short port ui-port-text().toUShort(); if(port 0) { ui-record-append(端口无效, 请输入: 1 - 65535); return; } // 必须设置客户端名字, 区分不同的客户端 QString name ui-clientName-text().trimmed(); if (name.isEmpty()) { ui-record-append(请先输入 客户端名字); return; } emit connectToServer(ip, port); ui-record-append(正在连接 ip : QString::number(port)); } void MainWindow::on_disconnectServer_clicked() { emit disconnectFromServer(); } void MainWindow::on_sendMsg_clicked() { if (!m_loggedIn) { ui-record-append(发送失败, 请先登录); return; } QString msg ui-msg-toPlainText().trimmed(); if (msg.isEmpty()) { ui-record-append(空消息发送失败); return; } QString target ui-targetCombox-currentText(); QByteArray data; if(target 所有人) { data clientMsgAll: msg.toUtf8(); } else { data clientMsgTo: target.toUtf8() : msg.toUtf8(); ui-record-append(我 - target : msg ); } emit sendMessage(data \n); }mainwindow.h 文件#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QByteArray #include QLabel QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void on_connectServer_clicked(); void on_disconnectServer_clicked(); void on_sendMsg_clicked(); void processLine(const QByteArray line); private: Ui::MainWindow *ui; QLabel* m_statusPixmap nullptr; QLabel* m_statusText nullptr; bool m_loggedIn false; // 子线程 QThread *m_netWorkThread nullptr; // 任务对象 NetworkWorker *m_netWorker nullptr; signals: void sendMessage(const QByteArray data); void connectToServer(const QString ip, quint16 port); void disconnectFromServer(); }; #endif // MAINWINDOW_Hnetworkworker.cpp 文件#include networkworker.h #include QHostAddress NetworkWorker::NetworkWorker(QObject *parent) : QObject{parent} { m_socket new QTcpSocket(this); connect(m_socket, QTcpSocket::connected, this, NetworkWorker::connected); connect(m_socket, QTcpSocket::disconnected, this, NetworkWorker::disconnected); connect(m_socket, QTcpSocket::errorOccurred, this, [](QAbstractSocket::SocketError) { emit errorOccurred(m_socket-errorString()); }); // 接收数据 connect(m_socket, QTcpSocket::readyRead, this, []() { m_buffer.append(m_socket-readAll()); while(true) { int idx m_buffer.indexOf(\n); if(idx -1) break; QByteArray line m_buffer.left(idx).trimmed(); m_buffer.remove(0,idx 1); if(line.isEmpty()) continue; emit dataReceived(line); // 按行解析后逐行发出 } }); } NetworkWorker::~NetworkWorker() { m_socket-close(); } void NetworkWorker::connectToServer(const QString ip,quint16 port) { m_socket-connectToHost(QHostAddress(ip), port); } void NetworkWorker::disconnectFromServer() { m_socket-close(); } void NetworkWorker::sendMessage(const QByteArray data) { m_socket-write(data); }networkworker.h 文件#ifndef NETWORKWORKER_H #define NETWORKWORKER_H #include QObject #include QTcpSocket class NetworkWorker : public QObject { Q_OBJECT public: explicit NetworkWorker(QObject *parent nullptr); ~NetworkWorker(); public slots: void connectToServer(const QString ip,quint16 port); void disconnectFromServer(); void sendMessage(const QByteArray data); signals: void connected(); void disconnected(); void errorOccurred(const QString error); void dataReceived(const QByteArray line); private: QTcpSocket *m_socket; QByteArray m_buffer; }; #endif // NETWORKWORKER_H-----------------------------------------------------------------------------------------------------服务器端 mainwindow.cpp 文件完整代码#include mainwindow.h #include ui_mainwindow.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); setWindowTitle(服务器); ui-port-setText(8899); ui-stopListen-setEnabled(false); // 创建服务器对象只连接一次 m_tcpServer new QTcpServer(this); connect(m_tcpServer, QTcpServer::newConnection, this, MainWindow::onNewConnection); // 状态栏 m_statusPixmap new QLabel; m_statusText new QLabel(连接状态: ); m_statusPixmap-setPixmap(QPixmap(:/tcp_disconnect).scaled(20, 20)); ui-statusbar-addWidget(m_statusText); ui-statusbar-addWidget(m_statusPixmap); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_setListen_clicked() { unsigned short port ui-port-text().toUShort(); if(port 0) { ui-record-append(端口号无效请输入 1-65535); return; } // if(m_tcpServer-listen(QHostAddress::Any, port)) if(m_tcpServer-listen(QHostAddress::AnyIPv4, port)) { quint16 realPort m_tcpServer-serverPort(); // 获取实际分配的端口 ui-record-append(开始监听端口: QString::number(realPort)); ui-setListen-setEnabled(false); ui-stopListen-setEnabled(true); } else { // 监听失败端口被占用、权限不足等 ui-record-append(监听失败: m_tcpServer-errorString()); } } void MainWindow::on_stopListen_clicked() { // 断开 所有客户端 for(QTcpSocket* client : m_clients) { client-close(); // disconnected 信号会自动清理列表 } m_tcpServer-close(); m_tcpServer-deleteLater(); // 创建服务器对象 m_tcpServer new QTcpServer(this); // 重新绑定 connect(m_tcpServer, QTcpServer::newConnection, this, MainWindow::onNewConnection); ui-setListen-setEnabled(true); ui-stopListen-setEnabled(false); ui-record-append(关闭监听); } // 客户端连接服务器 void MainWindow::onNewConnection() { // 取出 QTcpServer内部 已经创建好的资源, 获取下一个等待连接的客户端套接字 QTcpSocket *client m_tcpServer-nextPendingConnection(); // 加入列表 m_clients.append(client); m_statusPixmap-setPixmap(QPixmap(:/tcp_connect).scaled(20, 20)); // 获取客户端 IP 和端口 QString clientIp client-peerAddress().toString(); quint16 clientPort client-peerPort(); QString clientInfo clientIp : QString::number(clientPort); // 下拉框显示 clientInfo , 并且 socket指针 设置为下拉框的隐藏数据 ui-clientCombox-addItem(clientInfo, QVariant::fromValuequintptr(reinterpret_castquintptr(client))); bool *named new bool(false); QByteArray *buffer new QByteArray; // 接收到数据 connect(client, QTcpSocket::readyRead, this, []() { static const QByteArray CLIENTMSGALL clientMsgAll:; static const QByteArray CLIENTMSGTO clientMsgTo:; static const QByteArray LOGIN LOGIN:; static const QByteArray FILEREQUEST FILEREQUEST:; // 定义一个局部辅助函数专门用来通过 socket 找列表中的名字 auto getNameBySocket [](QTcpSocket* targetSock) - QString { for(int i 0; i ui-clientCombox-count(); i) { QTcpSocket* sock reinterpret_castQTcpSocket*( ui-clientCombox-itemData(i).valuequintptr()); if(sock targetSock) return ui-clientCombox-itemText(i); } return 未知用户; }; buffer-append(client-readAll()); while(true) { int idx buffer-indexOf(\n); // 标记:行尾在哪里 if(idx -1) break; QByteArray line buffer-left(idx).trimmed(); // 从缓冲区头部删除已经处理完的一行数据1 包括换行符为下一次处理做准备 buffer-remove(0, idx 1); if(line.startsWith(LOGIN)) { QString name QString(line.mid(LOGIN.length())).trimmed(); // 去掉首尾空白\n、空格等 // 验证,检查重名 bool exists false; for(int i 0; i ui-clientCombox-count(); i) { if(ui-clientCombox-itemText(i) name) { exists true; break; } } if(exists) { client-write((LOGIN_FAIL:名字 name 已被占用 \n).toUtf8()); client-close(); continue; } else { *named true; int currentRow -1; for(int i 0; i ui-clientCombox-count(); i) { // 取出第 i 行隐藏的 socket 指针 QTcpSocket* sock reinterpret_castQTcpSocket*( ui-clientCombox-itemData(i).valuequintptr()); if(sock client) { currentRow i; break; } } if(currentRow ! -1){ // 修改已有的显示文字, 只显示客户端名字区分不同的在线客户端 ui-clientCombox-setItemText(currentRow, name); } client-write(LOGIN_OK:\n); ui-record-append(上线: name); // 新客户端上线后服务器推送在线列表 - 给所有已上线的客户端 for (QTcpSocket *c : m_clients) { QStringList names; for (int i 0; i ui-clientCombox-count(); i) { names.append(ui-clientCombox-itemText(i)); } c-write((clientRequestList: names.join(,) \n).toUtf8()); } continue; } } // 客户端 向其他客户端 群发消息 if(line.startsWith(CLIENTMSGALL)) { QByteArray msg line.mid(CLIENTMSGALL.length()).trimmed(); QString sender getNameBySocket(client); for(QTcpSocket *c : m_clients) { c-write((sender (群发): msg \n).toUtf8()); } ui-record-append(sender (群发): msg); continue ; } // 客户端与客户端 私发消息 if(line.startsWith(CLIENTMSGTO)) { // 示例 clientMsgTo:Bob:你好 QByteArray payload line.mid(CLIENTMSGTO.length()).trimmed(); // Bob:你好 int sep payload.indexOf(:); if(sep -1) continue; QString target payload.left(sep); // Bob QByteArray msg payload.mid(sep1);// 你好 QString sender getNameBySocket(client); // 从下拉框找到私发目标 for (int i 0; i ui-clientCombox-count(); i) { QString name ui-clientCombox-itemText(i); // 提取 Bob if (name target) { QTcpSocket *sock reinterpret_castQTcpSocket*( ui-clientCombox-itemData(i).valuequintptr()); sock-write((sender : msg \n).toUtf8()); ui-record-append(sender - target : msg); break; } } continue ; } // 普通消息时从下拉框获取当前名字 QString senderName getNameBySocket(client); ui-record-append(senderName : line); } }); // 断开连接 connect(client, QTcpSocket::disconnected, this, []() { QString clientName; // 客户端断开时, 从下拉框移除 for(int i 0; i ui-clientCombox-count(); i) { QTcpSocket* sock reinterpret_castQTcpSocket*( ui-clientCombox-itemData(i).valuequintptr()); if(sock client) { clientName ui-clientCombox-itemText(i); // 找到名字 ui-clientCombox-removeItem(i); break; } } // 根据状态显示 if(*named !clientName.isEmpty()) { ui-record-append(下线: clientName); } else { ui-record-append(客户端: clientInfo 名字未通过验证被踢下线); } m_clients.removeOne(client); client-deleteLater(); // 将对象标记为待删除, 在事件循环空闲时 销毁对象 delete named; // 清理标志位 delete buffer; // 清理缓冲区 if(m_clients.isEmpty()) { m_statusPixmap-setPixmap(QPixmap(:/tcp_disconnect).scaled(20, 20)); } // 客户端断开后, 主动向所有在线用户推送更新 for(QTcpSocket *c : m_clients) { QStringList names; for(int i 0; i ui-clientCombox-count(); i) { names.append(ui-clientCombox-itemText(i)); } c-write((clientRequestList: names.join(,) \n).toUtf8()); } }); } // 服务器私发 void MainWindow::on_sendMsg_clicked() { if (m_clients.isEmpty() || ui-clientCombox-currentIndex() 0) { ui-record-append(没有客户端连接无法发送); return; } // 从下拉框获取选中的 socket QTcpSocket *selected reinterpret_castQTcpSocket*( ui-clientCombox-currentData().valuequintptr()); if (!selected || selected-state() ! QAbstractSocket::ConnectedState) { ui-record-append(选中的客户端已断开); return; } QString msg ui-msg-toPlainText().toUtf8(); if (msg.isEmpty()) { ui-record-append(空消息无法发送); return; } selected-write(服务器: msg.toUtf8() \n); ui-record-append(服务器 → ui-clientCombox-currentText() : msg); } // 服务器群发 void MainWindow::on_sendMsgAll_clicked() { if (m_clients.isEmpty()) { ui-record-append(没有客户端连接无法发送); return; } QString msg ui-msg-toPlainText().toUtf8(); if (msg.isEmpty()) { ui-record-append(空消息无法发送); return; } for (QTcpSocket *client : m_clients) { client-write(服务器群发: msg.toUtf8() \n); } ui-record-append(服务器群发: msg); }