0%

来说一说 ssdp 协议

本篇同样是Yeelight相关的文章的衍生。简单来说,Yeelight的设备会在监听组播地址 239.255.255.250:1982 的特定消息,收到这个特定消息之后,向发送特定消息的客户端发送自己的一些信息,方便这个客户端与自己进行交互。

ssdp(Simple Service Discovery Protocol) 协议原文可以查看 https://tools.ietf.org/html/draft-cai-ssdp-v1-00

我在这里找到一个Unix可用的一个ssdp服务端的一个小例子,稍微改造了一下用来模拟Yeelight的服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/*
* recvudp.c
*
* A simple UDP receiver with multicast addressing
* This mechanism based on the Simple Service Discovery Protocol (SSDP)
* Reference: https://tools.ietf.org/html/draft-cai-ssdp-v1-00
*
* THE SOFTWARE IS PROVIDED "AS IS",
* WITHOUT WARRANTY OF ANY KIND!
*
* Author: Andrejs Tihomirovs
* Email: armtech@its.lv
*/

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>

int main(int argc, char *argv[])
{
char* group = "239.255.255.250";
int port = 1982;

const char* sendData = "M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1982\r\nMAN: \"ssdp:discover\"\r\nST: wifi_bulb";
const char* info = "I'm server\n";

// create UDP socket
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("recvudp");
return 1;
}

// allow multiple sockets
int optname = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
(char *) &optname, sizeof(optname)) < 0) {
perror("recvudp");
return 1;
}

// set up destination address
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);

// socket binding
if (bind(sockfd, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
perror("recvudp");
return 1;
}

// join to multicast group
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(group);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);

if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
(char *) &mreq, sizeof(mreq)) < 0) {
perror("recvudp");
return 1;
}

// main loop
while (1)
{
#define BUFSIZE 256
char buf[BUFSIZE];
int numbytes, addrlen = sizeof(addr);
memset(buf, 0, sizeof(buf));

// receive UDP message
if ((numbytes = recvfrom(sockfd, buf, BUFSIZE-1 , 0,
(struct sockaddr *)&addr, &addrlen)) == -1) {
perror("recvudp");
return 1;
}

if(strcmp(buf,sendData)==0)
{
printf("\n%s\nLEN:%d bytes from %s\n", buf, numbytes,inet_ntoa(addr.sin_addr));
sendto(sockfd, info, strlen(info), 0,
(struct sockaddr*) &addr, sizeof(addr));
}
else
{
printf("LEN:%d bytes\n%s",numbytes,buf);
}
}
return 0;
}

需要注意的是,按照ssdp协议原本的定义,服务端应该监听的地址是 239.255.255.250:1900 。不过 Yeelight 选择监听 239.255.255.250:1982 而已。原理上还是一样的。

可以看到相比于通常所用的udp服务端,多了两个地方

1
2
3
4
5
// allow multiple sockets
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,(char *) &optname, sizeof(optname))

// join to multicast group
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,(char *) &mreq, sizeof(mreq))

参考上面的例子我们可以很容易的移植到Windows上。

一个简单的发现服务端可以写成下面这样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int CLIENT_PORT = 1982;
char *CLIENT_IP = "239.255.255.250";

int main()
{
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd == -1)
{
perror("failed to create socket. exit now.");
exit(-1);
}

struct sockaddr_in addr;
addr.sin_family = AF_INET;

addr.sin_port = htons(CLIENT_PORT);
addr.sin_addr.s_addr = inet_addr(CLIENT_IP);
if (addr.sin_addr.s_addr == INADDR_NONE)
{
printf("Incorrect ip address!");
close(socket_fd);
exit(1);
}

char buff[512];
socklen_t len = sizeof(addr);
const char *info = "M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1982\r\nMAN: \"ssdp:discover\"\r\nST: wifi_bulb";
int n;

n = sendto(socket_fd, info, strlen(info), 0, (struct sockaddr *)&addr, sizeof(addr));
if (n < 0)
{
perror("sendto");
close(socket_fd);
return 1;
}
while (1)
{
memset(buff, 0, sizeof(buff));
n = recvfrom(socket_fd, buff, 512, 0, (struct sockaddr *)&addr, &len);
if (n > 0)
{
buff[n] = 0;
printf("received from %s:\n%s", inet_ntoa(addr.sin_addr),buff);
}
else if (n == 0)
{
printf("server closed\n");
close(socket_fd);
return 1;
}
else if (n == -1)
{
perror("recvfrom");
close(socket_fd);
return 1;
}
}

return 0;
}

通过上边的服务端和客户端的小例子,也同样是可以试验出来,发送“查找消息之后”,客户端多循环几次就能收到多个服务端返回的消息了,如下图

并且可以看到为了防止客户端没有收到消息,Yeelight设备发了两次响应消息。

那么对于我们来说,ssdp的一个比较典型的使用场景就是,在同一个网段中,一个设备(这里称为客户端)想要找到另外一个设备(这里称为服务端)(在不手动指定地址的情况下),客户端往组播地址发送特定消息,获取到服务端的信息,然后与服务端交互。比如工厂流水线上,PC作为服务端,产品作为客户端,由于客户端的mac地址不同,当连接到路由器上是,路由器分配给产品的IP地址不一定是一样的,就可以用这种办法来了。

python 的服务端实现可以参考这个 https://www.cnblogs.com/chengyunshen/p/7196094.html