Qt 网络编程 UDP通信

UDP (User Datagram Protocol 即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。

协议数据包丢失会比较严重。但是由于 UDP的特性:它 不属于连接型协议 ,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用 UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

QUdpSocket类提供了一个 UDP套接字。 QUdpSocket是 QAbstractSocket的子类,允许发送和接收 UDP。

使用该类最常见的方法是使用 bind()绑定到一个地址和端口,然后调用 writeDatagram() 和 readDatagram() / receiveDatagram() 来传输数据(注意一般要少于512字节)。

UDP 通信示意图:

UDP 消息传送有三种模式,分别是单播、广播和组播三种模式。

单播:两主机之间端对端通信。

广播:广播一般使用广播地址 255.255.255.255,将消息发送到在同一广播(也就是局域网内同一网段)网络上的每个主机。

组播:组播(多点广播),也称为"多播",将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

注意: 单播一样和多播是允许在广域网即Internet 上进行传输的,而广播仅仅在同一局域

网上才能进行。

gemini:

这是一个非常实用的技术话题。UDP(用户数据报协议)是网络编程的基石之一。

为了让你彻底理解,我将把 UDP 比作现实生活中的场景,然后手把手教你在 Qt 中如何实现。

一、 生动形象讲解:什么是 UDP?

如果说 TCP(传输控制协议) 像是 "打电话"

你必须先拨通对方(建立连接),对方接听后说"喂"(握手),你们才能确认对方在听,按顺序一句句说,没听清会让对方重说(重传),挂断时还要互道再见(断开连接)。虽然可靠,但步骤繁琐,速度慢。

那么 UDP(用户数据报协议) 就像是 "扔纸飞机""寄明信片"

  1. 无连接(Connectionless): 你写好一张纸条,折成飞机扔出去。你不需要确定对方是否做好了准备,也不需要先打招呼。

  2. 不可靠(Unreliable): 纸飞机飞出去后,可能会被风吹跑,可能会掉进水里,也可能被别人截获。UDP 不保证数据一定送达,也不保证不丢失。

  3. 无序(Unordered): 你扔了第1、2、3架飞机。对方接到的顺序可能是 3、1、2。UDP 不保证顺序。

  4. 面向报文(Datagram Oriented): 每一架飞机(数据包)都是独立的,界限分明。

  5. 极速(Fast): 因为省去了打电话的那些繁琐步骤(握手、确认、排序),UDP 的速度非常快。

适用场景: 视频直播(丢几帧画面没关系,卡顿才要命)、在线游戏(位置同步)、DNS 查询。

二、 Qt 中的 UDP 核心知识点

在 Qt 中,处理 UDP 的核心类是 QUdpSocket。它继承自 QAbstractSocket

1. 核心流程
  • 接收端(Receiver): 就像在家里装了一个信箱。

    • 你需要 Bind(绑定) 一个端口(Port)。这等于告诉邮递员:"寄给 8888 号信箱的信,都放到我这里。"
  • 发送端(Sender): 就像写信人。

    • 你不需要绑定端口(系统会随机分配一个),你只需要知道对方的 IP地址端口号,然后把数据发出去即可。
2. 常用函数 (API)
函数名 作用 也就是...
bind(port) 绑定端口 "占领这个信箱"。通常用于接收端。
writeDatagram(data, ip, port) 发送数据报 "把信扔出去"。指定内容、目标IP和目标端口。
hasPendingDatagrams() 检查是否有待处理数据 "看看信箱里有没有信"。返回 true/false。
pendingDatagramSize() 获取待读数据的大小 "这封信有多大"。用于准备足够大的缓冲区。
readDatagram(buffer, size) 读取数据 "把信取出来读"
3. 关键信号 (Signal)
  • readyRead() :这是最重要的一环。当有新的数据包到达你的端口时,QUdpSocket 会自动发出这个信号。你需要用槽函数(Slot)连接它,以便一收到数据就立刻处理。

三、 实战:Qt 简单的 UDP 传输示例

我们将编写一个最简单的程序,包含两部分功能:发送接收。为了方便演示,我们可以在同一个程序里既发又收(自己发给自己)。

1. 准备工作

在你的 Qt 项目文件 (.pro) 中,必须添加网络模块:

cpp 复制代码
QT += core gui network
2. 代码实现

假设我们需要建立一个简单的类 UdpCommunicator

头文件 (udpcommunicator.h)

cpp 复制代码
#ifndef UDPCOMMUNICATOR_H
#define UDPCOMMUNICATOR_H

#include <QObject>
#include <QUdpSocket> // 引入UDP套接字类

class UdpCommunicator : public QObject
{
    Q_OBJECT
public:
    explicit UdpCommunicator(QObject *parent = nullptr);

    // 发送消息的函数供外部调用
    void sendMessage(const QString &message);

private slots:
    // 处理接收到的数据的槽函数
    void processPendingDatagrams();

private:
    QUdpSocket *m_udpSocket; // UDP套接字指针
    quint16 m_port;          // 通信端口
};

#endif // UDPCOMMUNICATOR_H

源文件 (udpcommunicator.cpp)

cpp 复制代码
#include "udpcommunicator.h"
#include <QDebug>
#include <QNetworkDatagram> // Qt5.8+ 推荐使用,或者直接用 readDatagram

UdpCommunicator::UdpCommunicator(QObject *parent)
    : QObject(parent)
{
    m_udpSocket = new QUdpSocket(this);
    m_port = 12345; // 约定一个端口号,发送和接收都要知道这个号码

    // 【接收端逻辑】
    // 1. 绑定端口。
    // QHostAddress::LocalHost 表示只监听本机的消息。
    // 如果要接收局域网其他电脑的消息,通常使用 QHostAddress::Any
    bool bindResult = m_udpSocket->bind(QHostAddress::LocalHost, m_port);
    
    if(bindResult) {
        qDebug() << "UDP Socket 绑定成功,正在监听端口:" << m_port;
    } else {
        qDebug() << "绑定失败!端口可能被占用。";
    }

    // 2. 连接信号。当有数据包来时,触发 processPendingDatagrams
    connect(m_udpSocket, &QUdpSocket::readyRead, 
            this, &UdpCommunicator::processPendingDatagrams);
}

// 【发送端逻辑】
void UdpCommunicator::sendMessage(const QString &message)
{
    QByteArray data = message.toUtf8(); // 将字符串转换为字节数组

    // 发送数据报
    // 参数1: 数据内容
    // 参数2: 目标IP (这里发给自己)
    // 参数3: 目标端口
    m_udpSocket->writeDatagram(data, QHostAddress::LocalHost, m_port);
    
    qDebug() << "已发送数据:" << message;
}

// 【接收处理逻辑】
void UdpCommunicator::processPendingDatagrams()
{
    // 循环读取,直到没有挂起的数据报
    while (m_udpSocket->hasPendingDatagrams()) {
        
        // 传统写法 (兼容性好):
        QByteArray buffer;
        buffer.resize(m_udpSocket->pendingDatagramSize()); // 调整缓冲区大小
        
        QHostAddress sender;
        quint16 senderPort;

        // 读取数据,同时获取发送者的IP和端口
        m_udpSocket->readDatagram(buffer.data(), buffer.size(),
                                  &sender, &senderPort);

        QString msg = QString::fromUtf8(buffer);
        qDebug() << "接收到来自" << sender.toString() << ":" << senderPort 
                 << "的消息:" << msg;
    }
}
3. 如何在 main.cpp 中使用
cpp 复制代码
#include <QCoreApplication>
#include "udpcommunicator.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    UdpCommunicator comm;

    // 模拟发送数据
    comm.sendMessage("Hello, UDP World!");
    comm.sendMessage("Qt 网络编程很简单!");

    return a.exec();
}

四、 深入一点:注意事项与常见坑

  1. 大小限制(MTU):

    • UDP 数据包最好不要超过 512 字节1400 字节。虽然理论上限是 65535 字节,但超过网络层的 MTU(通常 1500 字节)会导致分片,一旦分片中有一个丢失,整个包就废了,效率会大打折扣。

    • 形象比喻: 纸飞机折太大不仅飞不远,还容易散架。

  2. 绑定失败:

    • 如果你运行了两个该程序的实例,第二个程序会报错"绑定失败"。因为端口(比如 12345)已经被第一个程序占用了。

    • 解决方法: 接收端必须由不同的端口监听,或者使用 QUdpSocket::ShareAddress 模式(允许多个程序复用端口,常用于组播)。

  3. 单播 vs 广播 vs 组播:

    • 单播 (Unicast): 点对点,就像我只扔给你。(QHostAddress::LocalHost 或具体 IP)

    • 广播 (Broadcast): 一对所有,就像我在广场用大喇叭喊。发送给 QHostAddress::Broadcast。局域网所有人都能收到。

    • 组播 (Multicast): 一对多(特定组),就像我们在一个微信群里说话。需要加入组播组 (joinMulticastGroup)。

总结

Qt 使用 UDP 就三板斧:

  1. new QUdpSocket

  2. 接收端 bind() 端口并关联 readyRead 信号。

  3. 发送端直接 writeDatagram()


gpt:

下面以**"生动形象 + 详细原理 + Qt中使用方法 + 示例代码"**的方式,来讲解 UDP 协议 以及 在 Qt 中如何轻松使用它


🛰 一、UDP 协议是什么?(生动形象版)

想象你在一个操场上大声喊:"小明,你妈妈喊你回家吃饭啦!"

  • 你直接喊 → 没有建立对话

  • 小明听到就回复 → 有可能听不到

  • 你不会确认他是否听到

  • 喊一句就完事儿

这个过程就像 UDP(User Datagram Protocol,用户数据报协议)

UDP 的特点(操场喊话版)

  1. 无连接(Connectionless)

    不打招呼,不建立连接,直接喊。

  2. 不可靠(Unreliable,但并不表示容易坏)

    不保证对方一定收到。如果风大、小明离得太远,叫声可能丢。

  3. 面向数据报(Datagram)

    你一句一句地喊,每句话都是独立的。

  4. 速度快

    没有连接,不确认,不重传,自然效率极高。

  5. 适用于:

    • 实时视频/音频

    • 游戏同步

    • 直播

    • 局域网广播、发现服务


🧩 二、UDP 在 Qt 中的工具:QUdpSocket

Qt 提供了一个非常简单好用的类:

复制代码
QUdpSocket

它是 Qt 网络模块中最易上手的类之一。


🛠 三、QUdpSocket 常用方法(快速掌握)

功能 函数 说明
发送数据 writeDatagram() 最常用,发出一个 UDP 包
接收数据 readDatagram() 读取一个 UDP 包
绑定端口 bind() 允许 socket 在某端口收包
判断是否有数据可读 hasPendingDatagrams() 是否有未读数据报
获取数据报大小 pendingDatagramSize() 下一个包的长度
广播 writeDatagram(..., QHostAddress::Broadcast) 发广播
加入组播 joinMulticastGroup() 多播使用
信号 readyRead 当有数据到达触发

🧠 四、Qt 程序中使用 UDP 的知识点

Qt 中使用 UDP,你需要了解:

1. 端口号(port)

  • UDP 按端口区分应用

  • 收数据前要 bind 端口

2. IP 地址(IPv4/IPv6)

类型:

  • 本机地址:127.0.0.1

  • 局域网:如 192.168.x.x

  • 广播地址:255.255.255.255

  • 多播地址:224.0.0.0 ~ 239.255.255.255

3. 事件驱动模型(signals/slots)

有数据到来时 Qt 自动发信号 readyRead()

cpp 复制代码
connect(socket, &QUdpSocket::readyRead, this, &MyClass::onReadyRead);

无须阻塞等待。


💡 五、Qt 简单使用示例(最小可运行)

以下展示最简单的 发送端接收端


📤 1)UDP 发送端(Sender)

cpp 复制代码
#include <QUdpSocket>

QUdpSocket udp;

void sendMessage()
{
    QByteArray data = "Hello UDP!";
    udp.writeDatagram(data, QHostAddress("127.0.0.1"), 45454);
}

只需要一行 writeDatagram() 就可以发送消息。


📥 2)UDP 接收端(Receiver)

cpp 复制代码
#include <QUdpSocket>

QUdpSocket udp;

void init()
{
    // 绑定端口,用于接收
    udp.bind(45454);

    connect(&udp, &QUdpSocket::readyRead, [&](){
        while (udp.hasPendingDatagrams())
        {
            QByteArray buf;
            buf.resize(udp.pendingDatagramSize());

            QHostAddress sender;
            quint16 senderPort;

            udp.readDatagram(buf.data(), buf.size(), &sender, &senderPort);

            qDebug() << "收到消息:" << buf;
            qDebug() << "来自:" << sender.toString() << ":" << senderPort;
        }
    });
}

🔄 六、完整示例:两个窗口互相发送消息

假设两个界面:A 和 B

端口如下:

程序 端口
A 发送到 B 45454
B 发送到 A 45455

A 程序代码示例:

cpp 复制代码
// A.h
QUdpSocket* socket;

// A.cpp
socket = new QUdpSocket(this);
socket->bind(45455); // 用于接收来自 B 的消息

connect(socket, &QUdpSocket::readyRead, this, &A::onReadyRead);

void A::onReadyRead()
{
    while (socket->hasPendingDatagrams())
    {
        QByteArray data;
        data.resize(socket->pendingDatagramSize());
        socket->readDatagram(data.data(), data.size());
        qDebug() << "A 收到:" << data;
    }
}

void A::onSendClicked()
{
    QByteArray msg = "Hello, I am A!";
    socket->writeDatagram(msg, QHostAddress("127.0.0.1"), 45454);
}

B 程序类似,只是端口互换


📡 七、广播示例(局域网广播)

cpp 复制代码
udp.writeDatagram("Hello LAN!", QHostAddress::Broadcast, 45454);

所有绑定 45454 的设备都会收到。


🎯 八、知识总结

UDP 优点

  • 简单

  • 支持广播、多播

  • 高实时性

UDP 缺点

  • 不可靠(包会丢、乱序)

  • 常需要应用层自行补充可靠性

Qt 中使用 QUdpSocket 的关键点

  • bind() 才能收包

  • 用信号 readyRead 异步接收

  • writeDatagram() 即可发送

  • 无需 connect(),即"无连接式通信"


单播与广播代码:

cpp 复制代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName   10_udp_unicast_broadcast
* @brief         mainwindow.h
* @author        Deng Zhimao
* @email         1252699831@qq.com
* @net           www.openedv.com
* @date          2021-04-14
*******************************************************************/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QUdpSocket>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTextBrowser>
#include <QLabel>
#include <QComboBox>
#include <QSpinBox>
#include <QHostInfo>
#include <QLineEdit>
#include <QNetworkInterface>
#include <QDebug>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    /* Udp通信套接字 */
    QUdpSocket *udpSocket;

    /* 按钮 */
    QPushButton *pushButton[5];

    /* 标签文本 */
    QLabel *label[3];

    /* 水平容器 */
    QWidget *hWidget[3];

    /* 水平布局 */
    QHBoxLayout *hBoxLayout[3];

    /* 垂直容器 */
    QWidget *vWidget;

    /* 垂直布局 */
    QVBoxLayout *vBoxLayout;

    /* 文本浏览框 */
    QTextBrowser *textBrowser;

    /* 用于显示本地ip */
    QComboBox *comboBox;

    /* 用于选择端口 */
    QSpinBox  *spinBox[2];

    /* 文本输入框 */
    QLineEdit *lineEdit;

    /* 存储本地的ip列表地址 */
    QList<QHostAddress> IPlist;

    /* 获取本地的所有ip */
    void getLocalHostIP();

