当前位置: 首页 > ds >正文

Qt UDP 网络编程详解

一、UDP 协议核心特性

  1. 无连接协议:通信前无需建立连接

  2. 面向数据报:每次发送/接收独立的数据包

  3. 轻量高效:头部开销小(仅8字节)

  4. 不可靠传输

    • 不保证数据包顺序

    • 不保证数据包到达

    • 无自动重传机制

  5. 适用场景

    • 实时音视频传输

    • DNS 查询

    • 网络状态广播

    • 游戏状态同步

二、Qt UDP 核心类

类名

功能说明

QUdpSocket

UDP 数据报套接字

QHostAddress

IP 地址封装

QNetworkDatagram

数据报容器(Qt 5.8+)

三、UDP 发送端实现

#include <QUdpSocket>
#include <QDebug>class UdpSender : public QObject {Q_OBJECT
public:explicit UdpSender(QObject *parent = nullptr) : QObject(parent), udpSocket(new QUdpSocket(this)) {// 设置广播选项udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);}// 发送广播消息void broadcastMessage(const QString &message) {QByteArray datagram = message.toUtf8();// 发送到广播地址qint64 bytesSent = udpSocket->writeDatagram(datagram,QHostAddress::Broadcast,  // 255.255.255.25545454);if (bytesSent == -1) {qWarning() << "Failed to send datagram:" << udpSocket->errorString();} else {qInfo() << "Broadcasted message:" << message << "to" << QHostAddress::Broadcast.toString();}}// 发送到指定地址void sendTo(const QString &message, const QHostAddress &address, quint16 port) {QByteArray datagram = message.toUtf8();udpSocket->writeDatagram(datagram, address, port);}private:QUdpSocket *udpSocket;
};

四、UDP 接收端实现

#include <QUdpSocket>
#include <QNetworkDatagram>class UdpReceiver : public QObject {Q_OBJECT
public:explicit UdpReceiver(QObject *parent = nullptr) : QObject(parent), udpSocket(new QUdpSocket(this)) {// 绑定到端口接收数据if (!udpSocket->bind(45454, QUdpSocket::ShareAddress)) {qCritical() << "Bind failed:" << udpSocket->errorString();return;}qInfo() << "Listening on UDP port 45454...";// 连接数据到达信号connect(udpSocket, &QUdpSocket::readyRead, this, &UdpReceiver::processPendingDatagrams);}private slots:void processPendingDatagrams() {while (udpSocket->hasPendingDatagrams()) {// 使用QNetworkDatagram获取元数据(Qt 5.8+)QNetworkDatagram datagram = udpSocket->receiveDatagram();if (!datagram.isValid()) continue;QByteArray data = datagram.data();QHostAddress senderAddress = datagram.senderAddress();quint16 senderPort = datagram.senderPort();qInfo() << "Received from" << senderAddress.toString() << ":" << senderPort << "=>" << data;// 示例:处理特定命令if (data.startsWith("PING")) {sendResponse(senderAddress, senderPort);}}}void sendResponse(const QHostAddress &addr, quint16 port) {udpSocket->writeDatagram("PONG", addr, port);}private:QUdpSocket *udpSocket;
};

五、关键技术解析

1、绑定模式选项

// 常用绑定选项
udpSocket->bind(port, QUdpSocket::ShareAddress);      // 允许多个套接字绑定同一端口
udpSocket->bind(port, QUdpSocket::ReuseAddressHint);  // 地址重用
udpSocket->bind(QHostAddress::AnyIPv4);               // 监听所有IPv4接口

2、数据报读写方法对比

// 传统方法(Qt 4/5)
char buffer[1024];
qint64 size = udpSocket->pendingDatagramSize();
udpSocket->readDatagram(buffer, size, &senderAddr, &senderPort);// 现代方法(Qt 5.8+)
QNetworkDatagram datagram = udpSocket->receiveDatagram();
if (datagram.isValid()) {QByteArray data = datagram.data();// 使用datagram.senderAddress()等获取元数据
}

3、广播与组播

// IPv4 广播
udpSocket->writeDatagram(data, QHostAddress::Broadcast, port);// IPv6 组播
QHostAddress groupAddress("FF02::1"); // 所有节点组播地址
udpSocket->joinMulticastGroup(groupAddress);
udpSocket->writeDatagram(data, groupAddress, port);

六、高级应用场景

1、实现服务发现协议

// 服务端广播服务信息
void ServiceDiscoverer::broadcastService() {QJsonObject serviceInfo{{"name", "File Server"},{"type", "_fileserver._tcp"},{"port", 8080},{"ip", getLocalIP()}};QByteArray datagram = QJsonDocument(serviceInfo).toJson();udpSocket->writeDatagram(datagram, QHostAddress::Broadcast, 5353);
}// 客户端监听服务
void ServiceBrowser::startDiscovery() {udpSocket->bind(5353, QUdpSocket::ShareAddress);connect(udpSocket, &QUdpSocket::readyRead, [this]() {while (udpSocket->hasPendingDatagrams()) {QNetworkDatagram datagram = udpSocket->receiveDatagram();QJsonObject service = QJsonDocument::fromJson(datagram.data()).object();emit serviceDiscovered(service);}});
}

2、实现简单可靠传输

// 带序列号的数据报
struct ReliableDatagram {quint32 sequence;QByteArray payload;
};// 发送端
void sendReliable(const QByteArray &data) {static quint32 seq = 0;ReliableDatagram dg{++seq, data};QByteArray datagram;QDataStream out(&datagram, QIODevice::WriteOnly);out << dg.sequence << dg.payload;udpSocket->writeDatagram(datagram, receiverAddr, port);// 启动重传定时器QTimer::singleShot(200, [this, seq, datagram]() {if (!acknowledged.contains(seq)) {udpSocket->writeDatagram(datagram, receiverAddr, port); // 重传}});
}// 接收端
void processDatagram() {QNetworkDatagram dg = udpSocket->receiveDatagram();ReliableDatagram packet;QDataStream in(dg.data());in >> packet.sequence >> packet.payload;// 发送ACKQByteArray ack;QDataStream ackOut(&ack, QIODevice::WriteOnly);ackOut << packet.sequence;udpSocket->writeDatagram(ack, dg.senderAddress(), dg.senderPort());// 处理数据if (packet.sequence > lastSequence) {processPayload(packet.payload);lastSequence = packet.sequence;}
}

七、调试与优化技巧

1、网络诊断命令

# 查看UDP端口监听
netstat -anu# 测试UDP连通性
nc -u <host> <port>

2、性能优化

// 增大发送缓冲区
udpSocket->setSocketOption(QAbstractSocket::SendBufferSizeSocketOption, 1024 * 1024);// 禁用拥塞控制 (实时应用)
udpSocket->setSocketOption(QAbstractSocket::LowDelayOption, 1);// 使用原始套接字 (需要权限)
if (udpSocket->bind(QHostAddress::Any, port, QUdpSocket::DontShareAddress)) {udpSocket->setSocketOption(QAbstractSocket::SendBufferSizeSocketOption, 0);
}

3、错误处理

connect(udpSocket, &QAbstractSocket::errorOccurred, [](QAbstractSocket::SocketError error) {switch(error) {case QAbstractSocket::AddressInUseError:qCritical() << "Port already in use";break;case QAbstractSocket::DatagramTooLargeError:qWarning() << "Datagram exceeds MTU size";break;case QAbstractSocket::NetworkError:qWarning() << "Network error occurred";break;default:qWarning() << "UDP error:" << error;}
});

八、完整示例:局域网设备发现

