Qt Udp组播测试

网络调试助手发送组播 使用Qt程序接收组播


发送端:

IP:10.65.13.84 端口6000

接收端:

IP:10.65.13.83 端口4001

加入组播:225.5.5.1

接收方代码:

**OutIp :**10.65.13.83

cpp 复制代码
#define IPA_OutSendIP               "225.5.1.1"             // 外部组播IP地址
cpp 复制代码
    OutRecvSock = new QUdpSocket(this);
    bool bBindStatus = OutRecvSock->bind(QHostAddress::AnyIPv4,4001, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint);
    qDebug() << __FILE__ << __LINE__ << bBindStatus;
    OutRecvSock->setSocketOption(QAbstractSocket::MulticastTtlOption, 48);
cpp 复制代码
//获取IP地址的网卡
    QList<QNetworkInterface> interList = QNetworkInterface::allInterfaces();
    int index = -1;
    int i =0;
    foreach (QNetworkInterface inter,interList)
    {
        foreach (QNetworkAddressEntry entry, inter.addressEntries())
        {
            QHostAddress ip = entry.ip();
            if(ip == QHostAddress(OutIP))
            {
                index = i;
                break;
            }
            if(index != -1)
            {
                break;
            }
        }
        ++i;
    }

    if(index != -1)
    {
        // 加入组播
        bool b = OutRecvSock->joinMulticastGroup(QHostAddress(IPA_OutSendIP),interList[index]);
        if(b)
        {
            qDebug()<<"join Listen group success" <<interList[index].name();
        }
    }
cpp 复制代码
onnect(OutRecvSock,&QUdpSocket::readyRead,this,&OuterNetWorker::onOutMsg);
cpp 复制代码
void OuterNetWorker::onOutMsg()// 接收
{
    while(OutRecvSock->hasPendingDatagrams())
    {
        QByteArray datagram;
        datagram.resize(OutRecvSock->pendingDatagramSize());
        QHostAddress sender;
        quint16 port;
        OutRecvSock->readDatagram(datagram.data(),datagram.size(),&sender,&port);
        qDebug() << datagram.toHex();
    }
}


现在使用Qt来编写发送端代码,使用网络调试助手来接收组播报文。

发送端:

IP:10.65.13.83 端口24576

接收端:

IP:10.65.13.84 端口24584

组播地址:225.5.1.1

**OutIp :**10.65.13.83

cpp 复制代码
#define IPA_OutSendIP               "225.5.1.1"             // 外部发送组播IP地址
#define IPA_OutSendPort             24584                   // 外部发送组播端口号
cpp 复制代码
    // 外引导
    // Socket
    OutSendSock = new QUdpSocket(this);
    OutSendSock->bind(QHostAddress(OutIP),24576, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint);
    OutSendSock->setSocketOption(QAbstractSocket::MulticastTtlOption, 48);
    //获取IP地址的网卡
    QList<QNetworkInterface> interList = QNetworkInterface::allInterfaces();
    int index = -1;
    int i =0;
    foreach (QNetworkInterface inter,interList)
    {
        foreach (QNetworkAddressEntry entry, inter.addressEntries())
        {
            QHostAddress ip = entry.ip();
            if(ip == QHostAddress(OutIP))
            {
                index = i;
                break;
            }
            if(index != -1)
            {
                break;
            }
        }
        ++i;
    }

    if(index != -1)
    {
            // 加入组播
            bool b = OutSendSock->joinMulticastGroup(QHostAddress(IPA_OutSendIP),interList[index]);
            if(b)
            {
                qDebug()<<"OutSendSock join Listen group success" <<interList[index].name();
            }
    }
cpp 复制代码
    //发送组播
    QByteArray ba = data;
    if(OutSendSock)
    {
        quint32 len  = OutSendSock->writeDatagram(ba,QHostAddress(IPA_OutSendIP),IPA_OutSendPort);
    }

在组播的流程中,接收与发送都需要指定端口号。为什么?

这是一个非常核心的计算机网络问题。简单直接的回答是:IP地址(包括组播IP)负责找到"哪台机器",而端口号负责找到"机器上的哪个程序"(进程)。

在组播场景下,发送和接收都必须指定端口号,是为了完成网络层的寻址传输层的进程标识的分离。具体原因可以从发送端和接收端两个角度来看:

1. 发送端为什么需要指定端口号?

