我将详细介绍 Qt 的网络编程,重点展示如何使用 Qt 提供的网络模块(QtNetwork)实现 TCP 客户端和服务器

我将详细介绍 Qt 的网络编程,重点展示如何使用 Qt 提供的网络模块(QtNetwork)实现 TCP 客户端和服务器,与前文的 ReferenceClass、PointerClass、SerialPort 和 ModbusTCPDebugger 集成,替代原有的 WinSock2 实现。同时,我将扩展 Qt GUI(基于前文 DebugWindow),添加网络状态监控、数据发送/接收展示,并支持 Modbus TCP 功能码(0x01、0x03、0x06)和 TCP-to-RTU 转换。代码将使用 std::string、std::vector、C 风格数组和迭代器,保持与前文的兼容性,并提供 Linux 适配说明。Demo 将通过 main 函数实例化类并启动 GUI,展示网络通信。

1. Qt 网络编程概述

Qt 的 QtNetwork 模块提供跨平台的网络编程接口,支持 TCP、UDP、HTTP、SSL 等协议。主要类包括:

  • QTcpSocket:实现 TCP 客户端,发送和接收数据。

  • QTcpServer:实现 TCP 服务器,监听连接并处理客户端。

  • QNetworkAccessManager:用于 HTTP 请求(本例不重点)。

  • QAbstractSocket:提供异步信号(如 connected、readyRead)。

  • 特点:

    • 跨平台:无需区分 Windows/Linux。

    • 异步:基于信号与槽,避免阻塞。

    • 集成性:与 Qt GUI 和事件循环无缝结合。

2. 设计目标

  • 替换 WinSock2:将 ModbusTCPDebugger 和 PointerClass 的网络部分改为 QtNetwork。

  • GUI 增强:

    • 显示网络状态(连接/断开)。

    • 展示 Modbus 数据(HEX 和解析值)。

    • 支持发送 Modbus 请求和串口命令。

    • 实时更新寄存器/线圈表格和图表。

  • 功能:

    • TCP 客户端:PointerClass 使用 QTcpSocket 发送 Modbus 请求。

    • TCP 服务器:ModbusTCPDebugger 使用 QTcpServer 处理客户端连接。

    • 异步 I/O:利用 Qt 信号与槽处理数据。

    • TCP-to-RTU:集成 ModbusConverter。

  • 场景:调试工具通过 TCP 与 Modbus 设备通信,通过串口与 RTU 设备交互,GUI 显示数据。

3. 前置类调整

以下是 SerialPort(保持不变)、ModbusConverter(前文定义)、ReferenceClass 和 PointerClass 的调整,以及重写的 ModbusTCPDebugger。

SerialPort(简要说明)

使用前文的异步 SerialPort 类,支持 asyncRead 和 write,无需修改。

ModbusConverter(简要说明)

保持前文的 TCP-to-RTU 转换函数,包括 CRC 计算和验证。

ReferenceClass(调整)

cpp

#include 
#include 
#include 
#include 
#include "SerialPort.h"

class ReferenceClass : public QObject {
    Q_OBJECT
private:
    std::vector& data;
    std::string& buffer;
    SerialPort& serial;

public:
    ReferenceClass(std::vector& d, std::string& b, SerialPort& s, QObject* parent = nullptr)
        : QObject(parent), data(d), buffer(b), serial(s) {
        if (data.empty()) {
            throw std::runtime_error("ReferenceClass: Data vector cannot be empty");
        }
    }

    void asyncReadFromSerial(std::function&)> callback) {
        serial.asyncRead(data, [this, callback](const std::vector& serialData) {
            std::string str(serialData.begin(), serialData.end());
            buffer += str;
            std::string num;
            for (char c : str) {
                if (c == ' ') {
                    if (!num.empty()) data.push_back(std::stoi(num));
                    num.clear();
                } else {
                    num += c;
                }
            }
            if (!num.empty()) data.push_back(std::stoi(num));
            callback(serialData);
            emit dataUpdated();
        });
    }

    void writeToSerial(const std::string& command) {
        std::vector data(command.begin(), command.end());
        serial.write(data);
        buffer += "Sent: " + command;
        emit dataUpdated();
    }

    const std::vector& getData() const { return data; }
    const std::string& getBuffer() const { return buffer; }

