目录
一、项目介绍
1、项目背景
在网络应用开发和嵌入式系统调试过程中,开发者经常需要模拟 客户端-服务器 交互,以验证数据传输的正确性和稳定性。然而,传统的网络调试工具通常难以同时支持多个客户端的管理,或者缺乏灵活的调试能力。因此,本项目基于 Qt 框架实现了一款 功能强大、易于使用 的网络调试助手,旨在提高开发和测试效率。
2、项目功能
- 
支持TCP连接 :使用**Socket网络编程,**客户端和服务器的连接,是通过网络连接起来的,数据传输方式TCP,比较可靠。 
- 
监听指定 IP 和端口,支持多个客户端连接 
- 
管理多个客户端,支持客户端连接、断开及状态监控 
- 
收发消息,支持单个或所有客户端的消息广播 
- 
断开连接及停止监听,确保资源释放 
本项目为网络开发人员提供了一个高效、直观的调试工具,能够大幅提升 网络通信协议的调试效率 ,同时也可用于 嵌入式设备调试、物联网设备通信测试 等场景。
3、开发流程

二、UI设计
1、服务端

使用的控件命名如下图所示:

2、客户端
 使用的控件命名如下图所示:
使用的控件命名如下图所示:

在设计布局的时候,可以大体上从上到下分几块,每个小块里面先水平布局,每个小块布局好之后,在所有的小块进行垂直布局,在调整每个小块的占比,可以一点一点调整,上下左右再留一点空白,这样布局会看着比较舒服。

这样设计出来的窗口是固定大小,那么怎样可以在用户使用的时候,鼠标拉动窗口,控件可以跟着改变呢?
其实很简单,只需要一行代码就可以搞定,在Widget的构造方法中添加:
this->setLayout(ui->verticalLayout);三、代码实现
QTcpServer 是 Qt 框架中用于实现 TCP 服务器功能的类,属于 QtNetwork 模块。它是 Qt 网络编程的重要组成部分,主要用于创建和管理 TCP 服务器端。
主要功能
- 
TCP 服务端监听 :使用 QTcpServer可以轻松地创建一个 TCP 服务器,监听特定的 IP 地址和端口。
- 
连接管理 :当客户端尝试连接到服务器时, QTcpServer提供信号(如newConnection())通知应用程序有新的连接请求。你可以通过重写虚函数或使用信号与槽来处理这些事件。
- 
数据通信 :连接建立后,可以通过派生自 QTcpSocket的套接字类来进行客户端和服务器之间的数据收发。
- 
多线程支持:Qt 的网络模块默认是异步的,这意味着服务器可以在不阻塞主线程的情况下处理多个连接。 
1、QT中使用TCP的关键流程
首先需要在创建项目后,在.pro文件中加入network网络权限

