C++ QT项目:TCP网络调试助手的设计与实现

1 TCP网络调试助手

本次设计的网络调试助手简化界面UI的设计,重点在于网络通信的相关知识点,侧重服务器与客户端的创建以及服务器与客户端的数据交互,同时复习巩固之前UI控件

1.1 项目整体开发流程

1.2 QTtcp服务器的关键流程

工程建立,需要在.pro加入网络权限

创建一个基于 QTcpServer 的服务端涉及以下关键步骤:

1. 创建并初始化 QTcpServer 实例:

  • 实例化 QTcpServer
  • 调用listen 方法在特定端口监听传入的连接

2. 处理新连接:

  • 为 newConnection 信号连接一个槽函数。
  • 在槽函数中,使用 nextPendingConnection 获取 QTcpSocket 以与客户端通信。

3. 读取和发送数据:

  • 通过连接 QTcpSocket 的 readyRead 信号来读取来自客户端的数据。
  • 使用 write 方法发送数据回客户端

4. 关闭连接:

  • 在适当的时候关闭 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();
    	// 处理数据
    	// ...
 	}
};

确保在使用 QTcpServerQTcpSocket 时妥善处理网络错误和异常情况

1.3 QTtcp客户端的关键流程

工程建立,需要在.pro加入网络权限

创建一个基于 QTcpSocket 的Qt客户端涉及以下步骤:

1. 创建 QTcpSocket 实例:

  • 实例化 QTcpSocket

2. 连接到服务器:

  • 使用 connectToHost 方法连接到服务器的IP地址和端口。

3. 发送数据到服务器:

  • 使用 write 方法发送数据。

4. 接收来自服务器的数据:

  • 为 readyRead 信号连接一个槽函数来接收数据。

5. 关闭连接:

  • 关闭 QTcpSocket 连接。

示例代码如下:

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界面设计:

TCP客户端UI界面设计:

1.5 TCP协议理论知识

以下内容自行阅读和消化,主要在面试之前类似八股文问答,实际编程我们不需要关系这么多, QTcpSocket类底下的API已经做好所有的封装。

TCP(传输控制协议)是一种广泛使用的网络通信协议,设计用于在网络中的计算机之间可靠地传输数据。它是互联网协议套件的核心部分,通常与IP(互联网协议)一起使用,合称为TCP/IP。

以下是TCP协议的一些基本特点:

1. 面向连接: 在数据传输之前,TCP 需要在发送方和接收方之间建立一个连接。这包括三次握手过程,确保两端都准备好进行数据传输。

2. 可靠传输: TCP 提供可靠的数据传输服务,这意味着它保证数据包准确无误地到达目的地。如果发生数据丢失或错误,TCP 会重新发送数据包。

3. 顺序控制:TCP 保证数据包的传输顺序。即使数据包在网络中的传输顺序被打乱,接收方也能按照正确的顺重组这些数据。

4. 流量控制:TCP 使用窗口机制来控制发送方的数据传输速率,以防止网络过载。这有助于防止接收方被发送方发送的数据所淹没。

5. 拥塞控制:TCP 还包括拥塞控制机制,用来检测并防止网络拥塞。当网络拥塞发生时,TCP 会减少其数据传输速率。

6. 数据分段:大块的数据在发送前会被分割成更小的段,以便于传输。这些段会被独立发送并在接收端重新组装。

7. 确认和重传:接收方对成功接收的数据包发送确认(ACK)信号。如果发送方没有收到确认,它会重传丢失的数据包。

8. 终止连接:数据传输完成后,TCP 连接需要被正常关闭,这通常涉及到四次挥手过程。

TCP 适用于需要高可靠性的应用,如网页浏览、文件传输、电子邮件等。然而,由于它的这些特性,TCP在处理速度上可能不如其他协议(如UDP)那么快速。TCP协议中的三次握手和四次挥手是建立和终止连接的重要过程。

三次握手(建立连接)

三次握手的主要目的是在两台设备之间建立一个可靠的连接。它包括以下步骤:

1. SYN:客户端向服务器发送一个SYN(同步序列编号)报文来开始一个新的连接。此时,客户端进入SYN-SENT状态。

2. SYN-ACK:服务器接收到SYN报文后,回复一个SYN-ACK(同步和确认)报文。此时服务器进入SYN-RECEIVED状态。

3. ACK:客户端接收到SYN-ACK后,发送一个ACK(确认)报文作为回应,并进入ESTABLISHED(已建立)状态。服务器在收到这个ACK报文后,也进入ESTABLISHED状态。这标志着连接已经建立。

