监听UDP多播消息的UDP单播回复

3
我正在尝试在Linux上实现一个非常简单的UPnP控制器,以便我可以控制一个需要专有软件的设备。
文档中说,我需要发送一个特定形式的UDP多播请求(请参见下面代码中的“M-SEARCH”字符串)到特定的地址和端口,设备将通过UDP单播响应到我从发送的地址和端口。
但我无法使其工作。tcpdump显示UDP多播请求发送到了正确的地址和端口,并且格式似乎也是正确的,但我看不到回复。
我正在从环回接口发送和侦听(设备位于同一台机器上)。
另一个UPnP控制器(即不是我的)在环回接口上正常工作。
有人能建议我做错了什么吗?
以下是代码:
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <netinet/udp.h>
 #include <unistd.h>
 #include <fcntl.h>

 #define MAXBUFSIZE 65536

int main(int argc, char ** argv ) {

unsigned char loop;
loop = 0;
unsigned char ttl;
ttl = 4;
int bcast;
bcast = 1;

int sock;

sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
    perror("socket");
    exit(EXIT_FAILURE);
}

struct sockaddr_in destadd;
memset(&destadd, 0, sizeof(destadd));
destadd.sin_family = AF_INET;
destadd.sin_port = htons((uint16_t)1900);
if (inet_pton(AF_INET, "239.255.255.250", &destadd.sin_addr) < 1) {
    perror("inet_pton dest");
    exit(EXIT_FAILURE);
}

struct sockaddr_in interface_addr;
memset(&interface_addr, 0, sizeof(interface_addr));
interface_addr.sin_family = AF_INET;
interface_addr.sin_port = htons(0);
if (inet_pton(AF_INET, "127.0.0.1", &interface_addr.sin_addr) < 1) {
    perror("inet_pton interface");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0){
    perror("setsockopt loop");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0){
    perror("setsockopt ttl");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF,
               (struct in_addr *)&interface_addr.sin_addr,
               sizeof(interface_addr.sin_addr)) < 0) {
    perror("setsockopt if");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &bcast, sizeof(bcast)) < 0) {
    perror("setsockopt bcast");
    exit(EXIT_FAILURE);
}

struct ip_mreqn imr;
memset(&imr, 0, sizeof(imr));
if (inet_pton(AF_INET, "239.255.255.250", &imr.imr_multiaddr.s_addr) < 1) {
    perror("inet_pton");
    exit(EXIT_FAILURE);
}
inet_pton(AF_INET, "127.0.0.1", (struct in_addr *)&imr.imr_address);
imr.imr_ifindex = 0;
if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
               (void *)&imr, sizeof(imr)) < 0) {
    perror("setsockopt addmem");
    exit(EXIT_FAILURE);
}

if (bind(sock, (struct sockaddr *)&interface_addr,
    sizeof(struct sockaddr_in)) < 0) {
    perror("bind");
    exit(EXIT_FAILURE);
}

char buffer[1024];

strcpy(buffer, "M-SEARCH * HTTP/1.1\r\n"
                   "Host: 239.255.255.250:1900\r\n"
                   "Man: \"ssdp:discover\"\r\n"
                   "ST: upnp:rootdevice\r\n"
                   "MX: 3\r\n"
                   "User-Agent: Test/1.0\r\n"
                   "\r\n");

if (sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&destadd,
       sizeof(destadd)) < 0) {
    perror("sendto");
    exit(EXIT_FAILURE);
}

if (recvfrom(sock, &buffer, sizeof(buffer)-1, 0, NULL, NULL) < 0) {
    perror("recvfrom");
    exit(EXIT_FAILURE);
}


if (close(sock) < 0) {
    perror("close");
    exit(EXIT_FAILURE);
}

}

1
设备在本地主机上吗?怎么办?尝试删除bind()步骤和IP_MULTICAST_IF步骤。 - user207421
为了避免防火墙问题,我首先使用软件中的DLNA设备(minidlna),并在本地系统上运行。一旦我让它工作起来,我将尝试在局域网上使用真实设备。minidlna可以从本地主机响应到其他DLNA控制器。感谢您提供的建议。不幸的是,如果我删除IP_MULTICAST_IF步骤,则程序流量会通过eth0发送出去。仅删除绑定,或同时删除绑定和IP_MULTICAST_IF似乎无法解决问题。 - Tony
1
你的机器上执行 route -n 命令的输出是什么? - wick
嗨@wick。输出结果(对于糟糕的格式表示歉意)为:目标地址 网关 子网掩码 标志 距离 参考 使用 接口
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
- Tony
@Tony:根据这个,发送到239.x.x.x的数据包将通过eth0而不是环回接口,对吗?从我的实践来看,90%的组播故障排除都在路由表中...这不是一个答案,但我会朝那个方向看,并添加一个238.0.0.0/8的静态路由。 - wick
显示剩余2条评论
2个回答

