Qt_UDP协议工作原理及实战
Qt_UDP协议工作原理及实战目录文章目录Qt_UDP协议工作原理及实战目录一、UDP基础知识1.1 关键词拆解帮助理解第1条1.2 UDP报头结构图对应第3条1.3 QUdpSocket 与 QTcpSocket 的关系对应第4条1.4 UDP与TCP的直观对比二、QUdpSocket常用API函数2.1 常用函数速查表2.2 关键信号2.3 bind 两种常用写法三、UDP的三种通信模式含图解3.1 单播Unicast—— 一对一3.2 广播Broadcast—— 一对所有3.3 组播Multicast—— 一对一组3.4 三种模式对比表四、收发数据的标准套路4.1 完整模板代码4.2 三条重要提醒五、三种模式的代码实现5.1 单播示例5.2 广播示例5.3 组播示例完整可运行片段六、实战项目解析QUdpSocketpro6.1 程序整体流程图6.2 核心代码逐段解析1构造函数——初始化三件事2启动服务——bind 的意义3接收数据——为什么要 while 循环4发送数据——单播与广播只差一个地址6.3 三实例验证实验重要七、UDP的可靠性问题与应对【补充】7.1 TCP免费提供的功能 vs UDP需要自己实现7.2 两条路线的选择八、常见错误与排查8.1 绑定失败bind返回false8.2 发出去对方收不到8.3 中文乱码8.4 组播收不到8.5 收到重复/丢失数据8.6 数据报太大被截断/丢包率高九、什么时候用UDP什么时候用TCP一、UDP基础知识UDP(用户数据报协议)是轻量的不可靠的面向数据报无连接的协议用于可靠性要求不高的场合两个应用程序之间进行UPD通信不需要建立持久的socket连接UDP每次发送数据都需要指定目标地址和端口。UDP报文没有可靠性保证顺序保证和流量控制字段等可靠性较差但是正因为UDP协议的控制选项比较少在数据传输过程中延迟小数据传输效率高适合对可靠性要求不高的应用程序或者可以保障可靠性的应用程序如DNSTFTPSNMP等。UDP报头由4个组成其中每个域各占用2个字节具体包括源端口号目标端口号数据包长度校验值。【端口号有效范围0-65535】QUdpSocket类从QAbstractSocket类继承基本跟QTcpSocket共用大部分的接口函数主要区别在于QUdpSocket以数据传输数据不是以连续的数据流发送方发送数据报使用函数QUdpSocket::writeDataGram()数据报长度一般不超过512个字节每个数据报包含发送方和接收方的IP地址和端口数据等信息。1.1 关键词拆解帮助理解第1条关键词含义通俗类比轻量首部只有8字节没有复杂的控制机制明信片TCP是挂号信不可靠不保证送达、不保证顺序、丢了不重发寄出去就不管了面向数据报每个数据报都是独立的包裹有明确边界一个包裹一条完整消息无连接发送前不需要握手建立连接不用拨号直接扔出去1.2 UDP报头结构图对应第3条0 7 8 15 16 23 24 31 -------------------------------- | 源端口 (16位) | 目标端口 (16位) | ← 各2字节 -------------------------------- | 数据包长度 (16位) | 校验值 (16位) | ← 各2字节 -------------------------------- | 数据 (不定长) | -----------------------------------字段长度说明源端口2字节发送方端口号有效范围 0-65535目标端口2字节接收方端口号数据包长度2字节UDP首部数据的总长度校验值2字节检验数据在传输中是否损坏初学者注意端口号有效范围是0-65535。其中 0-1023 是知名端口如DNS的53自己写程序测试建议用1024-65535之间如 8888、8001。1.3 QUdpSocket 与 QTcpSocket 的关系对应第4条QAbstractSocket / | \ QTcpSocket QUdpSocket QSslSocket (字节流) (数据报) (加密TCP)1.4 UDP与TCP的直观对比对比维度UDPTCP连接无连接拿来就发三次握手建立连接可靠性不保证送达确认重传保证送达顺序可能乱序保证按序数据单位数据报有边界字节流无边界有黏包首部开销8字节至少20字节速度快相对慢收发APIwriteDatagram/readDatagramwrite/readQt类QUdpSocketQTcpSocket/QTcpServer典型应用DNS、视频流、广播发现网页、文件传输、聊天消息记忆口诀UDP像发传单印好就撒不看谁收到TCP像寄快递要签收确认丢了补寄。二、QUdpSocket常用API函数UDP数据接收使用QUdpSocket::bind()函数绑定端口用于接收传入的数据报当有数据报传入发射readyRead()信号使用ReadDatagram()函数来读取接收数据报UDP消息传送有单播广播和组播三种模式。2.1 常用函数速查表函数说明bool bind(quint16 port, BindMode mode)绑定本机端口用于接收传入的数据报qint64 writeDatagram(const QByteArray datagram, const QHostAddress address, quint16 port)发送数据报到指定IP和端口核心函数qint64 readDatagram(char *data, qint64 maxSize, QHostAddress *address nullptr, quint16 *port nullptr)读取一个数据报可选取出对方IP和端口bool hasPendingDatagrams()是否还有待读取的数据报配合while循环qint64 pendingDatagramSize()下一个待读数据报的字节数用来分配缓冲区bool joinMulticastGroup(const QHostAddress groupAddress)加入组播组bool leaveMulticastGroup(const QHostAddress groupAddress)退出组播组void abort()立即关闭套接字释放端口QHostAddress localAddress()/quint16 localPort()本机绑定的IP / 端口QHostAddress peerAddress()/quint16 peerPort()最后一次通信的对方地址UDP无连接仅作参考2.2 关键信号信号触发时机readyRead()有数据报传入时发射在槽函数中读取stateChanged()套接字状态变化errorOccurred()发生错误2.3 bind 两种常用写法// 写法一简单绑定单实例程序够用udpSocket-bind(8888);// 写法二指定地址 绑定模式多实例测试、组播场景推荐// AnyIPv4 : 绑定本机所有网卡的 IPv4 地址// ShareAddress : 允许同一台电脑多个程序绑定同一端口// ReuseAddressHint : 允许快速重新绑定避免端口被占用udpSocket-bind(QHostAddress::AnyIPv4,8888,QUdpSocket::ShareAddress|QUdpSocket::ReuseAddressHint);三、UDP的三种通信模式含图解单播一个UDP客户端发出的数据报只发送到另一个指定地址和端口的UDP客户端(一对一的数据传输)。广播一个UDP客户端发出的数据在同一个网络范围内其他UDP客户端都可以收到。组播多播UDP客户端加入到另一个组播IP地址指定的多播组成员向组播地址发送的数据报组内成员都可以接收到。3.1 单播Unicast—— 一对一[发送方] [接收方] 192.168.1.10 192.168.1.20 端口:随机 端口:8002(已bind) | ▲ ------ writeDatagram ---------| 指定 IP:8002 只有这1台收到写法udpSocket-writeDatagram(data, QHostAddress(192.168.1.20), 8002);特点点对点最精确但要知道对方IP。3.2 广播Broadcast—— 一对所有-- [主机A 192.168.1.20] 收到 [发送方] | 192.168.1.10 -- 255.255.255.255:8888 -- [主机B 192.168.1.30] 收到 | -- [主机C 192.168.1.40] 收到写法udpSocket-writeDatagram(data, QHostAddress::Broadcast, 8888);特点目标地址固定为255.255.255.255局域网内所有绑定了该端口的主机都收到。跨网段跨路由器广播无效。典型用途局域网设备发现如扫描局域网内的打印机、智能设备配对。3.3 组播Multicast—— 一对一组-- [主机A 已加入组播组 239.0.0.1] 收到 [发送方] | 也加入组 239.0.0.1 | 向 239.0.0.1:8888 发送 --------- [主机B 已加入组播组 239.0.0.1] 收到 | [主机C 没加入组] × 收不到组播地址范围224.0.0.0 ~ 239.255.255.255D类IP地址。特点比广播更精准——只有订阅了的主机收到减少对无关主机的打扰。跨路由器可通过配置支持。典型用途网络视频会议、股票行情推送、多人同步游戏状态。3.4 三种模式对比表模式目标地址谁能收到是否需要加入组典型用途单播具体IP如192.168.1.20仅指定的1台否点对点通信广播255.255.255.255局域网所有绑定该端口的主机否设备发现组播224.0.0.0~239.255.255.255加入组播组的成员是joinMulticastGroup群体推送四、收发数据的标准套路4.1 完整模板代码// 接收方三步走 // 第一步绑定端口不绑定就像没装信箱信到了也没人收udpSocket-bind(QHostAddress::AnyIPv4,8888,QUdpSocket::ShareAddress);// 第二步关联 readyRead 信号connect(udpSocket,QUdpSocket::readyRead,this,MyClass::onReadyRead);// 第三步在槽函数中循环读取所有数据报voidMyClass::onReadyRead(){while(udpSocket-hasPendingDatagrams()){// 可能一次到多个QByteArray datagram;datagram.resize(udpSocket-pendingDatagramSize());// 按实际大小分配QHostAddress senderIp;quint16 senderPort;udpSocket-readDatagram(datagram.data(),datagram.size(),senderIp,senderPort);// 读一个数据报qDebug()来自senderIpsenderPortQString::fromUtf8(datagram);}}// 发送方一步到位 QByteArray dataQString(hello UDP).toUtf8();udpSocket-writeDatagram(data,QHostAddress(192.168.1.20),8888);4.2 三条重要提醒writeDatagram()的数据报长度一般不超过512字节老建议值以太网实际上限约1472字节1500 MTU - 20 IP头 - 8 UDP头超出会在IP层分片丢包概率增大。大文件请用TCP或自行分包。发送方可以不bind——系统会自动分配一个随机端口发送但接收方必须bind。每个数据报包含发送方和接收方的IP地址和端口数据等信息所以接收方才知道是谁发来的。为什么UDP没有TCP那样的黏包问题每个数据报都有明确边界readDatagram()一次正好读完一个不像TCP的readAll()可能读到半条或多条消息。五、三种模式的代码实现5.1 单播示例// 发送方向指定IP的指定端口发送voidsendUnicast(){QByteArray data你好单播;udpSocket-writeDatagram(data,QHostAddress(192.168.1.20),8002);}// 接收方绑定8002端口等待接收参见第四章标准套路5.2 广播示例// 发送方广播给局域网所有主机voidsendBroadcast(){QByteArray data你好广播;// 目标地址使用 QHostAddress::Broadcast即255.255.255.255udpSocket-writeDatagram(data,QHostAddress::Broadcast,8888);}// 接收方绑定8888端口即可收到无需任何额外操作5.3 组播示例完整可运行片段// 接收方加入组播组 voidsetupMulticastReceiver(){udpSocketnewQUdpSocket(this);// 注意组播必须绑定 IPv4 ShareAddress 模式udpSocket-bind(QHostAddress::AnyIPv4,8888,QUdpSocket::ShareAddress);// 关键一步加入组播组 239.0.0.1if(udpSocket-joinMulticastGroup(QHostAddress(239.0.0.1))){qDebug()成功加入组播组 239.0.0.1;}connect(udpSocket,QUdpSocket::readyRead,this,MyClass::onReadyRead);}// 发送方向组播组发送 voidsendMulticast(){QByteArray data你好组播组的朋友们;// 目标地址是组播组地址不是具体某台主机的IPudpSocket-writeDatagram(data,QHostAddress(239.0.0.1),8888);}// 退出组播组程序关闭或不想再收时调用voidleaveGroup(){udpSocket-leaveMulticastGroup(QHostAddress(239.0.0.1));}组播避坑组播必须使用QHostAddress::AnyIPv4绑定绑Any含IPv6可能收不到。Windows防火墙可能拦截组播测试时注意放行。同一主机的多个程序绑定同端口时都加入组播组才能都收到。六、实战项目解析QUdpSocketpro以D:\qtCode\QUdpSocketpro项目为例逐行理解 UDP 程序的结构。6.1 程序整体流程图程序启动 | v 构造函数创建 QUdpSocket 关联 readyRead 信号 禁用[停止]按钮 | v 用户点[启动服务] —— bind(8888, ShareAddress) ———— 失败显示错误信息 | | | 成功禁用[启动] 启用[停止] v 等待数据到达 ———— readyRead() 信号 ———— SocketReadyReadData() 循环读取 ^ |同时用户可随时点按钮发送 | 用户点[发送] —— writeDatagram(数据, 目标IP, 目标端口) 单播 用户点[广播] —— writeDatagram(数据, Broadcast, 目标端口) 广播 用户点[停止服务] —— abort() 解除绑定6.2 核心代码逐段解析1构造函数——初始化三件事udpsocketnewQUdpSocket(this);// ① 创建套接字this作父对象自动释放connect(udpsocket,QUdpSocket::readyRead,// ② 收到数据触发this,MainWindow::SocketReadyReadData);ui-stopBtn-setEnabled(false);// ③ 初始状态服务未启动停止不可点2启动服务——bind 的意义// bind 就是向操作系统申请一个信箱把端口分配给这个套接字// 之后发往该端口的所有数据报都进入这个套接字的接收缓冲区if(udpsocket-bind(QHostAddress::AnyIPv4,port,QUdpSocket::ShareAddress|QUdpSocket::ReuseAddressHint)){// ShareAddress允许同一电脑多个程序绑同一端口多实例测试的关键// ReuseAddressHint允许快速重新绑定避免TIME_WAIT导致端口被占用}3接收数据——为什么要 while 循环while(udpsocket-hasPendingDatagrams()){QByteArray datagrams;datagrams.resize(udpsocket-pendingDatagramSize());// 按数据报实际大小分配QHostAddress paddress;quint16 pport;udpsocket-readDatagram(datagrams.data(),datagrams.size(),paddress,pport);// 一次读一个完整数据报QString strsQString::fromUtf8(datagrams);// 显式UTF-8转换防乱码ui-showMsg-appendPlainText([From:paddress.toString():QString::number(pport)]strs);}为什么while循环readyRead()一次触发时缓冲区可能堆积了多个数据报必须循环读完。只读一次会漏数据。4发送数据——单播与广播只差一个地址// 单播目标IP来自下拉框udpsocket-writeDatagram(str,QHostAddress(targetipAddress),targetport);// 广播目标固定为 255.255.255.255udpsocket-writeDatagram(str,QHostAddress::Broadcast,targetport);6.3 三实例验证实验重要实验A绑不同端口验证单播实例绑定端口目标IP目标端口结果实例18001本机IP8002只有实例2收到实例28002本机IP8001只有实例1收到实例38003本机IP8001只有实例1收到实验B绑同一端口验证广播实例绑定端口目标端口点哪个按钮结果实例188888888广播三个实例全部收到实例288888888广播三个实例全部收到实例388888888广播三个实例全部收到实验B能成功的前提是bind()带了ShareAddress参数——如果没有它第二个实例绑定 8888 就会报绑定失败。坑Windows上多个套接字绑同端口时单播数据报只投递给其中一个实例行为不确定广播才会全部收到。所以绑同端口时只能测广播测单播请绑不同端口。七、UDP的可靠性问题与应对UDP不可靠不代表不能用UDP做可靠的事只是需要自己在应用层补课。【补充】7.1 TCP免费提供的功能 vs UDP需要自己实现TCP免费提供的功能UDP需要自己实现常见做法确认送达序号确认应答给每个包编号收到回ACK丢包重传超时重传定时器没收到ACK就重发按序到达序号重排接收方按序号排序数据校验校验和已有首部自带首部8字节里的校验值已覆盖流量控制限速/丢帧策略视频丢帧不如降帧率7.2 两条路线的选择数据不能丢如文件传输→ 老老实实用TCP或用现成的可靠UDP库如 QUIC、KCP。丢一点没关系如视频、位置刷新→ 用UDP最新的数据覆盖旧的即可。QUIC 小知识HTTP/3 用的 QUIC 协议就是在 UDP 上重新实现了可靠传输——这正说明UDP 应用层可靠性是一条被主流认可的路线。八、常见错误与排查8.1 绑定失败bind返回false现象点启动服务显示绑定失败。原因端口被其他程序占用或没加ShareAddress却想多实例绑同端口。解决换个端口试试。命令行查占用netstat -ano | findstr 8888Windows。多实例同端口必须加QUdpSocket::ShareAddress。8.2 发出去对方收不到排查步骤接收方是否bind了正确端口不bind 没装信箱目标IP/端口是否填对单播IP写错直接石沉大海UDP没有错误回报接收方是否点了启动服务防火墙是否放行Windows防火墙对UDP很敏感调试期可先临时关闭测试跨网段广播无效——广播出不了路由器。8.3 中文乱码原因用datagram.data()直接转QString依赖null终止符。解决统一用QString::fromUtf8(datagram)解码发送方用toUtf8()编码。8.4 组播收不到排查是否用了QHostAddress::AnyIPv4绑定不能用Any。接收方是否调用了joinMulticastGroup()加入组。组播地址是否在224.0.0.0~239.255.255.255范围内。防火墙/路由器是否放行组播。8.5 收到重复/丢失数据本质这就是UDP的天然特性不是bug。应对加序号去重或按第七章方案补可靠性或换TCP。8.6 数据报太大被截断/丢包率高现象发大字符串几KB经常收不到或收不全。原因超过MTU约1472字节会在IP层分片任一分片丢失则整个数据报丢弃。解决单个数据报控制在512字节以内最稳妥大数据分包序号重组或直接用TCP。九、什么时候用UDP什么时候用TCP场景选择理由聊天消息收发TCP一条都不能丢文件传输TCP完整性要求高网页请求TCPHTTP基于TCP局域网设备发现/扫描UDP广播一问全答效率高实时视频/语音UDP丢帧无所谓延迟才致命游戏位置同步UDP旧位置没到也无所谓新位置马上就来传感器数据高频上报UDP丢一两条不影响整体心跳保活UDP包小、快、断了就断了重发即可一句话决策丢不得用TCP等不得用UDP。