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校验和用于检测数据报在传输过程中是否发生错误,计算过程较为特殊:

  1. 伪首部参与运算:校验和计算包括三部分:UDP伪首部、UDP首部、应用数据

  2. 二进制反码求和:所有参与运算的内容按16位对齐求和,求和过程中遇到进位都被回卷

  3. 结果取反:最后得到的和取反码,就是UDP的校验和

伪首部包含:源IP地址(4B)、目的IP地址(4B)、0(1B)、协议号(1B)和UDP长度(2B)。这样的设计既检查了UDP用户数据报的源端口号和目的端口号以及UDP用户数据报的数据部分,又检查了IP数据报的源IP地址和目的地址。

4. UDP数据包传输全过程

当UDP数据过大(超过MTU)时,会在IP层进行分片。以一个10KB的UDP包为例:

  1. 分片计算:10KB UDP数据会被IP层拆分为约7个IP数据报(分片)进行传输

  2. 独立传输:交换机和中间路由器都不会等待完整的10KB数据,而是逐帧/逐分片转发

  3. 目的地重组:只有目的主机的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广播的真实特性

  1. 广播有范围限制 - 只在当前子网

  2. 多接口独立 - 每个网络接口有自己的广播域

  3. 发送方自收 - 广播者自己也能收到

  4. 网络隔离 - 不同子网之间广播不互通

相关推荐
名字还没想好☜几秒前
Go 的 select 实战:超时、非阻塞收发与优雅退出的三个套路
开发语言·数据库·golang·go·并发
geovindu1 分钟前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
Scott9999HH8 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海10 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖10 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区13 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大13 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai14 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang