【Qt网络编程】Tcp多线程并发服务器和客户端通信
目录
一、编写思路
1、服务器
(1)总体思路widget.c(主线程)
(2)详细流程widget.c(主线程)
(1)总体思路chat_thread.c(处理聊天逻辑线程)
(2)详细流程chat_thread.h(处理聊天逻辑线程)
2、客户端
(1)总体思路widget.c(主线程)
(2)详细思路widget.c(主线程)
(1)总体思路chat_thread.c(处理聊天逻辑线程)
(2)详细流程chat_thread.c(处理聊天逻辑线程)
二、实现效果
1、服务器
2、客户端
完整代码请到指定链接下载:Qt网络编程-Tcp多线程并发服务器和客户端通信: 【Qt网络编程】Tcp多线程并发服务器和客户端通信
一、编写思路
1、服务器
(1)总体思路widget.c
(主线程)
初始化界面
创建窗口、输入框、按钮等基本UI元素。
创建服务器对象
实现
My_tcp_server
并监听客户端连接。处理新客户端连接
当有新客户端连接时,创建新的
Chat_thread
线程来处理通信。绑定信号槽
确保主线程与客户端处理线程间的信号槽连接,使用
Qt::QueuedConnection
处理跨线程通信。处理消息传递
接收和发送消息,并在界面上更新显示。
服务器启动与关闭
通过按钮控制服务器的启动和关闭,管理所有客户端线程的安全退出。
(2)详细流程widget.c
(主线程)
-
创建 Qt 界面及设置窗口属性: 首先通过
ui->setupUi(this);
来初始化用户界面,并设置窗口标题、大小等基本属性。这是 Qt 项目的常见步骤,通过.ui
文件生成的类进行界面管理。Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) // 初始化UI对象 { ui->setupUi(this); // 设置UI界面 this->setWindowTitle("--服务器--"); // 设置窗口标题 this->resize(1024, 960); // 设置窗口大小 ui->le_ip->setText("127.0.0.1"); ui->le_port->setText("9999"); }
-
初始化服务器对象
My_tcp_server
并处理客户端连接:-
创建
tcp_server
对象以处理客户端的连接。 -
使用
connect
函数连接tcp_server
的new_descriptor
信号和匿名槽函数,确保一旦有新客户端连接,便创建一个Chat_thread
来处理该客户端。
this->tcp_server = new My_tcp_server(this); connect(tcp_server, &My_tcp_server::new_descriptor, this, [=](qintptr socketDescriptor){ QMessageBox::information(this, "提示", "新的客户端连接!", QMessageBox::Ok, QMessageBox::Information); ui->btn_send->setEnabled(true); // 启用“发送消息”按钮 // 创建新线程处理客户端 Chat_thread *chat_thread = new Chat_thread(socketDescriptor); chat_thread->moveToThread(chat_thread); // 将线程和对象绑定到同一线程,防止冲突 thread_list.append(chat_thread); // 启动线程处理客户端通信 chat_thread->start(); });
-
-
管理客户端线程
Chat_thread
:-
每当有新客户端连接时,创建一个
Chat_thread
并启动它处理客户端通信。通过moveToThread
将Chat_thread
的执行线程与该对象保持一致,避免跨线程冲突。 -
使用
connect
绑定线程中的信号(如连接断开、接收消息)和主界面槽函数,确保客户端状态能够正确显示。
Chat_thread *chat_thread = new Chat_thread(socketDescriptor); chat_thread->moveToThread(chat_thread); // 将线程与对象绑定在同一线程 thread_list.append(chat_thread); // 连接信号和槽 connect(chat_thread, &Chat_thread::break_connect, this, [=](){ ui->te_receive->append(currentTime + "\n【状态】客户端断开连接..."); ui->btn_send->setEnabled(false); // 禁用“发送消息”按钮 }); connect(chat_thread, &Chat_thread::recv_info, this, [=](QString data){ currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ui->te_receive->append(currentTime + "form client\n 【数据】 " + data); // 在文本框中显示消息 }); chat_thread->start(); // 启动线程
-
-
处理启动和关闭服务器的按钮事件:
-
on_btn_connect_clicked()
处理连接按钮点击事件,启动或关闭服务器。 -
启动时,检查 IP 地址和端口的有效性,成功后开始监听客户端连接。
-
关闭服务器时,停止监听,并确保所有已连接客户端线程安全退出。
void Widget::on_btn_connect_clicked() { if (!is_server_running) { // 启动服务器 QString ip_address = ui->le_ip->text().trimmed(); QString port_text = ui->le_port->text().trimmed(); if (!tcp_server->listen(QHostAddress(ip_address), port_text.toUInt())) { QMessageBox::warning(this, "warning", "服务器监听失败"); return; } is_server_running = true; ui->btn_connect->setText("关闭服务器"); ui->te_receive->append(currentTime + "\n【状态】服务器开始监听..."); } else { // 停止服务器并关闭所有客户端线程 tcp_server->close(); for (Chat_thread *thread : qAsConst(thread_list)) { thread->exit(); thread->wait(); thread->deleteLater(); } thread_list.clear(); is_server_running = false; ui->btn_connect->setText("创建服务器"); ui->te_receive->append(currentTime + "\n【状态】服务器已停止监听..."); } }
-
-
处理发送消息按钮的点击事件:
-
当点击“发送消息”按钮时,触发
send_request
信号,利用信号槽机制将输入的消息发送给客户端。需要确保主线程和子线程的信号槽通信是异步进行的(通过Qt::QueuedConnection
)。
void Widget::on_btn_send_clicked() { QString data = ui->te_send->toPlainText().toUtf8(); emit send_request(data); // 发出 send_request 信号 }
-
-
服务器监听客户端的状态和信息传递:
-
服务器通过
recv_info
和send_info
信号接收客户端消息并在界面上显示。 -
在客户端连接成功或断开时,更新界面显示状态。
connect(chat_thread, &Chat_thread::recv_info, this, [=](QString data){ currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ui->te_receive->append(currentTime + "form client\n 【数据】 " + data); // 显示接收的客户端数据 }); connect(chat_thread, &Chat_thread::send_info, this, [=](QString data){ currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ui->te_receive->append(currentTime + "to client\n 【数据】 " + data); // 显示发送给客户端的数据 });
-
(1)总体思路chat_thread.c
(处理聊天逻辑线程)
构造函数
初始化
socketDescriptor
以供后续线程使用。线程启动与套接字初始化
在
run()
函数中创建QTcpSocket
,并关联socketDescriptor
。获取客户端信息
通过
peerAddress()
和peerPort()
获取客户端 IP 地址和端口号,并进行错误处理。信号槽机制连接
将套接字状态、接收数据、错误处理等信号连接到相应的槽函数。
处理连接状态变化
通过
handler_client_changed()
处理客户端的连接或断开,并发出相应的信号。处理接收消息
在
receive_message()
函数中处理客户端发送的消息,并发出信号recv_info
。发送消息
在
send_message()
函数中,检查连接状态并发送消息,发出send_info
信号。错误处理
处理客户端连接中的错误,删除资源并退出线程。
(2)详细流程chat_thread.h
(处理聊天逻辑线程)
-
构造函数初始化:
-
Chat_thread
构造函数接受一个socketDescriptor
参数,并将其存储为类的成员变量,以供run()
函数中使用。注意,QTcpSocket
对象将在run()
函数中创建,以确保在新线程中创建并使用。
Chat_thread::Chat_thread(qintptr socketDescriptor, QObject *parent) : QThread{parent} , socketDescriptor(socketDescriptor) { // socketDescriptor 存储为成员变量 }
-
-
线程启动和套接字初始化:
-
在
run()
函数中创建QTcpSocket
对象,并通过setSocketDescriptor()
将套接字描述符与QTcpSocket
关联。这允许线程使用此套接字与客户端通信。 -
如果套接字初始化失败,进行错误处理并返回。
void Chat_thread::run() { // 创建 QTcpSocket 对象,用于处理与客户端的通信 this->socket = new QTcpSocket(); // 将套接字描述符与 QTcpSocket 关联 if (!socket->setSocketDescriptor(socketDescriptor)) { qDebug() << "Error: Failed to get new socketDescriptor."; return; } // 错误处理:检查是否成功获取客户端连接 if (socket == nullptr) { qDebug() << "Error: Failed to get new client connection."; return; // 如果获取失败,直接返回 } }
-
-
获取客户端信息:
-
在成功创建套接字后,获取客户端的 IP 地址和端口号。
-
如果获取失败,进行错误处理并断开连接。
// 获取客户端的IP地址和端口号 QString ip_addr = socket->peerAddress().toString(); quint16 port = socket->peerPort(); // 错误处理:检查是否成功获取IP地址和端口号 if (ip_addr.isEmpty() || port == 0) { qDebug() << "Error: Failed to get client's IP address or port."; socket->disconnectFromHost(); // 断开连接 socket->deleteLater(); // 删除客户端套接字对象 return; // 如果获取失败,直接返回 }
-
-
信号槽机制的连接:
-
连接套接字的状态改变信号
stateChanged
到槽函数handler_client_changed
,以便监控客户端连接状态的变化。 -
连接
QTcpSocket
的readyRead
信号到receive_message
槽函数,用于处理接收数据。 -
处理套接字错误时,连接
errorOccurred
信号到handle_socket_error
槽函数。
// 处理连接状态变化的槽函数 connect(socket, &QTcpSocket::stateChanged, this, &Chat_thread::handler_client_changed); // 错误处理:处理客户端的异常断开情况 connect(socket, &QTcpSocket::errorOccurred, this, &Chat_thread::handle_socket_error); // 处理接收数据的槽函数 connect(socket, &QTcpSocket::readyRead, this, &Chat_thread::receive_message);
-
-
处理客户端连接状态变化:
-
在
handler_client_changed()
槽函数中,根据客户端的连接状态(如断开、已连接)做相应处理并发出信号,通知其他部分更新状态。
void Chat_thread::handler_client_changed(QAbstractSocket::SocketState socket_state) { socket = (QTcpSocket*)sender(); // 获取发信的客户端套接字 if(!socket) return; switch (socket_state) { case QAbstractSocket::UnconnectedState: // 客户端断开连接 emit break_connect(); break; case QAbstractSocket::ConnectedState: // 客户端已连接 emit complete_connect(); break; default: break; } }
-
-
接收消息的处理:
-
在
receive_message()
槽函数中,通过socket->readAll()
读取客户端发送的所有数据,并发出信号recv_info
通知上层处理。
void Chat_thread::receive_message() { if (socket) { QString data = socket->readAll(); // 读取客户端发送的所有数据 emit recv_info(data); // 发出信号,通知收到消息 } }
-
-
发送消息:
-
在
send_message()
函数中,检查客户端是否处于连接状态,如果是则发送消息,否则输出警告信息。 -
发送完成后,发出
send_info
信号。
void Chat_thread::send_message(QString data) { if (socket->state() == QAbstractSocket::ConnectedState) { socket->write(data.toUtf8()); // 发送数据 } else { qDebug() << "warning: 客户端未连接,无法发送消息"; // 输出警告 } emit send_info(data); // 发出信号,通知发送消息 }
-
-
错误处理:
-
在
handle_socket_error()
函数中处理QTcpSocket
的错误。如果出现错误,打印错误信息,并退出线程。 -
删除套接字对象并退出线程事件循环。
void Chat_thread::handle_socket_error(QAbstractSocket::SocketError socketError) { qDebug() << "Client connection error, error code: " << socketError; this->exit(); // 退出线程 this->wait(); // 等待线程完全退出 socket->deleteLater(); // 删除客户端套接字对象 // 停止线程事件循环 quit(); }
-
2、客户端
(1)总体思路widget.c
(主线程)
初始化界面
设置窗口属性并初始化用户输入的默认值。
创建线程和通信任务对象
实现异步通信,使用
QThread
和自定义Chat_thread
处理服务器交互。信号槽机制的建立
连接 UI 和工作线程之间的信号槽,确保各操作异步处理。
线程管理
在析构函数中确保线程安全退出,释放资源。
处理连接与断开
通过按钮触发连接和断开操作,并更新 UI 显示。
消息传递与显示
处理消息的发送与接收,并在 UI 界面上更新显示结果。
(2)详细思路widget.c
(主线程)
-
初始化界面:
-
使用
ui->setupUi(this)
初始化用户界面,并设置窗口标题和窗口大小。 -
初始化 IP 地址和端口号的默认值。
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) , is_connected(false) // 初始化连接状态为未连接 { ui->setupUi(this); // 设置UI界面 this->setWindowTitle("-客户端-"); // 设置窗口标题 this->resize(1024, 960); // 设置窗口大小 ui->le_ip->setText("127.0.0.1"); // 设置默认IP地址 ui->le_port->setText("8888"); // 设置默认端口号 }
-
-
创建线程和通信任务对象:
-
创建
QThread
对象以进行异步通信任务。 -
创建
Chat_thread
对象负责与服务器进行通信操作。 -
使用
moveToThread
将通信任务对象移到新的线程中执行,并启动该线程。
// 创建线程对象 thread = new QThread; // 创建任务对象,负责与服务器的通信 Chat_thread *worker = new Chat_thread; worker->moveToThread(thread); // 将任务对象移至线程 thread->start(); // 启动工作线程
-
-
信号槽机制的建立:
-
使用信号槽连接 UI 和工作线程之间的交互。例如,连接服务器、发送消息、断开连接等操作通过信号槽机制进行。
-
信号从 UI 线程发出,工作线程的槽函数接收信号并执行相关操作。
// 信号槽连接:从UI线程发出连接信号,worker线程接收并执行连接操作 connect(this, &Widget::connect_server, worker, &Chat_thread::start_connected); connect(this, &Widget::send_info, worker, &Chat_thread::start_send); connect(this, &Widget::quit_connect, worker, &Chat_thread::break_connected); // 连接断开信号槽,worker线程通知UI线程更新UI connect(worker, &Chat_thread::connect_cancel, this, &Widget::submit_connect_cancel); connect(worker, &Chat_thread::connected, this, &Widget::submit_connect_info); connect(worker, &Chat_thread::transfer_recv_info, this, &Widget::submit_recv_info);
-
-
管理线程的生命周期:
-
在析构函数中,确保工作线程在窗口关闭时被正确停止,并释放相关资源。
-
如果线程正在运行,需要先请求线程退出,然后等待其完全退出后再删除。
Widget::~Widget() { if (thread->isRunning()) { thread->quit(); // 请求线程退出 thread->wait(); // 等待线程结束 } delete worker; // 删除任务对象 delete thread; // 删除线程对象 delete ui; // 删除UI对象 }
-
-
处理连接成功或断开连接的槽函数:
-
当客户端成功连接到服务器时,工作线程发出
connected
信号,UI 界面通过槽函数submit_connect_info()
来更新显示状态,并启用“发送消息”按钮。 -
断开连接时,UI 界面通过槽函数
submit_connect_cancel()
来禁用“发送消息”按钮,并更新状态显示。
// 连接成功时的槽函数,更新UI显示信息 void Widget::submit_connect_info() { currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ui->te_receive->append(currentTime + "\n【状态】已成功连接到服务器"); ui->btn_connect->setText("断开服务器"); ui->btn_send->setEnabled(true); // 启用发送按钮 is_connected = true; } // 断开连接时的槽函数,更新UI显示信息 void Widget::submit_connect_cancel() { is_connected = false; }
-
-
处理消息的发送与接收:
-
当用户点击“发送消息”按钮时,获取文本框中的消息,发出
send_info
信号,将消息发送到服务器。 -
当从服务器接收到消息时,工作线程发出
transfer_recv_info
信号,UI 界面更新显示接收到的消息。
// 当用户点击发送按钮时,读取输入框中的内容并发送给服务器 void Widget::on_btn_send_clicked() { currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); QString message = ui->te_send->toPlainText().toUtf8(); // 获取用户输入的消息 ui->te_receive->append(currentTime + " to server\n 【数据】" + message + "\n"); emit send_info(message); // 发出信号,通知工作线程发送消息 } // 当接收到服务器发送的消息时,更新UI显示接收到的消息 void Widget::submit_recv_info(QString message) { currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ui->te_receive->append(currentTime + " form server\n 【数据】" + message + "\n"); // 显示服务器的消息 }
-
-
处理连接与断开的按钮事件:
-
当点击“连接”按钮时,获取 IP 地址和端口号,检查输入的有效性后发出
connect_server
信号,通知工作线程与服务器建立连接。 -
当点击“断开服务器”按钮时,发出
quit_connect
信号,通知工作线程断开连接。
// 当用户点击"连接"按钮时触发该槽函数 void Widget::on_btn_connect_clicked() { if (!is_connected) { QString ip_address = ui->le_ip->text().trimmed(); QString port_text = ui->le_port->text().trimmed(); QHostAddress address; if (!address.setAddress(ip_address)) // 检查IP地址的有效性 { QMessageBox::warning(this, "warning", "无效的IP地址,请重新输入!"); return; } bool ok; unsigned int port = port_text.toUInt(&ok); if (!ok || port == 0 || port > 65535) // 检查端口号的有效性 { QMessageBox::warning(this, "warning", "无效的端口号,请输入1到65535之间的数值!"); return; } emit connect_server(ip_address, port); // 发出连接服务器的信号 } else { currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ui->te_receive->append(currentTime + "\n【状态】已断开与服务器的连接"); ui->btn_send->setEnabled(false); // 禁用发送按钮 is_connected = false; emit quit_connect(); // 发出断开连接的信号 } }
-
(1)总体思路chat_thread.c
(处理聊天逻辑线程)
构造函数
初始化
Chat_thread
对象。接收消息
通过
readyRead
信号槽接收服务器发送的数据,并将其转发给主线程。处理连接状态变化
监控与服务器的连接状态,并打印调试信息。
断开连接
关闭套接字连接并释放资源,发出连接断开信号。
启动连接
通过指定的 IP 和端口号连接服务器,并处理连接成功、失败、断开、接收数据等事件。
发送消息
检查连接状态并发送消息。如果未连接,则发出未连接信号。
(2)详细流程chat_thread.c
(处理聊天逻辑线程)
-
构造函数:
-
构造函数
Chat_thread::Chat_thread(QObject *parent)
初始化Chat_thread
对象。在这个阶段不需要任何复杂的逻辑,主要是确保对象正常创建。
Chat_thread::Chat_thread(QObject *parent) : QObject{parent} {}
-
-
接收消息处理:
-
receive_message()
是一个槽函数,用于接收从服务器发送的数据。当QTcpSocket
对象有数据可读取时,信号readyRead
会被触发,调用此槽函数。读取数据后,通过transfer_recv_info
信号将接收到的消息发送出去。
void Chat_thread::receive_message() { QString message = socket->readAll(); // 从服务器读取数据 emit transfer_recv_info(message); // 发出信号,通知接收到的数据 }
-
-
处理连接状态变化:
-
state_changed()
函数是一个槽函数,用于处理客户端与服务器的连接状态变化。根据不同的QAbstractSocket::SocketState
枚举值,打印调试信息并处理相应状态的变化。
void Chat_thread::state_changed(QAbstractSocket::SocketState socketstate) { QString stateStr; // 用于保存状态的字符串 switch (socketstate) { case QAbstractSocket::UnconnectedState: qDebug()<< "\n【状态】与服务器断开连接..."; stateStr = "UnconnectedState"; break; case QAbstractSocket::ConnectedState: stateStr = "ConnectedState"; qDebug()<< "【状态】与服务器建立连接..."; break; case QAbstractSocket::HostLookupState: stateStr = "HostLookupState"; qDebug()<< "【状态】正在查找主机..."; break; case QAbstractSocket::ConnectingState: stateStr = "ConnectingState"; qDebug()<< "【状态】正在连接服务器..."; break; case QAbstractSocket::ClosingState: stateStr = "ClosingState"; qDebug()<< "【状态】正在关闭连接..."; break; default: stateStr = "UnknownState"; qDebug()<< "未知的错误, 当前状态: " + stateStr; break; } }
-
-
断开连接处理:
-
break_connected()
用于处理断开与服务器的连接。当套接字连接断开时,关闭并释放资源,并发出connect_cancel
信号通知主线程。
void Chat_thread::break_connected() { socket->close(); // 关闭套接字 socket->deleteLater(); // 延迟删除套接字,释放资源 emit connect_cancel(); // 发出连接断开信号 }
-
-
开始连接服务器:
-
start_connected()
用于发起连接服务器的请求。创建QTcpSocket
对象并尝试连接到指定的 IP 和端口。连接成功、失败、断开、接收数据等事件都会通过信号槽机制进行处理。
void Chat_thread::start_connected(QString IP, unsigned short PORT) { socket = new QTcpSocket; // 创建套接字对象 socket->connectToHost(QHostAddress(IP), PORT); // 连接到服务器 // 连接成功时,发送 connected 信号通知主线程上传消息 connect(socket, &QTcpSocket::connected, this, &Chat_thread::connected); // 连接失败时处理 connect(socket, &QTcpSocket::errorOccurred, this, [=](QAbstractSocket::SocketError socketError){ qDebug() << "连接失败"; QMessageBox::critical(nullptr, "连接失败", "连接失败,错误代码:" + QString::number(socketError)); }); // 连接断开时处理 connect(socket, &QTcpSocket::disconnected, this, &Chat_thread::break_connected); // 监听数据接收 connect(socket, &QTcpSocket::readyRead, this, &Chat_thread::receive_message); }
-
-
发送消息:
-
start_send()
用于发送消息到服务器。首先检查套接字是否处于连接状态,如果已连接,则发送消息。如果未连接,则发出not_connected
信号。
void Chat_thread::start_send(QString message) { if (socket && socket->state() == QAbstractSocket::ConnectedState) { socket->write(message.toUtf8()); // 发送消息 } else { emit not_connected(); // 如果未连接,发出未连接信号 } }
-