C++ Qt网络编程实战:跨平台TCP调试工具开发
作为一名 C++ 开发学习者,掌握 Qt 框架下的网络编程是提升综合开发能力的重要一环。
今天给大家带来 一个基于 TCP 协议的网络调试助手的设计与实现
如果你也在学习 Qt 或者准备做一个类似的网络通信小工具,希望这篇文章能为你提供清晰的开发思路和技术参考。欢迎留言交流你的实现经验!
Part1、TCP网络调试助手
本次设计的网络调试助手在简化界面UI的基础上,重点在于掌握网络通信的核心知识点,尤其是服务器与客户端的创建过程以及数据交互机制。同时,也借此机会复习和巩固了Qt UI控件的使用方法。
1.1、项目整体开发流程
1.2、Qt TCP服务器的关键流程
在创建基于QTcpServer
的服务端程序时,需遵循以下关键步骤:
1)、创建并初始化 QTcpServer
实例
- 实例化一个
QTcpServer
对象; - 调用
listen()
方法监听指定端口上的连接请求。
2)、处理新连接
- 将
newConnection
信号连接到对应的槽函数; - 在槽函数中通过
nextPendingConnection()
获取QTcpSocket
,用于与客户端通信。
3)、读取和发送数据
- 使用
readyRead
信号绑定槽函数,以接收来自客户端的数据; - 利用
write()
方法将响应数据发送回客户端。
4)、关闭连接
- 在适当的时候调用
close()
方法关闭QTcpSocket
连接。
示例代码如下:
class MyServer : public QObject {
Q_OBJECT
public:
MyServer() {
QTcpServer *server = new QTcpServer(this);
connect(server, &QTcpServer::newConnection, this, &MyServer::onNewConnection);
server->listen(QHostAddress::Any, 1234);
}
private slots:
void onNewConnection() {
QTcpSocket *clientSocket = server->nextPendingConnection();
connect(clientSocket, &QTcpSocket::readyRead, this, &MyServer::onReadyRead);
}
void onReadyRead() {
QTcpSocket *clientSocket = qobject_cast<QTcpSocket *>(sender());
QByteArray data = clientSocket->readAll();
// 处理收到的数据...
}
};
⚠️ 注意:在使用 QTcpServer 和 QTcpSocket 时,应妥善处理可能出现的网络错误和异常情况,如连接中断、超时等。
1.3、Qt TCP客户端的关键流程
创建基于 QTcpSocket
的客户端程序主要包括以下几个步骤:
- 创建
QTcpSocket
实例 - 连接到服务器
使用
connectToHost()
方法连接目标服务器的IP地址和端口号。
- 发送数据到服务器
使用
write()
方法发送请求或消息。
- 接收来自服务器的数据
将
readyRead
信号绑定到对应的槽函数,用于处理服务器返回的数据。
- 关闭连接
使用
close()
方法关闭当前连接。
示例代码如下:
class MyClient : public QObject {
Q_OBJECT
public:
MyClient() {
QTcpSocket *client = new QTcpSocket(this);
connect(client, &QTcpSocket::readyRead, this, &MyClient::onReadyRead);
client->connectToHost("server_address", 1234);
}
private slots:
void onReadyRead() {
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
QByteArray data = socket->readAll();
// 处理接收到的数据...
}
};
该客户端尝试连接到指定的服务器地址和端口,并等待服务器返回数据。开发者应根据实际需求合理管理网络错误与异常。
1.4、UI界面的设计
本项目中设计了两个主要界面:
TCP服务端UI界面
包括IP地址选择框、端口输入框、监听按钮、客户端连接状态显示区、数据收发文本框等;
TCP客户端UI界面
包括服务器IP与端口输入框、连接按钮、数据发送与接收框等。
1.5、TCP协议理论知识
以下为TCP协议的基础理论知识,虽在实际编程中由 QTcpSocket
类封装底层细节,但理解其原理对于面试准备及深入学习仍具有重要意义。
TCP协议的基本特点:
特性 | 描述 |
面向连接 | 通信前必须建立连接(三次握手) |
可靠传输 | 数据完整且无误地到达接收方 |
顺序控制 | 确保数据包按序重组 |
流量控制 | 使用滑动窗口机制避免过载 |
拥塞控制 | 动态调整传输速率防止网络拥塞 |
数据分段 | 大块数据自动分片传输 |
确认与重传 | 接收方确认接收,丢失则重传 |
终止连接 | 正常关闭连接(四次挥手) |
TCP连接建立 —— 三次握手
客户端发送SYN报文(同步);
服务器回复SYN-ACK(同步-确认);
客户端发送ACK报文,连接建立完成。
TCP连接终止 —— 四次挥手
一方发送FIN报文(结束);
对方回复ACK确认;
对方发送FIN报文;
原方回复ACK,连接关闭。
Socket的主要类型:
- TCP Socket
:面向连接、可靠;
- UDP Socket
:无连接、不可靠。
Socket的主要功能:
创建网络连接;
监听客户端连接;
发送与接收数据。
Qt中的Socket支持:
QTcpSocket
:用于实现TCP通信;
QUdpSocket
:用于实现UDP通信。
Socket抽象了网络通信的复杂性,是实现网络通信的重要基础工具之一。
Part2、网络通信核心代码
QTcpServer
是 Qt 网络模块的重要组成部分,用于构建TCP服务器。它可以异步监听客户端连接,并在连接建立后进行数据交换。
2.1、TCP服务端连接的核心代码
在类定义中声明服务器对象:
QTcpServer *server;
构造函数中实例化:
server = new QTcpServer(this);
点击监听按钮时启动监听并绑定信号:
void Widget::on_btnListen_clicked() {
QHostAddress addr("192.168.1.106");
quint16 port = 8888;
bool ret = server->listen(addr, port);
if (!ret) return;
connect(server, SIGNAL(newConnection()), this, SLOT(on_newClient_connect()));
}
当有客户端接入时获取连接并绑定接收数据信号:
void Widget::on_newClient_connect() {
if (server->hasPendingConnections()) {
QTcpSocket *tcpSocket = server->nextPendingConnection();
qDebug() << "client addr: " << tcpSocket->peerAddress().toString();
qDebug() << "client port: " << tcpSocket->peerPort();
ui->textEdit_Rev->append("addr: " + tcpSocket->peerAddress().toString());
ui->textEdit_Rev->append("port: " + QString::number(tcpSocket->peerPort()));
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(on_readyRead_handler()));
ui->comboBox_child->addItem(QString::number(tcpSocket->peerPort()));
ui->comboBox_child->setCurrentText(QString::number(tcpSocket->peerPort()));
}
}
2.2、TCP服务端的数据通信核心代码
当客户端发送数据时触发接收槽函数:
void Widget::on_readyRead_handler() {
QTcpSocket *tcpSocket = qobject_cast<QTcpSocket *>(sender());
QByteArray revData = tcpSocket->readAll();
ui->textEdit_Rev->append("client: " + revData);
}
发送按钮槽函数,支持向所有或指定客户端发送数据:
void Widget::on_btnSend_clicked() {
QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();
for (QTcpSocket *temp : clients) {
temp->write(ui->textEdit_Send->toPlainText().toStdString().c_str());
}
}
2.3、TCP客户端的核心代码
客户端对象定义与实例化:
QTcpSocket *client;
client = new QTcpSocket(this);
连接按钮槽函数:
void Widget::on_btnConnect_clicked() {
QString addr(ui->lineEdit_addr->text());
quint16 port = ui->lineEdit_port->text().toInt();
client->connectToHost(addr, port);
connect(client, SIGNAL(readyRead()), this, SLOT(on_readyRead_handler()));
}
数据接收槽函数:
void Widget::on_readyRead_handler() {
QByteArray revData = client->readAll();
ui->textEdit_rev->append("server: " + revData);
}
发送按钮槽函数:
void Widget::on_btnSend_clicked() {
QByteArray sendData = ui->textEdit_send->toPlainText().toUtf8();
client->write(sendData);
}
Part3TCP服务端项目功能优化
3.1、自动刷新IP地址
为了方便用户选择本地IP地址,可以在程序启动时自动扫描系统中可用的IPv4地址并填充至下拉框中:
QList<QHostAddress> addrList = QNetworkInterface::allAddresses();
for (QHostAddress addr : addrList) {
if (addr.protocol() == QAbstractSocket::IPv4Protocol) {
ui->comboBox_Addr->addItem(addr.toString());
}
}
3.2、服务器向不同客户端发数据
为实现向不同客户端单独发送数据的功能,可自定义一个继承于 QComboBox
的 myComboBox
类,并重写鼠标事件以触发自定义信号:
自定义类实现:
class myComboBox : public QComboBox {
Q_OBJECT
protected:
void mousePressEvent(QMouseEvent *e) override;
signals:
void on_ComboBox_clicked();
};
void myComboBox::mousePressEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton)
emit on_ComboBox_clicked();
QComboBox::mousePressEvent(e);
}
主界面中绑定信号与槽:
connect(ui->comboBox_child, &myComboBox::on_ComboBox_clicked, this, &Widget::on_refresh_comboBox);
刷新选项框内容:
void Widget::on_refresh_comboBox() {
ui->comboBox_child->clear();
QList<QTcpSocket*> clients = server->findChildren<QTcpSocket*>();
for (auto client : clients) {
ui->comboBox_child->addItem(QString::number(client->peerPort()));
}
ui->comboBox_child->addItem("all");
}
数据发送优化逻辑:
void Widget::on_btnSend_clicked() {
QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();
if (clients.isEmpty()) return;
QString target = ui->comboBox_child->currentText();
if (target != "all") {
for (auto client : clients) {
if (QString::number(client->peerPort()) == target) {
client->write(ui->textEdit_Send->toPlainText().toStdString().c_str());
}
}
} else {
for (auto client : clients) {
client->write(ui->textEdit_Send->toPlainText().toStdString().c_str());
}
}
ui->textEdit_Rev->moveCursor(QTextCursor::End);
ui->textEdit_Rev->ensureCursorVisible();
}
3.3、TextEdit 设置特定位置文字颜色
为了在 QTextEdit
控件中设置特定位置的文字颜色,需要通过光标级别的操作来实现。Qt 提供了 textCursor()
方法获取当前光标对象,并结合 setCharFormat()
实现字符格式的定制。
函数原型与嵌套关系如下:
QTextCursor: QTextEdit::textCursor() const
QTextCursor: void QTextCursor::setCharFormat(const QTextCharFormat &format) //方法
QTextCharFormat: void setForeground(const QBrush &brush) //方法
QBrush: QBrush(const QColor &color, const QPixmap &pixmap) //构造函数
QColor: QColor(const QColor &color) //构造函数
将该功能封装为一个函数,参数分别为字体颜色和待显示文本:
void Widget::setInsertColor(Qt::GlobalColor color, QString str)
{
// 获取当前光标位置
QTextCursor cursor = ui->textEdit_rev->textCursor();
QTextCharFormat format;
// 设置字符前景色
format.setForeground(QColor(color));
cursor.setCharFormat(format);
// 插入带颜色的文本并换行
cursor.insertText(str + "\n");
}
3.4、客户端断开检测
当客户端主动断开连接时,服务器会接收到 disconnected()
信号。通过绑定该信号与槽函数,可以及时检测到客户端的断开行为。
绑定 disconnected()
信号:
connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(on_disconnected()));
客户端断开连接槽函数实现:
在槽函数中完成以下操作:
在文本框中提示客户端已退出;
从下拉框中移除对应客户端的端口号;
删除客户端对象;
判断是否仍有连接中的客户端,决定是否禁用发送按钮。
void Widget::on_disconnected()
{
QTcpSocket *tcpSocket = qobject_cast<QTcpSocket *>(sender());
ui->textEdit_Rev->append("client quit!");
// 查找并移除选项框中对应的端口号
int tempIdx = ui->comboBox_child->findText(QString::number(tcpSocket->peerPort()));
if (tempIdx != -1) {
ui->comboBox_child->removeItem(tempIdx);
}
// 删除客户端对象
tcpSocket->deleteLater();
// 若无其他客户端,禁用发送按钮
if (server->findChildren<QTcpSocket*>().isEmpty()) {
ui->btnSend->setEnabled(false);
}
}
3.5、停止监听的实现
点击“停止监听”按钮后,需关闭所有已连接的客户端,并关闭服务器本身。
槽函数实现如下:
void Widget::on_btnStopListen_clicked()
{
// 获取所有已连接的客户端
QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();
for (QTcpSocket *temp : clients) {
temp->close(); // 关闭连接
}
server->close(); // 关闭服务器
}
Part4、TCP客户端项目开发及优化
4.1、检测连接状态
客户端成功连接服务器时会发出 connected()
信号,若连接失败则会发出 error()
信号。但由于错误信号响应存在延迟,因此可使用定时器机制判断是否超时。
构造函数中初始化定时器:
// 初始化定时器
timer = new QTimer(this);
timer->setSingleShot(true); // 单次触发
timer->setInterval(3000); // 超时时间为3秒
// 启动定时器
timer->start();
绑定信号与槽函数:
connect(client, SIGNAL(connected()), this, SLOT(on_connected()));
connect(timer, SIGNAL(timeout()), this, SLOT(on_timer_out()));
连接成功槽函数实现:
void Widget::on_connected()
{
timer->stop(); // 停止定时器
ui->textEdit_rev->append("连接成功");
// 更新控件状态
ui->btnDisconnect->setEnabled(true);
ui->btnSend->setEnabled(true);
ui->lineEdit_addr->setEnabled(false);
ui->lineEdit_port->setEnabled(false);
ui->btnConnect->setEnabled(false);
this->setEnabled(true);
// 光标定位至末尾
ui->textEdit_rev->moveCursor(QTextCursor::End);
ui->textEdit_rev->ensureCursorVisible();
}
连接超时槽函数实现:
void Widget::on_timer_out()
{
ui->textEdit_rev->append("连接超时");
client->abort(); // 中止连接
this->setEnabled(true);
on_btnDisconnect_clicked(); // 手动调用断开连接
}
4.2、其他细节功能
文本框特定颜色分区
与服务器端相同,使用自定义函数实现带颜色的文本插入:
void Widget::setInsertColor(Qt::GlobalColor color, QString str)
{
QTextCursor cursor = ui->textEdit_rev->textCursor();
QTextCharFormat format;
format.setForeground(QColor(color));
cursor.setCharFormat(format);
cursor.insertText(str + "\n");
}
控件使能与失能控制
通过 setEnabled()
方法控制控件的可用状态:
ui->btnDisconnect->setEnabled(false);
ui->btnSend->setEnabled(false);
文本框自动滚动到底部
确保每次插入新内容后,光标自动定位至最后一行:
ui->textEdit_rev->moveCursor(QTextCursor::End);
ui->textEdit_rev->ensureCursorVisible();
Part5、总结
主要知识点总结如下:
TCPServer 类相关 API 与常用信号
功能 | API |
创建服务器 | new QTcpServer(this) |
监听端口 | server->listen(addr, port) |
获取客户端 | nextPendingConnection() |
关闭连接 | close() |
常用信号 | 触发条件 |
newConnection() | 有新客户端连接时触发 |
QTcpSocket 类常用 API 与信号
功能 | API |
连接服务器 | connectToHost(addr, port) |
发送数据 | write(data) |
接收数据 | readAll() |
断开连接 | disconnectFromHost() 或 abort() |
常用信号 | 触发条件 |
readyRead() | 收到数据时触发 |
connected() | 成功连接服务器时触发 |
disconnected() | 客户端断开连接时触发 |
error() | 连接或通信过程中发生错误时触发 |
QTextEdit 内容读取与写入方法
操作 | 方法 |
读取全部内容 | toPlainText() |
插入带格式文本 | insertText() + setCharFormat() |
移动光标到底部 | moveCursor(QTextCursor::End) |
点击下方关注【Linux教程】,获取编程学习路线、项目教程、简历模板、大厂面试题pdf文档、大厂面经、编程交流圈子等等。