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(); // 处理数据 // ... } };
确保在使用 QTcpServer 和 QTcpSocket 时妥善处理网络错误和异常情况
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开发的同学,可以看下面这个教程,从基础入门到多个项目实战的讲解
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的内容读取方法,内容写入方法,在特定行写入特点颜色的方法
TCPServer 、 TCPScoket 和 EditText 的信息整合到一个表格中: