简介:这份基于 Qt5 的 UDP 局域网通信示例面向初学 Qt 网络编程、希望理解无连接通信流程的开发者与在校学生,核心围绕 QUdpSocket 的封装使用展开:在同一台主机或同一 IP 下为多个客户端绑定不同端口,完成消息发送与接收,从而直观看到 UDP 通信中地址、端口与数据报之间的关系。压缩包共 8 个文件,包含 3 个 cpp 源文件、2 个 h 头文件、2 个 ui 界面文件及 1 个 pro 工程文件,整体约 7KB,属于轻量级可直接用 Qt 编译的工程源码,便于读者在较小代码量中梳理界面、业务逻辑与网络收发模块的组织方式。已有 2282 人学习,适合作为 UDP 入门的对照案例:读者可借此理解端口绑定、数据报读写、客户端与服务端交互以及 Qt 信号槽在通信中的配合,并在此基础上扩展群聊、广播或简单局域网调试工具。
1. 需求与选型说明
做一个基于 Qt5 的 UDP 局域网通信软件,很多人第一版在自己电脑上跑得好好的,拷到第二台机器就只剩沉默。这个项目真正要解决的不是「会不会调 QUdpSocket」,而是把局域网内的一对多文本收发、在线用户列表、上下线通知做成结构清楚、能演示、能讲明白的完整程序。它适合两类人:要交课程设计、需要一套可复现代码与文档的学生;以及想拿一个最小可用的 UDP 通信骨架、再往上加文件传输或屏幕共享的开发者。QT TCP/IP UDP 这三块知识在课程里通常是分开讲的,而这个项目正好把它们串成一条线——UDP 负责实际收发,TCP/IP 的分层模型用来解释广播为什么出不了路由器,Qt 的信号槽负责把网络事件搬进界面。下面这套方案把协议、收发、界面、验收拆成几个能独立讲清的模块,照着搭一遍就能跑。
2. 总体架构设计:三层划分与工程目录的落地方式
先把分层定下来,后面写代码才不会越写越乱。这个项目最容易出现的问题是「所有逻辑都堆在 MainWindow 里」——收包、解析、刷列表、拼字符串全在一个类里,写到三四百行就开始互相干扰,改一处崩三处。我一般的做法是强行切三层:界面层只管显示和采集用户输入,通信层只管套接字的打开、收、发,协议层是一组不依赖界面的纯函数,负责打包和解包。三层之间用信号槽和普通函数调用连接,任何一层都不去#include另一层的窗口头文件。
2.1 三层职责边界:界面层、通信层、协议层各管什么
界面层的产物是「用户意图」,不是字节流。用户点了发送按钮,界面层做三件事:取出输入框文本、清空输入框、调用通信层的sendText()。它不知道端口是多少,也不知道帧头长什么样。通信层持有QUdpSocket,对外暴露sendText / sendFile / broadcastPresence这类语义化接口,对内负责绑定端口、循环读包、把原始QByteArray交给协议层解包,再把解出来的结构化结果用信号发出去。协议层最干净,只有pack()和unpack()两个函数加一个枚举,编译时不链接 widgets,将来想给它写单元测试,直接建一个控制台工程就能跑。
这样切分带来的直接好处是排错路径变短。收不到消息时,先看通信层的收包槽有没有被触发(打一行 qDebug),触发了就说明网络通了,问题在解包;没触发就说明包根本没到,往防火墙和网段方向查。判断依据清晰,不用满屏找逻辑。
2.2 工程目录与 .pro 构建配置
目录按层分文件夹,不要所有文件平铺在根目录。一个能直接交作业的结构大致是这样:src/ui/放窗口类,src/net/放通信封装,src/protocol/放协议头与实现,src/common/放全局常量。构建文件里必须显式加上network模块,这是新手第一个卡点——只写QT += core gui widgets,编译时QUdpSocket会报未声明,很多人以为是头文件路径问题,其实是模块没引。
# LanChat.pro QT += core gui widgets network # network 不加,QUdpSocket 直接编译不过 TARGET = LanChat TEMPLATE = app CONFIG += c++11 INCLUDEPATH += $$PWD/src HEADERS += \ src/ui/chatwindow.h \ src/net/udppeer.h \ src/protocol/protocol.h SOURCES += \ src/main.cpp \ src/ui/chatwindow.cpp \ src/net/udppeer.cpp \ src/protocol/protocol.cppCONFIG += c++11是给enum class和constexpr用的,Qt 5.9 以后的 qmake 默认可能还是 C++98 标准,不写这一行,协议头里的强类型枚举会编译失败。INCLUDEPATH加上源码根目录之后,各文件之间就能用#include "protocol/protocol.h"这种带路径的写法,比一堆../../好维护得多。
2.3 线程模型:默认不建线程,什么情况下才 moveToThread
UDP 的收发本身就是非阻塞的,readyRead信号在事件循环里触发,单线程完全够用。所以这个项目默认不引入工作线程,界面和套接字都在主线程,代码量少一半,调试也简单。真正需要动线程的只有一种场景:传大文件时用同步循环往套接字里灌数据,把主线程堵住,界面假死。这时候正确做法不是加个线程了事,而是把「发送」改造成定时器驱动的状态机——每次QTimer超时发一个分片,发完继续等下一次超时,事件循环始终有空转。只有在做多文件并发、或者要计算校验和这类耗时操作时,才把UdpPeer对象moveToThread出去,并且注意套接字必须在创建它的线程里使用,跨线程调用writeDatagram要改成信号槽投递。
2.4 全局常量集中管理:端口、超时、分片大小
把这些数字写死在各个 cpp 里是典型的自找麻烦。改个端口要全局搜索替换,漏一处就连不上。统一放一个头文件:
// common/config.h #pragma once #include <QtGlobal> namespace cfg { constexpr quint16 kPort = 45454; // 组内约定,避免和常用服务冲突 constexpr int kPeerTimeoutMs = 15000; // 15 秒没心跳就判离线 constexpr int kHeartbeatMs = 5000; // 心跳周期,取超时的 1/3 constexpr int kChunkSize = 1024; // 分片大小,给帧头留足余量 constexpr int kMaxDatagram = 1400; // 单包上限,超过就分片 }心跳周期取超时的三分之一,是让接收端至少能容忍两次丢包再判离线。如果两个值取得太接近,网络稍有抖动就会频繁上下线,列表一直闪,演示时特别难看。
3. 通信协议设计:自定义帧头、消息类型与分片边界
UDP 是面向报文的,一个writeDatagram对应一个readDatagram,不会像 TCP 那样粘包。但这不代表可以随便发裸字符串——你得知道这一包是文本、是上线通知还是心跳,得知道载荷有多长、版本对不对。所以仍然要设计一个轻量帧头。这个帧头的价值不在于「显得专业」,而在于接收端可以据此丢弃非法包,而不是拿着半截脏数据去QString::fromUtf8然后崩溃。
3.1 帧头设计:魔数、版本号、类型与长度四件套
帧头固定 12 字节:4 字节魔数、2 字节版本、1 字节类型、1 字节保留、4 字节载荷长度。魔数用来快速识别「这是不是本程序的包」,随便扫描到的其他 UDP 流量会被第一道关卡拦掉;版本号给将来改协议留退路,两端版本不一致时直接丢弃而不是解析出错;长度字段让接收端能校验数据完整性。
// protocol/protocol.h —— 只依赖 QtCore,方便单独测试 #pragma once #include <QByteArray> #include <QDataStream> namespace proto { constexpr quint32 kMagic = 0x4C414E43; // 'L''A''N''C' constexpr quint16 kVersion = 0x0001; constexpr int kHeaderSize = 12; // 4 + 2 + 1 + 1 + 4 enum class Type : quint8 { Text = 0x01, // 聊天文本,群发或单播 UserEnter = 0x02, // 上线通告 UserLeave = 0x03, // 主动下线通告 UserList = 0x04, // 在线列表应答 Heartbeat = 0x05, // 心跳保活 FileMeta = 0x06, // 文件元信息:文件名、大小、编号 FileChunk = 0x07, // 文件分片 FileDone = 0x08 // 传输结束 }; QByteArray pack(Type type, const QByteArray &payload); bool unpack(const QByteArray &datagram, Type &type, QByteArray &payload); } // namespace protoenum class而不是普通enum,是为了避免Text这种名字和 Qt 里其他标识符撞车,同时强转时必须显式写static_cast,减少手滑。
3.2 载荷序列化:QDataStream 的版本对齐
载荷里可能同时有QString昵称、quint32文件大小、QByteArray分片,这些类型长度不一样,必须用QDataStream顺序写入、顺序读出。这里有个必须记住的点:QDataStream有版本概念,不同版本对QString的编码方式不同,一端用默认版本写、另一端用默认版本读,看着没事,一旦两端 Qt 小版本不同就可能读出乱码或长度错位。所以打包和解包两处必须显式写死同一个版本。
// protocol/protocol.cpp #include "protocol.h" QByteArray proto::pack(Type type, const QByteArray &payload) { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_9); // 收发两端必须写死同一版本 out << kMagic << kVersion << static_cast<quint8>(type) << quint8(0) << quint32(payload.size()); block.append(payload); // 载荷按原始字节追加,不再套一层 return block; } bool proto::unpack(const QByteArray &datagram, Type &type, QByteArray &payload) { if (datagram.size() < kHeaderSize) return false; QDataStream in(datagram); in.setVersion(QDataStream::Qt_5_9); quint32 magic = 0, len = 0; quint16 ver = 0; quint8 t = 0, rsv = 0; in >> magic >> ver >> t >> rsv >> len; if (magic != kMagic || ver != kVersion) return false; // 非本程序包,丢弃 if (len != quint32(datagram.size() - kHeaderSize)) return false; // 长度对不上,丢弃 payload = datagram.mid(kHeaderSize, int(len)); type = static_cast<Type>(t); return true; }长度校验用!=而不是>,是因为 UDP 一个包就是一个完整报文,头里写的长度必须和实际剩余字节严格相等,不等就说明包被截断或者伪造了,直接丢比勉强解析安全。这两个函数没有任何网络调用,可以单独喂构造的字节数组做测试,答辩时被问到「怎么保证协议正确性」,这就是现成的答案。
3.3 消息类型定义与一次会话的完整时序
把消息类型和载荷内容定死,写代码时就不会临时发挥。下面这张表建议直接抄进设计文档:
| 类型值 | 名称 | 载荷内容 | 发送方式 |
|---|---|---|---|
| 0x01 | Text | 昵称 + 文本 | 群发广播或单播定向 |
| 0x02 | UserEnter | 昵称 + 主机名 | 广播 |
| 0x03 | UserLeave | 昵称 | 广播 |
| 0x04 | UserList | 昵称列表 | 单播应答 |
| 0x05 | Heartbeat | 昵称 + 时间戳 | 广播 |
| 0x06 | FileMeta | 文件编号 + 文件名 + 总字节 | 单播 |
| 0x07 | FileChunk | 文件编号 + 序号 + 分片 | 单播 |
| 0x08 | FileDone | 文件编号 | 单播 |
一次典型会话的时序是这样的:程序启动 → 绑定端口 → 广播一条 UserEnter → 其他节点收到后加入在线列表并回一条 UserList → 之后每 5 秒广播一次 Heartbeat → 用户发消息时广播 Text → 退出时广播 UserLeave。后启动的节点因为收不到之前的 UserEnter,所以要靠别人回的 UserList 补全列表,这一步很多人漏掉,导致「先开的窗口看不到后开的人」。
3.4 分片边界:为什么单包控制在 1400 字节以内
以太网 MTU 是 1500 字节,减去 IP 头 20 字节和 UDP 头 8 字节,留给应用层的是 1472。理论上是这个数,但实际链路上可能有 PPPoE、VLAN 标签再吃掉几个字节。所以工程上通常取 1400 作为安全线,超了就会被 IP 层分片——而 IP 分片对 UDP 来说是灾难:任何一个分片丢失,整个报文在接收端被丢弃,你连「丢了哪个包」都感知不到。这就是为什么传文件必须自己在应用层分片,每片 1024 字节,加上帧头和分片头也不到 1100,稳稳落在安全线内。
分片大小取 1024 而不是贴着 1400,还有一个现实考虑:将来如果要加校验字段或者加密,头部会长出来,留点余量免得回头改常量改出一堆连锁问题。
4. 核心模块实现:套接字初始化、收包分发与在线列表
设计定完之后就是落地。这一章按「打开套接字 → 收包 → 分发 → 维护列表 → 传文件」的顺序走,每一步都给可直接用的代码和它背后要注意的点。代码基于 Qt 5.9 以后的 API,信号槽用函数指针语法,编译期就能检查签名对不对。
4.1 套接字初始化:bind 参数与端口复用
绑定是第一个要写对的地方。同一台电脑上开两个实例做自测,如果不设端口复用,第二个实例bind直接返回 false,很多人以为是程序 bug,其实是系统在保护端口。
// src/net/udppeer.cpp bool UdpPeer::start() { m_socket = new QUdpSocket(this); // ShareAddress 允许多个实例绑同一端口;ReuseAddressHint 帮系统尽快回收 TIME_WAIT const bool ok = m_socket->bind(QHostAddress::AnyIPv4, cfg::kPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint); if (!ok) { emit errorOccurred(QString("端口 %1 绑定失败:%2") .arg(cfg::kPort).arg(m_socket->errorString())); return false; } connect(m_socket, &QUdpSocket::readyRead, this, &UdpPeer::onReadyRead); return true; }QHostAddress::AnyIPv4而不是LocalHost,是因为后者只监听回环地址,别的机器永远连不进来,这是「本机能发不能收」的经典原因。绑定的地址决定了监听的网卡范围,写错这一个参数,后面查一整天。
4.2 收包循环:hasPendingDatagrams 与分发
readyRead触发时可能积压了多个报文,必须用 while 循环取干净。每次取之前先问pendingDatagramSize(),按实际大小分配缓冲区,不要固定开 65536 的数组浪费栈空间。
void UdpPeer::onReadyRead() { while (m_socket->hasPendingDatagrams()) { const qint64 pending = m_socket->pendingDatagramSize(); if (pending <= 0) break; QByteArray datagram(int(pending), Qt::Uninitialized); QHostAddress sender; quint16 senderPort = 0; m_socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); dispatch(datagram, sender, senderPort); } } void UdpPeer::dispatch(const QByteArray &data, const QHostAddress &from, quint16 port) { proto::Type type; QByteArray payload; if (!proto::unpack(data, type, payload)) return; // 非法包直接丢弃 if (from == m_localAddr && port == cfg::kPort) return; // 过滤自己广播的回环 switch (type) { case proto::Type::Text: emit textReceived(from, payload); break; case proto::Type::UserEnter: emit peerEntered(from, port, payload); break; case proto::Type::Heartbeat: emit peerAlive(from, port, payload); break; case proto::Type::UserLeave: emit peerLeft(from, payload); break; case proto::Type::FileChunk: emit fileChunkReceived(from, payload); break; default: break; // 未知类型静默忽略 } }m_localAddr在初始化时用QNetworkInterface遍历拿到本机第一个非回环 IPv4 地址。过滤自己广播的回环是必须的,否则本机发出的每条消息都会在本地窗口里显示两遍。
4.3 上下线广播与心跳保活
广播不能只往255.255.255.255发。这个地址在有些系统、有些交换设备上会被直接丢弃,尤其是笔记本同时开着有线和无线的时候,默认路由走哪张卡都不一定。稳妥的做法是遍历所有处于活动状态的非回环网卡,取出各自的定向广播地址,逐个发一遍。
QList<QHostAddress> UdpPeer::broadcastAddresses() const { QList<QHostAddress> list; for (const QNetworkInterface &iface : QNetworkInterface::allInterfaces()) { const auto f = iface.flags(); if (!f.testFlag(QNetworkInterface::IsUp) || !f.testFlag(QNetworkInterface::IsRunning) || f.testFlag(QNetworkInterface::IsLoopBack)) continue; for (const QNetworkAddressEntry &e : iface.addressEntries()) { if (e.ip().protocol() == QAbstractSocket::IPv4Protocol && !e.broadcast().isNull()) list << e.broadcast(); } } if (list.isEmpty()) list << QHostAddress(QHostAddress::Broadcast); // 兜底 return list; } void UdpPeer::broadcastPresence(proto::Type type) { QByteArray payload; QDataStream out(&payload, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_9); out << m_userName << QHostInfo::localHostName(); const QByteArray block = proto::pack(type, payload); for (const QHostAddress &addr : broadcastAddresses()) m_socket->writeDatagram(block, addr, cfg::kPort); }QHostInfo::localHostName()一起发过去,是为了在列表里区分同名用户——两台机器都叫「admin」的时候,光看昵称分不清谁是谁,加上主机名就好办了。
4.4 在线列表维护与超时下线
在线列表用QMap<QString, PeerInfo>存,键用「IP:端口」而不是昵称,因为昵称可以随便改、可以重复,IP 加端口才是唯一标识。
struct PeerInfo { QString name; QString host; QHostAddress addr; quint16 port = 0; QDateTime lastSeen; };心跳收到就更新lastSeen,另外开一个 5 秒周期的QTimer扫描全表,超过 15 秒没更新就判定离线并从表里删掉。
void UdpPeer::sweepPeers() { const QDateTime now = QDateTime::currentDateTime(); bool changed = false; for (auto it = m_peers.begin(); it != m_peers.end(); ) { if (it->lastSeen.secsTo(now) * 1000 > cfg::kPeerTimeoutMs) { emit peerOffline(it->name); it = m_peers.erase(it); changed = true; } else { ++it; } } if (changed) emit peerListChanged(m_peers.keys()); }注意peerOffline信号要区分「主动下线」和「超时下线」两种情况。用户正常关窗口会广播UserLeave,这时候立刻删;进程被强杀或者网线被拔,只能靠超时兜底。演示的时候两种都展示一次,评委会觉得你考虑得比较全。
4.5 文件分片传输的停等实现
文件传输是这个项目里唯一需要分片的地方,也是最容易讲出彩的部分。最简单可靠的方案是停等式:发一片,等对方回确认,再发下一片。虽然慢,但不用处理乱序和重传窗口,代码量小,答辩时讲得清楚。
void UdpPeer::sendFile(const QString &path, const QHostAddress &to) { QFile f(path); if (!f.open(QIODevice::ReadOnly)) { emit errorOccurred("文件打不开:" + path); return; } const quint16 fileId = m_nextFileId++; // 先发元信息,让接收端建好缓冲文件和进度条 QByteArray meta; QDataStream mOut(&meta, QIODevice::WriteOnly); mOut.setVersion(QDataStream::Qt_5_9); mOut << fileId << QFileInfo(f).fileName() << quint32(f.size()); m_socket->writeDatagram(proto::pack(proto::Type::FileMeta, meta), to, cfg::kPort); quint32 seq = 0; while (!f.atEnd()) { const QByteArray chunk = f.read(cfg::kChunkSize); // 1024 字节 QByteArray payload; QDataStream out(&payload, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_9); out << fileId << seq++ << chunk; const QByteArray block = proto::pack(proto::Type::FileChunk, payload); if (block.size() > cfg::kMaxDatagram) { // 超限就别发,防止被分片 emit errorOccurred("分片过大,检查 kChunkSize"); break; } m_socket->writeDatagram(block, to, cfg::kPort); QThread::msleep(2); // 简单限速,避免瞬间打满接收端缓冲区 } QByteArray done; QDataStream dOut(&done, QIODevice::WriteOnly); dOut.setVersion(QDataStream::Qt_5_9); dOut << fileId << seq; // 带上总分片数,接收端可校验是否收全 m_socket->writeDatagram(proto::pack(proto::Type::FileDone, done), to, cfg::kPort); }这段是演示版写法,msleep放在主线程里会让界面短暂卡顿,文件大了很明显。要正式用,就改成状态机:把QFile和当前seq存成成员,用QTimer::singleShot(0, ...)驱动下一片,每发一片就返回事件循环。改法不难,但要把while拆成「发一片 + 记状态 + 再排一次定时器」,这一步改造过程本身就是答辩时很好的讲解素材。
5. 避坑与排查:五类高频故障的现象、成因与处置
这些坑基本都是我在带人做类似项目时反复见到的。共同特点是:现象看起来像「程序写错了」,实际原因都在配置或网络环境上。下面每条按现象、原因、解决三步写,遇到问题时对着查,比盲改代码快得多。
5.1 绑定失败提示 Address already in use
现象:程序第二次启动,弹窗报端口占用,或者干脆没反应,日志里bind返回 false。
原因分两种。一是没设ShareAddress和ReuseAddressHint,系统认为这个 UDP 端口已被独占;二是上一个进程还没退干净,任务管理器里能看到两个同名进程,端口自然抢不到。
解决:bind时把两个标志都带上,这是本机双实例自测的前提。同时在bind失败时把errorString()打到界面上,不要只return false让程序静默退出——很多人因为这个白白花半天怀疑代码逻辑。如果要求单实例运行,就别靠端口占用来判断,用QLockFile或者QSharedMemory在启动时检测,提示更友好。
5.2 本机双实例能通,两台机器收不到
现象:一台电脑上开两个窗口,互发消息完全正常;换成两台电脑,一个包都收不到,但ping是通的。
原因按概率排:Windows 防火墙默认拦截入站 UDP,第一次运行时弹的那个允许对话框被随手点了取消;两台机器不在同一子网,广播出不了网关;程序只往255.255.255.255发,这个地址在部分网络环境里会被丢弃;bind用了LocalHost,只监听回环。
解决:先给程序加上防火墙入站规则,或者测试时临时关掉防火墙确认是不是它的问题;确认两台机器的 IP 前三段一致(同一网段);把广播地址改成遍历网卡取定向广播,别只发全局广播;检查bind的第一个参数是AnyIPv4。排查顺序建议从防火墙开始,它是最常见也最容易验证的一环。
5.3 中文昵称变乱码或空字符串
现象:接收端显示的昵称是问号、方块,或者干脆是空的,文本消息内容也缺一截。
原因通常是三个:代码里用了toLatin1()转字节;QDataStream两端版本不一致,导致读QString时长度字段错位;打包时把QString先toUtf8()塞进QByteArray,解包时又用QDataStream去读,两套编码规则混用。
解决:统一走QDataStream,写和读都用<< QString和>> QString,并且两端setVersion写同一个值。如果一定要手动转字节,那就全用toUtf8()/fromUtf8(),绝不要出现toLatin1。改完之后用「张三」「测试昵称」这类多字节字符做一轮回归,别只用英文名测。
5.4 发送大包后界面卡住、后续消息丢失
现象:发一条很长的文本或者一张图片,界面卡住一两秒,接收端只显示前半截,后面的消息也乱了。
原因:单包超过 MTU 被 IP 层分片,任何一个分片丢失整个报文就废了;同时发送端如果在主线程里用while循环连续写,事件循环被堵住,readyRead无法处理,收发两侧一起卡。
解决:发之前判断block.size(),超过 1400 就走分片逻辑,别硬发。发送循环改成定时器驱动,一片一片来,每片之间让事件循环转一圈。接收端做一个字节计数器,收到FileDone时对比总长度,不一致就提示重传,别默默把半个文件写进磁盘。
5.5 自己发的消息在自己窗口里重复出现两遍
现象:本机发出的每条广播,本地窗口都显示两次;对方发来的消息正常。
原因:UDP 广播会回环到本机所有绑定了该端口的套接字,发出去的那一包自己也会收到一次。如果程序里又是「发送时本地追加一条 + 收到时再追加一条」,就变成两条。
解决:在分发函数里过滤,if (from == m_localAddr && port == cfg::kPort) return;。注意要同时比较 IP 和端口,只比 IP 的话,同机多实例场景下会把另一个实例的消息也误过滤掉。另一种更稳的做法是每条消息带一个 UUID,本地记一份已处理的集合,重复的丢弃——代价是多 16 字节和一点内存,但逻辑上更干净。
6. 验收与答辩:从单机双实例到双机互通的检查清单
代码写完不等于项目成了,验收这一步很多人在答辩现场才补,结果当场翻车。我一般把验收拆成三个阶段,每个阶段有明确的通过标准,全部走完再谈演示。
第一阶段单机自测:开两个实例,端口复用生效,互发文本正常,在线列表里能看到对方,关掉一个窗口另一个列表里 15 秒内消失。第二阶段双机同网段:两台机器互相发现,双向消息正常,抓一份包确认帧头魔数和类型字段对得上。第三阶段异常场景:拔网线、强杀进程、改昵称、端口被占,看程序有没有崩溃或者卡死。
下面这张表可以直接当验收单用,逐项打勾:
| 验收项 | 操作方式 | 预期结果 |
|---|---|---|
| 端口复用 | 同机启动两个实例 | 都能绑定成功,互不干扰 |
| 上线发现 | 后启动一个实例 | 已在线节点列表自动新增对方 |
| 文本互通 | 双向各发一条中文 | 内容完整,无乱码,不重复 |
| 超时下线 | 强杀一个进程 | 15 秒内对方列表移除该项 |
| 主动下线 | 正常关闭窗口 | 对方列表立即移除 |
| 文件传输 | 传一个 200KB 文件 | 大小一致,校验通过 |
| 异常鲁棒 | 发送非法构造包 | 程序不崩溃,包被静默丢弃 |
抓包这一步别省。用 Wireshark 过滤udp.port == 45454,能看到每一包的十六进制内容,帧头前四个字节应该是4C 41 4E 43,跟着是版本00 01、类型、保留位、长度。答辩时如果能当场把抓包结果和协议表对上,说服力比念 PPT 强得多——这说明你不是抄了一份代码,而是真的理解每一字节的去处。
另一个常被问到的点是「为什么用 UDP 不用 TCP」。准备一个具体的回答:这是局域网内的短消息广播场景,需要一对多同时通知所有节点,UDP 天然支持广播和组播,TCP 只能一对一,要为每个节点维护一条连接,连接数随人数线性增长;同时聊天消息丢一条可以接受,不值得为可靠性付出三次握手的延迟。说完紧接着补一句「文件传输对可靠性要求高,所以我在应用层做了分片序号和完成校验」,把短板当场补上,比等评委追问要好。
我自己的习惯是:每次交这类项目之前,一定把防火墙关掉、两台机器都重启一遍、从零开始走一遍验收单,绝不用「刚才还好的」这种状态去演示。有一回就是靠这个习惯,在正式演示前十分钟发现新配的无线网卡广播地址取不到,改了两行兜底逻辑才没翻车。从那以后我每次做完网络相关的作业,都强制走一遍「换机器、换网络、关防火墙」的三换流程。希望这套结构和验收单能帮到你。
需要完整工程的话,标题里那份基于 Qt5 的 UDP 局域网通信软件压缩包里有可直接打开的.pro工程、分层后的源码目录和配套说明,解压后用 Qt Creator 打开就能编译,照着本文的顺序改常量、跑验收单,一晚上应该能完整走通。
本文还有配套的精品资源,点击获取