四次挥手(断开连接)

四次挥手的目的是终止已经建立的连接。这个过程包括以下步骤:

1. FIN:当通信的一方完成数据发送任务后,它会发送一个FIN(结束)报文来关闭连接。发送完FIN报文后,该方进入FIN-WAIT-1状态。

2. ACK: 另一方接收到FIN报文后,发送一个ACK报文作为回应,并进入CLOSE-WAIT状态。发送FIN报文的一方在收到ACK后,进入FIN-WAIT-2状态。

3. FIN: 在等待一段时间并完成所有数据的发送后,CLOSE-WAIT状态的一方也发送一个FIN报文来请求关闭连接。

4. ACK:最初发送FIN报文的一方在收到这个FIN报文后,发送一个ACK报文作为最后的确认,并进入TIME-WAIT状态。经过一段时间后,确保对方接收到了最后的ACK报文,该方最终关闭连接。

在这两个过程中,三次握手主要确保双方都准备好进行通信,而四次挥手则确保双方都已经完成通信并同意关闭连接。

1.6 Socket网络通信理论

Socket 不是一个协议,而是一种编程接口(API)或机制,用于在网络中实现通信。Socket 通常在应用层和传输层之间提供一个端点,使得应用程序可以通过网络发送和接收数据。它支持多种协议,主要是TCP 和 UDP。

以下是 Socket 的一些基本特点:

  • 类型: 有两种主要类型的 Sockets —— TCP Socket(面向连接,可靠)和 UDP Socket(无连接,不可靠)。
  • 应用: 在各种网络应用中广泛使用,如网页服务器、聊天应用、在线游戏等。
  • 编程语言支持: 大多数现代编程语言如 Python, Java, C++, 等都提供 Socket 编程的支持。
  • 功能: 提供了创建网络连接、监听传入的连接、发送和接收数据等功能。
  • QT: 在QT组件中,QTcpSocket用来管理和实现TCP Socket通信,QUdpSocket用来管理和实现UDP Socket通信

总之,Socket 是实现网络通信的基础工具之一,它抽象化了网络层的复杂性,为开发者提供了一种相对简单的方式来建立和管理网络连接

如果正在学习QT开发的同学,可以看下面这个教程,从基础入门到多个项目实战的讲解

C++ Qt开发入门到实战项目教程(项目含源码)

2 网络通信核心代码

QTcpServer是 Qt 网络模块的一部分,用于构建 TCP 服务器。它提供了一种机制来异步监听来自客户端的连接。一旦接受了一个连接,服务器就可以与客户端进行数据交换。

2.1 TCP服务端连接的核心代码

在类定义时,将服务器对象定义为成员变量,供槽函数调用,在构造函数中对服务器对象进行实例化。

//类定义
QTcpServer *server;

//构造函数内
//1. 创建服务器对象
server = new QTcpServer(this);

启动监听槽函数

在监听槽函数中,启动监听,在有新的客户端接入时会发出newConnection信号,因此通过绑定信号与槽可检测客户端接入。

//监听按钮槽函数 启动监听
void Widget::on_btnListen_clicked()
{
    //设置服务器IP地址和端口号变量 启动监听
    QHostAddress addr("192.168.1.106");
    quint16 port = 8888;

    //2. 启动监听
    bool ret = server->listen(addr, port); //自动获取IP地址
    if(ret == false)
        return;

    //绑定监听客户端接入的信号与槽函数
    connect(server, SIGNAL(newConnection()), this, SLOT(on_newClient_connect()));
}

客户端成功接入槽函数

在有客户端成功接入时,通过nextPendingConnection方法获取客户端对象,将客户端的IP地址与端口号分别打印在数据接收框内,同时绑定数据接收函数(当有数据达到时,会触发readyRead()信号)

