QT_一个UDP通信程序(单播+广播)

Qt实战案例:从零写一个UDP通信程序(单播+广播,含避坑指南)

完整的Qt UDP实战案例复盘,从创建工程到运行验证,支持单播广播的UDP通信程序。

目录

文章目录


一、效果预览与功能清单

程序界面一共三行控件:上方一排是端口和IP配置,中间一排是消息输入和发送按钮,下方是聊天记录窗口。

程序运行后可实现:

功能 操作 说明
启动服务 填本机端口 → 点"启动服务" bind() 绑定端口,开始监听
停止服务 点"停止服务" abort() 解除绑定
单播发送 填目标IP+目标端口 → 输入消息 → 点"发送消息" 点对点发给一台主机
广播发送 填目标端口 → 输入消息 → 点"广播消息" 局域网内所有绑定该端口的主机都收到
接收显示 无需操作 收到消息自动显示 [From:IP:端口] 内容

通信流程示意:

复制代码
   实例A (绑定端口8001)                        实例B (绑定端口8002)
        |                                          ▲
        +--- 单播: writeDatagram(数据, B的IP, 8002) -->|
        ▲                                          |
        +<--- 回复: writeDatagram(数据, A的IP, 8001) --+

   广播: writeDatagram(数据, 255.255.255.255, 8888)
        --> 局域网内所有绑定8888端口的主机全部收到

二、UDP协议快速入门

写代码前,用3分钟了解UDP的核心概念。

1. UDP是什么

UDP(用户数据报协议)是一种轻量、无连接、不可靠、面向数据报的传输层协议:

特性 含义 通俗类比
无连接 发送前不需要握手,直接"扔"出去 不用拨号,直接发传单
面向数据报 每个数据报是独立完整的"包裹" 一次writeDatagram对应一次readDatagram
不可靠 可能丢包、乱序,丢了不重发 发出去就不管了
高效 首部仅8字节,无复杂控制机制 延迟小、速度快

2. 与TCP的直观区别(本项目相关)

对比项 UDP(本项目) TCP
建立连接 不需要 三次握手
服务器概念 没有,双方都是"平级",bind即可收 QTcpServer监听+accept
消息边界 数据报自带边界,无黏包问题 字节流,需自己处理黏包
发送函数 writeDatagram() write()
接收函数 readDatagram() read()/readAll()

一句话理解:UDP程序 = "bind装个信箱" + "writeDatagram寄传单" + "readyRead收信"。

3. 三种通信模式

复制代码
单播(一对一):发给指定IP           →  本项目【发送消息】按钮
广播(一对所有):发给255.255.255.255 →  本项目【广播消息】按钮
组播(一对一组):发给组播地址(224~239开头,需先加入组) → 本项目未实现,见扩展方向

三、开发环境

项目 版本/说明
Qt Qt 5.x / Qt 6.x 均可(本文代码兼容)
编译器 MinGW 或 MSVC 均可
Qt Creator 任意版本
系统 Windows(Linux/macOS同样适用)
前置知识 C++基础、Qt信号槽、Qt控件基本使用

四、第一步:创建工程

  1. Qt Creator → 文件 → 新建文件或项目 → Application (Qt) → Qt Widgets Application
  2. 项目名称填 QUdpSocketpro,构建系统选 qmake
  3. 类信息:类名 MainWindow,基类 QMainWindow(勾选"创建界面"生成 .ui 文件)
  4. Kit 选择默认即可,完成创建

关键一步:添加network模块

创建完成后,打开 .pro 文件,确保包含 network

pro 复制代码
QT       += core gui
QT       += network        # ← 必须加这一行!否则编译报错找不到 QUdpSocket

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

新手第一大坑 :忘了加 QT += network,编译时报 QUdpSocket: No such file or directory


五、第二步:界面设计

双击打开 mainwindow.ui,按下面表格拖放控件(布局:垂直布局包含两行水平布局+一个文本框):

控件清单