signals:
    void dataUpdated();
};

调整:

  • 继承 QObject 以支持信号与槽。

  • 添加 dataUpdated 信号,通知 GUI 数据变化。

PointerClass(调整)

cpp

#include 
#include 
#include 
#include 
#include 
#include 

class PointerClass : public QObject {
    Q_OBJECT
private:
    std::vector* data;
    std::string* buffer;
    std::unique_ptr dynamicArray;
    size_t arraySize;
    QTcpSocket* tcpSocket;
    uint16_t transactionId;

public:
    PointerClass(std::vector* d, std::string* b, size_t size, const std::string& host, int port, QObject* parent = nullptr)
        : QObject(parent), data(d), buffer(b), arraySize(size), tcpSocket(new QTcpSocket(this)), transactionId(0) {
        if (!data || !buffer) {
            throw std::runtime_error("PointerClass: Null pointer provided");
        }
        dynamicArray = std::unique_ptr(new int[size]);
        for (size_t i = 0; i < size; ++i) {
            dynamicArray[i] = static_cast(i);
        }
        tcpSocket->connectToHost(QString::fromStdString(host), port);
        connect(tcpSocket, &QTcpSocket::connected, this, &PointerClass::onConnected);
        connect(tcpSocket, &QTcpSocket::readyRead, this, &PointerClass::onReadyRead);
        connect(tcpSocket, &QTcpSocket::disconnected, this, &PointerClass::onDisconnected);
    }

    void asyncModbusRequest(uint16_t functionCode, uint16_t startAddr, uint16_t countOrValue,
                           std::function&)> callback) {
        if (tcpSocket->state() != QAbstractSocket::ConnectedState) {
            emit errorOccurred("Not connected");
            return;
        }
        std::vector frame = createModbusFrame(functionCode, startAddr, countOrValue);
        tcpSocket->write(QByteArray(reinterpret_cast(frame.data()), frame.size()));
        *buffer += "Sent Modbus: ";
        for (auto it = frame.begin(); it != frame.end(); ++it) {
            *buffer += std::to_string(static_cast(*it) & 0xFF) + " ";
        }
        // 存储回调
        connect(this, &PointerClass::responseReceived, this, [=](const std::vector& data) {
            callback(data);
            emit dataUpdated();
        }, Qt::SingleShotConnection);
    }

    void updateDynamicArray(size_t index, int value) {
        if (index >= arraySize) {
            throw std::out_of_range("Dynamic array index out of range");
        }
        dynamicArray[index] = value;
    }

    std::vector parseModbusResponse(const std::vector& response) {
        std::vector values;
        if (response.size() >= 9) {
            uint8_t functionCode = response[7];
            if (functionCode == 0x03) {
                for (size_t i = 0; i < response[8] / 2; ++i) {
                    values.push_back((response[9 + 2 * i] << 8) | response[9 + 2 * i + 1]);
                    data->push_back(values.back());
                }
            } else if (functionCode == 0x01) {
                for (size_t i = 0; i < response[8]; ++i) {
                    uint8_t byte = response[9 + i];
                    for (size_t j = 0; j < 8; ++j) {
                        values.push_back((byte >> j) & 1);
                        data->push_back(values.back());
                    }
                }
            } else if (functionCode == 0x06) {
                values.push_back((response[10] << 8) | response[11]);
                data->push_back(values.back());
            }
        }
        return values;
    }

    const std::vector& getData() const { return *data; }
    const std::string& getBuffer() const { return *buffer; }

signals:
    void dataUpdated();
    void responseReceived(const std::vector& data);
    void errorOccurred(const QString& error);
    void connectionStateChanged(bool connected);

private slots:
    void onConnected() {
        emit connectionStateChanged(true);
    }

    void onDisconnected() {
        emit connectionStateChanged(false);
    }

    void onReadyRead() {
        QByteArray response = tcpSocket->readAll();
        std::vector data(response.begin(), response.end());
        emit responseReceived(data);
    }

