多播
多播(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_mreq
和ip_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中,如果不设置发送对应多播地址使用的网卡,则会使用系统会使用路由表自动选择默认网卡进行发送(如果存在默认路由)。但通常多播数据包可能不是需要发送到默认路由,因此我们需要指定发送多播数据包使用的网卡,有以下三种方式:
- 使用
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;
}
- 使用
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;
}
- 在路由表中指定对应多播地址使用的网卡地址(即将多播地址当作一个普通网段的地址,为该网段增加一个路由表项)。
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>