
简介这是一份基于Qt实现的UDP通信演示工程包含客户端与服务端两套完整源码面向需要快速掌握QUdpSocket收发流程的开发者也适合作为网络编程课程的小型参考案例。包体共计12个文件以cpp源文件、h头文件、pro工程文件和ui界面文件为主rar压缩包整体仅10KB结构精简便于阅读和二次修改。已有821人学习/下载适合入门级开发者对照练习。Demo覆盖了UDP通信的关键环节创建QUdpSocket、绑定本地端口、通过writeDatagram发送数据报、利用readyRead信号配合receiveDatagram接收数据并包含基本错误处理思路。同时附带可视化界面可以直观演示数据交互过程在此基础上稍作扩展即可用于实际项目中低延迟、轻量级的网络通信场景。 我最初写这个 QTUDP 通信 Demo起因特别简单Qt 官方文档里关于 QUdpSocket 的例子太零碎了要么只讲发、要么只讲收真正要做个拿来就能用的调试工具还得自己拼。UDP 这东西在工控、视频传输、自定义协议调试里天天见但很多 Qt 新手卡在 bind、readyRead、字节序这些细节上一卡就是半天。这篇文章不是文档翻译是我自己实际把 Demo 从零写到能用的全过程。包含完整代码思路、几个我踩过的坑还有一些网上不太好搜到的排查技巧。不管你是刚开始学 Qt 网络编程还是想快速验证一个 UDP 协议格式这篇文章应该都能帮上忙。1. 这个 Demo 到底要解决什么问题1.1 为什么选 UDP 而不是 TCP先聊几句协议选型。经常有人问“UDP 不是不可靠吗为啥还要用它”。UDP 确实不保证送达、不保证顺序但它有一个 TCP 比不了的优势无连接、开销小、延迟低。一个 QUdpSocket 实例既能发又能收不需要三次握手不需要维护连接状态非常适合局域网内的数据采集、设备状态广播、音视频实时传输这类场景。你往 UDP 缓冲区里扔数据包操作系统会尽力帮你转发至于对方收没收到、按什么顺序收到协议本身不负责。打个比方TCP 像两个人打电话要先拨号、接通、然后一句一句确认“你说啥我没听清再说一遍”UDP 更像对讲机按下按键说一句就马上完事对方听没听清那是对方的事。这个特性在实时性要求高的场景里非常吃香代价是应用层得自己处理丢包和乱序。1.2 功能定位给自己的协议调试工具箱我写这个 Demo 的目标很明确做一个可以在同一界面上完成 UDP 数据接收和发送的小工具箱要求能查看十六进制内容能配置本地端口也能指定远端目标。这个需求源于一次飞控数据调试。当时我需要同时监听几个端口的数据并且要往其中一个端口发送测试指令。用网络调试助手也凑合但每次想改一下界面、加点解析逻辑就得回到代码里去改干脆自己封装一个。核心功能我锁定了四条绑定本地端口接收 UDP 数据支持十六进制和 ASCII 两种显示启动后自动开启接收收包计数实时更新支持向指定目标地址和端口发送自定义内容提供一个“定时自动发送”选项方便压力测试再往后我又给它加了组播Multicast接收支持以及一串极简的时域转频域波形显示这些后面分节细说。一开始做这类功能时建议把代码拆出来而不是堆在 MainWindow 里不然后面加功能会很难受。2. 工程搭建和核心设计思路2.1 最小工程配置与 pro 文件环境我用的 Qt 5.15.2 MinGW 64 位这套组合在 Windows 下最省心。创建工程的时候选 Qt Widgets Application然后在.pro文件里加一行QT core gui network widgetsnetwork 模块一定要加上否则编译直接报“找不到 QAbstractSocket 头文件”之类的错误。加上之后顺手在 mainwindow.h 里 include#include QUdpSocket然后声明一个私有成员private: QUdpSocket *udpSocket; bool isBound false;整个 Demo 的功能都会围绕这个成员展开。2.2 界面元素规划界面我做了个上下分区。上半部分是接收框用一个 QTextEdit 设置只读属性显示收到的数据旁边放一个 QLabel 显示包计数再配一个“清空”按钮。下半部分是发送区一个 QLineEdit 填目标 IP一个 QSpinBox 填端口一个 QTextEdit 填要发送的内容最后是“发送”按钮和“定时发送”复选框。接收显示区建议用 QPlainTextEdit 而不是 QTextEdit数据量大时 QPlainTextEdit 性能好不少不会因为文本过多而卡界面。如果你要显示实时波形那就要单独接 QCustomPlot这个是后话。界面布局用 QVBoxLayout 套 QHBoxLayout 就行不用 Designer 拖也能写明白。绑定端口这一步放构造函数里不够严谨我建议放在一个公开方法里比如initUdpSocket(quint16 port)这样以后换端口方便。3. 核心代码实现与细节解析3.1 初始化与 bind这一步很多人忽略了初始化 socket 是这样写的void MainWindow::initUdpSocket(quint16 localPort) { udpSocket new QUdpSocket(this); if (!udpSocket-bind(localPort)) { qDebug() Bind failed: udpSocket-errorString(); return; } connect(udpSocket, QUdpSocket::readyRead, this, MainWindow::onReadyRead); }bind 这一步是新手踩坑重灾区。有人会问接收端需要 bind那发送端呢如果只是发送不 bind 也能发操作系统会自动分配一个临时端口。但如果你想同时接收回包就一定要 bind而且 bind 的端口要固定方便对端把数据回传过来。还有一个细节同一台电脑上两个程序绑同一个 UDP 端口默认会失败。这在调试中很常见后面我会展开讲。bind 之后还要注意错误处理。bind 返回 false 时常见两个原因端口被占用或者权限不足Linux 下端口小于 1024 时会有这种问题。Windows 下 1024 以内的端口其实也要管理员权限建议一律用 1024 以上。3.2 接收数据与 readyRead 信号QUdpSocket 收到数据后会发出 readyRead 信号。和 TCP 不同UDP 的 readyRead 不保证一个包就是一个完整的应用消息但一般情况下可以认为一次 readyRead 对应一个 UDP 数据报。稳妥的做法是在槽函数里用一个循环把 socket 里的数据报全部读出来。void MainWindow::onReadyRead() { while (udpSocket-hasPendingDatagrams()) { QByteArray datagram; datagram.resize(udpSocket-pendingDatagramSize()); QHostAddress sender; quint16 senderPort; udpSocket-readDatagram(datagram.data(), datagram.size(), sender, senderPort); // 转换显示 QString srcInfo QString([%1:%2]).arg(sender.toString()).arg(senderPort); QString hexData QString(datagram.toHex( )); QString text srcInfo hexData; // 追加到界面 auto *textEdit findChildQPlainTextEdit *(receiveEdit); if (textEdit) textEdit-appendPlainText(text); // 计数 packetCount; } }readDatagram 的返回值是实际读取的字节数。如果你传给它的缓冲区比数据报还小多余的字节会被丢弃这点和文件读取不太一样记一下就行。发包那部分逻辑相对简单void MainWindow::sendData(const QHostAddress target, quint16 port, const QByteArray data) { qint64 written udpSocket-writeDatagram(data, target, port); if (written 0) { qDebug() Send failed: udpSocket-errorString(); } }一个容易忽略的问题writeDatagram 是异步操作但它只保证数据被操作系统接收不代表已经发出。如果你的程序随后立刻退出还没发出的数据可能直接丢失。调试的时候如果你发现”明明 sendto 返回成功了对端却什么都没收到”先加个延迟再退出程序往往就能收到。3.3 组播接收的扩展调试过程中我遇到一个需求多台设备都在向同一个组播地址发状态帧主机只在一个端口上收。这就不能靠普通 bind 解决了要加入组播组。udpSocket-bind(QHostAddress::AnyIPv4, 45000, QUdpSocket::ShareAddress); udpSocket-joinMulticastGroup(QHostAddress(239.255.42.99));注意 bind 的第一个参数用了 QHostAddress::AnyIPv4而不是某个具体 IP。如果写成 127.0.0.1你就只能收到本机的组播数据局域网里其他设备发来的流量会被过滤掉。离开组播的时候调 leaveMulticastGroup这里不展开了。我最初绑定组播的时候用了 QHostAddress(239.255.42.99) 作为 bind 的地址然后死活收不到数据。后来查了文档才知道bind 的地址是本机网卡地址组播组地址要交给 joinMulticastGroup。这个顺序非常重要绕了一圈才意识到是自己把概念搞混了。3.4 时域转频域把数据可视化的扩展点在热词里看到“qt时域图转换为频域图使用qcustomplot显示”这个方向我也在 Demo 里顺手做了。做法并不复杂先用 Qt 的 QCustomPlot 控件把接收到的字节流按时间轴画成时域波形再用 kissfft 这个轻量级 FFT 库做一次快速傅里叶变换把频域幅值画出来。步骤是把 QByteArray 按 int16 解析成 QVector 然后填进 QCustomPlot 的 graph最后添加一个 QCPGraph 显示频谱。具体来说关键代码如下QVectordouble sampleData; for (int i 0; i datagram.size() - 1; i 2) { qint16 val; memcpy(val, datagram.constData() i, 2); sampleData.append(val); } // 对 sampleData 做 FFT得到频域幅值 kiss_fft_cfg cfg kiss_fft_alloc(nfft, 0, nullptr, nullptr); kiss_fft_cpx *in new kiss_fft_cpx[nfft]; kiss_fft_cpx *out new kiss_fft_cpx[nfft]; for (int i 0; i nfft; i) in[i].r (i sampleData.size()) ? sampleData[i] : 0; kiss_fft(cfg, in, out); // 计算幅值 sqrt(re^2 im^2)填给频谱 graph这个扩展点真正适合的场景是你在调试振动传感器、麦克风采集卡或者某个 IO 采样盒可以用它快速确认数据的频率分布是否合理。QCustomPlot 的性能在几万个点内都扛得住仪表盘式的界面做起来也不难。不过这个属于进阶玩法如果只是验证连通性可以不急着上 FFT先把收发调通再说。3.5 字节序一个非常容易埋坑的点UDP 传输的是字节流和 TCP 没有本质区别。但很多人自定义协议时会踩到字节序的坑设备端大端发送到 PC 上小端解析看起来数值就不对。我写 Demo 时做了个简单的演示逻辑收到 4 字节以上数据时把前 4 字节解析成 quint32 显示出来同时提供“大小端切换”的复选框。核心就是 Qt 的 qFromBigEndian 和 qFromLittleEndianquint32 val; if (ui-bigEndianCheckBox-isChecked()) val qFromBigEndianquint32(reinterpret_castconst uchar *(datagram.constData())); else val qFromLittleEndianquint32(reinterpret_castconst uchar *(datagram.constData()));这个功能做好之后再也不用拿着计算器对着十六进制傻算了。经验之谈调试自定义协议时别一上来就看 ASCII 字符很多二进制协议中间有很多不可见字符十六进制视图才是调试时的第一视角。4. 抓包验证与性能细节4.1 用 Wireshark 验证数据包真的发出去了有时候程序看起来发了对端却收不到。这种时候别急着怀疑制代码先打开 Wireshark 抓个包看看确认数据确实出网卡后再说。过滤条件写udp.port 45000就能只看到相关端口的 UDP 流量。这里说个经验Wireshark 默认只能实时抓网卡上的包本机到本机的 UDP 流量抓不抓得到要看平台设置。如果你绑的是 127.0.0.1Windows 上通常抓不到回环流量建议用 Npcap 勾选“loopback”选项或者干脆发到局域网内另一台机器上验证。有个热词“wireshark添加了滤波条件udp但是我还是抓到了icmp的数据”这个其实不奇怪抓包界面上显示的是“当前显示过滤器”它只是过滤显示内容不是过滤采集过程ICMP 报文之类你自己通信产生的其他报文也会被采集下来。线的设备若开着 ping 或进行链路检测也会出现在列表里。想只抓 UDP可以在“Capture Filter”里填udp或者在显示过滤器里加一个icmp排除条件像这样udp !icmp4.2 缓存区设置网上很少提到的坑Windows 默认的 UDP 接收缓冲区是 64 KB 左右。如果你的对端以高频率狂发数据而你的界面刷新和读包线程稍微慢那么一拍缓冲区满了以后新的数据包会被内核直接丢弃。表现就是抓包软件能看到包已经到达网卡但是你的程序收不到。这个问题的解决办法有两个层面一个是调大系统级缓存区另一个是在应用层把读取逻辑弄得更快。调整 Windows 的 UDP 缓存区参数网上有一种说法是修改注册表但我实测更靠谱的做法是先找到系统限制值再动态调大int nBufSize 1024 * 1024; // 1MB int nOptVal nBufSize; int nOptLen sizeof(int); setsockopt(udpSocket-socketDescriptor(), SOL_SOCKET, SO_RCVBUF, (const char*)nOptVal, nOptLen);但是这里有个坑setsockopt 设置的值可能被系统悄悄限制住实际生效的仍然不会是1MB。想要确切修改系统的最大 UDP 缓存区需要去确认和调整“全局 UDP 系统缓存区”的大小Windows 上这一步和注册表相关Linux 上则常用 sysctl。不过对绝大多数 Demo 级别的调试来说先确保你的接收线程不卡界面数据就不会轻易被丢。如果你真要做高速丢包测试建议先用 iperf3 的 UDP 模式扫一遍链路iperf3 -u -c 192.168.1.100 -b 10M先看链路的真实丢包率再判断到底是程序收包慢还是网络本身在丢包。这样排查起来不容易被假象误导。4.3 定时发送与发送频率控制定时发送功能在 Demo 里用来验证对端是否在线、通信是否通畅挺好用的。我用一个 QTimer每 100ms 触发一次发送timer new QTimer(this); connect(timer, QTimer::timeout, this, MainWindow::sendTimerTrigger); timer-start(100);如果还想测更精确的发送频率可以用QElapsedTimer记录实际间隔别相信定时器设置的数值系统定时器并不是精确到纳秒的。这一点在压力测试时特别重要你以为自己在按 10 Hz 发实际上 QTimer 的误差可能让实际发包间隔有波动这会影响对端对你协议的判断。5. 常见问题速查与排障实录我这里整理了一个速查表都是实际中容易碰到的情况现象常见原因解决办法bind 失败端口被占用 / 权限不足换端口Windows 管理员运行 / Linux 避开 1024 以下端口本机收不到本机发的 UDP绑定了 127.0.0.1或抓包工具没开回环绑 AnyIPv4抓包时启用回环捕获程序收到包但界面乱码显示方式为 ASCII但数据是二进制切换成十六进制显示发送成功但对端没反应写入了序包但字节序不对或对端端口没监听先抓包确认内容再查大小端和 IP/端口数据一多就丢包到达网卡但缓冲区溢出调大 SO_RCVBUF提速读取线程用带缓存的生产者-消费者模型readyRead 触发多次解析出来数据是碎片UDP 包被上层拆包或应用层收到多个数据报叠加用 pendingDatagramSize 循环读完整数据按包重建消息5.1 Bind 失败而且绑定的端口明明没程序在用有一次我在调试时遇到 bind 失败端口明明没被占用但程序一直报错。后来发现是之前调试的进程没退干净在任务管理器里看不到但用netstat -ano | findstr 45000一查就看到了占用进程的 PID。Windows 下这种情况特别多建议 bind 之前先查占用。5.2 发到局域网对端包抓到了但程序就是没反应这种问题十有八九是 IP 地址写错了。UDP 不像 TCP它没有连接建立的过程写错 IP 时不会立即报错数据包发到错误地址上对方可能会直接返回 ICMP 端口不可达但你的程序不会感知到。所以看到“发送成功”别急着高兴先确认对端 IP、端口、防火墙放行状态三个条件。5.3 待发送内容为空writeDatagram 返回 0Qt 里 writeDatagram 传入一个空的 QByteArray返回值可能是 0并不算错但发出的就是一个空的数据报。某些对端设备会把它当成心跳或者协议帧有些则直接忽略。如果你不希望发空包发送之前在界面上做个内容校验就行。5.4 高频率收发时界面卡顿怎么办IO 线程和 UI 线程不分家界面会卡。这个问题的根治方案是用一个独立线程处理 socket 收发通过信号槽把数据用队列连接发回 UI 线程显示。Demo 阶段图省事可以把 received 数据的 append 和刷新放慢一点比如每 200ms 刷新一次界面而不是每收到一个包就刷一次。这样对协议调试的影响很小对界面流畅度的提升非常明显。// 使用 QTimer 定时刷新接收区显示 QTimer *refreshTimer new QTimer(this); connect(refreshTimer, QTimer::timeout, this, MainWindow::flushReceiveBuffer); refreshTimer-start(200);UDP 调试本身门槛不高难的是在真实环境里排查各种边界情况。上面这些坑基本都是我在实际调试过程中撞过的写出来给你避雷。6. 基于实际排障的几点补充建议前面把结构和代码逻辑讲得差不多了但还有几个经验性的补充属于那种“踩过坑之后才会去查资料”的东西也放在这里。第一协议设计阶段建议把“帧头 长度 payload 校验” 的格式尽量收拢到同一个解析函数里。UDP 包一旦出现半包、粘包问题解析逻辑散落在各处会让你查到头秃。哪怕只是做个 Demo也建议把数据报解析整理成一个独立函数返回一个结构体这样后面扩展到 CAN、串口、SPI 等场景时能直接复用。第二关于 socket 兼容性。QUdpSocket 在 Qt 5.10 之后基本都支持但在 Qt 6 里头文件路径和部分枚举名有变化比如 QHostAddress::AnyIPv4 在 Qt 6 里仍然保留但有些网络相关枚举改用 QAbstractSocket 里的枚举类型了。如果你的项目用了 Qt 6建议看一眼官方文档再改。第三考虑跨平台时注意 Windows 防火墙。绑定了 UDP 端口之后Windows 安全中心经常弹出一个“允许访问”的对话框如果点了取消后续进程要么收不到外部数据要么内部双向都断。这类问题在 Linux 下通常不常见但在 Windows 上几乎是必坑的调试时先确认一下防火墙状态。第四做一个“可视化波形”功能时数据源建议从缓存队列里取而不是直接从 QUdpSocket 里读。我做过一个版本直接在 readyRead 里做 FFT数据一多直接把界面卡死。后来改成每隔 50ms 取一次缓存里的最新一段数据时域和频域图才稳定。另外有同学会问“Demo 要不要做成线程安全的”。如果你用 Qt 的信号槽机制并且 socket 只在主线程里使用一般不需要加锁。但若你把接收逻辑放到 QThread 中一定要用信号槽或 QMetaObject::invokeMethod 来安全交接数据千万别直接跨线程更新 UI 控件。7. 最后的经验之谈这个 QTUDP 通信 Demo 我前前后后改了三版第一版纯功能验证第二版加了组播和自定义序列解析第三版加了波形显示和定时发送。改到最后我发现很多调试工具的价值不在于功能多而在于帮助你把通信系统中的“可信部分”和“可疑部分”快速隔离出来。如果别人问你写这个女巫干嘛你解释一句“用于协议调试和快速验证”大概率对方就懂了。从个人体会出发UDP 通信在工控和数据采集里出现频率是相当高的基于 Qt 的 QUdpSocket 也确实是跨平台实现 UDP 通信最省心的方式之一。它不像底层 socket 那样要求你手动处理各种平台差异也不像高层的 HTTP 封装那样给你加很多用不上的东西。如果你正开始学 Qt 网络编程或者需要一个简单但完整的 UDP 调试工具强烈建议照着这个思路自己写一个代码控制在几百行以内就行。最后再分享一个小技巧写 UDP 调试工具时一定要保留“原始十六进制显示”和“保存接收记录”这两个能力即使你现在觉得用不上。遇到底层设备发来一份看不懂的二进制帧时你会发现手上有一个能记录原始报文的工具排查效率会高很多。别的花哨功能可以后面再加这两个能力一定要先有。本文还有配套的精品资源点击获取