private slots:
    /* 绑定端口 */
    void bindPort();

    /* 解绑端口 */
    void unbindPort();

    /* 清除文本框时的内容 */
    void clearTextBrowser();

    /* 接收到消息 */
    void receiveMessages();

    /* 发送消息 */
    void sendMessages();

    /* 广播消息 */
    void sendBroadcastMessages();

    /* 连接状态改变槽函数 */
    void socketStateChange(QAbstractSocket::SocketState);
};
#endif // MAINWINDOW_H
cpp 复制代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName   10_udp_unicast_broadcast
* @brief         mainwindow.cpp
* @author        Deng Zhimao
* @email         1252699831@qq.com
* @net           www.openedv.com
* @date          2021-04-14
*******************************************************************/
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    /* 设置主窗体的位置与大小 */
    this->setGeometry(0, 0, 800, 480);

    /* udp套接字 */
    udpSocket = new QUdpSocket(this);

    /* 绑定端口按钮 */
    pushButton[0] = new QPushButton();
    /* 解绑端口按钮 */
    pushButton[1] = new QPushButton();
    /* 清空聊天文本按钮 */
    pushButton[2] = new QPushButton();
    /* 发送消息按钮 */
    pushButton[3] = new QPushButton();
    /* 广播消息按钮 */
    pushButton[4] = new QPushButton();

    /* 水平布局一 */
    hBoxLayout[0] = new QHBoxLayout();
    /* 水平布局二 */
    hBoxLayout[1] = new QHBoxLayout();
    /* 水平布局三 */
    hBoxLayout[2] = new QHBoxLayout();
    /* 水平布局四 */
//    hBoxLayout[3] = new QHBoxLayout();

    /* 水平容器一 */
    hWidget[0] =  new QWidget();
    /* 水平容器二 */
    hWidget[1] =  new QWidget();
    /* 水平容器三 */
    hWidget[2] =  new QWidget();


    vWidget = new QWidget();
    vBoxLayout = new QVBoxLayout();

    /* 标签实例化 */
    label[0] = new QLabel();
    label[1] = new QLabel();
    label[2] = new QLabel();

    lineEdit = new QLineEdit();
    comboBox = new QComboBox();
    spinBox[0] = new QSpinBox();
    spinBox[1] = new QSpinBox();
    textBrowser = new QTextBrowser();

    label[0]->setText("目标IP地址:");
    label[1]->setText("绑定端口:");
    label[2]->setText("目标端口:");

    /* 设置标签根据文本文字大小自适应大小  */
    label[0]->setSizePolicy(QSizePolicy::Fixed,
                            QSizePolicy::Fixed);
    label[1]->setSizePolicy(QSizePolicy::Fixed,
                            QSizePolicy::Fixed);
    label[2]->setSizePolicy(QSizePolicy::Fixed,
                            QSizePolicy::Fixed);

    /* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
    spinBox[0]->setRange(10000, 99999);
    spinBox[1]->setRange(10000, 99999);

    pushButton[0]->setText("绑定端口");
    pushButton[1]->setText("解除绑定");
    pushButton[2]->setText("清空文本");
    pushButton[3]->setText("发送消息");
    pushButton[4]->setText("广播消息");

    /* 设置停止监听状态不可用 */
    pushButton[1]->setEnabled(false);

    /* 设置输入框默认的文本 */
    lineEdit->setText("你好!");

    /* 水平布局一添加内容 */
    hBoxLayout[0]->addWidget(pushButton[0]);
    hBoxLayout[0]->addWidget(pushButton[1]);
    hBoxLayout[0]->addWidget(pushButton[2]);

    /* 设置水平容器的布局为水平布局一 */
    hWidget[0]->setLayout(hBoxLayout[0]);

    hBoxLayout[1]->addWidget(label[0]);
    hBoxLayout[1]->addWidget(comboBox);
    hBoxLayout[1]->addWidget(label[1]);
    hBoxLayout[1]->addWidget(spinBox[0]);
    hBoxLayout[1]->addWidget(label[2]);
    hBoxLayout[1]->addWidget(spinBox[1]);

    /* 设置水平容器的布局为水平布局二 */
    hWidget[1]->setLayout(hBoxLayout[1]);

    /* 水平布局三添加内容 */
    hBoxLayout[2]->addWidget(lineEdit);
    hBoxLayout[2]->addWidget(pushButton[3]);
    hBoxLayout[2]->addWidget(pushButton[4]);

    /* 设置水平容器三的布局为水平布局一 */
    hWidget[2]->setLayout(hBoxLayout[2]);

    /* 垂直布局添加内容 */
    vBoxLayout->addWidget(textBrowser);
    vBoxLayout->addWidget(hWidget[1]);
    vBoxLayout->addWidget(hWidget[0]);
    vBoxLayout->addWidget(hWidget[2]);

    /* 设置垂直容器的布局为垂直布局 */
    vWidget->setLayout(vBoxLayout);

    /* 居中显示 */
    setCentralWidget(vWidget);

    /* 获取本地ip */
    getLocalHostIP();

    /* 信号槽连接 */
    connect(pushButton[0], SIGNAL(clicked()),
            this, SLOT(bindPort()));
    connect(pushButton[1], SIGNAL(clicked()),
            this, SLOT(unbindPort()));
    connect(pushButton[2], SIGNAL(clicked()),
            this, SLOT(clearTextBrowser()));
    connect(pushButton[3], SIGNAL(clicked()),
            this, SLOT(sendMessages()));
    connect(pushButton[4], SIGNAL(clicked()),
            this, SLOT(sendBroadcastMessages()));
    connect(udpSocket, SIGNAL(readyRead()),
            this, SLOT(receiveMessages()));
    connect(udpSocket,
            SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,
            SLOT(socketStateChange(QAbstractSocket::SocketState)));
}

MainWindow::~MainWindow()
{
}

void MainWindow::bindPort()
{
    quint16 port = spinBox[0]->value();

    /* 绑定端口需要在socket的状态为UnconnectedState */
    if (udpSocket->state() != QAbstractSocket::UnconnectedState)
        udpSocket->close();

    if (udpSocket->bind(port)) {
        textBrowser->append("已经成功绑定端口:"
                            + QString::number(port));

        /* 设置界面中的元素的可用状态 */
        pushButton[0]->setEnabled(false);
        pushButton[1]->setEnabled(true);
        spinBox[1]->setEnabled(false);
    }
}

void MainWindow::unbindPort()
{
    /* 解绑,不再监听 */
    udpSocket->abort();

    /* 设置界面中的元素的可用状态 */
    pushButton[0]->setEnabled(true);
    pushButton[1]->setEnabled(false);
    spinBox[1]->setEnabled(true);
}

/* 获取本地IP */
void MainWindow::getLocalHostIP()
{
    // /* 获取主机的名称 */
    // QString hostName = QHostInfo::localHostName();

    // /* 主机的信息 */
    // QHostInfo hostInfo = QHostInfo::fromName(hostName);

    // /* ip列表,addresses返回ip地址列表,注意主机应能从路由器获取到
    // * IP,否则可能返回空的列表(ubuntu用此方法只能获取到环回IP) */
    // IPlist = hostInfo.addresses();
    // qDebug()<<IPlist<<endl;

    // /* 遍历IPlist */
    // foreach (QHostAddress ip, IPlist) {
    //      if (ip.protocol() == QAbstractSocket::IPv4Protocol)
    //          comboBox->addItem(ip.toString());
    //    }

    /* 获取所有的网络接口,
     * QNetworkInterface类提供主机的IP地址和网络接口的列表 */
    QList<QNetworkInterface> list
            = QNetworkInterface::allInterfaces();

    /* 遍历list */
    foreach (QNetworkInterface interface, list) {

        /* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
        QList<QNetworkAddressEntry> entryList
                = interface.addressEntries();

        /* 遍历entryList */
        foreach (QNetworkAddressEntry entry, entryList) {
            /* 过滤IPv6地址,只留下IPv4 */
            if (entry.ip().protocol() ==
                    QAbstractSocket::IPv4Protocol) {
                comboBox->addItem(entry.ip().toString());
                /* 添加到IP列表中 */
                IPlist<<entry.ip();
            }
        }
    }
}

/* 清除文本浏览框里的内容 */
void MainWindow::clearTextBrowser()
{
    /* 清除文本浏览器的内容 */
    textBrowser->clear();
}

/* 客户端接收消息 */
void MainWindow::receiveMessages()
{
    /* 局部变量,用于获取发送者的IP和端口 */
    QHostAddress peerAddr;
    quint16 peerPort;

    /* 如果有数据已经准备好 */
    while (udpSocket->hasPendingDatagrams()) {
        /* udpSocket发送的数据报是QByteArray类型的字节数组 */
        QByteArray datagram;

        /* 重新定义数组的大小 */
        datagram.resize(udpSocket->pendingDatagramSize());

        /* 读取数据,并获取发送方的IP地址和端口 */
        udpSocket->readDatagram(datagram.data(),
                                datagram.size(),
                                &peerAddr,
                                &peerPort);
        /* 转为字符串 */
        QString str = datagram.data();

        /* 显示信息到文本浏览框窗口 */
        textBrowser->append("接收来自"
                            + peerAddr.toString()
                            + ":"
                            + QString::number(peerPort)
                            + str);
    }
}

/* 客户端发送消息 */
void MainWindow::sendMessages()
{
    /* 文本浏览框显示发送的信息 */
    textBrowser->append("发送:" + lineEdit->text());

    /* 要发送的信息,转为QByteArray类型字节数组,数据一般少于512个字节 */
    QByteArray data = lineEdit->text().toUtf8();

    /* 要发送的目标Ip地址 */
    QHostAddress peerAddr = IPlist[comboBox->currentIndex()];

    /* 要发送的目标端口号 */
    quint16 peerPort = spinBox[1]->value();

    /* 发送消息 */
    udpSocket->writeDatagram(data, peerAddr, peerPort);
}