private:
    std::vector createModbusFrame(uint16_t functionCode, uint16_t startAddr, uint16_t countOrValue) {
        std::vector frame(12);
        transactionId++;
        frame[0] = (transactionId >> 8) & 0xFF;
        frame[1] = transactionId & 0xFF;
        frame[2] = 0x00;
        frame[3] = 0x00;
        frame[4] = 0x00;
        frame[5] = 0x06;
        frame[6] = 0x01;
        frame[7] = functionCode;
        frame[8] = (startAddr >> 8) & 0xFF;
        frame[9] = startAddr & 0xFF;
        frame[10] = (countOrValue >> 8) & 0xFF;
        frame[11] = countOrValue & 0xFF;
        return frame;
    }
};

调整:

  • 使用 QTcpSocket 替换 WinSock2。

  • 继承 QObject,添加信号(如 dataUpdated、responseReceived)。

  • 异步请求通过信号与槽处理响应。

ModbusTCPDebugger(重写)

cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class ModbusTCPDebugger : public QObject {
    Q_OBJECT
private:
    QTcpServer* server;
    std::vector coils = std::vector(100, false);
    std::vector registers = {0x1234, 0x5678, 0x9ABC, 0xDEF0};
    std::mutex dataMutex;
    std::ofstream logFile;
    QThreadPool* threadPool;

public:
    ModbusTCPDebugger(const std::string& host, int port, QObject* parent = nullptr)
        : QObject(parent), server(new QTcpServer(this)), logFile("modbus_log.txt", std::ios::app), threadPool(new QThreadPool(this)) {
        threadPool->setMaxThreadCount(4);
        if (!server->listen(QHostAddress(QString::fromStdString(host)), port)) {
            throw std::runtime_error("Server failed to listen");
        }
        connect(server, &QTcpServer::newConnection, this, &ModbusTCPDebugger::handleNewConnection);
    }

private slots:
    void handleNewConnection() {
        QTcpSocket* clientSocket = server->nextPendingConnection();
        connect(clientSocket, &QTcpSocket::readyRead, this, [=]() {
            class ClientTask : public QRunnable {
                QTcpSocket* socket;
                ModbusTCPDebugger* debugger;
            public:
                ClientTask(QTcpSocket* s, ModbusTCPDebugger* d) : socket(s), debugger(d) {}
                void run() override {
                    QByteArray data = socket->readAll();
                    std::vector buffer(data.begin(), data.end());
                    debugger->logData("Received: ", buffer);
                    std::vector response;
                    {
                        std::lock_guard lock(debugger->dataMutex);
                        response = debugger->processModbusFrame(buffer);
                    }
                    if (!response.empty()) {
                        socket->write(QByteArray(reinterpret_cast(response.data()), response.size()));
                        debugger->logData("Sent: ", response);
                    }
                }
            };
            threadPool->start(new ClientTask(clientSocket, this));
        });
        connect(clientSocket, &QTcpSocket::disconnected, clientSocket, &QTcpSocket::deleteLater);
    }

private:
    std::vector processModbusFrame(const std::vector& frame) {
        if (frame.size() < 8) return {};
        uint16_t transactionId = (frame[0] << 8) | frame[1];
        uint16_t protocolId = (frame[2] << 8) | frame[3];
        uint16_t length = (frame[4] << 8) | frame[5];
        uint8_t unitId = frame[6];
        uint8_t functionCode = frame[7];

        if (protocolId != 0x0000 || length != frame.size() - 6) return {};

        if (functionCode == 0x01 && frame.size() >= 12) { // 读线圈
            uint16_t startAddr = (frame[8] << 8) | frame[9];
            uint16_t count = (frame[10] << 8) | frame[11];
            if (startAddr + count <= coils.size()) {
                size_t byteCount = (count + 7) / 8;
                std::vector response(9 + byteCount);
                response[0] = frame[0]; response[1] = frame[1];
                response[2] = 0x00; response[3] = 0x00;
                response[4] = 0x00; response[5] = 3 + byteCount;
                response[6] = unitId; response[7] = functionCode;
                response[8] = byteCount;
                for (size_t i = 0; i < byteCount; ++i) {
                    uint8_t byte = 0;
                    for (size_t j = 0; j < 8 && (i * 8 + j) < count; ++j) {
                        byte |= (coils[startAddr + i * 8 + j] ? 1 : 0) << j;
                    }
                    response[9 + i] = byte;
                }
                return response;
            }
        } else if (functionCode == 0x03 && frame.size() >= 12) { // 读寄存器
            uint16_t startAddr = (frame[8] << 8) | frame[9];
            uint16_t count = (frame[10] << 8) | frame[11];
            if (startAddr + count <= registers.size()) {
                std::vector response(9 + 2 * count);
                response[0] = frame[0]; response[1] = frame[1];
                response[2] = 0x00; response[3] = 0x00;
                response[4] = 0x00; response[5] = 3 + 2 * count;
                response[6] = unitId; response[7] = functionCode;
                response[8] = 2 * count;
                for (size_t i = 0; i < count; ++i) {
                    response[9 + 2 * i] = (registers[startAddr + i] >> 8) & 0xFF;
                    response[9 + 2 * i + 1] = registers[startAddr + i] & 0xFF;
                }
                return response;
            }
        } else if (functionCode == 0x06 && frame.size() >= 12) { // 写寄存器
            uint16_t addr = (frame[8] << 8) | frame[9];
            uint16_t value = (frame[10] << 8) | frame[11];
            if (addr < registers.size()) {
                registers[addr] = value;
                return frame;
            }
        }
        std::vector response(9);
        response[0] = frame[0]; response[1] = frame[1];
        response[2] = 0x00; response[3] = 0x00;
        response[4] = 0x00; response[5] = 0x03;
        response[6] = unitId; response[7] = functionCode | 0x80;
        response[8] = 0x02;
        return response;
    }

    void logData(const std::string& prefix, const std::vector& data) {
        std::lock_guard lock(dataMutex);
        logFile << prefix;
        for (auto it = data.begin(); it != data.end(); ++it) {
            logFile << std::hex << std::setw(2) << std::setfill('0') << (static_cast(*it) & 0xFF) << " ";
        }
        logFile << std::dec << "\n";
        logFile.flush();
    }
};

