Qt-UDP
一、绪论
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
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QUdpSocket>
#include <QNetworkDatagram>QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic: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
#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广播的真实特性:
广播有范围限制 - 只在当前子网
多接口独立 - 每个网络接口有自己的广播域
发送方自收 - 广播者自己也能收到
网络隔离 - 不同子网之间广播不互通