控件类型 objectName 显示文本/默认值 作用
QLabel label "原机器端口" 提示
QSpinBox bindPort 默认1567,max 65535 本机绑定端口输入
QLabel label_2 "目标IP地址" 提示
QComboBox targetIp (代码中动态添加) 目标IP下拉框
QLabel label_3 "目标端口号" 提示
QSpinBox targetPort 默认1568,max 65535 目标端口输入
QPushButton startBtn "启动服务" 绑定端口启动接收
QPushButton stopBtn "停止服务" 解除绑定
QLineEdit inputMsg (空) 消息输入框
QPushButton sendBtn "发送消息" 单播发送
QPushButton broadcastBtn "广播消息" 广播发送
QPlainTextEdit showMsg (空) 聊天记录显示窗

界面结构示意

复制代码
+--------------------------------------------------------------------+
| 原机器端口[1567▼] 目标IP地址[192.168.x.x▼] 目标端口号[1568▼]        |
| [启动服务] [停止服务]                                               |
+--------------------------------------------------------------------+
| [输入消息.......................] [发送消息] [广播消息]              |
+--------------------------------------------------------------------+
|  [From:192.168.1.10:53214] 你好                                    |
|  [out]:收到,你好                                                  |
|  ...                                                               |
+--------------------------------------------------------------------+

提示 :QSpinBox 的 maximum 属性记得改成 65535(默认99),否则端口号填不进去。


六、第三步:代码实现(核心)

6.1 mainwindow.h ------ 类结构设计

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QUdpSocket>   // UDP 套接字类:发送/接收 UDP 数据报
#include <QtNetwork>    // Qt 网络模块(QHostAddress、QHostInfo 等)

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

/**
 * 主窗口类 ------ 基于 QUdpSocket 的 UDP 通信示例
 *   1. 启动服务:bind() 绑定本机端口,开始接收
 *   2. 停止服务:abort() 解除绑定
 *   3. 发送消息:向目标IP单播
 *   4. 广播消息:向局域网所有主机广播
 */
class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    Ui::MainWindow *ui;       // 界面指针
    QUdpSocket *udpsocket;    // UDP套接字:所有收发都通过它

    QString GetLocalIpAddress();   // 获取本机IPv4地址

private slots:
    // 按钮槽函数(与.ui控件自动关联)
    void on_startBtn_clicked();       // 启动服务
    void on_stopBtn_clicked();        // 停止服务
    void on_sendBtn_clicked();        // 单播发送
    void on_broadcastBtn_clicked();   // 广播发送

    // 网络槽函数(手动connect关联)
    void SocketReadyReadData();       // 读取接收到的数据报
};
#endif // MAINWINDOW_H

设计要点说明:

  • Q_OBJECT 宏:使用信号槽的类必须写,否则 connect 无效(且报 undefined reference to vtable)。
  • udpsocket 声明为成员变量:整个程序生命周期复用同一个套接字。
  • on_xxx_clicked() 命名规则:Qt 的自动关联机制 ------on_ + 控件objectName + _clicked 格式的槽函数会自动和对应按钮的clicked信号连接,不需要手写connect。

6.2 构造函数 ------ 初始化三件事

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);   // 初始化UI控件(必须最先调用)

    // ① 把本机IP和广播地址加入"目标IP"下拉框,方便自己测试
    QString str = GetLocalIpAddress();
    ui->targetIp->addItem(str);                                              // 本机IP
    ui->targetIp->addItem(QHostAddress(QHostAddress::Broadcast).toString()); // 255.255.255.255

    // ② 创建UDP套接字(this作父对象 → 析构时自动释放,防内存泄漏)
    udpsocket = new QUdpSocket(this);

    // ③ 关联"有数据到达"信号(函数指针写法,编译期查错)
    connect(udpsocket, &QUdpSocket::readyRead,
            this, &MainWindow::SocketReadyReadData);

    // ④ 初始状态:服务未启动,"停止"按钮禁用
    ui->stopBtn->setEnabled(false);
}