void MainWindow::sendBroadcastMessages()
{
    /* 文本浏览框显示发送的信息 */
    textBrowser->append("发送:" + lineEdit->text());

    /* 要发送的信息,转为QByteArray类型字节数组,数据一般少于512个字节 */
    QByteArray data = lineEdit->text().toUtf8();

    /* 广播地址,一般为255.255.255.255,
     * 同一网段内监听目标端口的程序都会接收到消息 */
    QHostAddress peerAddr = QHostAddress::Broadcast;

    /* 要发送的目标端口号 */
    quint16 peerPort = spinBox[1]->text().toInt();

    /* 发送消息 */
    udpSocket->writeDatagram(data, peerAddr, peerPort);
}
/* socket状态改变 */
void MainWindow::socketStateChange(QAbstractSocket::SocketState state)
{
    switch (state) {
    case QAbstractSocket::UnconnectedState:
        textBrowser->append("scoket状态:UnconnectedState");
        break;
    case QAbstractSocket::ConnectedState:
        textBrowser->append("scoket状态:ConnectedState");
        break;
    case QAbstractSocket::ConnectingState:
        textBrowser->append("scoket状态:ConnectingState");
        break;
    case QAbstractSocket::HostLookupState:
        textBrowser->append("scoket状态:HostLookupState");
        break;
    case QAbstractSocket::ClosingState:
        textBrowser->append("scoket状态:ClosingState");
        break;
    case QAbstractSocket::ListeningState:
        textBrowser->append("scoket状态:ListeningState");
        break;
    case QAbstractSocket::BoundState:
        textBrowser->append("scoket状态:BoundState");
        break;
    default:
        break;
    }
}

组播代码:

本例大体流程首先获取本地 IP地址。创建一个 udpSocket套接字, 加入组播前必须绑定本机主机的端口。加入组播使用 joinMulticastGroup,退出组播使用 leaveMulticastGroup。其他收发消息的功能与上一节单播和广播一样。

cpp 复制代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName   10_udp_unicast_broadcast
* @brief         mainwindow.h
* @author        Deng Zhimao
* @email         1252699831@qq.com
* @net           www.openedv.com
* @date          2021-04-14
*******************************************************************/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QUdpSocket>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTextBrowser>
#include <QLabel>
#include <QComboBox>
#include <QSpinBox>
#include <QHostInfo>
#include <QLineEdit>
#include <QNetworkInterface>
#include <QDebug>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    /* Udp通信套接字 */
    QUdpSocket *udpSocket;

    /* 按钮 */
    QPushButton *pushButton[4];

    /* 标签文本 */
    QLabel *label[3];

    /* 水平容器 */
    QWidget *hWidget[3];

    /* 水平布局 */
    QHBoxLayout *hBoxLayout[3];

    /* 垂直容器 */
    QWidget *vWidget;

    /* 垂直布局 */
    QVBoxLayout *vBoxLayout;

    /* 文本浏览框 */
    QTextBrowser *textBrowser;

    /* 用于显示本地ip */
    QComboBox *comboBox[2];

    /* 用于选择端口 */
    QSpinBox  *spinBox;

    /* 文本输入框 */
    QLineEdit *lineEdit;

    /* 存储本地的ip列表地址 */
    QList<QHostAddress> IPlist;

    /* 获取本地的所有ip */
    void getLocalHostIP();

private slots:
    /* 加入组播 */
    void joinGroup();

    /* 退出组播 */
    void leaveGroup();

    /* 清除文本框时的内容 */
    void clearTextBrowser();

    /* 接收到消息 */
    void receiveMessages();

    /* 组播消息 */
    void sendMessages();

    /* 连接状态改变槽函数 */
    void socketStateChange(QAbstractSocket::SocketState);
};
#endif // MAINWINDOW_H
cpp 复制代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName   10_udp_unicast_broadcast
* @brief         mainwindow.cpp
* @author        Deng Zhimao
* @email         1252699831@qq.com
* @net           www.openedv.com
* @date          2021-04-14
*******************************************************************/
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    /* 设置主窗体的位置与大小 */
    this->setGeometry(0, 0, 800, 480);

    /* udp套接字 */
    udpSocket = new QUdpSocket(this);

    /* 参数1是设置IP_MULTICAST_TTL套接字选项允许应用程序主要限制数据包在Internet中的生存时间,
     * 并防止其无限期地循环,数据报跨一个路由会减一,默认值为1,表示多播仅适用于本地子网。*/
    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);

    /* 加入组播按钮 */
    pushButton[0] = new QPushButton();
    /* 退出组播按钮 */
    pushButton[1] = new QPushButton();
    /* 清空聊天文本按钮 */
    pushButton[2] = new QPushButton();
    /* 组播消息按钮 */
    pushButton[3] = new QPushButton();

    /* 水平布局一 */
    hBoxLayout[0] = new QHBoxLayout();
    /* 水平布局二 */
    hBoxLayout[1] = new QHBoxLayout();
    /* 水平布局三 */
    hBoxLayout[2] = new QHBoxLayout();
    /* 水平布局四 */
    hBoxLayout[3] = new QHBoxLayout();

    /* 水平容器一 */
    hWidget[0] =  new QWidget();
    /* 水平容器二 */
    hWidget[1] =  new QWidget();
    /* 水平容器三 */
    hWidget[2] =  new QWidget();


    vWidget = new QWidget();
    vBoxLayout = new QVBoxLayout();

    /* 标签实例化 */
    label[0] = new QLabel();
    label[1] = new QLabel();
    label[2] = new QLabel();

    lineEdit = new QLineEdit();
    comboBox[0] = new QComboBox();
    comboBox[1] = new QComboBox();
    spinBox = new QSpinBox();
    textBrowser = new QTextBrowser();

    label[0]->setText("本地IP地址:");
    label[1]->setText("组播地址:");
    label[2]->setText("组播端口:");

    /* 设置标签根据文本文字大小自适应大小  */
    label[0]->setSizePolicy(QSizePolicy::Fixed,
                            QSizePolicy::Fixed);
    label[1]->setSizePolicy(QSizePolicy::Fixed,
                            QSizePolicy::Fixed);
    label[2]->setSizePolicy(QSizePolicy::Fixed,
                            QSizePolicy::Fixed);

    /* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
    spinBox->setRange(10000, 99999);

    pushButton[0]->setText("加入组播");
    pushButton[1]->setText("退出组播");
    pushButton[2]->setText("清空文本");
    pushButton[3]->setText("组播消息");

    /* 设置停止监听状态不可用 */
    pushButton[1]->setEnabled(false);

    /* 设置输入框默认的文本 */
    lineEdit->setText("你好!");

    /* 默认添加范围内的一个组播地址 */
    comboBox[1]->addItem("239.255.255.1");

    /* 设置可编辑,用户可自行修改此地址 */
    comboBox[1]->setEditable(true);

    /* 水平布局一添加内容 */
    hBoxLayout[0]->addWidget(pushButton[0]);
    hBoxLayout[0]->addWidget(pushButton[1]);
    hBoxLayout[0]->addWidget(pushButton[2]);

    /* 设置水平容器的布局为水平布局一 */
    hWidget[0]->setLayout(hBoxLayout[0]);

    hBoxLayout[1]->addWidget(label[0]);
    hBoxLayout[1]->addWidget(comboBox[0]);
    hBoxLayout[1]->addWidget(label[1]);
    hBoxLayout[1]->addWidget(comboBox[1]);
    hBoxLayout[1]->addWidget(label[2]);
    hBoxLayout[1]->addWidget(spinBox);

    /* 设置水平容器的布局为水平布局二 */
    hWidget[1]->setLayout(hBoxLayout[1]);

    /* 水平布局三添加内容 */
    hBoxLayout[2]->addWidget(lineEdit);
    hBoxLayout[2]->addWidget(pushButton[3]);

    /* 设置水平容器三的布局为水平布局一 */
    hWidget[2]->setLayout(hBoxLayout[2]);

    /* 垂直布局添加内容 */
    vBoxLayout->addWidget(textBrowser);
    vBoxLayout->addWidget(hWidget[1]);
    vBoxLayout->addWidget(hWidget[0]);
    vBoxLayout->addWidget(hWidget[2]);

    /* 设置垂直容器的布局为垂直布局 */
    vWidget->setLayout(vBoxLayout);

    /* 居中显示 */
    setCentralWidget(vWidget);

    /* 获取本地ip */
    getLocalHostIP();

    /* 信号槽连接 */
    connect(pushButton[0], SIGNAL(clicked()),
            this, SLOT(joinGroup()));
    connect(pushButton[1], SIGNAL(clicked()),
            this, SLOT(leaveGroup()));
    connect(pushButton[2], SIGNAL(clicked()),
            this, SLOT(clearTextBrowser()));
    connect(pushButton[3], SIGNAL(clicked()),
            this, SLOT(sendMessages()));
    connect(udpSocket, SIGNAL(readyRead()),
            this, SLOT(receiveMessages()));
    connect(udpSocket,
            SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,
            SLOT(socketStateChange(QAbstractSocket::SocketState)));
}

MainWindow::~MainWindow()
{
}

void MainWindow::joinGroup()
{
    /* 获取端口 */
    quint16 port = spinBox->value();
    /* 获取组播地址 */
    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());

    /* 绑定端口需要在socket的状态为UnconnectedState */
    if (udpSocket->state() != QAbstractSocket::UnconnectedState)
        udpSocket->close();

    /* 加入组播前必须先绑定端口 */
    if (udpSocket->bind(QHostAddress::AnyIPv4,
                        port, QUdpSocket::ShareAddress)) {

        /* 加入组播组,返回结果给ok变量 */
        bool ok = udpSocket->joinMulticastGroup(groupAddr);

        textBrowser->append(ok ? "加入组播成功" : "加入组播失败");

        textBrowser->append("组播地址IP:"
                            + comboBox[1]->currentText());

        textBrowser->append("绑定端口:"
                            + QString::number(port));

        /* 设置界面中的元素的可用状态 */
        pushButton[0]->setEnabled(false);
        pushButton[1]->setEnabled(true);
        comboBox[1]->setEnabled(false);
        spinBox->setEnabled(false);
    }
}

void MainWindow::leaveGroup()
{
    /* 获取组播地址 */
    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());

    /* 退出组播 */
    udpSocket->leaveMulticastGroup(groupAddr);

    /* 解绑,不再监听 */
    udpSocket->abort();

    /* 设置界面中的元素的可用状态 */
    pushButton[0]->setEnabled(true);
    pushButton[1]->setEnabled(false);
    comboBox[1]->setEnabled(true);
    spinBox->setEnabled(true);
}