发送端实际上需要指定两个 端口,但通常我们常说的是目标端口

  • 目标端口(必须) :发送端的 writeDatagram(data, groupAddress, port)中的 port 就是目标端口。接收端必须绑定在这个端口上,发送端才能把数据送过去。这相当于信封上的"收件人部门",操作系统看到这个端口号,才知道该把数据包交给机器上哪个正在监听的应用进程

  • 源端口(可选但强烈建议) :如果不手动绑定,系统会自动分配一个临时端口。但如果接收端需要给发送端回传数据(如ACK确认),就需要发送端提供源端口。因此在实际通信中,发送端通常会 bind() 一个固定端口,既作为源端口,也方便接收数据

2. 接收端为什么需要指定端口号(绑定)?

接收端必须调用 bind(port)监听一个特定的端口。原因在于:

  • 操作系统是多任务并行的 :同一台电脑上同时运行着浏览器(端口443)、游戏(端口27015)和你的组播程序。当网卡收到一个 UDP 数据包时,操作系统内核需要根据数据包中的"目标端口号",决定把数据递给哪个进程

  • 注册机制bind() 的本质是向操作系统内核注册:"凡是发往 目标IP:该端口 的包,都交给我的套接字"。如果接收端不绑定端口,内核收到组播包后就找不到接收者,会直接丢弃。

3. 特别澄清:组播IP与端口的关系

