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):
- 创建
QTcpServer对象。 - 调用
listen(QHostAddress::Any, 8888)绑定地址与端口。 - 连接
newConnection信号,在槽中调用nextPendingConnection()获取客户端套接字。 - 为每个客户端套接字连接
readyRead、disconnected信号,处理数据与断开。
客户端(QTcpSocket):
- 创建
QTcpSocket对象。 - 调用
connectToHost(ip, port)发起连接。 - 监听
connected信号,连接成功后调用write()发送数据。 - 监听
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 / disconnected | readyRead |
| 服务端角色 | QTcpServer 监听,派生 QTcpSocket | 同一 QUdpSocket 收发 |
五、典型场景与误区复盘
场景:嵌入式网关需要先通过 UDP 广播发现局域网内的设备,再与目标设备建立 TCP 长连接下发控制指令。此时 QHostAddress 用于保存广播地址与设备回传的源地址,QUdpSocket 负责发现,QTcpServer/QTcpSocket 负责后续可靠通信。
常见误区一:在 GUI 线程中执行阻塞式
常见误区二:TCP 的
常见误区三:UDP 绑定端口后忘记循环读取,导致同一时刻多个数据报只处理了第一个。
常见误区四:直接使用
waitForConnected(),导致界面卡死。应优先使用信号槽异步方式。常见误区二:TCP 的
readyRead 只触发一次就认为收到完整报文。TCP 是字节流,必须自行设计长度字段或分隔符进行拆包。常见误区三:UDP 绑定端口后忘记循环读取,导致同一时刻多个数据报只处理了第一个。
常见误区四:直接使用
QHostAddress("localhost"),它只接受 IP 字面量,域名解析应使用 QHostInfo。
掌握以上四类与两条流程后,即可在嵌入式 Qt 工程中搭建稳定的设备通信骨架。实际开发中建议将 TCP 连接封装为独立管理类,统一处理重连、心跳与拆包,避免业务代码与套接字细节耦合。