Linux上的UDP connect()和recv()函数

5
根据connect(2)手册页面,如果socket sockfd 的类型为SOCK_DGRAM,则serv_addr是默认发送数据报的地址,也是接收数据报的唯一地址。如果socket的类型为SOCK_STREAM或SOCK_SEQPACKET,则此调用尝试连接到绑定到serv_addr指定的地址的套接字。我正在尝试过滤从相同端口广播的两个不同组播组中传输的数据包,我认为connect()可以完成这项工作,但我无法使其正常工作。事实上,当我将它添加到我的程序中时,我没有收到任何数据包。更多信息请参见此thread。这是我设置连接参数的方式:
memset(&mc_addr, 0, sizeof(mc_addr));
mc_addr.sin_family = AF_INET;
mc_addr.sin_addr.s_addr = inet_addr(multicast_addr);
mc_addr.sin_port = htons(multicast_port);
printf("Connecting...\n");
if( connect(sd, (struct sockaddr*)&mc_addr, sizeof(mc_addr)) < 0 ) {
  perror("connect");
  return -1;
}

printf("Receiving...\n");
while( (len = recv(sd, msg_buf, sizeof(msg_buf), 0)) > 0 )
  printf("Received %d bytes\n", len);
4个回答

12

您的程序(可能)存在以下问题:

  • 您应该使用bind()而不是connect(),
  • 您缺少setsockopt(..., IP_ADD_MEMBERSHIP, ...)。

这里有一个接收多播消息的示例程序。它使用recvfrom()而不是recv(),但本质是相同的,只不过您还可以获取每个接收到的数据包的源地址。

要从多个多播组接收,请选择种选项之一。

第一种选择:为每个多播组使用单独的套接字,并将每个套接字绑定到一个多播地址。这是最简单的选择。

第二种选择:为每个多播组使用单独的套接字,将每个套接字绑定到INADDR_ANY,并使用套接字过滤器来过滤所有除一个多播组之外的所有内容。

因为您已经绑定到了INADDR_ANY,所以仍然可能会收到其他多播组的数据包。但是,可以使用内核的套接字过滤器将它们过滤掉:

#include <stdint.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <linux/filter.h>

/**
 * Adds a Linux socket filter to a socket so that only IP
 * packets with the given destination IP address will pass.
 * dst_addr is in network byte order.
 */
int add_ip_dst_filter (int fd, uint32_t dst_addr)
{
    uint16_t hi = ntohl(dst_addr) >> 16;
    uint16_t lo = ntohl(dst_addr) & 0xFFFF;

    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD + BPF_H + BPF_ABS, SKF_NET_OFF + 16), // A <- IP dst high
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, hi, 0, 3),        // if A != hi, goto ignore
        BPF_STMT(BPF_LD + BPF_H + BPF_ABS, SKF_NET_OFF + 18), // A <- IP dst low
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, lo, 0, 1),        // if A != lo, goto ignore
        BPF_STMT(BPF_RET + BPF_K, 65535),                     // accept
        BPF_STMT(BPF_RET + BPF_K, 0)                          // ignore
    };

    struct sock_fprog fprog = {
        .len = sizeof(filter) / sizeof(filter[0]),
        .filter = filter
    };

    return setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
}    

第三种选择:使用单个套接字接收所有多播组的多播。

在这种情况下,您应该为每个组执行IP_ADD_MEMBERSHIP。这样,您就可以在一个套接字上获取所有数据包。

但是,您需要额外的代码来确定接收到的数据包被寻址到哪个多播组。要做到这一点,您必须:

  • 使用recvmsg()接收数据包并读取IP_PKTINFO或等效的辅助数据消息。但是,要使recvmsg()给您提供此消息,您首先必须
  • 使用setsockopt()启用接收IP_PKTINFO辅助数据消息。

您需要执行的确切操作取决于IP协议版本和操作系统。以下是我执行此操作的方式(IPv6代码未经过测试):启用PKTINFO读取选项

这是一个简单的接收多播的程序,演示了第一种选择(绑定到多播地址)。

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXBUFSIZE 65536

int main (int argc, char **argv)
{
    if (argc != 4) {
        printf("Usage: %s <group address> <port> <interface address>\n", argv[0]);
        return 1;
    }

    int sock, status, socklen;
    char buffer[MAXBUFSIZE+1];
    struct sockaddr_in saddr;
    struct ip_mreq imreq;

    // set content of struct saddr and imreq to zero
    memset(&saddr, 0, sizeof(struct sockaddr_in));
    memset(&imreq, 0, sizeof(struct ip_mreq));

    // open a UDP socket
    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        perror("socket failed!");
        return 1;
    }

    // join group
    imreq.imr_multiaddr.s_addr = inet_addr(argv[1]);
    imreq.imr_interface.s_addr = inet_addr(argv[3]);
    status = setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
    (const void *)&imreq, sizeof(struct ip_mreq));

    saddr.sin_family = PF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    status = bind(sock, (struct sockaddr *)&saddr, sizeof(struct sockaddr_in));
    if (status < 0) {
        perror("bind failed!");
        return 1;
    }

    // receive packets from socket
    while (1) {
        socklen = sizeof(saddr);
        status = recvfrom(sock, buffer, MAXBUFSIZE, 0, (struct sockaddr *)&saddr, &socklen);
        if (status < 0) {
            printf("recvfrom failed!\n");
            return 1;
        }

        buffer[status] = '\0';
        printf("Received: '%s'\n", buffer);
    }
}