初学者容易混淆"加入组播组"和"绑定端口":

  • 加入组播组(joinMulticastGroup 是针对 IP 层 的操作。它告诉交换机/路由器,本机想接收发往 239.255.43.21 这个 IP 地址的数据包。

  • 绑定端口(bind 是针对 传输层 的操作。它告诉操作系统,将发往 12345 端口的数据递交给该套接字

两者缺一不可:如果只加入组播组而不绑定端口,数据包到了网卡但操作系统不知道给哪个程序;如果只绑定端口而不加入组播组,路由器不会把组播流量转发给这台机器,也收不到包。

4. 有趣的现象:端口复用

正因为端口是"进程门牌号",所以会出现以下情况,可以帮助你加深理解:

  • 不同组播地址,可以共用同一个端口 :比如程序A监听 239.0.0.1:9999,程序B监听 239.0.0.2:9999。两台程序可以同时运行,因为虽然端口都是9999,但操作系统根据五元组(源IP、源端口、目标IP、目标端口、协议)能区分开。此时在 Qt 中需要开启**QUdpSocket::ShareAddress**模式。

  • 同一个组播地址,不同端口互不干扰:这就像同一个小区(组播IP)有不同的楼栋号(端口),快递员(数据包)根据楼栋号送信,互不影响。

一句话总结 :组播 IP 解决的是"数据包该去哪台主机(或哪些主机)",端口号解决的是"数据包到主机后该找哪个应用程序"。两者结合,才能完成从网络到进程的最终交付。

解析代码:

cpp 复制代码
//获取IP地址的网卡
    QList<QNetworkInterface> interList = QNetworkInterface::allInterfaces();
    int index = -1;
    int i =0;
    foreach (QNetworkInterface inter,interList)
    {
        foreach (QNetworkAddressEntry entry, inter.addressEntries())
        {
            QHostAddress ip = entry.ip();
            if(ip == QHostAddress(OutIP))
            {
                index = i;
                break;
            }
            if(index != -1)
            {
                break;
            }
        }
        ++i;
    }

这段代码的目标是遍历系统中所有网络接口,找到与指定 IP 地址(变量 OutIP)匹配的那个接口,并记录其在列表中的索引

cpp 复制代码
    //获取IP地址的网卡
    QList<QNetworkInterface> interList = QNetworkInterface::allInterfaces();
    qDebug() << __FILE__ << __LINE__ <<interList.count();
    foreach (QNetworkInterface inter,interList)
    {
        qDebug() << __FILE__ << __LINE__ << inter.name() << inter.type();
    }
cpp 复制代码
../../../LPL/T/ttt/main1.cpp 12 7
../../../LPL/T/ttt/main1.cpp 15 "lo" QNetworkInterface::Loopback
../../../LPL/T/ttt/main1.cpp 15 "sit0" QNetworkInterface::Unknown
../../../LPL/T/ttt/main1.cpp 15 "ip6tnl0" QNetworkInterface::Virtual
../../../LPL/T/ttt/main1.cpp 15 "can0" QNetworkInterface::CanBus
../../../LPL/T/ttt/main1.cpp 15 "can1" QNetworkInterface::CanBus
../../../LPL/T/ttt/main1.cpp 15 "can2" QNetworkInterface::CanBus
../../../LPL/T/ttt/main1.cpp 15 "enp5s0" QNetworkInterface::Ethernet

输出结果各接口含义

接口名 type() 返回类型 说明
lo Loopback 本地回环接口(127.0.0.1),仅用于本机通信,不能用于组播发送到网络
sit0 Unknown IPv6-in-IPv4 隧道接口(Simple Internet Transition),用于 IPv6 过渡技术,一般不用于普通组播。
ip6tnl0 Virtual IPv6 隧道接口(IPv6 tunnel),同样属于虚拟隧道,不参与局域网组播。
can0 / can1 / can2 CanBus CAN 总线接口 (Controller Area Network),常见于汽车电子或工业控制,不是 IP 网络接口,不能用于 UDP 组播。
enp5s0 Ethernet 有线以太网网卡,这是你真正需要的物理网卡,可用来发送/接收 UDP 组播。

QNetworkInterface::InterfaceFlag

|----------------------|----------|------------------------|----------------------------------------------------------------------------|
| IsUp | 0x1 | 接口已启用(管理状态为 up) | 必须条件 。如果网卡被 ifconfig down 停用,不能发数据。 |
| IsRunning | 0x2 | 接口已分配资源(驱动就绪、物理链路通常已通) | 强烈建议 。代表系统已准备好收发数据包。有时 IsUp 为真但 IsRunning 为假(如网线没插),此时发送会失败。 |
| CanBroadcast | 0x4 | 支持广播模式(如以太网) | 参考项。通常物理网卡都支持,但组播不依赖广播,所以非必须。 |
| IsLoopBack | 0x8 | 回环接口(即 127.0.0.1) | 必须排除(除非你只想本机进程间通信)。发往组播地址的数据包走回环接口不会到达局域网。 |
| IsPointToPoint | 0x10 | 点对点接口(如 PPP 拨号、VPN 隧道) | 谨慎对待。某些 VPN(如 OpenVPN 的 tun 模式)支持组播,但大多数情况下组播无法穿越点到点链路,一般建议排除或让用户手动选择。 |
| CanMulticast | 0x20 | 支持 IP 组播 | 核心必须条件!这是操作系统明确告诉你的:这个接口支持加入组播组和发送组播包。 |

改进后的代码:

cpp 复制代码
    //获取IP地址的网卡
    QList<QNetworkInterface> interList = QNetworkInterface::allInterfaces();
    qDebug() << __FILE__ << __LINE__ <<interList.count();
    foreach (QNetworkInterface inter,interList)
    {
        QNetworkInterface::InterfaceFlags flags = inter.flags();
        qDebug() << __FILE__ << __LINE__ << inter.name() << inter.type() << flags;
        // 1. 必须处于启用且运行状态
        if (!(flags & QNetworkInterface::IsUp) ||
            !(flags & QNetworkInterface::IsRunning)) {
            continue;
        }

        // 2. 必须支持组播(核心条件)
        if (!(flags & QNetworkInterface::CanMulticast)) {
            continue;
        }

        // 3. 排除回环接口
        if (flags & QNetworkInterface::IsLoopBack) {
            continue;
        }

        // 4. (强烈建议) 排除点对点接口,避免无效发送
        if (flags & QNetworkInterface::IsPointToPoint) {
            continue;
        }
        qDebug() << __FILE__ << __LINE__;
    }
cpp 复制代码
../../../LPL/T/ttt/main1.cpp 12 7
../../../LPL/T/ttt/main1.cpp 16 "lo" QNetworkInterface::Loopback QFlags<QNetworkInterface::InterfaceFlag>(IsUp|IsRunning|IsLoopBack)
../../../LPL/T/ttt/main1.cpp 16 "sit0" QNetworkInterface::Unknown QFlags<QNetworkInterface::InterfaceFlag>()
../../../LPL/T/ttt/main1.cpp 16 "ip6tnl0" QNetworkInterface::Virtual QFlags<QNetworkInterface::InterfaceFlag>()
../../../LPL/T/ttt/main1.cpp 16 "can0" QNetworkInterface::CanBus QFlags<QNetworkInterface::InterfaceFlag>()
../../../LPL/T/ttt/main1.cpp 16 "can1" QNetworkInterface::CanBus QFlags<QNetworkInterface::InterfaceFlag>()
../../../LPL/T/ttt/main1.cpp 16 "can2" QNetworkInterface::CanBus QFlags<QNetworkInterface::InterfaceFlag>()
../../../LPL/T/ttt/main1.cpp 16 "enp5s0" QNetworkInterface::Ethernet QFlags<QNetworkInterface::InterfaceFlag>(IsUp|IsRunning|CanBroadcast|CanMulticast)
../../../LPL/T/ttt/main1.cpp 37

进一步在循环的末尾添加打印:

cpp 复制代码
        foreach (QNetworkAddressEntry entry, inter.addressEntries())
        {
            QHostAddress ip = entry.ip();
            qDebug() << ip;
        }
cpp 复制代码
QHostAddress("10.65.13.83")
QHostAddress("fe80::4166:f91e:8e5:9b20%enp5s0")
地址 类型 作用域 特点
10.65.13.83 IPv4 私有地址 全局/站点(依赖路由) 全网唯一(在局域网内),可直接用于比较。
fe80::4166:f91e:8e5:9b20%enp5s0 IPv6 链路本地地址 仅限本链路(同一网段) %enp5s0 是作用域ID(Scope ID) ,它告诉操作系统这个地址属于哪个网卡。因为 fe80::/10 地址段在所有网卡上都存在(每块网卡都有一个),没有 %enp5s0,这个地址在全球范围内是不唯一的

fe80::4166:f91e:8e5:9b20%enp5s0 这是什么?

这是一个 IPv6 链路本地地址(Link-Local IPv6 Address)。为了让非网络专业的开发者也能完全理解,我们把它拆成三部分来看:

fe80::4166:f91e:8e5:9b20 + % + enp5s0

地址部分:fe80::4166:f91e:8e5:9b20

  • fe80::/10 前缀 :这是 IPv6 中专门保留给"链路本地"使用的地址段。只要你的电脑开启了 IPv6,每块网卡都会自动生成一个以 fe80 开头的地址,不需要 DHCP 服务器分配,也不需要手动配置。

  • 后半部分 :通常是由网卡的MAC 地址通过**算法(EUI-64)生成的,或者是随机生成的(隐私扩展)。它的作用是确保在同一根网线(同一链路)**里,这块网卡的身份是唯一的。

核心特性 :这个地址只能在当前物理链路(比如同一个交换机下的局域网)内有效路由器绝对不会转发fe80 开头的包,所以它不能用于互联网通信,只用于局域网内的底层协议(如自动发现、邻居探测、DHCPv6 请求等)。


作用域 ID(Scope ID):%enp5s0

这是最容易让新手困惑的地方,也是最关键的部分。

  • 为什么需要 % 后缀? 因为 fe80:: 地址是"链路本地"的。想象一下,你的电脑上可能同时有 有线网卡(enp5s0)Wi-Fi(wlan0)虚拟机网卡(vmnet) 。因为每个网卡都会自动生成一个 fe80:: 地址,所以单纯说"发数据给 fe80::4166:..."是有歧义的 ------操作系统不知道你要发给哪个网卡相连的设备

  • %enp5s0 的作用 :它就是一个 "网卡标识符" ,告诉操作系统:"这个地址是在 enp5s0 这块网卡上的"。在 Linux 系统中,enp5s0 是物理以太网卡的名称(en 代表以太网,p5s0 代表 PCI 总线位置)。在 Windows 上,你通常会看到**%3%4**这样的数字索引。

相关推荐
tju新生代魔迷2 小时前
Verilog HDL 学习笔记(十二)| 第12章 用户自定义原语(UDP)
笔记·学习·udp
2501_9151063219 小时前
安卓抓包软件2026,免证书抓包 应用层抓包 代理抓包全解析
网络协议·计算机网络·网络安全·ios·adb·https·udp
啊阿狸不会拉杆1 天前
《计算机网络-自顶向下方法》3.3 无连接运输:UDP 读书笔记
网络协议·计算机网络·udp
好评1241 天前
【Linux】传输层协议UDP
linux·运维·udp
SKH.1 天前
网络(2)UDP通信
网络·单片机·udp
susplus1 天前
【linux应用软件编程】进程间的通信方式2【网络通信从入门到UDP编程】
linux·udp·ip
张小姐的猫1 天前
【C++开发脚手架】 —— Jsoncpp上手
java·linux·开发语言·网络·c++·tcp/ip·udp
2401_862880822 天前
Linux应用层开发 --- UDP&TCP
linux·运维·tcp/ip·udp
爱研究的小梁2 天前
打破时延‑带宽悖论|乾元通 QMP‑UDP,为无人载具提供低延时高带宽原生通信方案
网络·网络协议·udp