Qt Udp组播测试
2026/9/3 14:46:11 网站建设 项目流程

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


发送端:

IP:10.65.13.84 端口6000

接收端:

IP:10.65.13.83 端口4001

加入组播:225.5.5.1

接收方代码:

OutIp :10.65.13.83

#define IPA_OutSendIP "225.5.1.1" // 外部组播IP地址
OutRecvSock = new QUdpSocket(this); bool bBindStatus = OutRecvSock->bind(QHostAddress::AnyIPv4,4001, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint); qDebug() << __FILE__ << __LINE__ << bBindStatus; OutRecvSock->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 = OutRecvSock->joinMulticastGroup(QHostAddress(IPA_OutSendIP),interList[index]); if(b) { qDebug()<<"join Listen group success" <<interList[index].name(); } }
onnect(OutRecvSock,&QUdpSocket::readyRead,this,&OuterNetWorker::onOutMsg);
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

#define IPA_OutSendIP "225.5.1.1" // 外部发送组播IP地址 #define IPA_OutSendPort 24584 // 外部发送组播端口号
// 外引导 // 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(); } }
//发送组播 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 解决的是“数据包该去哪台主机(或哪些主机)”,端口号解决的是“数据包到主机后该找哪个应用程序”。两者结合,才能完成从网络到进程的最终交付。

解析代码:

//获取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)匹配的那个接口,并记录其在列表中的索引

//获取IP地址的网卡 QList<QNetworkInterface> interList = QNetworkInterface::allInterfaces(); qDebug() << __FILE__ << __LINE__ <<interList.count(); foreach (QNetworkInterface inter,interList) { qDebug() << __FILE__ << __LINE__ << inter.name() << inter.type(); }
../../../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()返回类型说明
loLoopback本地回环接口(127.0.0.1),仅用于本机通信,不能用于组播发送到网络
sit0UnknownIPv6-in-IPv4 隧道接口(Simple Internet Transition),用于 IPv6 过渡技术,一般不用于普通组播。
ip6tnl0VirtualIPv6 隧道接口(IPv6 tunnel),同样属于虚拟隧道,不参与局域网组播。
can0/can1/can2CanBusCAN 总线接口(Controller Area Network),常见于汽车电子或工业控制,不是 IP 网络接口,不能用于 UDP 组播。
enp5s0Ethernet有线以太网网卡,这是你真正需要的物理网卡,可用来发送/接收 UDP 组播。

QNetworkInterface::InterfaceFlag

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

改进后的代码:

//获取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__; }
../../../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

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

foreach (QNetworkAddressEntry entry, inter.addressEntries()) { QHostAddress ip = entry.ip(); qDebug() << ip; }
QHostAddress("10.65.13.83") QHostAddress("fe80::4166:f91e:8e5:9b20%enp5s0")
地址类型作用域特点
10.65.13.83IPv4 私有地址全局/站点(依赖路由)全网唯一(在局域网内),可直接用于比较。
fe80::4166:f91e:8e5:9b20%enp5s0IPv6 链路本地地址仅限本链路(同一网段)%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这样的数字索引。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询