云计算百科
云计算领域专业知识百科平台

我将详细介绍 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 <string>
#include <vector>
#include <functional>
#include <QObject>
#include "SerialPort.h"

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

public:
ReferenceClass(std::vector<int>& 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<void(const std::vector<uint8_t>&)> callback) {
serial.asyncRead(data, [this, callback](const std::vector<uint8_t>& 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<uint8_t> data(command.begin(), command.end());
serial.write(data);
buffer += "Sent: " + command;
emit dataUpdated();
}

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

signals:
void dataUpdated();
};

调整:

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

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

PointerClass(调整)

cpp

#include <memory>
#include <string>
#include <vector>
#include <QObject>
#include <QTcpSocket>
#include <QHostAddress>

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

public:
PointerClass(std::vector<int>* 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<int[]>(new int[size]);
for (size_t i = 0; i < size; ++i) {
dynamicArray[i] = static_cast<int>(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<void(const std::vector<uint8_t>&)> callback) {
if (tcpSocket->state() != QAbstractSocket::ConnectedState) {
emit errorOccurred("Not connected");
return;
}
std::vector<uint8_t> frame = createModbusFrame(functionCode, startAddr, countOrValue);
tcpSocket->write(QByteArray(reinterpret_cast<const char*>(frame.data()), frame.size()));
*buffer += "Sent Modbus: ";
for (auto it = frame.begin(); it != frame.end(); ++it) {
*buffer += std::to_string(static_cast<unsigned int>(*it) & 0xFF) + " ";
}
// 存储回调
connect(this, &PointerClass::responseReceived, this, [=](const std::vector<uint8_t>& 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<int> parseModbusResponse(const std::vector<uint8_t>& response) {
std::vector<int> 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<int>& getData() const { return *data; }
const std::string& getBuffer() const { return *buffer; }

signals:
void dataUpdated();
void responseReceived(const std::vector<uint8_t>& 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<uint8_t> data(response.begin(), response.end());
emit responseReceived(data);
}

private:
std::vector<uint8_t> createModbusFrame(uint16_t functionCode, uint16_t startAddr, uint16_t countOrValue) {
std::vector<uint8_t> 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 <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <string>
#include <vector>
#include <fstream>
#include <mutex>
#include <QThreadPool>
#include <QRunnable>

class ModbusTCPDebugger : public QObject {
Q_OBJECT
private:
QTcpServer* server;
std::vector<bool> coils = std::vector<bool>(100, false);
std::vector<uint16_t> 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<uint8_t> buffer(data.begin(), data.end());
debugger->logData("Received: ", buffer);
std::vector<uint8_t> response;
{
std::lock_guard<std::mutex> lock(debugger->dataMutex);
response = debugger->processModbusFrame(buffer);
}
if (!response.empty()) {
socket->write(QByteArray(reinterpret_cast<const char*>(response.data()), response.size()));
debugger->logData("Sent: ", response);
}
}
};
threadPool->start(new ClientTask(clientSocket, this));
});
connect(clientSocket, &QTcpSocket::disconnected, clientSocket, &QTcpSocket::deleteLater);
}

private:
std::vector<uint8_t> processModbusFrame(const std::vector<uint8_t>& 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t>& data) {
std::lock_guard<std::mutex> lock(dataMutex);
logFile << prefix;
for (auto it = data.begin(); it != data.end(); ++it) {
logFile << std::hex << std::setw(2) << std::setfill('0') << (static_cast<unsigned int>(*it) & 0xFF) << " ";
}
logFile << std::dec << "\\n";
logFile.flush();
}
};

重写:

  • 使用 QTcpServer 和 QTcpSocket。

  • QThreadPool 处理客户端任务。

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

4. 扩展的 Qt GUI

cpp

#include <QApplication>
#include <QMainWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QTableWidget>
#include <QComboBox>
#include <QTimer>
#include <QLabel>
#include <QtCharts/QChart>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#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<int> 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<uint8_t>& data) {
logText->append("Serial: " + QString::fromStdString(formatData(data)));
});
ptrClass->asyncModbusRequest(0x03, 0, 4, [this](const std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<uint8_t>& data) {
logText->append("Modbus Write: " + QString::fromStdString(formatData(data)));
});
// TCP-to-RTU 示例
std::vector<uint8_t> tcpFrame = ptrClass->createModbusFrame(0x06, addr, value);
std::vector<uint8_t> 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<size_t>(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<uint8_t>& data) {
auto values = ptrClass->parseModbusResponse(data);
if (!values.empty()) {
dataTable->setItem(i, 2, new QTableWidgetItem(QString::number(values[0])));
}
});
}
}

void updateChart(const std::vector<int>& 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<uint8_t>& data) {
std::stringstream ss;
for (auto byte : data) {
ss << std::hex << std::setw(2) << std::setfill('0') << (static_cast<unsigned int>(byte) & 0xFF) << " ";
}
return ss.str();
}
};

5. Main 函数

cpp

#include <QApplication>
#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<int> 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<uint8_t> 存储 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),或更详细的日志保存,请提供细节,我可以进一步扩展!

赞(0)
未经允许不得转载:网硕互联帮助中心 » 我将详细介绍 Qt 的网络编程,重点展示如何使用 Qt 提供的网络模块(QtNetwork)实现 TCP 客户端和服务器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!