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::fromValue<quintptr>(reinterpret_cast<quintptr>(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_cast<QTcpSocket*>( ui->clientCombox->itemData(i).value<quintptr>()); 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_cast<QTcpSocket*>( ui->clientCombox->itemData(i).value<quintptr>()); 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(sep+1);// "你好" 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_cast<QTcpSocket*>( ui->clientCombox->itemData(i).value<quintptr>()); 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_cast<QTcpSocket*>( ui->clientCombox->itemData(i).value<quintptr>()); 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_cast<QTcpSocket*>( ui->clientCombox->currentData().value<quintptr>()); 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); }