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. 网络隔离 - 不同子网之间广播不互通

相关推荐
有时间要学习3 小时前
Qt——系统相关
qt
橘颂TA3 小时前
【QSS】软件界面的美工操作——Qt 界面优化
开发语言·qt·c/c++·界面设计
Evand J3 小时前
【MATLAB例程】二维环境定位,GDOP和CRLB的计算,锚点数=4的情况(附代码下载链接)
开发语言·matlab·定位·toa·crlb·gdop
郝学胜-神的一滴4 小时前
使用现代C++构建高效日志系统的分步指南
服务器·开发语言·c++·程序人生·个人开发
你不是我我4 小时前
【Java 开发日记】我们来讲一讲阻塞队列及其应用
java·开发语言
互联网中的一颗神经元4 小时前
小白python入门 - 9. Python 列表2 ——从基础操作到高级应用
java·开发语言·python
wjs20244 小时前
PHP 表单:深入浅出地掌握表单处理
开发语言
摇滚侠4 小时前
Spring Boot3零基础教程,生命周期监听,自定义监听器,笔记59
java·开发语言·spring boot·笔记
凯子坚持 c4 小时前
Llama-2-7b在昇腾NPU上的六大核心场景性能基准报告
java·开发语言·llama