/* 获取本地IP */
void MainWindow::getLocalHostIP()
{
    // /* 获取主机的名称 */
    // QString hostName = QHostInfo::localHostName();

    // /* 主机的信息 */
    // QHostInfo hostInfo = QHostInfo::fromName(hostName);

    // /* ip列表,addresses返回ip地址列表,注意主机应能从路由器获取到
    // * IP,否则可能返回空的列表(ubuntu用此方法只能获取到环回IP) */
    // IPlist = hostInfo.addresses();
    // qDebug()<<IPlist<<endl;

    // /* 遍历IPlist */
    // foreach (QHostAddress ip, IPlist) {
    //      if (ip.protocol() == QAbstractSocket::IPv4Protocol)
    //          comboBox->addItem(ip.toString());
    //    }

    /* 获取所有的网络接口,
     * QNetworkInterface类提供主机的IP地址和网络接口的列表 */
    QList<QNetworkInterface> list
            = QNetworkInterface::allInterfaces();

    /* 遍历list */
    foreach (QNetworkInterface interface, list) {

        /* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
        QList<QNetworkAddressEntry> entryList
                = interface.addressEntries();

        /* 遍历entryList */
        foreach (QNetworkAddressEntry entry, entryList) {
            /* 过滤IPv6地址,只留下IPv4,并且不需要环回IP */
            if (entry.ip().protocol() ==
                    QAbstractSocket::IPv4Protocol &&
                    ! entry.ip().isLoopback()) {
                /* 添加本地IP地址到comboBox[0] */
                comboBox[0]->addItem(entry.ip().toString());
                /* 添加到IP列表中 */
                IPlist<<entry.ip();
            }
        }
    }
}

/* 清除文本浏览框里的内容 */
void MainWindow::clearTextBrowser()
{
    /* 清除文本浏览器的内容 */
    textBrowser->clear();
}

/* 客户端接收消息 */
void MainWindow::receiveMessages()
{
    /* 局部变量,用于获取发送者的IP和端口 */
    QHostAddress peerAddr;
    quint16 peerPort;

    /* 如果有数据已经准备好 */
    while (udpSocket->hasPendingDatagrams()) {
        /* udpSocket发送的数据报是QByteArray类型的字节数组 */
        QByteArray datagram;

        /* 重新定义数组的大小 */
        datagram.resize(udpSocket->pendingDatagramSize());

        /* 读取数据,并获取发送方的IP地址和端口 */
        udpSocket->readDatagram(datagram.data(),
                                datagram.size(),
                                &peerAddr,
                                &peerPort);
        /* 转为字符串 */
        QString str = datagram.data();

        /* 显示信息到文本浏览框窗口 */
        textBrowser->append("接收来自"
                            + peerAddr.toString()
                            + ":"
                            + QString::number(peerPort)
                            + str);
    }
}

/* 客户端发送消息 */
void MainWindow::sendMessages()
{
    /* 文本浏览框显示发送的信息 */
    textBrowser->append("发送:" + lineEdit->text());

    /* 要发送的信息,转为QByteArray类型字节数组,数据一般少于512个字节 */
    QByteArray data = lineEdit->text().toUtf8();

    /* 要发送的目标Ip地址 */
    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());

    /* 要发送的目标端口号 */
    quint16 groupPort = spinBox->value();

    /* 发送消息 */
    udpSocket->writeDatagram(data, groupAddr, groupPort);
}

/* socket状态改变 */
void MainWindow::socketStateChange(QAbstractSocket::SocketState state)
{
    switch (state) {
    case QAbstractSocket::UnconnectedState:
        textBrowser->append("scoket状态:UnconnectedState");
        break;
    case QAbstractSocket::ConnectedState:
        textBrowser->append("scoket状态:ConnectedState");
        break;
    case QAbstractSocket::ConnectingState:
        textBrowser->append("scoket状态:ConnectingState");
        break;
    case QAbstractSocket::HostLookupState:
        textBrowser->append("scoket状态:HostLookupState");
        break;
    case QAbstractSocket::ClosingState:
        textBrowser->append("scoket状态:ClosingState");
        break;
    case QAbstractSocket::ListeningState:
        textBrowser->append("scoket状态:ListeningState");
        break;
    case QAbstractSocket::BoundState:
        textBrowser->append("scoket状态:BoundState");
        break;
    default:
        break;
    }
}
相关推荐
hfut02883 小时前
第25章 interface
linux·服务器·网络
草莓熊Lotso3 小时前
unordered_map/unordered_set 使用指南:差异、性能与场景选择
java·开发语言·c++·人工智能·经验分享·python·网络协议
Sinowintop5 小时前
易连EDI-EasyLink SFTP文件传输
运维·服务器·网络·sftp·edi·ftp·国产edi软件
二狗mao11 小时前
Uniapp使用websocket进行ai回答的流式输出
websocket·网络协议·uni-app
7***u21611 小时前
显卡(Graphics Processing Unit,GPU)架构详细解读
大数据·网络·架构
河北瑾航科技14 小时前
广西水资源遥测终端 广西水利遥测终端 广西用水监测遥测终端 河北瑾航科技遥测终端机HBJH-B01说明书
网络·科技·水文遥测终端机·遥测终端机·广西水资源遥测终端机·广西水利遥测终端·广西用水终端
羑悻的小杀马特15 小时前
轻量跨云·掌控无界:Portainer CE + cpolar 让远程容器运维像点外卖一样简单——免复杂配置,安全直达对应集群
运维·网络·安全·docker·cpolar
愚戏师16 小时前
Python3 Socket 网络编程复习笔记
网络·笔记
降临-max16 小时前
JavaSE---网络编程
java·开发语言·网络·笔记·学习