网络调试助手发送组播 使用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()返回类型 | 说明 |
|---|---|---|
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 组播 | 核心必须条件!这是操作系统明确告诉你的:这个接口支持加入组播组和发送组播包。 |
改进后的代码:
//获取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.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这样的数字索引。