26. UDP多播:多播地址、IGMP协议、加入多播组(IP_ADD_MEMBERSHIP)

各位,今天我们来聊聊UDP多播。说实话,多播这个概念,我早年刚接触网络编程时,总觉得它有点「玄乎」。单播好理解,就是一对一聊天;广播也好理解,就是对着所有人喊一嗓子。但多播呢?它像是拉了一个微信群——只有群里的人才能收到消息。

嗯,咱们今天就把它彻底讲透。

什么是多播?为什么需要它?

先说说场景。假设你写了一个股票行情推送服务,有1000个客户端要接收实时数据。如果用单播,你得给每个客户端单独发一份数据,服务器带宽直接爆炸。如果用广播,那整个局域网都被你刷屏了,其他无关应用也得跟着遭殃。

多播就是来解决这个问题的。它允许你把数据发到一个「组地址」,只有加入了这个组的机器才能收到。说白了,就是精准投递,不浪费资源。

我在项目中遇到过类似的需求——一个视频监控系统,需要把摄像头画面同时推送给多个监控终端。用单播?服务器扛不住。用广播?整个园区网络都得瘫痪。最后就是用多播搞定的。

多播地址:224.0.0.0/4 这个特殊网段

多播地址是IPv4中保留的一段特殊地址,范围从 224.0.0.0239.255.255.255。你想想看,整个D类地址段都给了多播。

这里有几个常用的,我列个表给你看:

地址范围 用途 说明
224.0.0.0 - 224.0.0.255 本地链路控制 路由器协议用,比如OSPF、IGMP
224.0.1.0 - 238.255.255.255 全球范围多播 互联网上可用,需要IGMP支持
239.0.0.0 - 239.255.255.255 本地管理多播 企业内部用,不会出路由器

我个人习惯,做局域网内的多播应用时,直接用 239.0.0.0/8 这个段,省心。比如 239.0.0.1 就挺好。

注意:224.0.0.1 是所有主机的多播地址,224.0.0.2 是所有路由器的多播地址。这些是保留的,别乱用。

IGMP协议:谁在听?

IGMP,全称Internet Group Management Protocol,互联网组管理协议。它的作用很简单——让路由器知道,某个网段里有没有机器想收某个多播组的数据。

为什么需要这个?你想啊,如果路由器不知道下面有没有人听,它就只能把多播数据往所有端口都发一遍,那跟广播有什么区别?

IGMP有三个版本:

  • IGMPv1:基本功能,主机可以加入组,但离开时不会通知路由器。
  • IGMPv2:增加了离开组消息,路由器能及时清理。
  • IGMPv3:支持源过滤,可以指定只收某个来源的数据。

我建议你直接用IGMPv3,兼容性最好。不过说实话,在应用层编程时,你基本不用直接处理IGMP报文——操作系统和网卡驱动帮你搞定了。

加入多播组:IP_ADD_MEMBERSHIP

好,到了最核心的部分。在C语言里,怎么让一个socket加入多播组?

答案是 setsockopt() 函数,配合 IP_ADD_MEMBERSHIP 选项。

看代码:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    struct ip_mreq mreq;
    struct sockaddr_in addr;

    // 1. 创建UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // 2. 绑定到本地端口(必须!)
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8888);
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

    // 3. 设置多播组信息
    mreq.imr_multiaddr.s_addr = inet_addr("239.0.0.1");
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);

    // 4. 加入多播组
    if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
                   &mreq, sizeof(mreq)) < 0) {
        perror("加入多播组失败");
        return -1;
    }

    printf("已加入多播组 239.0.0.1\n");

    // 5. 接收数据
    char buf[1024];
    struct sockaddr_in from;
    socklen_t fromlen = sizeof(from);
    recvfrom(sockfd, buf, sizeof(buf), 0,
             (struct sockaddr*)&from, &fromlen);

    printf("收到: %s\n", buf);

    close(sockfd);
    return 0;
}

这里有几个关键点,我得跟你强调一下:

  • 必须先bind:很多人忘了这步。多播接收端必须绑定到一个端口,否则数据过不来。
  • ip_mreq结构体:包含两个字段——imr_multiaddr 是多播组地址,imr_interface 是本地网卡地址。用 INADDR_ANY 表示用默认网卡。
  • IP_ADD_MEMBERSHIP:这个选项告诉内核,把当前socket加入指定的多播组。内核会帮你处理IGMP报文。
小技巧:如果你有多块网卡,想指定用哪块加入多播组,就把 imr_interface 设成那块网卡的IP地址。我曾经在一个双网卡服务器上踩过坑,默认网卡不对,数据收不到。

发送多播数据

发送端就简单多了。你只需要把目标地址设成多播地址,然后正常 sendto() 就行。

struct sockaddr_in dest;
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = inet_addr("239.0.0.1");
dest.sin_port = htons(8888);

sendto(sockfd, "hello", 5, 0,
       (struct sockaddr*)&dest, sizeof(dest));

嗯,就这么简单。不过发送端通常不需要加入多播组,也不需要bind端口。

离开多播组

程序退出时,socket关闭后,内核会自动帮你离开多播组。但如果你想在运行中离开,可以用 IP_DROP_MEMBERSHIP

setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
           &mreq, sizeof(mreq));

我曾经在写一个动态频道切换的程序时用过这个——用户换台时,先离开当前多播组,再加入新的。

多播的核心逻辑

为了让你更直观地理解,我画了一张图:

UDP多播通信模型 发送端 sendto(239.0.0.1:8888) 多播组 239.0.0.1:8888 接收端 A 已加入多播组 接收端 B 已加入多播组 接收端 C 未加入多播组 ✗ 收不到 图例: 发送端 已加入多播组的接收端 未加入的接收端 多播组范围

你看,发送端把数据发到多播地址,只有加入了组的A和B能收到,C收不到。这就是多播的精髓。

避坑指南

最后,分享几个我踩过的坑:

  • 防火墙:很多系统默认会拦截多播数据。我曾经在CentOS上折腾了半天,最后发现是iptables在作怪。
  • 回环问题:默认情况下,发送端自己也能收到自己发的多播数据。如果你不想这样,可以用 IP_MULTICAST_LOOP 选项关掉。
  • TTL:多播数据包的TTL默认是1,意味着只能在本网段内传播。如果需要跨路由器,记得用 IP_MULTICAST_TTL 设大一点。

核心总结:

  • 多播地址用 239.0.0.0/8 最安全
  • 接收端必须 bind 端口,然后 setsockopt(IP_ADD_MEMBERSHIP)
  • 发送端直接 sendto 多播地址即可
  • IGMP协议由内核处理,应用层不用操心

好了,多播这块就讲到这里。代码不多,但背后的网络原理值得你多琢磨琢磨。下次遇到「一对多」的通信场景,记得用多播,别再用广播糊弄了。


公众号:蓝海资料掘金营,微信deep3321