Windows多网卡环境接收组播包

跨过千山万水,踏遍海角天涯--I got it!

Posted by cj on January 14, 2018

说明:本文仅针对IPv4

局域网发现技术有很多,常用组播(或称为多播):一台设备发送组播包,其他设备加入组播组,接收到组播包时即可知晓发送端IP,接收端回应约定数据即可让发送端也得知这些接收端的IP。

组播地址与端口号

IPv4的D类地址(224.0.0.0至239.255.255.255)是IPv4多播地址。D类地址的低28位构成多播组ID(group ID),整个32位地址则成为组地址(group address)。

224.0.0.1为all-hosts组,224.0.0.2是all-routers组。 224.0.0.0~224.0.0.255之间的地址(224.0.0.0/24)称为链路局部(link-local)多播地址,是为低级拓扑发现和维护协议保留。

一般应用程序使用239.0.0.0~239.255.255.255之间的地址,称作可管理地划分范围的IPv4多播空间(administratively scoped IPv4 multicast space)(RFC2365)。

本文使用239.254.43.21:45454作为组播地址和端口号

static const auto MULTICAST_GROUP_ADDRESS = "239.255.43.21";
static const unsigned short MULTICAST_GROUP_PORT = 45454;
static const auto LOCAL_IP = "192.168.1.222";

WinSock2编程须知

winsock.h与winsock2.h的一些宏定义如IP_ADD_MEMBERSHIP使用了不同的值,因此须特别注意。这破问题我是在join gruop(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP...))始终失败,后来检查错误码为10042 (WSAENOPROTOOPT),搜索得知不可直接#include <windows.h>, 而需要

#include <WinSock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

具体可参考INFO: Header and Library Requirement When Set/Get Socket Options at the IPPROTO_IP Level

发送端

单网卡环境

无需多提,仅需创建UDP socket,将数据报发送至D类地址的某个约定好的端口即可。

struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(MULTICAST_GROUP_PORT);
addr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP_ADDRESS);
int addr_len = sizeof(addr);

char host[1024] = { 0 };
gethostname(host, 1024);

int msgNo = 0;
char msg[1024] = { 0 };
while (true) {
    sprintf(msg, "Groupcast Message %s No.%d", host, msgNo++);
    int ret = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&addr, addr_len);
    if (ret < 0) {
        perror("sendto");
        return -1;
    } else {
        printf("Sent msg: %s\n", msg);
    }
    Sleep(1000);
}

多网卡环境

必须绑定并设置出口网卡,否则会使用系统网卡列表的第一个,有可能不是与其他设备同一个局域网的网卡。

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
    printf("socket error!!!\n");
    perror("socket:");
    return -1;
}

int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0) {
    perror("Setting SO_REUSEADDR error");
    closesocket(sockfd);
    return -1;
}

struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(MULTICAST_GROUP_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int addr_len = sizeof(addr);
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr));
if (-1 == ret) {
    printf("bind localaddr error!!!\n");
    perror("bind:");
    closesocket(sockfd);
    return -1;
}

unsigned long if_addr = inet_addr(LOCAL_IP);
ret = setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, (const char*)&if_addr, sizeof(if_addr));
if (-1 == ret) {
    printf("IP_MULTICAST_IF error!!!\n");
    perror("setsockopt:");
    closesocket(sockfd);
    return -1;
}

char host[1024] = { 0 };
gethostname(host, 1024);

int msgNo = 0;
char msg[1024] = { 0 };
addr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP_ADDRESS);
while (true) {
    sprintf(msg, "Groupcast Message %s No.%d", host, msgNo++);
    int ret = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&addr, addr_len);
    if (ret < 0) {
        perror("sendto");
        return -1;
    } else {
        printf("Sent msg: %s\n", msg);
    }
    Sleep(1000);
}

接收端

单网卡环境

非常简单,网上的demo也大多针对这种情况。 创建UDP socket,绑定INADDR_ANY、约定的端口,加入组播组,接收即可。

int ret = 0;

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
    printf("socket error!!!\n");
    perror("socket:");
    return -1;
}

int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0) {
    perror("Setting SO_REUSEADDR error");
    closesocket(sockfd);
    return -1;
}

struct sockaddr_in localaddr = { 0 };
localaddr.sin_family = AF_INET;
localaddr.sin_port = htons(MULTICAST_GROUP_PORT);
localaddr.sin_addr.s_addr = /*inet_addr(LOCAL_IP)*/ htonl(INADDR_ANY);
ret = bind(sockfd, (struct sockaddr*)&localaddr, sizeof(struct sockaddr));
if (-1 == ret) {
    printf("bind localaddr error!!!\n");
    perror("bind:");
    closesocket(sockfd);
    return -1;
}

/*设置是否支持本地回环接收*/
/*int loopBack = 1;
ret = setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, (const char *)&loopBack, sizeof(loopBack));
if (-1 == ret) {
printf("setsockopt broadcaset error!!!\n");
perror("setsockopt:");
closesocket(sockfd);
return -1;
}*/

struct ip_mreq ipmr = { 0 };
ipmr.imr_interface.s_addr = /*inet_addr(LOCAL_IP)*/ (INADDR_ANY);
ipmr.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP_ADDRESS);
int len = sizeof(ipmr);
ret = setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&ipmr, len);
if (-1 == ret) {
    printf("set error IP_ADD_MEMBERSHIP %d\n", WSAGetLastError());
    perror("setsockopt:");
    closesocket(sockfd);
    return -1;
}

/* now just enter a read-print loop */
char msgbuf[MSGBUFSIZE];
int nbytes = 0;
localaddr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP_ADDRESS);
while (1) {
    int addrlen = sizeof(localaddr);
    if ((nbytes = recvfrom(sockfd, msgbuf, MSGBUFSIZE, 0, (struct sockaddr *) &localaddr, &addrlen)) < 0) {
        perror("recvfrom");
        return -1;
    }
    msgbuf[nbytes] = 0;
    puts(msgbuf);
}

多网卡环境

有些不同,加入组播组时若依然使用INADDR_ANY,则内核默认使用网络设备列表的第一个设备,有可能并不是该局域网。因此,需要将INADDR_ANY替换为LOCAL_IP,即本设备与其他互相发现的设备所在局域网的网卡IP。

ipmr.imr_interface.s_addr = inet_addr(LOCAL_IP);

备注

以上所有源码可在https://github.com/captainwong/mcast获取。

Reference