一、绪论
1. UDP协议核心特性
UDP(User Datagram Protocol)在计算机网络中与TCP同为传输层协议,但设计哲学截然不同。它具有以下核心特点:
-
无连接通信:发送数据前不需要建立连接,减少了开销和发送数据之前的时延。
-
不可靠传输:不保证数据包能够送达目的地,也不保证数据包的顺序性。
-
面向报文:对于应用层交下来的报文,UDP既不合并,也不拆分,而是保留这些报文的边界。
-
无拥塞控制:网络出现拥塞时不会降低源主机的发送速率,适合实时应用。
-
支持多播广播:支持一对一、一对多、多对一和多对多的交互通信。
2. UDP报文格式详解
每个UDP报文分为UDP报头 和UDP数据区两部分。报头由四个16位长(8字节)字段组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| 源端口 | 16位 | 标识源端进程所使用的端口 |
| 目的端口 | 16位 | 标识目的端进程所使用的端口 |
| 报文长度 | 16位 | 指定UDP报头和数据总共占用的长度 |
| 校验和 | 16位 | 用于发现头部信息和数据中的传输错误 |
3. UDP校验和计算机制
注意这个校验和不可靠传输的区别
这个是为了确保数据包的正确性:保证接收到的数据与发送时一致,没有在传输过程中被破坏
不可靠传输时指:不保证数据包能够送达目的地,也不保证数据包的顺序性。
UDP校验和用于检测数据报在传输过程中是否发生错误,计算过程较为特殊:
-
伪首部参与运算:校验和计算包括三部分:UDP伪首部、UDP首部、应用数据
-
二进制反码求和:所有参与运算的内容按16位对齐求和,求和过程中遇到进位都被回卷
-
结果取反:最后得到的和取反码,就是UDP的校验和
伪首部包含:源IP地址(4B)、目的IP地址(4B)、0(1B)、协议号(1B)和UDP长度(2B)。这样的设计既检查了UDP用户数据报的源端口号和目的端口号以及UDP用户数据报的数据部分,又检查了IP数据报的源IP地址和目的地址。
4. UDP数据包传输全过程
当UDP数据过大(超过MTU)时,会在IP层进行分片。以一个10KB的UDP包为例:
-
分片计算:10KB UDP数据会被IP层拆分为约7个IP数据报(分片)进行传输
-
独立传输:交换机和中间路由器都不会等待完整的10KB数据,而是逐帧/逐分片转发
-
目的地重组:只有目的主机的IP层会等待所有分片到达后重组为原始UDP报文
二、案例
.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QUdpSocket>
#include <QNetworkDatagram>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_sendButton_clicked();
void on_bindButton_clicked();
void processPendingDatagrams();
void on_targetIP_textChanged(const QString &arg1);
private:
Ui::MainWindow *ui;
QUdpSocket *udpSocket;
quint16 currentPort;
bool isBound;
void updateStatus(const QString &message, bool isError = false);
};
#endif // MAINWINDOW_H
.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QDateTime>
#include <QNetworkInterface>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, udpSocket(new QUdpSocket(this))
, currentPort(0)
, isBound(false)
{
ui->setupUi(this);
// 设置默认端口
ui->portEdit->setText("12345");
ui->targetPort->setText("12345");
ui->targetIP->setText("127.0.0.1");
// 连接信号槽
connect(udpSocket, &QUdpSocket::readyRead,
this, &MainWindow::processPendingDatagrams);
// 获取本机IP地址并显示
QString localIPs;
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
for (const QHostAddress &address : ipAddressesList) {
if (address.protocol() == QAbstractSocket::IPv4Protocol &&
address != QHostAddress::LocalHost) {
localIPs += address.toString() + "\n";
}
}
ui->localIPLabel->setText("本机IP:\n" + localIPs);
updateStatus("准备就绪 - 请先绑定端口");
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_bindButton_clicked()
{
if (isBound) {
// 解绑
udpSocket->close();
isBound = false;
ui->bindButton->setText("绑定端口");
ui->portEdit->setEnabled(true);
updateStatus("端口已解绑");
return;
}
quint16 port = ui->portEdit->text().toUShort();
if (port == 0) {
QMessageBox::warning(this, "错误", "请输入有效的端口号 (1-65535)");
return;
}
if (udpSocket->bind(QHostAddress::Any, port)) {
currentPort = port;
isBound = true;
ui->bindButton->setText("解绑端口");
ui->portEdit->setEnabled(false);
updateStatus(QString("已绑定端口: %1").arg(port));
} else {
updateStatus(QString("绑定端口失败: %1").arg(udpSocket->errorString()), true);
}
}
void MainWindow::on_sendButton_clicked()
{
if (!isBound) {
QMessageBox::warning(this, "错误", "请先绑定端口");
return;
}
QString message = ui->messageEdit->toPlainText().trimmed();
if (message.isEmpty()) {
QMessageBox::warning(this, "错误", "请输入要发送的消息");
return;
}
QString targetIP = ui->targetIP->text();
quint16 targetPort = ui->targetPort->text().toUShort();
if (targetPort == 0) {
QMessageBox::warning(this, "错误", "请输入有效的目标端口号");
return;
}
QHostAddress targetAddress;
if (targetIP == "255.255.255.255" || targetIP == "广播") {
targetAddress = QHostAddress::Broadcast;
} else {
targetAddress = QHostAddress(targetIP);
}
if (targetAddress.isNull()) {
QMessageBox::warning(this, "错误", "请输入有效的IP地址");
return;
}
QByteArray data = message.toUtf8();
qint64 sent = udpSocket->writeDatagram(data, targetAddress, targetPort);
if (sent == -1) {
updateStatus(QString("发送失败: %1").arg(udpSocket->errorString()), true);
} else {
// 在消息显示区域添加发送的消息
QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
ui->messageDisplay->append(QString("[%1] 发送 → %2:%3\n%4\n")
.arg(timestamp)
.arg(targetAddress.toString())
.arg(targetPort)
.arg(message));
ui->messageEdit->clear();
updateStatus(QString("消息已发送到 %1:%2").arg(targetAddress.toString()).arg(targetPort));
}
}
void MainWindow::processPendingDatagrams()
{
while (udpSocket->hasPendingDatagrams()) {
QNetworkDatagram datagram = udpSocket->receiveDatagram();
QByteArray data = datagram.data();
QHostAddress senderAddress = datagram.senderAddress();
quint16 senderPort = datagram.senderPort();
QString message = QString::fromUtf8(data);
QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
// 在消息显示区域添加接收的消息
ui->messageDisplay->append(QString("[%1] 接收 ← %2:%3\n%4\n")
.arg(timestamp)
.arg(senderAddress.toString())
.arg(senderPort)
.arg(message));
updateStatus(QString("收到来自 %1:%2 的消息").arg(senderAddress.toString()).arg(senderPort));
}
}
void MainWindow::on_targetIP_textChanged(const QString &arg1)
{
if (arg1.toLower() == "broadcast" || arg1 == "255.255.255.255") {
ui->targetIP->setText("255.255.255.255");
updateStatus("设置为广播模式 - 消息将发送到同一网络的所有主机");
}
}
void MainWindow::updateStatus(const QString &message, bool isError)
{
QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
QString statusMessage = QString("[%1] %2").arg(timestamp).arg(message);
ui->statusLabel->setText(statusMessage);
if (isError) {
ui->statusLabel->setStyleSheet("color: red;");
} else {
ui->statusLabel->setStyleSheet("color: green;");
}
}
结果:

这里可以看到我用一个受限广播地址去发送消息,我本机有三个IP,却只有一个ip收到消息。
这个结果完美体现了UDP广播的真实特性:
-
广播有范围限制 - 只在当前子网
-
多接口独立 - 每个网络接口有自己的广播域
-
发送方自收 - 广播者自己也能收到
-
网络隔离 - 不同子网之间广播不互通