我不是要求一个基本的UDP程序,我需要过滤在同一端口广播的不同多播组。有关我的完整程序版本,请查看我在问题中提到的链接。 - Robert Kubrick
1
这是一个很棒的答案,它救了我的命! - Guillaume

1

首先需要注意的是,多播数据包是发送到多播地址而不是从多播地址发送的。
connect() 函数会允许或不允许从指定地址接收数据包。

要配置套接字以接收多播数据包,您需要使用两个套接字选项之一:

  • IP_ADD_MEMBERSHIP
  • IP_ADD_SOURCE_MEMBERSHIP

前者允许您指定一个多播地址,后者允许您指定一个多播地址和发送者的源地址。

可以使用以下方式完成这个操作:


struct ip_mreq groupJoinStruct;
unsigned long groupAddr = inet_addr("239.255.0.1");

groupJoinStruct.imr_multiaddr.s_addr = groupAddr;
groupJoinStruct.imr_interface.s_addr = INADDR_ANY;   // or the address of a specific network interface
setsockopt( yourSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &groupJoinStruct );

(为了简洁起见,省略了错误处理)

要停止接收此组地址的多播数据包,请使用套接字选项:

  • IP_DROP_MEMBERSHIP,或
  • IP_DROP_SOURCE_MEMBERSHIP

请注意,一个套接字可以有多个多播成员身份。但是,由于多播地址是数据包的目标地址,因此您需要能够抓取数据包的目标地址,以区分不同多播地址的数据包。

要抓取数据包的目标地址,您需要使用 recvmsg() 而不是 recv()recvfrom()。目标地址包含在类型为 DSTADDR_SOCKOPT 的 IPPROTO_IP 消息级别中。 正如 @Ambroz Bizjak 所述,您需要设置 IP_PKTINFO 套接字选项才能读取此信息。


需要检查的其他事项包括:

  • 您的内核是否支持组播?检查 /proc/net/igmp 是否存在以确保已启用。
  • 您的网络接口是否启用了组播?在运行 ifconfig 命令时,检查接口上是否列出了“MULTICAST”。
  • 您的网络接口是否支持组播?历史上,并非所有接口都支持。如果不支持,则可以通过将接口设置为混杂模式来解决此问题。例如:ifconfig eth0 promisc

Andrew,我很感激你的清晰解释,但正如我之前评论的那样,我想知道为什么内核不能为我过滤多播数据包。我确实添加了成员身份(事实上,我认为如果不设置此选项,程序将无法接收任何数据包),但问题是,如果另一个进程订阅了相同端口广播的相同源,我将开始接收两个多播。 - Robert Kubrick

0
将每个套接字的 bind(2) 绑定到相应多播组和端口的地址,而不是 INADDR_ANY。这样就可以为你过滤数据了。

没错,那样做可以。但问题是关于connect()和man页面中的内容。 - Robert Kubrick

0
只要所有的发送套接字都使用 bind 绑定到相应的多播地址,这个方法就可以正常工作。在 connect 中指定的地址将与接收数据包的源地址进行匹配,因此您需要确保所有数据包具有相同的(多播)源和目标地址。

Chris,我无法控制发送程序,但我可以从tcpdump或辅助数据中知道源地址。我已经尝试使用多播地址调用connect(),就像我在问题中解释的那样,但是这种方式我没有收到任何数据包。您是否建议使用源单播地址调用connect()?您能提供一个例子吗? - Robert Kubrick
@Ambroz:我不想在应用程序代码中过滤数据包。我已经通过查看每个数据包中的应用程序数据来实现了这一点。我正在寻找一种指示内核进行过滤的方法。我很难理解内核如何不能将特定的组播数据包分派到每个套接字。 - Robert Kubrick
@Robert:你不需要任何过滤。只需使用多个套接字,并将每个套接字与其自己的多播地址(IP_ADD_MEMBERSHIP)相关联。我在我的答案中提到了这一点,而且这似乎很明显? - Ambroz Bizjak
@Robert:另外,您可能需要在绑定套接字之前设置SO_REUSEADDR选项,否则bind()可能会因EADDRINUSE而失败。 - Ambroz Bizjak
无论如何,原始问题是关于UDP connect()以及Linux手册页面上的内容。 - Robert Kubrick
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接