2

最终适用于我的代码如下。

方法概述:

  • 创建一个UDP套接字
  • 启用IF_MULTICAST_LOOP(允许与同一主机上的客户端通信)
  • 设置IF_MULTICAST_TTL
  • 绑定到INADDR_ANY和端口8201(端口号任意选择)
  • 向239.255.255.250:1900发送多播消息
  • 使用相同的套接字接收回复

开始吧:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char ** argv ) {

unsigned char loop;
loop = 1; // Needs to be on to get replies from clients on the same host
unsigned char ttl;
ttl = 4;
int bcast;
bcast = 1;

int sock;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
    perror("socket");
    exit(EXIT_FAILURE);
}

// Multicast message will be sent to 239.255.255.250:1900
struct sockaddr_in destadd;
memset(&destadd, 0, sizeof(destadd));
destadd.sin_family = AF_INET;
destadd.sin_port = htons((uint16_t)1900);
if (inet_pton(AF_INET, "239.255.255.250", &destadd.sin_addr) < 1) {
    perror("inet_pton dest");
    exit(EXIT_FAILURE);
}

// Listen on all interfaces on port 8201 
struct sockaddr_in interface_addr;
memset(&interface_addr, 0, sizeof(interface_addr));
interface_addr.sin_family = AF_INET;
interface_addr.sin_port = htons(8201);
interface_addr.sin_addr.s_addr = htonl(INADDR_ANY);

// Got to have this to get replies from clients on same machine
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0){
    perror("setsockopt loop");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0){
    perror("setsockopt ttl");
    exit(EXIT_FAILURE);
}

// Bind to port 8201 on all interfaces
if (bind(sock, (struct sockaddr *)&interface_addr,
    sizeof(struct sockaddr_in)) < 0) {
    perror("bind");
    exit(EXIT_FAILURE);
}

char buffer[1024];

strcpy(buffer, "M-SEARCH * HTTP/1.1\r\n"
               "Host: 239.255.255.250:1900\r\n"
               "Man: \"ssdp:discover\"\r\n"
               "ST: upnp:rootdevice\r\n"
               "MX: 3\r\n"
               "User-Agent: Test/1.0\r\n"
               "\r\n");

if (sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&destadd,
       sizeof(destadd)) < 0) {
    perror("sendto");
    exit(EXIT_FAILURE);
}

if (recvfrom(sock, &buffer, sizeof(buffer)-1, 0, NULL, NULL) < 0) {
    perror("recvfrom");
    exit(EXIT_FAILURE);
}


printf("%s\n", buffer);

if (close(sock) < 0) {
    perror("close");
    exit(EXIT_FAILURE);
}
}

为了从外部客户端获得回复,请确保端口8201未被阻止。

那么,究竟这两者之间有什么区别呢? - user207421
@EJP。进行了编辑以指明更改。 - Tony
消息通过IP路由表指示的任何接口发送。这与“最低编号接口是什么”无关。 - user207421
@EJP。我被这个参考文献搞混了(https://www.cs.cmu.edu/~srini/15-441/F01.full/www/assignments/P2/htmlsim_split/node18.html)。现在我看到至少有一个(http://www.linux.com/learn/docs/ldp/Multicast-HOWTO#ss2.4)是指路由表(在订阅组的上下文中)。我已经对答案进行了编辑,希望能减少错误的可能性。 - Tony

0

根据评论中的发现,route -n 的输出如下:

0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0 192.168.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0

这表明发送到 239.x.x.x 的数据包将通过 eth0 而不是环回接口。

因此,最好添加一个静态路由来强制使用环回接口发送出站数据包,或者通过 IF_MULTICAST_LOOP 标志确保可以通过环回接口接收。


静态路由是否可行?当我运行ifconfig时,lo是否支持多播并不清楚。使用IF_MULTICAST_LOOP更直接,据我所知,它将数据包直接放入lo队列,而不需要lo检测到多播数据包并将其添加到队列中。如果我没有理解得对,请见谅——这超出了我的深度。 - Tony
@Tony:不幸的是,我无法测试它,因为您有非常特定的配置,但是使用route命令添加静态路由并进行检查很容易。route的man页面可以解释如何操作。 - wick
我尝试使用 sudo ip route add 239.0.0.0/8 via 127.0.0.1 添加静态路由。路由已添加,ip route get to 239.255.255.250 生成了 multicast 239.255.255.250 dev lo src 192.168.1.xxx。然而,无论是否启用 IF_MULTICAST_LOOP,组播都停止工作了。 - Tony
不太对,不要使用通过,尝试添加以下内容:route add -net 239.0.0.0 netmask 255.0.0.0 dev lo0。 - wick
谢谢。我尝试了,但是得到了相同的结果 - 没有成功。不过,没有静态路由的IF_MULTICAST_LOOP可以完成任务。 - Tony

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