多播

多播(multicast)又称为组播,是一种介于单播(一对一)和广播(一对全部)之间的一种数据发送方式,只有位于一个多播组内的实体能够接收到发送到该多播组的数据包。

多播地址范围

多播地址总的范围为224.0.0.0~239.255.255.255,每一个地址表示一个多播组,简单的细分范围如下:

地址范围

说明

224.0.0.0~224.0.0.255

仅本地同一个子网使用,不可路由

224.0.1.0~224.0.1.255

公网可以使用的多播地址,可以在公网路由

224.0.2.0~239.255.255.255

内部网络可用,可路由

更完整的地址细分参考 Multicast address - Wikipedia

关键数据结构

ip_mreq和ip_mreqn

ip_mreqip_mreqn是用于设置网卡加入多播组数据结构,两个数据结构基本功能相同,可以替换使用,区别在于ip_mreqn是Linux2.2之后加入的新数据结构,比ip_mreq多了一个字段,具体如下:

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

// ip_mreq是一个旧的数据结构,但目前仍然可用
struct ip_mreq
{
    /* IP multicast address of group.  */
    struct in_addr imr_multiaddr;

    // 设置加入多播组的的网卡ip, 注意这里并不表示socket同该网卡绑定
    // 该socket仍然能够接收到不是该网卡的数据包,该设置仅仅表示该ip
    // 对应的网卡能够接收对应多播组的数据包
    struct in_addr imr_interface;
};

// ip_mreqn是从Linux 2.2之后可用的新的数据结构,相比ip_mreq,其多了一个imr_ifindexy
struct ip_mreqn {
    struct in_addr imr_multiaddr;
    // 设置加入多播组的的网卡ip, 注意这里并不表示socket同该网卡绑定
    // 该socket仍然能够接收到不是该网卡的数据包,该设置仅仅表示该ip
    // 对应的网卡能够接收对应多播组的数据包
    struct in_addr imr_address;   
    // 设置加入多播组的网卡的index,该设置项优先级高于上边的网卡ip
    int            imr_ifindex; 
};

ip_mreqn结构记录了需要加入的的多播组的网卡ip(网卡index)

使用如下函数可以将网卡名称(ifconfig查看)转换为对应的index,用于填充imr_ifindex域,也可以将imr_ifindex设置为0表示使用默认网卡

#include <net/if.h> unsigned int if_nametoindex (const char *__ifname)

发送多播数据包

仅发送多播数据包不需要加入多播组,将目的地址设置为对应的多播地址即可,在Linux中,如果不设置发送对应多播地址使用的网卡,则会使用系统会使用路由表自动选择默认网卡进行发送(如果存在默认路由)。但通常多播数据包可能不是需要发送到默认路由,因此我们需要指定发送多播数据包使用的网卡,有以下三种方式:

  1. 使用setsockopt通过IP_MULTICAST_IF选项设置发送数据包到特定多播地址时使用的对应网络接口地址。
struct ip_mreq mreq;
// 设置多播地址
mreq.imr_multiaddr.s_addr = inet_addr("226.1.11.111");
// 设置使用该多播地址发送数据时使用的网卡ip
mreq.imr_interface.s_addr = inet_addr("192.168.1.2");
// 调用setsockopt
ret = setsockopt(socks[i], IPPROTO_IP, IP_MULTICAST_IF, &mreq,sizeof(mreq));
if ( ret < 0) {
    LOG_ERROR("Fail to join udp group, err: %s", strerror(errno));
    goto out;
}
  1. 使用setsockopt设置SO_BINDTODEVICE将该socket绑定到对应的网卡,此后通过该socket接收和发送的数据均只能通过绑定的网卡(需要root权限)。
char* interface = "eth0";
ret = setsockopt(socks[i], SOL_SOCKET,SO_BINDTODEVICE, interface, strlen(interface));
if ( ret < 0 ) {
    LOG_ERROR("Fail to bind to interface, err: %s", strerror(errno));
    goto out;
}
  1. 在路由表中指定对应多播地址使用的网卡地址(即将多播地址当作一个普通网段的地址,为该网段增加一个路由表项)。
route add -net 226.1.11.0 netmask 255.255.255.0 dev eth0

发送多播数据包时,通常不希望收到自己发送出去的包(当自己也位于该多播组时),因此需要设置IP_MULTICAST_LOOP选项。

// 禁止组播回送(防止收到自己发送的组播包)
op = 0;
ret = setsockopt(socks[i], IPPROTO_IP , IP_MULTICAST_LOOP, &op, sizeof(op));
if (ret < 0) {
    LOG_ERROR("Fail to disable multicast loop, err: %s",strerror(errno));
    goto out;
}

接收多播数据包

接收多播数据包需要使用IP_ADD_MEMBERSHIP加入一个多播组。

// 加入组播
struct ip_mreq mreq;
// 设置需要接收的多播地址
mreq.imr_multiaddr.s_addr = inet_addr("226.1.11.111");
// 设置接收该多播地址数据的网卡ip
mreq.imr_interface.s_addr = inet_addr("192.168.1.2");
// 调用setsockopt
ret = setsockopt(socks[i], IPPROTO_IP, IP_ADD_MEMBERSHIP , &mreq,sizeof (mreq));
if ( ret < 0) {
    LOG_ERROR("Fail to join udp group, err: %s", strerror(errno));
    goto out;
}

如果需要一个socket仅接收特定网卡的多播数据包,在Linux中目前我找到的最好的办法就是设置SO_BINDTODEVICE选项将该socket绑定到指定网卡,如上一小节所述。

关于socket的各种设置项目,参考ip(7) - Linux manual page (man7.org)

关于Windows和Linux中socket绑定的区别

在windows中,使用bind函数可以将socket绑定到对应ip的网卡上,而在Linux中,bind函数更像一个ip地址过滤的功能,并不会将socket同网卡进行绑定,Linux中必须使用setsocketopt来设置SO_BINDTODEVICE选项进行socket和网卡的绑定。

上述结论是在多播发送和接收实验过程中得到的。windows中socket绑定网卡对应的ip后可以正常接收发送到该网卡的多播数据包,并且通过该socket发送的多播数据包也是通过绑定的网卡进行发送的。Linux中socket绑定网卡对应的ip后无法接收到发送到该网卡的多播数据包,必须绑定0.0.0.0或者多播地址才能接收到对应的多播数据包。

附录

socket相关的api

#include <sys/socket.h>

ip地址相关api

#include <netinet/in.h>