重写:

  • 使用 QTcpServer 和 QTcpSocket。

  • QThreadPool 处理客户端任务。

  • 线程安全通过 std::mutex 保证。

4. 扩展的 Qt GUI

cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "ReferenceClass.h"
#include "PointerClass.h"
#include "ModbusTCPDebugger.h"
#include "ModbusConverter.h"

class DebugWindow : public QMainWindow {
    Q_OBJECT
private:
    ReferenceClass* refClass;
    PointerClass* ptrClass;
    QTextEdit* logText;
    QLineEdit* commandInput;
    QComboBox* commandType;
    QTableWidget* dataTable;
    QChart* chart;
    QChartView* chartView;
    QLineSeries* regSeries;
    QTimer* updateTimer;
    QLabel* statusLabel;
    std::vector lastRegValues;

public:
    DebugWindow(ReferenceClass* ref, PointerClass* ptr, QWidget* parent = nullptr)
        : QMainWindow(parent), refClass(ref), ptrClass(ptr) {
        setWindowTitle("Qt Network Debug Tool");
        resize(900, 700);
        QWidget* centralWidget = new QWidget(this);
        setCentralWidget(centralWidget);
        QVBoxLayout* mainLayout = new QVBoxLayout(centralWidget);

        // 状态标签
        statusLabel = new QLabel("Disconnected", this);
        mainLayout->addWidget(statusLabel);

        // 日志区域
        logText = new QTextEdit(this);
        logText->setReadOnly(true);
        mainLayout->addWidget(logText);

        // 数据表格
        dataTable = new QTableWidget(10, 3, this);
        dataTable->setHorizontalHeaderLabels({"Address", "Register", "Coil"});
        mainLayout->addWidget(dataTable);

        // 图表
        chart = new QtCharts::QChart;
        regSeries = new QtCharts::QLineSeries;
        chart->addSeries(regSeries);
        chart->createDefaultAxes();
        chart->setTitle("Register Values");
        chartView = new QtCharts::QChartView(chart);
        chartView->setRenderHint(QPainter::Antialiasing);
        mainLayout->addWidget(chartView);

        // 命令输入
        QHBoxLayout* inputLayout = new QHBoxLayout;
        commandType = new QComboBox(this);
        commandType->addItems({"Serial", "Modbus Read Coils (0x01)", "Modbus Read Registers (0x03)", "Modbus Write Register (0x06)"});
        commandInput = new QLineEdit(this);
        QPushButton* sendButton = new QPushButton("Send", this);
        inputLayout->addWidget(commandType);
        inputLayout->addWidget(commandInput);
        inputLayout->addWidget(sendButton);
        mainLayout->addLayout(inputLayout);

        // 定时器
        updateTimer = new QTimer(this);
        updateTimer->start(500);

        // 信号与槽
        connect(sendButton, &QPushButton::clicked, this, &DebugWindow::sendCommand);
        connect(updateTimer, &QTimer::timeout, this, &DebugWindow::updateUI);
        connect(refClass, &ReferenceClass::dataUpdated, this, &DebugWindow::updateUI);
        connect(ptrClass, &PointerClass::dataUpdated, this, &DebugWindow::updateUI);
        connect(ptrClass, &PointerClass::connectionStateChanged, this, &DebugWindow::updateStatus);
        connect(ptrClass, &PointerClass::errorOccurred, this, &DebugWindow::showError);

        // 初始请求
        refClass->asyncReadFromSerial([this](const std::vector& data) {
            logText->append("Serial: " + QString::fromStdString(formatData(data)));
        });
        ptrClass->asyncModbusRequest(0x03, 0, 4, [this](const std::vector& data) {
            logText->append("Modbus: " + QString::fromStdString(formatData(data)));
        });
    }

private slots:
    void sendCommand() {
        std::string cmd = commandInput->text().toStdString();
        std::stringstream ss(cmd);
        std::string type = commandType->currentText().toStdString();
        if (type == "Serial") {
            refClass->writeToSerial(cmd + "\n");
            logText->append("Sent Serial: " + QString::fromStdString(cmd));
        } else if (type == "Modbus Read Coils (0x01)") {
            int start, count;
            ss >> start >> count;
            ptrClass->asyncModbusRequest(0x01, start, count, [this](const std::vector& data) {
                logText->append("Modbus Coils: " + QString::fromStdString(formatData(data)));
            });
        } else if (type == "Modbus Read Registers (0x03)") {
            int start, count;
            ss >> start >> count;
            ptrClass->asyncModbusRequest(0x03, start, count, [this](const std::vector& data) {
                logText->append("Modbus Registers: " + QString::fromStdString(formatData(data)));
            });
        } else if (type == "Modbus Write Register (0x06)") {
            int addr, value;
            ss >> addr >> value;
            ptrClass->asyncModbusRequest(0x06, addr, value, [this](const std::vector& data) {
                logText->append("Modbus Write: " + QString::fromStdString(formatData(data)));
            });
            // TCP-to-RTU 示例
            std::vector tcpFrame = ptrClass->createModbusFrame(0x06, addr, value);
            std::vector rtuFrame = ModbusConverter::tcpToRtu(tcpFrame);
            refClass->writeToSerial(std::string(rtuFrame.begin(), rtuFrame.end()));
        }
        commandInput->clear();
    }

