组播与单播

组播( Multicast,又称群播、多播) 与单播 ( Unicast ) -都是数据传输的一种方式,如果要映射现实世界中的信息传递方式,单播可以类比为打电话,有人呼出电话然后有人接听,然后他们互相畅谈最后互道再见;而组播则更加类似于广播,电台的主播会在收音机的那一头说话,所有打开收音机的人都可以收到,你也可以随时关闭收音机,你只能单方面的听到来自对方的声音。

-

虽然今天到处都是打电话的人,而我们已经很少能在身边看到广播电台的身影,但是 “广播” 仍在某些领域大放异彩。

+都是数据传输的一种方式,如果要映射现实世界中的信息传递方式,单播可以类比为打电话,有人呼出电话然后有人接听,然后他们互相畅谈最后互道再见;而组播则更加类似于广播,电台的主播会在收音机的那一头说话,所有打开收音机的人都可以收到,你也可以随时关闭收音机,只能单方面的听到来自对方的声音。

+

虽然今天到处都是打电话的人,而我们已经很少能在身边看到广播电台的身影,但是 “广播” 这种形式仍在某些领域大放异彩。

试想一下,这么一个场景,你有一份数据,要发送给十个 IP,利用常规的 TCP/IP 协议栈知识我可以简单的写出一段代码,依次连接每一个 -IP,然后发送数据,这样就可以实现数据的传递,这种方式就叫单播。 -但是如果我们把接收方的数量乘上 10 、100 甚至是 100000 呢? +IP,然后发送数据,这样就可以实现数据的传递,这种方式就叫单播。

+

但是如果我们把接收方的数量乘上 10 、100 甚至是 100000 呢? 那这个循环可真是太吓人了,我要是能发送一份数据能让所有的接收方都能收到,那该多好啊!

组播的诞生就源自这种朴素的想法,要是数据只用发一份就好了,组播是一种一对多的通信方式,它可以让一个发送方同时向多个接收方发送数据,而不需要发送方和接收方之间建立多个连接。

组播的传输模型具体实现的时候,也还是要回归到基于 OSI 七层计算机网络模型来的,理论上我们可以在任何一层实现组播, @@ -322,45 +322,6 @@

组播与单播

  • 在应用层,自由度就更大了,我们可以为所欲为的按照自己的喜好来实现一个应用层协议模拟组播,这种方式叫做应用层组播;
  • 不过在实际应用中,IP 组播几乎就是组播的代名词,应用范围最为广泛,本文要也主要讨论 IP 组播相关内容。

    -
    -

    鉴于大家肯定对于单播(TCP/UDP)更加熟悉,我们这里先列出一些概念上的对比,这样能帮助我们直观的理解组播网络中涉及到的概念:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    单播组播
    通信方式一对一一对多
    网络层协议IPIP
    传输层协议TCP/UDPUDP
    地址单播地址组播地址
    传输方式可靠传输不可靠传输
    -

    单播的传输

    首先让我们回顾一下单播的传输链路,现在我们有一个发送方 A 和一个接收方 BA 有一份数据要发送给 B,那么这个过程是怎么样的呢?

    首先 A 需要确认 B 的 IP 地址,可能是口耳相传的也有可能是 DNS 查询的,总而言之,数据有了方向,才能启航。

    @@ -373,12 +334,12 @@

    单播的传输

  • 网络包顺着光纤进入 B 的网卡,进行各种校验之后,就成功抵达
  • 这是已有的单播的传输链路,那么组播的传输链路又是怎样的呢?

    -

    多播的传输

    -

    多播会复用单播的传输链路,但是具体又肯定有一些地方会不一样。

    -

    多播如何与单播区分

    -

    第一个问题是,多播的数据包如何与单播的数据包区分开来呢?

    +

    组播的传输

    +

    组播会复用单播的传输链路,但是具体又肯定有一些地方会不一样。

    +

    组播如何与单播区分

    +

    第一个问题是,组播的数据包如何与单播的数据包区分开来呢?

    特殊的 IP 地址

    -

    上文已经提及了,本文讨论的是基于 IP 层的多播,因此区分的关键就在于 IP 地址本身。

    +

    上文已经提及了,本文讨论的是基于 IP 层的组播,因此区分的关键就在于 IP 地址本身。

    IPv4 协议确立之初,IANA 将 2^32 四十亿个 IPv4 地址被分为了 A B C 三类地址,这三类地址我们应该都相当熟悉:

    A 0xxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx 大型网络
     B 10xxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx 中型网络
    @@ -386,7 +347,7 @@ 

    特殊的 IP 地址

    大中小全覆盖都有了,够了,那就先这样。

    而组播是后面才诞生的,不过好在一开始地址划分的时候还留了一点,还有一个 D 类和 E 类地址是保留的,不然 IP -地址就像泼出去的水,一旦分发出去了就再也收不回来了。

    +地址就像泼出去的水,一旦分发出去就再也收不回来了。

    要是没有预留,那可能后面诞生的组播就只能另辟蹊径了,比如在传输层做文章,或者不得不在应用层实现,这样的话效率和复杂度都会大大增加。

    幸运的是,D 类地址正好尚未被占领,可以用来做组播:

    D 1110xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx 组播地址
    @@ -396,7 +357,8 @@ 

    特殊的 IP 地址

    随着 IPv6 的普及,(2^128)海量的地址空间足以满足未来几十年甚至上百年的需求,根本不需要在 IPv4 上动什么心思了。

    image-20241127233459148

    D类地址(组播地址)虽然看起来很大,但也不是随意使用的。

    -

    为了更好地管理和使用这些地址,IANA对其进行了仔细的划分(在 RFC 1112 “Host Extensions for IP Multicasting” 中定义):

    +

    为了更好地管理和使用这些地址,IANA对其进行了仔细的划分(在 RFC 1112 “Host Extensions for IP Multicasting” +中定义):

    224.0.0.0-224.0.0.255:局域网组播地址,专门用于网络协议(如OSPF、RIP),不会跨路由器转发
     224.0.1.0-238.255.255.255:全球范围组播地址,用于互联网范围的应用(如流媒体、视频会议),但是这一部分应用并不不广泛
     239.0.0.0-239.255.255.255:管理权限范围组播地址,类似私有IP,供组织内部使用,这是最常见的地址了
    @@ -489,7 +451,8 @@ 

    PIM

    我们需要一种额外的路由协议来帮助我们的数据包合理规划路径,有许多协议尝试解决这个问题,例如 DVMRP、MOSPF、CBT 等, 而其中最为著名的就是 PIM 协议。

    报文

    -

    PIM (Protocol Independent Multicast) 是基于 IP 协议之上的一种网络层路由协议,用于在不同的组播路由器之间传递组播数据包。

    +

    PIM (Protocol Independent Multicast) 是基于 IP 协议之上的一种网络层路由协议,用于在不同的组播路由器之间传递组播数据包,* +注意,这是一个路由器之间的协议*,协议的发送者和接收者都是路由器。

    协议报文格式如下:

                                PIM 报文格式
     +--------+--------+----------+----------------------------------------+
    @@ -566,6 +529,156 @@ 

    BSR: 自动中间人

    PIM Snooping

    与 IGMP Snooping 类似,PIM Snooping 也是二层交换机的一种优化机制,区别在于它是通过监听 PIM 协议报文(而不是 IGMP 报文)来确定哪些端口需要组播数据,从而实现更精确的组播转发。

    +

    组播安全

    +

    组播系统本身并不是一个安全的系统,因为它的设计初衷是为了提供一种高效的数据传输方式,而并没有考虑安全性。

    +

    但是在实际应用中,这是一个不容忽视的问题,理论上一个网络中任何一台主机都可以接收甚至是发送组播数据包,它只需要在 UDP +包中填入对应的地址即可,这就给网络安全带来了很大的隐患。

    +

    我们通常可以通过以下几种方式来提高组播的安全性:

    +
      +
    • IPSec: 在 IP 层之上再套一层 IPSec 传输协议来进对数据进行加密防护
    • +
    • VLAN or VPN:将组播流量隔离到一个独立、可控网络中
    • +
    • ACL:在路由器上配置访问控制列表(ACL)限制哪些主机可以发送组播数据包
    • +
    • 应用层校验、加密、或者判断组播数据包源地址
    • +
    +

    组播的发送与接收

    +

    使用组播网络其实是一件非常简单的事情,IGMPPIM 等这些都是组播网络的提供者需要去考虑的问题,而作为一名使用者,在使用方式上而言, +接发组播和接发 UDP 单播几乎没什么区别

    +

    这里我们用通过代码来演示组播的发送与接收过程,我们将会使用 C 语言来实现,C 的接口和系统调用相对较为底层,更有利于我们理解网络协议的细节。

    +

    发送

    +

    发送者会定时发送 UDP 组播包, sender.c:

    +
    #include <arpa/inet.h>
    +#include <stdio.h>
    +#include <stdlib.h>
    +#include <string.h>
    +#include <unistd.h>
    +
    +#define MCAST_GRP "224.1.1.1"// 组播地址( D类 IP 224.0.0.0 - 239.255.255.255 是组播地址范围)
    +#define MCAST_PORT 5000      // 组播使用的 UDP 端口号
    +
    +int main() {
    +    int sock;
    +    struct sockaddr_in multicast_addr;
    +    char *message = "Hello, Multicast!";
    +
    +    // 创建 UDP 套接字,组播是基于 UDP 协议传输的
    +    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    +        perror("Socket creation failed");
    +        exit(1);
    +    }
    +
    +    // 设置 TTL 值,确保组播包不会超出本地网络
    +    int ttl = 2;
    +    if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0) {
    +        perror("Setting TTL option failed");
    +        close(sock);
    +        exit(1);
    +    }
    +
    +    // 配置组播地址
    +    memset(&multicast_addr, 0, sizeof(multicast_addr));
    +    multicast_addr.sin_family = AF_INET;
    +    multicast_addr.sin_addr.s_addr = inet_addr(MCAST_GRP);
    +    multicast_addr.sin_port = htons(MCAST_PORT);
    +
    +    printf("Sending multicast messages to %s:%d...\n", MCAST_GRP, MCAST_PORT);
    +
    +    while (1) {
    +        // 每隔 2 秒发送一次,发送消息到组播地址,就是调用的很普通的 socket 函数
    +        // 和发送普通 UDP 包就只有地址的区别
    +        if (sendto(sock, message, strlen(message), 0, (struct sockaddr *) &multicast_addr, sizeof(multicast_addr)) < 0) {
    +            perror("Failed to send message");
    +        } else {
    +            printf("Sent: %s\n", message);
    +        }
    +        sleep(2);
    +    }
    +
    +    close(sock);
    +    return 0;
    +}
    +

    接收

    +

    接收者会监听组播地址,接收到数据包后打印出来, receiver.c:

    +
    #include <arpa/inet.h>
    +#include <stdio.h>
    +#include <stdlib.h>
    +#include <string.h>
    +#include <unistd.h>
    +
    +#define MCAST_GRP "224.1.1.1"// 组播地址
    +#define MCAST_PORT 5000      // 组播端口
    +#define BUFFER_SIZE 1024     // 缓冲区大小
    +
    +int main() {
    +    int sock;
    +    struct sockaddr_in local_addr;
    +    struct ip_mreq mreq;
    +    char buffer[BUFFER_SIZE];
    +
    +    // 创建 UDP 套接字
    +    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    +        perror("Socket creation failed");
    +        exit(1);
    +    }
    +
    +    // 绑定本地地址和端口
    +    memset(&local_addr, 0, sizeof(local_addr));
    +    local_addr.sin_family = AF_INET;
    +    local_addr.sin_addr.s_addr = htonl(INADDR_ANY);// 接收所有本地接口的数据
    +    local_addr.sin_port = htons(MCAST_PORT);
    +
    +    if (bind(sock, (struct sockaddr *) &local_addr, sizeof(local_addr)) < 0) {
    +        perror("Binding failed");
    +        close(sock);
    +        exit(1);
    +    }
    +
    +    // 加入组播组
    +    mreq.imr_multiaddr.s_addr = inet_addr(MCAST_GRP);// 组播地址
    +    mreq.imr_interface.s_addr = htonl(INADDR_ANY);   // 本地接口
    +    if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
    +        perror("Joining multicast group failed");
    +        close(sock);
    +        exit(1);
    +    }
    +
    +    printf("Listening for multicast messages on %s:%d...\n", MCAST_GRP, MCAST_PORT);
    +
    +    while (1) {
    +        // 接收组播消息
    +        int nbytes = recvfrom(sock, buffer, BUFFER_SIZE, 0, NULL, NULL);
    +        if (nbytes < 0) {
    +            perror("Receive failed");
    +            break;
    +        } else {
    +            buffer[nbytes] = '\0';// 确保字符串以 '\0' 结尾
    +            printf("Received: %s\n", buffer);
    +        }
    +    }
    +
    +    // 离开组播组
    +    if (setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
    +        perror("Dropping multicast group failed");
    +    }
    +
    +    close(sock);
    +    return 0;
    +}
    +

    Run

    +

    首先将上面两个程序编译:

    +
    gcc sender.c -o sender
    +gcc receiver.c -o receiver
    +

    然后我们创建三个容器,模拟三个网络节点,分别运行 sender 和两个 receiver:

    +
    docker network create --driver bridge multicast-net
    +docker run -it --rm --network host --name=sender -v .:/root  ubuntu bash
    +docker run -it --rm --network host --name=receiver1 -v .:/root  ubuntu bash
    +docker run -it --rm --network host --name=receiver2 -v .:/root  ubuntu bash
    +

    image-20241215001942029

    +

    这样一个简单的组播网络就搭建好了,sender 会定时发送组播消息,receiver1 和 receiver2 会接收到这个消息。

    +

    总结

    +

    组播允许数据包同时发送给特定的一组接收者。

    +

    它使用特殊的D类IP地址(224.0.0.0到239.255.255.255)来标识组播组,通过 UDP 协议传输数据。

    +

    为了实现组播通信,网络中采用了IGMP协议来管理主机与路由器间的组成员关系,同时使用PIM协议来处理组播数据的路由转发。

    +

    未命名文件(10)