2、服务器开发
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QNetworkInterface>
#include<QTcpSocket>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    //初始化 UI 界面的代码,它的作用是加载 Qt Designer 设计的 UI 布局,并将界面元素绑定到 ui 指针中
    ui->setupUi(this);
    //布局自适应窗口
    this->setLayout(ui->verticalLayout);
    
    //实例化一个QTcpServer对象
    server = new QTcpServer(this);
    //绑定信号与槽
    connect(ui->comboBoxChildren,&MyComboBox::on_comboBox_clicked,this,&Widget::mComboBox_refresh);
    connect(server,SIGNAL(newConnection()),this,SLOT(on_newClient_connect()));
    //一些细节的修改,在没有客户端连接的时候,停止监听、断开连接以及发送按钮不可用
    ui->btnLineout->setEnabled(false);
    ui->btnStopListen->setEnabled(false);
    ui->btnSend->setEnabled(false);
    //获取本机的所有网络接口地址,并将 IPv4 地址添加到 comboBoxAddr 下拉框中,供用户选择服务器监听的地址。
    QList<QHostAddress> addresss = QNetworkInterface::allAddresses();
    for(QHostAddress tmp :addresss){
        if(tmp.protocol()==QAbstractSocket::IPv4Protocol){
            ui->comboBoxAddr->addItem(tmp.toString());
        }
    }
}
Widget::~Widget()
{
    //析构
    delete ui;
}
void Widget::on_newClient_connect()
{
    qDebug()<<"newClient In!";
    //如果有新连接进来
    if(server->hasPendingConnections()){
        //获得客户端连接信息
        QTcpSocket *connection = server->nextPendingConnection();
        qDebug()<<"client Addr:"<< connection->peerAddress().toString() <<"port:"<<connection->peerPort();
        ui->textEditRev->insertPlainText("客户端地址:"+connection->peerAddress().toString()+
                                         "\n客户端端口号:"+QString::number(connection->peerPort())+"\n");
        connect(connection,SIGNAL(readyRead()),this,SLOT(on_readyRead_handler()));
        connect(connection,SIGNAL(disconnected()),this,SLOT(mdisconnected()));
        connect(connection,SIGNAL(stateChange(QAbstractSocket::SocketState)),
                this,SLOT(mststeChanged(QAbstractSocket::SocketState socketState)));
        //有新连接进来的时候在comboBoxChildren添加新条目,并显示当前最新进入的连接
        ui->comboBoxChildren->addItem(QString::number(connection->peerPort()));
        ui->comboBoxChildren->setCurrentText(QString::number(connection->peerPort()));
        
        if(!ui->btnSend->isEnabled()){
            ui->btnSend->setEnabled(true);
        }
    }
}
void Widget::on_readyRead_handler()
{
    //获取当前连接对象,sender() 返回当前触发信号的对象,但它是一个 QObject* 类型。转换为 QTcpSocket*。
    QTcpSocket *tmpSock = qobject_cast<QTcpSocket*>(sender());
    QByteArray revData = tmpSock->readAll();
    //光标移到末尾
    ui->textEditRev->moveCursor(QTextCursor::End);
    ui->textEditRev->ensureCursorVisible();
    //将数据显示到textEditRev
    ui->textEditRev->insertPlainText("客户端:"+revData);
}
void Widget::mdisconnected()
{
    //当客户端断开连接时,删除相应的 QTcpSocket 对象,并在 UI 上显示 客户端断开!
    QTcpSocket *tmpSock = qobject_cast<QTcpSocket *>(sender());
    ui->textEditRev->insertPlainText("客户端断开!");
    tmpSock->deleteLater();
}
void Widget::mststeChanged(QAbstractSocket::SocketState socketState)
{
    int tmpIndex;
    QTcpSocket *tmpSock = qobject_cast<QTcpSocket *>(sender());
    switch (socketState) {
    //如果客户端断开,通过端口号查找comboBoxChildren的索引,并删除条目,最后删除相应的 QTcpSocket 对象
    case QAbstractSocket::UnconnectedState:
        //    case QAbstractSocket::ClosingState:
        ui->textEditRev->insertPlainText("客户端断开!");
        tmpIndex = ui->comboBoxChildren->findText(QString::number(tmpSock->peerPort()));
        ui->comboBoxChildren->removeItem(tmpIndex);
        tmpSock->deleteLater();
        //如果所有客户端都断开连接后,发送键不可用
        if(ui->comboBoxChildren->count()==0)
            ui->btnSend->setEnabled(false);
        break;
    case QAbstractSocket::ConnectedState:
    case QAbstractSocket::ConnectingState:
        ui->textEditRev->insertPlainText("客户端接入!");
        break;
    }
}
void Widget::mComboBox_refresh()
{
    ui->comboBoxChildren->clear();
    //接收所有连接的客户端,将端口号添加下拉框条目中
    QList<QTcpSocket*> tcpSocketClients = server->findChildren<QTcpSocket*>();
    for(QTcpSocket* tmp:tcpSocketClients){
        if(tmp!=nullptr)
            if(tmp->peerPort()!=0)
                ui->comboBoxChildren->addItem(QString::number(tmp->peerPort()));
    }
    //all支持向所有客户端通信
    ui->comboBoxChildren->addItem("all");
}
void Widget::on_btnListen_clicked()
{
    //服务器开始监听指定的 IP 地址和端口,等待客户端连接
    //    QHostAddress addr("");
    int port = ui->lineEditPort->text().toInt();
    //    if(!server->listen(QHostAddress::Any,port))
    if(!server->listen(QHostAddress(ui->comboBoxAddr->currentText()),port)){
        qDebug()<< "listenError";
        QMessageBox msgBox;
        msgBox.setWindowTitle("监听失败");
        msgBox.setText("端口号被占用");
        msgBox.exec();
        return;
    }
    
    //监听键不可用,停止监听、断开连接可用
    ui->btnListen->setEnabled(false);
    ui->btnLineout->setEnabled(true);
    ui->btnStopListen->setEnabled(true);
}
void Widget::on_btnSend_clicked()
{
    QList<QTcpSocket*> tcpSocketClients = server->findChildren<QTcpSocket*>();
    //如果当前无连接,提示发送失败
    if(tcpSocketClients.isEmpty()){
        QMessageBox msgBox;
        msgBox.setWindowTitle("发送错误!");
        msgBox.setText("当前无连接!");
        msgBox.exec();
        ui->btnSend->setEnabled(false);
        return;
    }
    if(ui->comboBoxChildren->currentText() != "all"){
        //根据用户选择,找到指定客户端进行通信,通过childIndex来查找,该变量在用户选择条目后触发的on_comboBoxChildren_activated去赋值
        QString currentName = ui->comboBoxChildren->currentText();
        for(QTcpSocket* tmp:tcpSocketClients){
            if(QString::number(tmp->peerPort())==currentName){
                tmp->write(ui->textEditSend->toPlainText().toStdString().c_str());
            }
        }
//        tcpSocketClients[childIndex]->write(ui->textEditSend->toPlainText().toStdString().c_str());
    }else{
        //遍历所有子客户端,并一一调用write函数,向所有客户端发送消息
        for(QTcpSocket* tmp:tcpSocketClients){
            tmp->write(ui->textEditSend->toPlainText().toStdString().c_str());
        }
    }
}
void Widget::on_btnStopListen_clicked()
{
    //关闭所有的客户端连接
    QList<QTcpSocket*> tcpSocketClients = server->findChildren<QTcpSocket *>();
    for(QTcpSocket* tmp:tcpSocketClients){
        tmp->close();
    }
    //关闭服务器,停止监听
    server->close();
    ui->btnListen->setEnabled(true);
    ui->btnLineout->setEnabled(false);
    ui->btnStopListen->setEnabled(false);
}
void Widget::on_btnLineout_clicked()
{
    //停止监听
    on_btnListen_clicked();
    //释放服务器对象 server,避免内存泄漏
    delete server;
    //关闭窗口
    this->close();
}给代码添加了详细的注释,有问题欢迎一起讨论。】
3、客户端开发
#include "widget.h"
#include "ui_widget.h"
#include <QTimer>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    //初始化ui界面
    ui->setupUi(this);
    //布局自适应
    this->setLayout(ui->verticalLayout);
    //设置按钮状态
    ui->btndiscon->setEnabled(false);
    ui->btnSend->setEnabled(false);
    //实例化一个QTcpSocket对象
    client = new QTcpSocket(this);
    connect(client,SIGNAL(readyRead()),this,SLOT(mRead_Data_From_Server()));
}
Widget::~Widget()
{
    //析构
    delete ui;
}
void Widget::on_btnConnect_clicked()
{
    //通过ip端口号创建连接
    client->connectToHost(ui->lineEditIPAddr->text(),ui->lineEditPort->text().toInt());
    //实例化一个计时器,五秒没连接成功给提示
    timer = new QTimer(this);
    timer->setInterval(5000);
    connect(timer,SIGNAL(timeout()),this,SLOT(onTimeOut()));
    connect(client,SIGNAL(connected()),this,SLOT(onConnected()));
    connect(client,SIGNAL(QAbstractSocket::SocketError),this,SLOT(onError(QAbstractSocket::SocketError)));
    //连接按钮按下的时候全窗口不可用,等成功连接时设置可用
    this->setEnabled(false);
    //启动定时器
    timer->start();
}
void Widget::mRead_Data_From_Server()
{
    //读取数据
    ui->textEditRev->moveCursor(QTextCursor::End);
    ui->textEditRev->ensureCursorVisible();
    //服务端数据接收之后用黑色字体显示
    mInsertTextByColor(Qt::black,client->readAll());
}
void Widget::on_btnSend_clicked()
{
    //发送数据
    QByteArray sendData = ui->textEditSend->toPlainText().toUtf8();
    client->write(sendData);
    //客户端发送数据之后用红色字体显示
    mInsertTextByColor(Qt::red,sendData);
}
void Widget::on_btndiscon_clicked()
{
    //关闭客户端
    client->close();
    ui->textEditRev->append("终止连接!");
    //设置按钮状态
    ui->btnConnect->setEnabled(true);
    ui->lineEditIPAddr->setEnabled(true);
    ui->lineEditPort->setEnabled(true);
    ui->btndiscon->setEnabled(false);
    ui->btnSend->setEnabled(false);
}
void Widget::onConnected()
{
    //连接成功之后关闭计时器,设置按钮状态
    timer->stop();
    this->setEnabled(true);
    ui->textEditRev->append("连接成功!");
    ui->btnConnect->setEnabled(false);
    ui->lineEditIPAddr->setEnabled(false);
    ui->lineEditPort->setEnabled(false);
    ui->btndiscon->setEnabled(true);
    ui->btnSend->setEnabled(true);
}
void Widget::onError(QAbstractSocket::SocketError error)
{
    qDebug()<<"连接错误!"<<error;
    ui->textEditRev->insertPlainText("连接出问题啦:"+client->errorString());
    this->setEnabled(true);
    on_btndiscon_clicked();
}
void Widget::onTimeOut()
{
    ui->textEditRev->insertPlainText("连接超时");
    client->abort();
    this->setEnabled(true);
}
void Widget::mInsertTextByColor(Qt::GlobalColor color,QString str)
{
    QTextCursor cursor = ui->textEditRev->textCursor();
    QTextCharFormat format;
    format.setForeground(QBrush(QColor(color)));
    cursor.setCharFormat(format);
    cursor.insertText(str);
}四、结果测试
连接成功


服务端客户端进行通信


服务器停止监听断开连接

客户端断开连接


五、项目总结
|--------------|--------|---------------------------------------------------------|
| 类别           | 功能     | API/方法                                                  |
| TcpServer    | 监听     | bool listen(const QHostAddress& address, quint16 port) |
| TcpServer    | 连接     | QTcpSocket*nextPendingConnection()                     |
| TcpServer    | 关闭     | void close()                                            |
| TcpServer 信号 | 新连接    | newConnection()                                         |
| TcpScoket    | 连接     | void connectToHost(const QString &host, quint16 port)  |
| TcpScoket    | 发送     | qint64 write(const QByteArray& data)                   |
| TcpScoket    | 接收     | QByteArray readAll()                                    |
| TcpScoket    | 断开     | void disconnectFromHost()                               |
| TcpScoket 信号 | 连接 成功  | connected()                                             |
| TcpScoket 信号 | 断开 连接  | disconnected()                                          |
| TcpScoket 信号 | 有可读数据  | readyRead()                                             |
| TcpScoket 信号 | 成功写入数据 | bytesWritten(qint64 bytes)                              |
| EditText     | 读数据    | String getText()                                        |
| EditText     | 写数据    | void setText(String text)                               |
| EditText     | 改变颜色   | void changeTextColor(int start, int end, int color)     |