知识点:

  • readyRead() 信号:每当有新数据报到达套接字接收缓冲区时发射,这是事件驱动的核心------程序不用一直轮询,来数据了Qt自动叫你。
  • connect 用函数指针写法(&QUdpSocket::readyRead)而非老式 SIGNAL(readyRead()) 宏:写错函数名会编译报错,而宏写错只在运行时静默失败。

6.3 启动/停止服务 ------ bind 与 abort

cpp 复制代码
// 【启动服务】绑定本机UDP端口
void MainWindow::on_startBtn_clicked()
{
    quint16 port = ui->bindPort->value();   // 从界面读取要绑定的端口

    // bind = 向操作系统"申请信箱":之后发往该端口的数据报都进入本套接字
    if (udpsocket->bind(QHostAddress::AnyIPv4, port,
                        QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) {
        ui->showMsg->appendPlainText(
            QString("服务启动成功,监听端口:%1").arg(udpsocket->localPort()));
        ui->startBtn->setEnabled(false);   // 防止重复绑定
        ui->stopBtn->setEnabled(true);
    } else {
        // 常见失败原因:端口被占用
        ui->showMsg->appendPlainText(
            QString("绑定失败:%1").arg(udpsocket->errorString()));
    }
}

// 【停止服务】解除端口绑定
void MainWindow::on_stopBtn_clicked()
{
    udpsocket->abort();                // 立即关闭套接字,释放端口
    ui->startBtn->setEnabled(true);
    ui->stopBtn->setEnabled(false);
    ui->showMsg->appendPlainText("已经停止服务");
}

知识点:bind 参数详解(本文重点)

cpp 复制代码
bind(QHostAddress::AnyIPv4, port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)
参数 作用
QHostAddress::AnyIPv4 绑定本机所有网卡的IPv4地址(不指定具体哪块网卡)
ShareAddress 允许同一台电脑多个程序绑定同一端口------多实例测试的关键
ReuseAddressHint 允许快速重新绑定,避免停止后立即启动报"端口被占用"

如果不加 ShareAddress,同一程序开第二个实例绑同端口会直接失败。这也是后文三实例广播实验能成功的前提。

6.4 单播发送 ------ writeDatagram 三要素

cpp 复制代码
// 【发送消息】向"目标IP:目标端口"单播
void MainWindow::on_sendBtn_clicked()
{
    // 三要素:发给谁(IP) + 发到哪个端口 + 发什么内容
    QString targetipAddress = ui->targetIp->currentText();  // 目标IP
    QHostAddress targetaddress(targetipAddress);            // 字符串→IP对象
    quint16 targetport = ui->targetPort->value();           // 目标端口
    QString strmsg = ui->inputMsg->text();                  // 消息内容

    QByteArray str = strmsg->toUtf8();   // 转UTF-8字节数组(网络传输必须)

    udpsocket->writeDatagram(str, targetaddress, targetport);   // 发送!

    ui->showMsg->appendPlainText("[out]:" + str);   // 显示发送记录
    ui->inputMsg->clear();           // 清空输入框
    ui->inputMsg->setFocus();        // 光标回到输入框,方便连续输入
}

6.5 广播发送 ------ 只差一个地址

cpp 复制代码
// 【广播消息】发给局域网内所有主机
void MainWindow::on_broadcastBtn_clicked()
{
    quint16 targetport = ui->targetPort->value();
    QString strmsg = ui->inputMsg->text();
    QByteArray str = strmsg->toUtf8();

    // 与单播唯一区别:目标地址固定为 QHostAddress::Broadcast (255.255.255.255)
    udpsocket->writeDatagram(str, QHostAddress::Broadcast, targetport);

    ui->showMsg->appendPlainText("[broadcast]:" + str);
    ui->inputMsg->clear();
    ui->inputMsg->setFocus();
}

对比总结------单播和广播的代码只差目标地址:

cpp 复制代码
udpsocket->writeDatagram(str, QHostAddress("192.168.1.20"), 8002);  // 单播:指定IP
udpsocket->writeDatagram(str, QHostAddress::Broadcast,   8002);     // 广播:255.255.255.255

6.6 接收数据 ------ while 循环读取

cpp 复制代码
// 读取接收到的数据报(readyRead信号触发)
void MainWindow::SocketReadyReadData()
{
    // 必须循环:readyRead一次触发时,缓冲区可能堆积多个数据报
    while (udpsocket->hasPendingDatagrams()) {

        // ① 按数据报实际大小分配缓冲区
        QByteArray datagrams;
        datagrams.resize(udpsocket->pendingDatagramSize());

        // ② 读取数据报,同时取出发送方的IP和端口
        QHostAddress paddress;   // 输出参数:对方IP
        quint16 pport;           // 输出参数:对方端口
        udpsocket->readDatagram(datagrams.data(), datagrams.size(),
                                &paddress, &pport);

        // ③ 字节数组→字符串(显式UTF-8,防中文乱码)
        QString strs = QString::fromUtf8(datagrams);

        // ④ 拼接显示:[From:对方IP:对方端口] 消息内容
        QString peer = "[From:" + paddress.toString() + ":"
                     + QString::number(pport) + "]";
        ui->showMsg->appendPlainText(peer + strs);
    }
}

两个必懂知识点:

  1. 为什么要while循环? 信号触发时可能一次到达多个数据报,只读一次会漏数据。hasPendingDatagrams() 返回true表示还有数据报没读完。
  2. 为什么UDP没有黏包问题? 每个数据报有明确边界,readDatagram() 一次正好读一个完整"包裹"。这是UDP相对TCP最省心的地方(TCP要用长度前缀等方案自己切分消息)。

6.7 获取本机IP ------ QHostInfo 反查

cpp 复制代码
QString MainWindow::GetLocalIpAddress()
{
    // 步骤1:获取主机名(如 "DESKTOP-ABC123")
    QString strHostName = QHostInfo::localHostName();

    // 步骤2:根据主机名查询其所有IP(可能同时有IPv4和IPv6)
    QHostInfo hostinfo = QHostInfo::fromName(strHostName);

    QString strlocalIP = "";
    QList<QHostAddress> qaddresslist = hostinfo.addresses();

    // 步骤3:遍历,取第一个IPv4地址(过滤掉IPv6)
    if (!qaddresslist.isEmpty()) {
        for (int i = 0; i < qaddresslist.count(); i++) {
            QHostAddress hostaddr = qaddresslist.at(i);
            if (QAbstractSocket::IPv4Protocol == hostaddr.protocol()) {
                strlocalIP = hostaddr.toString();
                break;   // 找到第一个就退出
            }
        }
    }
    return strlocalIP;
}

为什么过滤IPv4 :本机地址列表通常同时含IPv6(如 fe80::1 这种),而局域网通信测试用IPv4更直观。


七、第四步:运行与验证实验

7.1 基础自测(两步验证程序没写错)

  1. 启动程序 → 绑定端口填 8888 → 点"启动服务" → 应显示 服务启动成功,监听端口:8888

  2. 目标IP选本机IP、目标端口填 8888、输入 hello → 点"发送消息" → 窗口应出现两行:

    [From:192.168.x.x:53214]hello ← 自己发自己也能收到,说明收发都通了!

7.2 实验A:三实例绑不同端口(验证单播)

打开三个程序实例(Qt Creator运行一次,再到构建目录双击exe开两个):

实例 绑定端口 目标IP 目标端口 点哪个按钮 预期结果
实例1 8001 本机IP 8002 发送消息 只有实例2显示 [From:...]
实例2 8002 本机IP 8001 发送消息 只有实例1显示 [From:...]
实例3 8003 本机IP 8002 发送消息 只有实例2显示 [From:...]

结论:单播 = 点对点,数据报只到达指定IP和端口的那一台。

7.3 实验B:三实例绑同一端口(验证广播)

三个实例的绑定端口都填 8888 ,各点"启动服务"(能全部成功,靠的就是 ShareAddress 参数)。

任一实例:目标端口填 8888,输入 hello broadcast,点"广播消息":

复制代码
三个实例的窗口同时出现:
[From:192.168.x.x:随机端口]hello broadcast

结论:广播 = 一对多,局域网内所有绑定该端口的主机全部收到。

注意:实验B中不要用"发送消息"按钮测单播!Windows上多个套接字绑同端口时,单播数据报只投递给其中一个实例(行为不确定),广播才会全部收到。

7.4 进阶观察

  • 发送者窗口多一行 [out]/[broadcast],接收方只有 [From:...] ------ 对照看懂数据流向。
  • [From: 后面的端口是发送方系统随机分配的发送端口,和绑定端口无关(UDP发送不bind时系统自动分配)。

八、踩坑记录与避坑指南

以下是本项目开发过程中实际遇到/容易遇到的坑:

# 现象 解决方案
1 忘加 QT += network QUdpSocket: No such file or directory .pro 中添加后重新执行qmake
2 接收方没bind 发了消息对方毫无反应 接收前必须 bind() 端口并点启动
3 QSpinBox默认max=99 端口8888填不进去 maximum 改为 65535
4 data()直接转QString 中文乱码 QString::fromUtf8(datagram)
5 readyRead里只读一次 偶尔漏收消息 while(hasPendingDatagrams()) 循环
6 多实例绑同端口失败 第二个实例"绑定失败" bind加 ShareAddress 参数
7 绑同端口测单播 只有某一个实例收到 Windows机制限制,绑同端口只测广播
8 按钮setEnabled顺序写反 启动后还能再点启动 启动成功→禁用启动钮;停止→反过来
9 双击exe报缺dll 无法独立运行 用 windeployqt 打包 或 加环境变量
10 跨网段广播收不到 不同VLAN/路由器另一侧收不到 广播出不了路由器,跨网段用组播

调试三板斧:

  1. netstat 查端口netstat -ano | findstr 8888 确认程序真的绑上了端口。
  2. 防火墙排查:Windows防火墙对UDP很敏感,调试期可临时关闭对比测试。
  3. 本机自测:先自己发自己收(7.1节),通了再测双机/多实例,逐步缩小问题范围。

九、主要代码与效果

mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QUdpSocket>   // UDP 套接字类:发送/接收 UDP 数据报
#include <QtNetwork>    // Qt 网络模块(QHostAddress、QHostInfo 等)

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }  // UI 界面类(由 mainwindow.ui 自动生成)
QT_END_NAMESPACE

/**
 * ============================================================
 *  主窗口类 ------ 基于 QUdpSocket 的 UDP 通信示例
 * ============================================================
 *  功能概述:
 *    1. 【启动服务】绑定本机 UDP 端口,开始监听并接收数据报
 *    2. 【停止服务】解除端口绑定,停止接收
 *    3. 【发送】向"目标IP + 目标端口"单播发送一条消息
 *    4. 【广播】向局域网内所有主机(255.255.255.255)广播消息
 *
 *  UDP 与 TCP 的区别(初学者须知):
 *    - UDP 无需三次握手建立连接,直接把数据报"扔"给对方
 *    - UDP 不保证送达、不保证顺序,但速度快、开销小
 *    - 一对一发送叫"单播",发给 255.255.255.255 叫"广播"
 *
 *  典型应用:局域网设备发现、视频流、心跳检测、简单聊天工具
 * ============================================================
 */
class MainWindow : public QMainWindow
{
    Q_OBJECT    // Qt 元对象宏:支持信号槽机制,有信号槽的类必须写

public:
    MainWindow(QWidget *parent = nullptr);   // 构造函数:初始化 UI 和 UDP 套接字
    ~MainWindow();                           // 析构函数:释放资源

private:
    Ui::MainWindow *ui;          // 界面指针(负责操作 .ui 文件中的控件)
    QUdpSocket *udpsocket;       // UDP 套接字:本程序所有收发都通过它完成

    /**
     * 获取本机的 IPv4 地址
     * 实现原理:先拿到主机名,再通过主机名反查其所有 IP,
     * 从中筛选出第一个 IPv4 地址返回
     * @return 本机 IPv4 字符串(如 "192.168.1.100"),获取失败返回空串
     */
    QString GetLocalIpAddress();

private slots:
    /* ---------- 按钮槽函数(与 .ui 中控件自动关联) ---------- */

    void on_startBtn_clicked();       // 【启动服务】:bind() 绑定本机端口
    void on_stopBtn_clicked();        // 【停止服务】:abort() 解除绑定
    void on_sendBtn_clicked();        // 【发送】:向目标 IP 单播一条消息
    void on_broadcastBtn_clicked();   // 【广播】:向局域网所有主机广播消息

    /* ---------- 网络槽函数(手动 connect 关联) ---------- */

    /**
     * 读取套接字接收到的数据报
     * 与 udpsocket 的 readyRead() 信号关联:
     * 每当有新的 UDP 数据报到达,该信号就会触发本槽函数
     */
    void SocketReadyReadData();
};

#endif // MAINWINDOW_H

mainwindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QMessageBox>

/**
 * ============================================================
 *  构造函数:初始化界面 + 创建 UDP 套接字 + 关联收数据信号
 * ============================================================
 */
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);   // 初始化 .ui 文件中设计的所有控件(必须最先调用)

    // --- 第一步:把本机 IP 填入"目标IP"下拉框,方便自己测试 ---
    // 启动时默认目标就是自己(本机两个进程互发即可测试 UDP 通信)
    QString str = GetLocalIpAddress();          // 获取本机 IPv4 地址
    ui->targetIp->addItem(str);                 // 加入下拉框
    ui->targetIp->addItem(QHostAddress(QHostAddress::Broadcast).toString()); // 加入广播地址 255.255.255.255

    // --- 第二步:创建 UDP 套接字 ---
    // this 作为父对象 → MainWindow 析构时 udpsocket 会被自动 delete,防止内存泄漏
    udpsocket = new QUdpSocket(this);

    // --- 第三步:关联"有数据到达"信号 ---
    // readyRead():每当套接字的接收缓冲区有新数据报可读时发出
    // 推荐使用函数指针写法(编译期检查拼写,优于老式 SIGNAL/SLOT 宏)
    connect(udpsocket, &QUdpSocket::readyRead, this, &MainWindow::SocketReadyReadData);

    // --- 第四步:初始化按钮状态 ---
    // 服务尚未启动,"停止"按钮应处于禁用状态(防止误点击)
    ui->stopBtn->setEnabled(false);
}