//检测客户端接入槽函数 对应newConnection信号
void Widget::on_newClient_connect()
{
    //判断是否有新的客户端接入
    if(server->hasPendingConnections())
    {
        //获取客户端对象
        QTcpSocket *tcpSocket = server->nextPendingConnection();

        //打印客户端IP地址与端口号
        qDebug() << "client addr: " << tcpSocket->peerAddress().toString();
        qDebug() << "client port: " << tcpSocket->peerPort();


        //接收消息框显示客户端IP地址与端口号
        ui->textEdit_Rev->append("addr: " + tcpSocket->peerAddress().toString());
        ui->textEdit_Rev->append("port: " +QString::number(tcpSocket->peerPort()));

        //绑定接收readyRead信号与槽函数
        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进行发送,以此需要通过 `qobject_cast<>(sender())` 来获取信号发送方
    //模仿 串口调试助手中的on_command_button_clicked 获取信号发送者
    QTcpSocket *tcpSocket = qobject_cast<QTcpSocket *>(sender());
    //接收数据
    QByteArray revData = tcpSocket->readAll();
    //将接收到的数据显示到文本框
    ui->textEdit_Rev->append("client: " + revData);
}

数据发送按钮槽函数

void Widget::on_btnSend_clicked()
{
    //通过server的findChildren方法获取客户端对象
    QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();

    //遍历所有子客户端,调用write方法像所有客户端发送消息
    for(QTcpSocket *temp: clients)
    {
        temp->write(ui->textEdit_Send->toPlainText().toStdString().c_str());
    }
}

2.3 TCP客户端的核心代码

客户端的设计与服务端类似,定义客户端对象并在构造函数内实例化对象

QTcpSocket *client;

//实例化客户端对象
client = new QTcpSocket(this);

连接按钮槽函数

在连接按钮槽函数中主要利用connectToHost方法将客户端与服务器建立连接,同时绑定readyRead()信号与数据接收槽函数

//连接按钮槽函数
void Widget::on_btnConnect_clicked()
{
    //获取行编辑器内IP地址与端口号
    QString addr(ui->lineEdit_addr->text());
    quint16 port = ui->lineEdit_port->text().toInt();

    //请求连接服务器
    client->connectToHost(addr, port);

    //绑定接收readyRead信号与槽函数
    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);
}

3 TCP服务端项目功能优化

3.1 自动刷新IP地址

为了程序自主检测系统上可用的IP地址,为了实现自主刷新可用IP地址并添加至选项框,在构造函数中采用QNetworkInterface下的allAddresses()来检测可用的IP地址,并添加至选项框,同时在启动监听时读取选项框当前IP地址数据即可。

//自动扫描IP地址 存放在列表中
QList<QHostAddress> addrList = QNetworkInterface::allAddresses();
for(QHostAddress addr :  addrList)
{
//IP地址协议为IPv4协议
    if(addr.protocol() == QAbstractSocket::IPv4Protocol)
    {
//将网络添加至选项框
        ui->comboBox_Addr->addItem(addr.toString());
    }
}

3.2 服务器向不同客户端发数据

通过自定义控件、左键点击选项框触发事件获取当前接口的客户端的端口号,并添置至选项框,最后通过不同的端口号向不同的客户端发送数据。自定义myComboBox类,并继承于QComboBox,同时在UI界面将IP地址选项框提升为自定义控件

1. 自定义类自定义继承实现:

2. 重写鼠标左键事件,触发事件发生自定义信号,最后将事件重新传回原路径执行

//声明
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);
}

3. 绑定信号与槽,该自定义信号供主页面程序检测,若检测到该信号则跳转到相应的槽函数进行端口号的刷新。

//绑定信号与槽,检测comboBox按下,扫描已接入的客户端
connect(ui->comboBox_child, &myComboBox::on_ComboBox_clicked, this, &Widget::on_refresh_comboBox);

选项框按下槽函数:刷新不同客户端的端口号

//刷新不同客户端接入的端口号
void Widget::on_refresh_comboBox()
{
    ui->comboBox_child->clear();
    //通过server获取客户单对象
    QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();
    for(QTcpSocket *temp: clients)
    {
        //客户端不为空 且端口号不为0 加入选项框
        ui->comboBox_child->addItem(QString::number(temp->peerPort()));
    }
    ui->comboBox_child->addItem("all");
}

4. UI界面的控件提升:

5. 优化发送函数,根据个人选项,向不同的客户端发送数据,根据用户选择 找出具体客户端进行数据通信, 通过端口查找下标确定

//数据发送槽函数
void Widget::on_btnSend_clicked()
{
    //通过server获取客户单对象
    QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();

    if(clients.isEmpty())
        return;


    //选择特定端口发送
    if(ui->comboBox_child->currentText() != "all")
    {
        //根据用户选择 找出具体客户端进行数据通信, 通过端口查找下标确定
        QString currentName = ui->comboBox_child->currentText();
        for(QTcpSocket *temp : clients)
        {
            if(QString::number(temp->peerPort()) == currentName)
            {
                temp->write(ui->textEdit_Send->toPlainText().toStdString().c_str());
            }
        }
    }
    //所有端口发送
    else
    {
        //遍历所有子客户端,调用write方法像所有客户端发送消息
        for(QTcpSocket *temp: clients)
        {
            temp->write(ui->textEdit_Send->toPlainText().toStdString().c_str());
        }
    }

    //光标移至末尾
    ui->textEdit_Rev->moveCursor(QTextCursor::End);
    ui->textEdit_Rev->ensureCursorVisible();
}

