Qt多进程(五)QUdpSocket

前言

在 Qt 开发中,除了 QTcpSocket,QUdpSocket 也是实现进程间通信(IPC)或网络通信的重要工具。虽然 UDP(User Datagram Protocol)不像 TCP 那样"可靠",但在特定场景下(如低延迟、广播、实时数据)具有不可替代的优势。

一、UDP 基础知识

1.UDP 是什么?

User Datagram Protocol(用户数据报协议)

无连接、不可靠、基于数据报(Datagram) 的传输层协议。

每个数据包独立发送,不保证顺序、不重传、可能丢失。

2. UDP 用于 IPC 的可行性

和 TCP 一样,UDP 也支持 本地回环通信(127.0.0.1)。

进程 A 向 127.0.0.1:9999 发送 UDP 包,进程 B 在该地址端口监听,即可通信。

由于无需建立连接,启动更快、开销更低。

因此,UDP 可作为轻量级、低延迟的本地 IPC 方案,尤其适合"发即忘"(fire-and-forget)场景。

3.Qt 对 UDP 的封装:QUdpSocket

QUdpSocket(继承自 QAbstractSocket),面向无连接,支持单播、广播、多播

核心方法:

writeDatagram() 发送一个数据报

readDatagram() 接收一个数据报

bind() 绑定本地端口(接收方必需)

信号 readyRead() ------ 有数据报到达时触发

与 QTcpSocket 不同:QUdpSocket 没有"连接"概念,同一个 socket 可向多个目标发包,也可接收来自任意源的数据。

4.UDP 作为 IPC 方式的优缺点

✅ 优点:

极低延迟,无连接建立、无确认、无重传,内核处理快

开销小,头部仅 8 字节(TCP 为 20+ 字节)

天然支持广播/多播,适合一对多通知(如服务发现)

无粘包问题,每个 writeDatagram() 对应一个完整消息

跨平台 & 跨语言,任何支持 UDP 的程序都能互通

❌ 缺点:

不可靠,数据包可能丢失、重复、乱序

无流量控制,高频发送可能导致内核丢包

消息大小限制,单个数据报 ≤ 64KB(实际建议 < 1500 字节,避免 IP 分片)

无内置身份验证,无法知道数据来自哪个"可信进程"(需应用层处理)

调试困难,丢包不易察觉,需日志或抓包确认

5.单播、广播、组播的区别

简单示例:

单播:

cpp 复制代码
// Qt 单播示例
socket.writeDatagram(data, QHostAddress("192.168.1.100"), 8888);

广播:

cpp 复制代码
// Qt 广播示例
socket.writeDatagram(data, QHostAddress::Broadcast, 9999);
// 接收端需 bind(QHostAddress::Any, 9999);

组播:

cpp 复制代码
// Qt 组播发送
socket.writeDatagram(data, QHostAddress("239.255.0.1"), 8888);

// Qt 组播接收(关键!)
socket.bind(QHostAddress::Any, 8888);
socket.joinMulticastGroup(QHostAddress("239.255.0.1"));

二、代码示例

为udp单独设计界面类:

cpp 复制代码
#ifndef UDPWINDOW_H
#define UDPWINDOW_H

#include <QWidget>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QUdpSocket>
#include <QSpinBox>
#include <QDateTime>

// 🌟 新增:端口常量,一目了然!
static const int SERVER_PORT = 8888;
static const int CLIENT_PORT = 8889;

class UDPWindow : public QWidget
{
    Q_OBJECT

public:
    explicit UDPWindow(const QString &role, QWidget *parent = nullptr);

private slots:
    void onSendMessage();
    void onPortChanged(int port);
    void onReadyRead();

private:
    void appendLog(const QString &msg);
    void setupUDPSocket();

    QString m_role;
    int m_port; // 本地绑定端口(服务器用8888,客户端用8889)
    QTextEdit *m_logView;
    QLineEdit *m_inputEdit;
    QPushButton *m_sendButton;
    QSpinBox *m_portSpinBox;
    QUdpSocket *m_udpSocket;
    QString m_targetHost; // 目标主机(固定为127.0.0.1)
    int m_targetPort;     // 发送目标端口(服务器→客户端8889,客户端→服务器8888)
};

#endif // UDPWINDOW_H
cpp 复制代码
#include "udpwindow.h"
#include <QLabel>
#include <QMessageBox>

UDPWindow::UDPWindow(const QString &role, QWidget *parent)
    : QWidget(parent)
    , m_role(role)
    , m_udpSocket(nullptr)
    , m_targetHost("127.0.0.1")
{
    // 🌟 修复1:按角色初始化本地端口和目标端口
    if (role == "Server") {
        m_port = SERVER_PORT;
        m_targetPort = CLIENT_PORT; // 服务器发送目标:客户端的端口
    } else if (role == "Client") {
        m_port = CLIENT_PORT;
        m_targetPort = SERVER_PORT; // 客户端发送目标:服务器的端口
    } else {
        // 安全处理:如果角色不对,用默认值
        m_port = SERVER_PORT;
        m_targetPort = CLIENT_PORT;
        appendLog("⚠️ 角色无效,使用默认配置(服务器模式)");
    }

    setWindowTitle("UDP Socket - " + role);
    resize(600, 500);

    m_logView = new QTextEdit();
    m_logView->setReadOnly(true);
    m_inputEdit = new QLineEdit();
    m_inputEdit->setPlaceholderText("Enter message to send...");
    m_sendButton = new QPushButton("Send Message");

    QHBoxLayout *portLayout = new QHBoxLayout();
    QLabel *portLabel = new QLabel("Port:");
    m_portSpinBox = new QSpinBox();
    m_portSpinBox->setRange(1024, 65535);
    m_portSpinBox->setValue(m_port); // 用初始化的端口

    portLayout->addWidget(portLabel);
    portLayout->addWidget(m_portSpinBox);

    QVBoxLayout *mainLayout = new QVBoxLayout();
    mainLayout->addWidget(m_logView);
    mainLayout->addLayout(portLayout);
    mainLayout->addWidget(m_inputEdit);
    mainLayout->addWidget(m_sendButton);

    setLayout(mainLayout);

    connect(m_sendButton, &QPushButton::clicked, this, &UDPWindow::onSendMessage);
    connect(m_portSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &UDPWindow::onPortChanged);

    setupUDPSocket();
    onPortChanged(m_port); // 初始化绑定

    appendLog("UDP " + m_role + " initialized on port " + QString::number(m_port)
              + " (target: " + QString::number(m_targetPort) + ")");
}

void UDPWindow::setupUDPSocket()
{
    m_udpSocket = new QUdpSocket(this);
    connect(m_udpSocket, &QUdpSocket::readyRead, this, &UDPWindow::onReadyRead);
}

void UDPWindow::onSendMessage()
{
    QString msg = m_inputEdit->text();
    if (msg.isEmpty()) return;

    QByteArray datagram = msg.toUtf8();
    qint64 sent = m_udpSocket->writeDatagram(datagram, QHostAddress(m_targetHost), m_targetPort);

    if (sent == -1) {
        appendLog("❌ Failed to send: " + m_udpSocket->errorString());
    } else {
        appendLog("✅ Sent to " + m_targetHost + ":" + QString::number(m_targetPort) + " | " + msg);
        m_inputEdit->clear();
    }
}

void UDPWindow::onPortChanged(int port)
{
    m_port = port;

    // 关闭当前绑定(如果已绑定)
    if (m_udpSocket->state() == QAbstractSocket::BoundState) {
        m_udpSocket->close();
    }

    // 重新绑定到新端口
    if (!m_udpSocket->bind(QHostAddress::Any, m_port)) {
        // 🌟 修复2:更友好的错误提示
        appendLog("⚠️ Failed to bind to port " + QString::number(m_port) + ": " + m_udpSocket->errorString());
        // 建议:弹出提示框(可选,这里用日志代替)
        QMessageBox::warning(this, "Binding Error", "Could not bind to port " + QString::number(m_port) + ".\nCheck if port is in use.");
    } else {
        appendLog("📌 UDP socket bound to port " + QString::number(m_port));
    }
}

void UDPWindow::onReadyRead()
{
    while (m_udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(m_udpSocket->pendingDatagramSize());

        QHostAddress sender;
        quint16 senderPort;

        m_udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);

        QString msg = QString::fromUtf8(datagram);
        appendLog("🔄 Received from " + sender.toString() + ":" + QString::number(senderPort) + " | " + msg);
    }
}

void UDPWindow::appendLog(const QString &msg)
{
    m_logView->append(QDateTime::currentDateTime().toString("hh:mm:ss") + " | " + msg);
}

运行效果:

这里实现了udp的双向通信,本质上是双端各自绑定不同的本地端口号,然后发送消息的时候,往对方的端口写入消息,因此,其实开启了两个不同的udp socket bind绑定。

三、总结

udp的相关内容就不多赘述了,直接把三种方式拉个对比表格吧!

最后提一下udp的不可靠性,也就是可能会丢包的问题。既然我们需要做通信,丢包导致的消息未送达问题一定是需要考虑的。

在某些场合下,udp的丢包是可以接受的,比如心跳发送、状态发送这些允许轻量丢包的业务。

但有些情况下,udp的丢包可能会导致问题,比如一次性的命令发送,丢包直接就导致命令未响应了。

实际工作中就遇到过这样的问题,我们需要对底下的几十台机子发送命令,比如执行x操作。因为机子都是连接到局域网内同网段的,那我们直接用自定义协议的udp广播就行了,但实际情况出现了个别机子无响应,具体情况是没有接收到消息。这个时候,我们需要对它重新发送一次消息。

怎么做到呢?机子收到消息之后,会主动向服务端也就是电脑发送应答信号。电脑端记录下它们的ip,并

在短暂延时(如1s)后,判断有哪些设备未应答。之后,电脑端给这些未应答的设备单独补发一次命令,也就是单播。如果还是未应答,那就判定掉线即可。

也就是说,需要通过应答交互,来尽可能确保消息送达。

相关推荐
De-Alf2 小时前
Megatron-LM学习笔记(6)Megatron Model Attention注意力与MLA
笔记·学习·算法·ai
Grassto2 小时前
从 GOPATH 到 Go Module:Go 依赖管理机制的演进
开发语言·后端·golang·go
懒惰蜗牛2 小时前
Day66 | 深入理解Java反射前,先搞清楚类加载机制
java·开发语言·jvm·链接·类加载机制·初始化
hudawei9962 小时前
flutter路由传参接收时机
开发语言·flutter·异步
3824278272 小时前
python:Ajax爬取电影详情实战
开发语言·python·ajax
微爱帮监所写信寄信2 小时前
微爱帮监狱写信寄信工具服务器【Linux篇章】再续:TCP协议——用技术隐喻重构网络世界的底层逻辑
linux·服务器·开发语言·网络·网络协议·小程序·监狱寄信
冰西瓜6002 小时前
STL——vector
数据结构·c++·算法
赵庆明老师2 小时前
VS2026扩展插件Visual Commander
java·开发语言