    void updateUI() {
        updateTable();
        updateChart(ptrClass->getData());
    }

    void updateStatus(bool connected) {
        statusLabel->setText(connected ? "Connected" : "Disconnected");
        statusLabel->setStyleSheet(connected ? "color: green" : "color: red");
    }

    void showError(const QString& error) {
        logText->append("Error: " + error);
    }

private:
    void updateTable() {
        const auto& data = ptrClass->getData();
        for (size_t i = 0; i < std::min(data.size(), 10); ++i) {
            dataTable->setItem(i, 0, new QTableWidgetItem(QString::number(i)));
            dataTable->setItem(i, 1, new QTableWidgetItem(QString::number(data[i])));
            ptrClass->asyncModbusRequest(0x01, i, 1, [this, i](const std::vector& data) {
                auto values = ptrClass->parseModbusResponse(data);
                if (!values.empty()) {
                    dataTable->setItem(i, 2, new QTableWidgetItem(QString::number(values[0])));
                }
            });
        }
    }

    void updateChart(const std::vector& values) {
        if (values == lastRegValues) return;
        lastRegValues = values;
        regSeries->clear();
        for (size_t i = 0; i < values.size(); ++i) {
            regSeries->append(i, values[i]);
        }
        chart->axes(Qt::Horizontal)->first()->setRange(0, values.size());
        chart->axes(Qt::Vertical)->first()->setRange(0, *std::max_element(values.begin(), values.end()) + 100);
    }

