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

相关推荐
用户805533698034 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner4 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz9 天前
QML Hello World 入门示例
qt
xcyxiner12 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner13 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner13 天前
DicomViewer (添加模型类)3
qt
xcyxiner14 天前
DicomViewer (目录调整) 2
qt
xcyxiner14 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00616 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术16 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript