4. Qt网络编程基础

在嵌入式设备控制工程中,Qt 的网络模块(Qt Network)承担着与上位机、云端、其他节点通信的职责。本章聚焦四个最核心的类:QTcpSocket、QTcpServer、QUdpSocket 和 QHostAddress,并梳理 TCP 与 UDP 两条通信主线的完整流程。理解它们的职责边界与信号槽机制,是后续实现设备发现、指令下发、数据上报等功能的基础。

一、核心类职责与定位

Qt 将网络通信抽象为“套接字对象 + 事件驱动”的模型。所有网络类都继承自 QIODevice 或 QAbstractSocket,因此可以像读写文件一样使用 read()、write(),但真正的数据到达与连接变化通过信号通知。

  • QHostAddress:表示一个 IP 地址,支持 IPv4 与 IPv6。它不负责通信,只负责地址的解析、比较与转换,例如 QHostAddress::LocalHost、QHostAddress::Any、QHostAddress("192.168.1.10")。
  • QTcpSocket:面向连接的 TCP 客户端套接字,负责发起连接、收发字节流、感知断开。
  • QTcpServer:TCP 服务端监听器,负责绑定端口、监听连接,并在有新客户端接入时通过 nextPendingConnection() 返回一个 QTcpSocket。
  • QUdpSocket:无连接的数据报套接字,既可发送也可接收,常用于广播、组播和低延迟小数据场景。
关键区别:QTcpServer 只负责“接客”,真正通信的是它派生出的 QTcpSocket;而 QUdpSocket 一个对象同时承担收发两端角色,没有连接建立过程。

二、TCP 通信流程

TCP 是可靠的、面向连接的字节流协议,适合设备控制中的指令下发与状态回传。服务端与客户端的典型流程如下。

服务端(QTcpServer):

  1. 创建 QTcpServer 对象。
  2. 调用 listen(QHostAddress::Any, 8888) 绑定地址与端口。
  3. 连接 newConnection 信号,在槽中调用 nextPendingConnection() 获取客户端套接字。
  4. 为每个客户端套接字连接 readyRead、disconnected 信号,处理数据与断开。

客户端(QTcpSocket):

  1. 创建 QTcpSocket 对象。
  2. 调用 connectToHost(ip, port) 发起连接。
  3. 监听 connected 信号,连接成功后调用 write() 发送数据。
  4. 监听 readyRead 读取服务端返回,监听 disconnected 处理断线。
// 服务端关键代码
QTcpServer *server = new QTcpServer(this);
connect(server, &QTcpServer::newConnection, this, [=](){
    QTcpSocket *client = server->nextPendingConnection();
    connect(client, &QTcpSocket::readyRead, this, [=](){
        QByteArray data = client->readAll();
        qDebug() << "recv:" << data;
    });
});
server->listen(QHostAddress::Any, 8888);

// 客户端关键代码
QTcpSocket *sock = new QTcpSocket(this);
connect(sock, &QTcpSocket::connected, this, [=](){
    sock->write("HELLO_DEVICE");
});
sock->connectToHost(QHostAddress("192.168.1.10"), 8888);

三、UDP 通信流程

UDP 无连接、不保证可靠,但开销小、延迟低,适合设备广播发现、周期性传感器数据上报。QUdpSocket 的使用比 TCP 更直接:绑定端口后即可收发。

QUdpSocket *udp = new QUdpSocket(this);
udp->bind(QHostAddress::AnyIPv4, 9999);

// 接收
connect(udp, &QUdpSocket::readyRead, this, [=](){
    while (udp->hasPendingDatagrams()) {
        QByteArray buf;
        buf.resize(udp->pendingDatagramSize());
        QHostAddress sender;
        quint16 senderPort;
        udp->readDatagram(buf.data(), buf.size(), &sender, &senderPort);
        qDebug() << sender.toString() << senderPort << buf;
    }
});

// 发送广播
udp->writeDatagram("DISCOVER", QHostAddress::Broadcast, 9999);

注意:UDP 接收必须循环读取,直到 hasPendingDatagrams() 为假,否则可能遗漏同一事件中的多个数据报。

四、TCP 与 UDP 对比清单

对比项TCP(QTcpSocket/QTcpServer)UDP(QUdpSocket)
连接方式面向连接,三次握手无连接,直接发送
可靠性可靠,有序,自动重传不可靠,可能丢包、乱序
数据边界字节流,需自定义分包保留数据报边界
典型场景指令下发、文件传输、长连接设备发现、广播、周期上报
Qt 关键信号connected / readyRead / disconnectedreadyRead
服务端角色QTcpServer 监听,派生 QTcpSocket同一 QUdpSocket 收发

五、典型场景与误区复盘

场景:嵌入式网关需要先通过 UDP 广播发现局域网内的设备,再与目标设备建立 TCP 长连接下发控制指令。此时 QHostAddress 用于保存广播地址与设备回传的源地址,QUdpSocket 负责发现,QTcpServer/QTcpSocket 负责后续可靠通信。

常见误区一:在 GUI 线程中执行阻塞式 waitForConnected(),导致界面卡死。应优先使用信号槽异步方式。
常见误区二:TCP 的 readyRead 只触发一次就认为收到完整报文。TCP 是字节流,必须自行设计长度字段或分隔符进行拆包。
常见误区三:UDP 绑定端口后忘记循环读取,导致同一时刻多个数据报只处理了第一个。
常见误区四:直接使用 QHostAddress("localhost"),它只接受 IP 字面量,域名解析应使用 QHostInfo。

掌握以上四类与两条流程后,即可在嵌入式 Qt 工程中搭建稳定的设备通信骨架。实际开发中建议将 TCP 连接封装为独立管理类,统一处理重连、心跳与拆包,避免业务代码与套接字细节耦合。

Qt 网络核心类与 TCP / UDP 通信流程 QHostAddress IP 地址表示与解析 TCP 面向连接 QTcpServer listen / newConnection QTcpSocket connect / write / read 派生 connected → readyRead → disconnected 字节流,需自定义拆包 可靠、有序、自动重传 适用:指令下发、长连接 UDP 无连接 QUdpSocket bind / writeDatagram readyRead → readDatagram 保留数据报边界 不可靠、低延迟、可广播 适用:设备发现、周期上报 地址用于绑定与连接 TCP 客户端 / 服务端交互时序 客户端 QTcpSocket 服务端 QTcpServer connectToHost() 发起连接 newConnection → nextPendingConnection() write() 发送指令 readyRead() 回传状态