Qt实战案例:从零写一个UDP通信程序(单播+广播,含避坑指南)
完整的Qt UDP实战案例复盘,从创建工程到运行验证,支持单播 和广播的UDP通信程序。

目录
文章目录
- Qt实战案例:从零写一个UDP通信程序(单播+广播,含避坑指南)
-
- Qt实战案例:从零写一个UDP通信程序(单播+广播,含避坑指南)
- 目录
- 一、效果预览与功能清单
- 二、UDP协议快速入门
-
- [1. UDP是什么](#1. UDP是什么)
- [2. 与TCP的直观区别(本项目相关)](#2. 与TCP的直观区别(本项目相关))
- [3. 三种通信模式](#3. 三种通信模式)
- 三、开发环境
- 四、第一步:创建工程
- 五、第二步:界面设计
- 六、第三步:代码实现(核心)
-
- [6.1 mainwindow.h ------ 类结构设计](#6.1 mainwindow.h —— 类结构设计)
- [6.2 构造函数 ------ 初始化三件事](#6.2 构造函数 —— 初始化三件事)
- [6.3 启动/停止服务 ------ bind 与 abort](#6.3 启动/停止服务 —— bind 与 abort)
- [6.4 单播发送 ------ writeDatagram 三要素](#6.4 单播发送 —— writeDatagram 三要素)
- [6.5 广播发送 ------ 只差一个地址](#6.5 广播发送 —— 只差一个地址)
- [6.6 接收数据 ------ while 循环读取](#6.6 接收数据 —— while 循环读取)
- [6.7 获取本机IP ------ QHostInfo 反查](#6.7 获取本机IP —— QHostInfo 反查)
- 七、第四步:运行与验证实验
-
- [7.1 基础自测(两步验证程序没写错)](#7.1 基础自测(两步验证程序没写错))
- [7.2 实验A:三实例绑不同端口(验证单播)](#7.2 实验A:三实例绑不同端口(验证单播))
- [7.3 实验B:三实例绑同一端口(验证广播)](#7.3 实验B:三实例绑同一端口(验证广播))
- [7.4 进阶观察](#7.4 进阶观察)
- 八、踩坑记录与避坑指南
- 九、主要代码与效果
一、效果预览与功能清单
程序界面一共三行控件:上方一排是端口和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控件基本使用 |
四、第一步:创建工程
- Qt Creator → 文件 → 新建文件或项目 → Application (Qt) → Qt Widgets Application
- 项目名称填
QUdpSocketpro,构建系统选 qmake - 类信息:类名
MainWindow,基类QMainWindow(勾选"创建界面"生成 .ui 文件) - 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);
}
}
两个必懂知识点:
- 为什么要while循环? 信号触发时可能一次到达多个数据报,只读一次会漏数据。
hasPendingDatagrams()返回true表示还有数据报没读完。 - 为什么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 基础自测(两步验证程序没写错)
-
启动程序 → 绑定端口填
8888→ 点"启动服务" → 应显示服务启动成功,监听端口:8888 -
目标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/路由器另一侧收不到 | 广播出不了路由器,跨网段用组播 |
调试三板斧:
- netstat 查端口 :
netstat -ano | findstr 8888确认程序真的绑上了端口。 - 防火墙排查:Windows防火墙对UDP很敏感,调试期可临时关闭对比测试。
- 本机自测:先自己发自己收(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);
}
}