// 设备发现广播器
class DeviceBroadcaster : public QObject {
public:DeviceBroadcaster() {connect(&timer, &QTimer::timeout, this, &DeviceBroadcaster::broadcast);timer.start(5000); // 每5秒广播一次}private:void broadcast() {QJsonObject deviceInfo{{"name", QSysInfo::machineHostName()},{"os", QSysInfo::prettyProductName()},{"ip", getLocalIP()},{"timestamp", QDateTime::currentSecsSinceEpoch()}};QByteArray datagram = QJsonDocument(deviceInfo).toJson();QUdpSocket socket;socket.writeDatagram(datagram, QHostAddress::Broadcast, 37020);}QString getLocalIP() {foreach (const QHostAddress &address, QNetworkInterface::allAddresses()) {if (address.protocol() == QAbstractSocket::IPv4Protocol && address != QHostAddress::LocalHost) {return address.toString();}}return "127.0.0.1";}QTimer timer;
};// 设备发现监听器
class DeviceListener : public QObject {
public:DeviceListener() {udpSocket.bind(37020, QUdpSocket::ShareAddress);connect(&udpSocket, &QUdpSocket::readyRead, this, &DeviceListener::processDatagrams);}signals:void deviceFound(const QJsonObject &deviceInfo);private:void processDatagrams() {while (udpSocket.hasPendingDatagrams()) {QNetworkDatagram datagram = udpSocket.receiveDatagram();QJsonObject device = QJsonDocument::fromJson(datagram.data()).object();// 过滤过期的广播qint64 now = QDateTime::currentSecsSinceEpoch();if (now - device["timestamp"].toInt() < 10) {emit deviceFound(device);}}}QUdpSocket udpSocket;
};

最佳实践:对于需要可靠传输的场景,可在应用层实现ACK机制和序列号;对于实时流媒体,可结合RTP协议;在公网环境中使用时,需注意NAT穿透问题(可借助STUN/TURN服务器)。

九、qudpsocket在writeDatagram发送数据后,如果指定的地址没有绑定端口,居然会触发readyRead信号,是什么原因?

核心原因:ICMP “端口不可达” 报文

当你使用 QUdpSocket::writeDatagram 向一个目标IP地址的未绑定端口发送UDP数据报时,整个流程如下:

  1. 你的程序

    :调用 socket.writeDatagram(datagram, QHostAddress("目标IP"), 1234),假设目标主机的1234端口没有任何进程在监听。

  2. 你的操作系统网络栈

    :将UDP数据包封装好,通过网络发送到目标主机。

  3. 目标主机的操作系统网络栈

    :接收到这个数据包,发现本机上没有任何应用程序绑定到UDP 1234端口。

  4. 目标主机生成ICMP响应

    :根据网络协议规范,目标主机会自动生成一个 ICMP “Destination Unreachable” (目的地不可达) 报文,其具体类型是 “Port Unreachable” (端口不可达)。这个报文是对你发出的原始UDP包的响应。

  5. ICMP报文返回

    :这个ICMP错误报文被发回给你的机器。

  6. 你的操作系统接收

    :你的操作系统内核收到了这个来自目标的ICMP响应。

  7. Qt的抽象层 (QUdpSocket)

    QUdpSocket 在底层会监听到这个传入的ICMP报文。虽然这不是一个“数据”报文,但它是一个与你的socket相关的传入网络事件

  8. 触发信号

    QUdpSocket 将这个事件解释为“有东西到达了这个socket”,因此它最通用的方式就是触发 readyRead() 信号,通知应用程序可能有数据可读。

为什么Qt要这样设计?

  1. 统一的事件处理模型QUdpSocket 的设计理念是提供一个统一的、异步的接口。任何与socket相关的传入活动(无论是实际的数据还是像ICMP错误这样的控制信息)都会通过 readyRead() 信号来通知。这简化了API,你只需要监听一个信号来处理所有“读”相关的事件。

  2. 错误信息也是重要信息:接收到“端口不可达”错误是一个非常有用的诊断信息。它告诉你目标服务可能没有运行。虽然Qt没有直接提供一个“icmpErrorReceived()` 信号,但它通过这种方式让你知道发生了某事,然后由你决定如何处理。

如何区分是真实数据还是错误?

当你收到 readyRead() 信号后,调用 readDatagram() 时,你无法读取到任何有效的UDP数据,因为实际上并没有数据传来。

更专业的处理方式是检查读取操作的返回值以及错误状态:

// 在连接readyRead信号的槽函数中
while(udpSocket.hasPendingDatagrams()){QByteArray datagram;datagram.resize(udpSocket.pendingDatagramSize());QHostAddress sender;quint16 senderPort;// 尝试读取数据qint64 bytesRead = udpSocket.readDatagram(datagram.data(), datagram.size(),&sender,&senderPort);if(bytesRead ==-1){// 读取失败!这里很可能就是因为收到了ICMP错误。qDebug()<<"Error while reading:"<< udpSocket.errorString();// udpSocket.error() 可能会返回 QAbstractSocket::ConnectionRefusedError 或其他错误}else{// 成功读取到有效数据,正常处理processData(datagram, sender, senderPort);}
}
  • 关键点

    :当因为ICMP错误而触发 readyRead 时,readDatagram() 会返回 -1,并且通过 udpSocket.error() 可以获取到具体的错误码(例如 QAbstractSocket::ConnectionRefusedError)。

  • 而正常接收到UDP数据包时,readDatagram() 会返回读取到的字节数(>=0),并且 error() 会是 QAbstractSocket::UnknownSocketError

总结

步骤

事件

结果

1

向未绑定端口发送UDP数据

数据包发出

2

目标主机生成 ICMP端口不可达 错误

错误报文返回

3

你的操作系统接收该ICMP报文

内核通知socket

4

QUdpSocket

 检测到该socket有活动

触发 readyRead() 信号

5

你的槽函数调用 readDatagram()

读取失败,返回-1并设置错误状态

所以,这并不是一个Bug,而是UDP协议标准和Qt网络抽象层结合的预期行为。你需要在你的代码中通过检查 readDatagram() 的返回值和错误状态来妥善处理这种情况。

http://www.xdnf.cn/news/19733.html

相关文章:

  • 【学Python自动化】5.1 Python 与 Rust 数据结构对比学习笔记
  • (Arxiv-2025)VACE:一体化视频创作与编辑
  • (纯新手教学)计算机视觉(opencv)实战十一——轮廓近似(cv2.approxPolyDP)
  • C++实时视频抽帧抓图功能(附源码)
  • 几种特殊的数字滤波器---原理及设计
  • 基于springboot生鲜交易系统源码和论文
  • Beego: Go Web Framework 详细指南
  • Eclipse使用教程_自用
  • vite基础讲解
  • 【C++】C++14新特性
  • Jenkins大总结 20250901
  • Abaqus后处理常见问题汇总
  • python生成器与协程深度剖析
  • 腾讯位置商业授权微信小程序获取城市列表
  • 数据分析编程第八步:文本处理
  • flex布局order改变排列顺序
  • 前沿科技竞速:脑机接口、AI芯片与半导体生态上的新突破
  • Product Hunt 每日热榜 | 2025-08-31
  • 记录我的第一次挖洞
  • 蓝牙BLE modem调制里面phase manipulation什么意思
  • Proteus8 仿真教学全指南:从入门到实战的电子开发利器
  • 【数据可视化-103】蜜雪冰城门店分布大揭秘:2025年8月数据分析及可视化
  • Dify之插件开发之Crawl4ai 爬虫打包与发布
  • SERL——针对真机高效采样的RL系统:基于图像观测和RLPD算法等,开启少量演示下的RL精密插拔之路(含插入基准FMB的详解)
  • 【STM32】中断软件分支处理( NVIC 和 GIC)
  • Rviz-Gazebo联动
  • C语言数据结构之双向链表
  • 详细介绍 JMeter 性能测试
  • Mac idea 格式化代码快捷键
  • 第 94 场周赛:叶子相似的树、模拟行走机器人、爱吃香蕉的珂珂、最长的斐波那契子序列的长度