3.3 TextEdit设置特定位置文字颜色

为了设置 textEdit 中特定位置的文字颜色,需要光标级别的设置。通过 textCursor() 方法来获取当前的光标位置, 继而通过 cursor 下的 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() 信号,通过绑定信号与槽实现对客户端断开连接的检测

//绑定disconnect断开连接信号
connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(on_disconnected()));

客户端断开连接槽函数:检测到客户端断开后,主要在文本框内进行提示,在端口选项框中移除该客户端的端口号,并将客户端进行删除,

//客户端断开连接信号槽函数
void Widget::on_disconnected()
{
	//通过 qobject_cast 获取信号发送者
    QTcpSocket *tcpSocket = qobject_cast<QTcpSocket *>(sender());
    ui->textEdit_Rev->append("client quit!");

    //通过内容找到选项框索引,并在选项框中移除
    int tempIdx;
    tempIdx = ui->comboBox_child->findText(QString::number(tcpSocket->peerPort()));
    ui->comboBox_child->removeItem(tempIdx);

    //删除客户端
    tcpSocket->deleteLater();

    //判断是否还存在连接着的客户端
    if(server->findChildren<QTcpSocket*>().count() == 0)
        ui->btnSend->setEnabled(false);
}

3.5 停止监听的实现

停止监听槽函数主要通过服务器的 findChildren<>()查找所有接入的客户端并遍历删除即可,同时关闭服务器

//停止监听槽函数
void Widget::on_btnStopListen_clicked()
{
    //获取所有客户端
    QList<QTcpSocket *> clients = server->findChildren<QTcpSocket*>();
    for(QTcpSocket *temp: clients)
    {
        temp->close();
    }
    server->close();
}

4 TCP客户端项目开发及优化

4.1 检测连接状态

客户端在成功接入服务器后会发出 connected() 信号,接入失败会发出 error() 信号。但实际接入失败发出 error() 信号需要一定的时间,因此通过定时器计算接入的时间,若接入时间大于3秒则认为连接超时。

构造函数内定时器的设置

//设置定时器 检测连接超时
timer = new QTimer(this);
timer->setSingleShot(true); //触发一次
timer->setInterval(3000);   //设置连接超时时间
//启动定时器
timer->start();

绑定信号与槽

//成功连接发出connected信号
connect(client, SIGNAL(connected()), this, SLOT(on_connected()));
//绑定定时器溢出槽函数
connect(timer, SIGNAL(timeout()), this, SLOT(on_timer_out()));

成功接入

连接成功进入 connected() 信号的槽函数

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();
}

连接超时

当定时器超过3秒溢出后则认为是连接超时(失败),中止本次连接

void Widget::on_timer_out()
{
    ui->textEdit_rev->append("连接超时");
    client->abort();			//中止客户端连接
    this->setEnabled(true);
    on_btnDisconnect_clicked(); //手动调用断开连接
}

4.2 其他细节功能

文本框特定颜色分区:同TCP服务器功能的3.3

//设置插入数据的颜色
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方法可设置控件的有效性,其中参数为true时表使能控件,false失能控件

//设置控件属性
ui->btnDisconnect->setEnabled(false);
ui->btnSend->setEnabled(false);

文本框显示最后一行(即最新的内容)

//光标移至末尾
ui->textEdit_rev->moveCursor(QTextCursor::End);
ui->textEdit_rev->ensureCursorVisible();

5 项目总结

  • TCPServer类关于监听,连接,发送,接收的API
  • TCPServer在网络通信中常用的信号
  • TCPScoket在QT实现Socket常用的API
  • TCPScoket在QT实现Socket常用的信号
  • EditText的内容读取方法,内容写入方法,在特定行写入特点颜色的方法

TCPServerTCPScoketEditText 的信息整合到一个表格中:

引用:【C++ QT项目3】--网络调试助手的设计与实现

#c++##实习##校招##秋招##项目#
全部评论

相关推荐

评论
1
3
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务