/**
 * 析构函数:释放 UI 资源
 * (udpsocket 已通过父子对象树自动销毁,无需手动 delete)
 */
MainWindow::~MainWindow()
{
    delete ui;
}

/**
 * ============================================================
 *  【启动服务】按钮槽函数
 * ============================================================
 *  bind() 的作用:让本机操作系统把"本机端口"分配给这个套接字,
 *  之后发往该端口的所有 UDP 数据报都会进入套接字的接收缓冲区。
 *
 *  注意:UDP 的 bind 不需要像 TCP 那样等待客户端连接,
 *        绑定成功即可直接收发数据。
 */
void MainWindow::on_startBtn_clicked()
{
    quint16 port = ui->bindPort->value();   // 从界面读取要绑定的本机 UDP 端口

    // 绑定到本机任意网卡的指定端口
    // ShareAddress:允许同一台电脑上多个程序绑定同一端口(便于多实例测试)
    if (udpsocket->bind(QHostAddress::AnyIPv4, port,
                        QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) {
        // 绑定成功:更新界面状态
        ui->showMsg->appendPlainText(QString("服务启动成功,监听端口:%1").arg(udpsocket->localPort()));
        ui->startBtn->setEnabled(false);   // 禁用"启动"按钮(服务已在运行,防止重复绑定)
        ui->stopBtn->setEnabled(true);     // 启用"停止"按钮
    } else {
        // 绑定失败:常见原因是端口号被其他程序占用
        ui->showMsg->appendPlainText(QString("绑定失败:%1").arg(udpsocket->errorString()));
    }
}

/**
 * ============================================================
 *  【停止服务】按钮槽函数
 * ============================================================
 *  abort():立即关闭套接字并释放端口绑定(丢弃未处理的数据报)。
 *  与 disconnectFromHost() 不同,abort() 不做任何收尾工作,直接复位。
 */
void MainWindow::on_stopBtn_clicked()
{
    udpsocket->abort();                 // 解除端口绑定,停止接收数据
    ui->startBtn->setEnabled(true);     // 重新启用"启动"按钮
    ui->stopBtn->setEnabled(false);     // 禁用"停止"按钮(服务已停止)
    ui->showMsg->appendPlainText("已经停止服务");
}

/**
 * ============================================================
 *  【发送】按钮槽函数:向"目标IP:目标端口"单播一条消息
 * ============================================================
 *  writeDatagram() 是 UDP 发送的核心:
 *    参数1:要发送的字节数组(QByteArray)
 *    参数2:目标 IP 地址(QHostAddress)
 *    参数3:目标端口号(quint16)
 *  UDP 发送是"发完即忘"------不关心对方是否收到,也无连接概念。
 */
void MainWindow::on_sendBtn_clicked()
{
    // --- 依次从界面读取三个要素:发给谁(IP) + 发到哪个端口 + 发什么内容 ---
    QString targetipAddress = ui->targetIp->currentText();       // 目标 IP(下拉框当前选中项)
    QHostAddress targetaddress(targetipAddress);                 // 字符串转 IP 地址对象
    quint16 targetport = ui->targetPort->value();                // 目标端口
    QString strmsg = ui->inputMsg->text();                       // 待发送的文本内容

    // --- 转为 UTF-8 编码的字节数组(网络传输必须用字节数组) ---
    QByteArray str = strmsg.toUtf8();

    // --- 发送数据报 ---
    udpsocket->writeDatagram(str, targetaddress, targetport);

    // --- 在聊天窗口显示发送记录,并清空输入框准备下一条 ---
    ui->showMsg->appendPlainText("[out]:" + str);
    ui->inputMsg->clear();
    ui->inputMsg->setFocus();   // 光标定位回输入框,方便连续输入
}

/**
 * ============================================================
 *  【广播】按钮槽函数:向局域网内所有主机发送消息
 * ============================================================
 *  广播地址 255.255.255.255 表示"本局域网段内的所有设备"。
 *  局域网内任何主机只要绑定了相同端口,都能收到这条广播。
 *
 *  典型用途:设备发现(如局域网打印机扫描、智能设备配对)。
 */
void MainWindow::on_broadcastBtn_clicked()
{
    quint16 targetport = ui->targetPort->value();   // 广播目标端口
    QString strmsg = ui->inputMsg->text();          // 广播内容
    QByteArray str = strmsg.toUtf8();

    // 直接使用 QHostAddress::Broadcast(即 255.255.255.255),无需指定具体 IP
    udpsocket->writeDatagram(str, QHostAddress::Broadcast, targetport);

    ui->showMsg->appendPlainText("[broadcast]:" + str);
    ui->inputMsg->clear();
    ui->inputMsg->setFocus();   // 光标定位回输入框
}

/**
 * ============================================================
 *  获取本机 IPv4 地址
 * ============================================================
 *  实现步骤:
 *    1. QHostInfo::localHostName()   获取主机名(如 "DESKTOP-ABC123")
 *    2. QHostInfo::fromName(主机名)  通过主机名反查所有关联 IP
 *    3. 遍历 IP 列表,筛选出第一个 IPv4 协议的地址
 */
QString MainWindow::GetLocalIpAddress()
{
    // 第一步:获取本机主机名
    QString strHostName = QHostInfo::localHostName();

    // 第二步:根据主机名查询其所有 IP 地址(可能同时有 IPv4 和 IPv6)
    QHostInfo hostinfo = QHostInfo::fromName(strHostName);

    QString strlocalIP = "";

    // 第三步:遍历地址列表,取第一个 IPv4 地址
    QList<QHostAddress> qaddresslist = hostinfo.addresses();

    if (!qaddresslist.isEmpty()) {
        for (int i = 0; i < qaddresslist.count(); i++) {
            QHostAddress hostaddr = qaddresslist.at(i);

            // 只认 IPv4(过滤掉 IPv6 的 "::1" 之类的地址)
            if (QAbstractSocket::IPv4Protocol == hostaddr.protocol()) {
                strlocalIP = hostaddr.toString();
                break;   // 找到第一个就退出
            }
        }
    }
    return strlocalIP;
}

/**
 * ============================================================
 *  读取 socket 接收到的数据报(与 readyRead 信号关联)
 * ============================================================
 *  为什么用 while 循环?
 *    readyRead 一次触发时,接收缓冲区里可能堆积了多个数据报,
 *    必须循环把所有待处理数据报都读完(hasPendingDatagrams()
 *    返回 true 表示还有数据报等待读取)。
 *
 *  UDP 数据报的特点:每个数据报都是独立完整的"包裹",
 *  一次 readDatagram() 正好读完一个,不存在 TCP 那样的黏包问题。
 */
void MainWindow::SocketReadyReadData()
{
    // 循环读取:只要缓冲区还有未处理的数据报就继续
    while (udpsocket->hasPendingDatagrams()) {

        // --- 第一步:按数据报实际大小分配缓冲区 ---
        QByteArray datagrams;
        datagrams.resize(udpsocket->pendingDatagramSize());   // 获取下一个数据报的字节数

        // --- 第二步:读取数据报,同时取出发送方的 IP 和端口 ---
        QHostAddress paddress;   // 输出参数:发送方 IP
        quint16 pport;           // 输出参数:发送方端口
        udpsocket->readDatagram(datagrams.data(), datagrams.size(), &paddress, &pport);

        // --- 第三步:字节数组转字符串(显式指定 UTF-8,避免中文乱码) ---
        QString strs = QString::fromUtf8(datagrams);

        // --- 第四步:拼接"来源信息 + 消息内容"并显示到聊天窗口 ---
        QString peer = "[From:" + paddress.toString() + ":" + QString::number(pport) + "]";
        ui->showMsg->appendPlainText(peer + strs);
    }
}
相关推荐
城管不管20 分钟前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
霸道流氓气质33 分钟前
Java开发者AI编程常用MCP、Skills、Rules全景汇总
java·开发语言·ai编程
zoujiahui_20181 小时前
Python 包与环境管理工具 uv
开发语言·python·uv
小小龙学IT1 小时前
Qt 模型/视图框架(Model/View)深度解析:从 MVC 演化到数据展示架构的王者
qt·架构·mvc
吴声子夜歌1 小时前
Java面试题——JVM(二)
java·开发语言·jvm
CRMEB1 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php
denglinqings1 小时前
Java开发从入门到精通所有课程
java·开发语言
00后程序员张1 小时前
SSL Pinning 抓包抓不到明文?绕过证书固定的几种方案
网络协议·计算机网络·网络安全·ios·adb·https·udp
程序猿阿森2 小时前
Python 闭包与装饰器:从入门到精通
开发语言·python·面试
余额瞒着我当琳2 小时前
C++--vector底层,leetcode118杨辉三角实战,对于传统reserve的坑点,迭代器失效
开发语言·c++