    std::string formatData(const std::vector& data) {
        std::stringstream ss;
        for (auto byte : data) {
            ss << std::hex << std::setw(2) << std::setfill('0') << (static_cast(byte) & 0xFF) << " ";
        }
        return ss.str();
    }
};

5. Main 函数

cpp

#include 
#include "ReferenceClass.h"
#include "PointerClass.h"
#include "ModbusTCPDebugger.h"
#include "ModbusConverter.h"

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    try {
        std::vector data = {10, 20, 30};
        std::string buffer = "Initial";
        SerialPort serial("COM3", CBR_9600);

        ModbusTCPDebugger server("0.0.0.0", 502);
        ReferenceClass refClass(data, buffer, serial);
        PointerClass ptrClass(&data, &buffer, 3, "127.0.0.1", 502);

        DebugWindow window(&refClass, &ptrClass);
        window.show();
        return app.exec();
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
}

6. 代码说明

  • Qt 网络编程:

    • PointerClass:使用 QTcpSocket 连接服务器,信号 readyRead 触发数据读取。

    • ModbusTCPDebugger:使用 QTcpServer 监听端口,newConnection 处理客户端,QThreadPool 并发处理。

    • 异步处理通过信号与槽实现,无需手动线程管理。

  • GUI 增强:

    • 状态标签 (QLabel):显示连接状态,绿色表示连接,红色表示断开。

    • 日志区域 (QTextEdit):显示 HEX 格式的串口和 Modbus 数据。

    • 表格 (QTableWidget):实时更新寄存器和线圈。

    • 图表 (QChart):绘制寄存器值曲线。

    • 命令输入:支持串口命令和 Modbus 请求(0x01、0x03、0x06)。

  • TCP-to-RTU:

    • 在 Modbus Write Register 时,生成 RTU 帧并通过串口发送。

  • 数据结构:

    • std::vector 存储 Modbus 帧。

    • std::string 存储日志。

    • std::unique_ptr 管理动态数组。

7. 输出示例

假设串口返回 "1 2 3",Modbus 服务器返回寄存器 {0x1234, 0x5678} 和线圈 {0, 1}:

  • 日志区域:

    Serial: 01 02 03
    Modbus Registers: 00 01 00 00 00 07 01 03 04 12 34 56 78
    Modbus Coils: 00 02 00 00 00 04 01 01 01 02
  • 表格:

    Address | Register | Coil
    0       | 4660     | 0
    1       | 22136    | 1
  • 图表:显示曲线(4660, 22136)。

  • 状态:Connected(绿色)。

8. Linux 适配

  • QtNetwork:跨平台,无需修改。

  • SerialPort:替换 Windows API 为 POSIX(如 open, read, write)。

  • 编译:

    g++ -o app main.cpp ReferenceClass.cpp PointerClass.cpp SerialPort.cpp ModbusTCPDebugger.cpp ModbusConverter.cpp -std=c++11 -pthread $(pkg-config --cflags --libs Qt5Widgets Qt5Charts Qt5Network)
  • 依赖:安装 libqt5network5-dev.

9. 扩展建议

  • UDP 支持:使用 QUdpSocket 实现广播通信。

  • SSL 加密:使用 QSslSocket 保护 Modbus 通信。

  • 更多控件:添加 QProgressBar 显示数据传输进度。

  • 保存日志:使用 QFile 保存日志为 CSV。

10. 注意事项

  • 线程安全:Qt 信号与槽确保主线程更新 UI。

  • 性能:大量数据时,限制日志行数或使用 QAbstractTableModel。

  • 错误处理:通过 errorOccurred 信号显示网络错误。

如果你需要 UDP 实现、SSL 加密、特定控件(如 QProgressBar),或更详细的日志保存,请提供细节,我可以进一步扩展!

你可能感兴趣的:(C++,网络,qt,tcp/ip)