UDP 单播、广播和多播

324 阅读15分钟

写本篇文章的原因:在查看molecularjs源码的时候,当采用了tcp作为服务间的transporter,不同的service由不同的进程启动,服务间是怎么互相发现的?

参考文章:UDP 单播、广播和多播

服务端和客户端工作概述

服务端

  • 创建套接字: 使用 socket 函数创建一个 UDP 套接字。
  • 绑定地址(可选): 如果需要接收来自特定客户端的响应,可以使用 bind 函数将套接字绑定到一个特定的 IP 地址和端口。
  • 发送数据: 使用 sendto 函数向指定的 IP 地址和端口发送数据。
  • 接收数据(可选): 如果需要接收来自客户端的响应,可以使用 recvfrom 函数接收数据。

客户端

  • 创建套接字: 使用 socket 函数创建一个 UDP 套接字 (SOCK_DGRAM)。

  • 绑定地址(可选) :

    • 对于广播接收,需要使用 bind 函数将套接字绑定到广播地址和指定的端口。
    • 对于普通的 UDP 通信,如果需要接收来自特定服务器的响应,也可以绑定到一个特定的 IP 地址和端口。
  • 发送数据: 使用 sendto 函数向指定的 IP 地址和端口发送数据。

  • 接收数据: 使用 recvfrom 函数接收来自服务器的数据。

在 UDP 广播通信中,只有客户端接收到消息后,才能知道服务端的信息包括 IP 地址和端口号等。

这种模式的优点在于:

  • 高效性:服务器只需要发送一次广播消息,所有监听该广播地址和端口的客户端都可以接收到,无需服务器与每个客户端建立单独的连接。
  • 简单性:客户端无需知道服务器的具体地址,只需监听广播地址即可。

客户端和服务端在 UDP 通信中的角色和行为不同之处:

  • 主动性

    • 服务端通常扮演主动角色,监听特定端口,等待客户端的请求或主动向客户端发送数据(如广播)。
    • 客户端通常扮演被动角色,向服务器发送请求或接收服务器发送的数据。
  • 绑定地址

    • 服务端通常绑定到一个固定的、众所周知的端口,以便客户端知道向哪里发送请求。
    • 客户端通常不需要绑定地址,除非需要接收来自特定服务器的响应或接收广播消息。
  • 数据流向

    • 服务端通常接收来自多个客户端的请求,并向这些客户端发送响应。
    • 客户端通常向一个或多个服务器发送请求,并接收来自这些服务器的响应。

UDP 单播、广播和多播

使用UDP协议进行信息的传输之前不需要建议连接。换句话说就是客户端向服务器发送信息,客户端只需要给出服务器的ip地址和端口号,然后将信息封装到一个待发送的报文中并且发送出去。至于服务器端是否存在,或者能否收到该报文,客户端根本不用管。

单播用于两个主机之间的端对端通信,广播用于一个主机对整个局域网上所有主机上的数据通信。单播和广播是两个极端,要么对一个主机进行通信,要么对整个局域网上的主机进行通信。实际情况下,经常需要对一组特定的主机进行通信,而不是整个局域网上的所有主机,这就是多播的用途。

UDP 广播

广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。

广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。

其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。

服务端代码

 1 #include<iostream>
 2 #include<stdio.h>
 3 #include<sys/socket.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<netdb.h>
 7 #include<netinet/in.h>
 8 #include<arpa/inet.h>
 9 #include<string.h>
10 using namespace std;
11 int main()
12 {
13     setvbuf(stdout,NULL,_IONBF,0);
14     fflush(stdout);
15     int sock=-1;
16     if((sock=socket(AF_INET,SOCK_DGRAM,0))==-1)
17     {
18         cout<<"sock error"<<endl;
19         return -1;
20     }
21     const int opt=-1;
22     int nb=0;
23     nb=setsockopt(sock,SOL_SOCKET,SO_BROADCAST,(char*)&opt,sizeof(opt));//设置套接字类型
24     if(nb==-1)
25     {
26         cout<<"set socket error...\n"<<endl;
27         return -1;
28     }
29     struct sockaddr_in addrto;
30     bzero(&addrto,sizeof(struct sockaddr_in));
31     addrto.sin_family=AF_INET;
32     addrto.sin_addr.s_addr=htonl(INADDR_BROADCAST);// 套接字地址为广播地址,INADDR_BROADCAST:是一个特殊的常量,代表有限广播地址(limited broadcast address)。它的值通常是 255.255.255.255,但具体取决于你所在的子网。当在 sendto 函数中使用这个地址时,操作系统会自动将其转换为你所在子网的广播地址,例如 192.168.1.255。
33     addrto.sin_port=htons(6000);//套接字广播端口号为6000,客户端监听6000端口接收消息。
34     int nlen=sizeof(addrto);
35     while(1)
36     {
37         sleep(1);
38         char msg[]={"the message broadcast"};
39         int ret=sendto(sock,msg,strlen(msg),0,(sockaddr*)&addrto,nlen);//向广播地址发布消息
40         if(ret<0)
41         {
42             cout<<"send error...\n"<<endl;
43             return -1;
44         }
45         else 
46         {
47             printf("ok\n");
48         }
49     }
50     return 0;
51 }

客户端代码

 1 #include<iostream>
 2 #include<stdio.h>
 3 #include<sys/socket.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<netdb.h>
 7 #include<netinet/in.h>
 8 #include<arpa/inet.h>
 9 #include<string.h>
10 
11 
12 using namespace std;
13 int main()
14 {
15         setvbuf(stdout,NULL,_IONBF,0);
16         fflush(stdout);
17         struct sockaddr_in addrto;
18         bzero(&addrto,sizeof(struct sockaddr_in));
19         addrto.sin_family=AF_INET;
20         addrto.sin_addr.s_addr=htonl(INADDR_ANY);
21         addrto.sin_port=htons(6000); // 服务端仅向 6000 广播消息
22         socklen_t len=sizeof(addrto);
23         int sock=-1;
24         if((sock=socket(AF_INET,SOCK_DGRAM,0))==-1)
25         {
26                 cout<<"socket error..."<<endl;
27                 return -1;
28         }
29         const int opt=-1;
30         int nb=0;
31         nb=setsockopt(sock,SOL_SOCKET,SO_BROADCAST,(char*)&opt,sizeof(opt));
32         if(nb==-1)
33         {
34                 cout<<"set socket errror..."<<endl;
35                 return -1;
36         }
37         if(bind(sock,(struct sockaddr*)&(addrto),len)==-1)
38         {
39                 cout<<"bind error..."<<endl;
40                 return -1;
41         }
42         char msg[100]={0};
43         while(1)
44         {
45                 int ret=recvfrom(sock,msg,100,0,(struct sockaddr*)&addrto,&len);
46                 if(ret<=0)
47                 {
48                         cout<<"read error..."<<endl;
49                 }
50                 else
51                 {
52                         printf("%s\t",msg);
53                 }
54                 sleep(1);
55         }
56         return 0;
57 }

UDP 多(组)播

多播

多播,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

在广域网上多播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通信。

组播的实现离不开路由器。

路由器在组播中的作用

  • 维护组播组成员信息:路由器通过 IGMP 协议与主机交互,了解哪些主机属于哪些组播组。

  • 转发组播数据:当路由器接收到组播数据包时,它会根据组播组成员信息,有选择地将数据包复制并转发到属于该组播组的接口,从而实现只向组内主机发送数据。

  • 构建组播分发树:路由器之间通过组播路由协议(如 PIM)交换信息,构建组播分发树,确保组播数据能够高效地到达所有组内主机。

    加入或退出组播组

  • 主机发起:主机通过向连接的路由器发送 IGMP 消息来请求加入或退出某个组播组。

  • 路由器响应:路由器接收到 IGMP 消息后,会更新其组播组成员信息,并相应地调整组播数据的转发策略。

相对于传统的一对一的单播,多播具有如下的优点:

  1、具有同种业务的主机加入同一数据流,共享同一通道,节省了带宽和服务器的优点,具有广播的优点而又没有广播所需要的带宽。

  2、服务器的总带宽不受客户端带宽的限制(服务器仅发送一次,转发给谁,转发几次由路由器决定)。由于组播协议由接收者的需求来确定是否进行数据流的转发,所以服务器端的带宽是常量,与客户端的数量无关。

  3、与单播一样,多播是允许在广域网即Internet上进行传输的,而广播仅仅在同一局域网上才能进行。

组播的缺点:

  1、多播与单播相比没有纠错机制,当发生错误的时候难以弥补,但是可以在应用层来实现此种功能。

  2、多播的网络支持存在缺陷,需要路由器及网络协议栈的支持。

  3、多播的应用主要有网上视频、网上会议等。

广域网的多播

多播的地址是特定的,D类地址用于多播。D类IP地址就是多播IP地址,即224.0.0.0至239.255.255.255之间的IP地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:

  1、局部多播地址:在224.0.0.0~224.0.0.255之间,这是为路由协议和其他用途保留的地址,路由器并不转发属于此范围的IP包。

  2、预留多播地址:在224.0.1.0~238.255.255.255之间,可用于全球范围(如Internet)或网络协议。

  3、管理权限多播地址:在239.0.0.0~239.255.255.255之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围。

多播的程序设计使用setsockopt()函数和getsockopt()函数来实现,组播的选项是IP层的,其选项值和含义参见下表。

getsockopt()/setsockopt() 的选项含 义
IP_MULTICAST_TTL设置多播组数据的TTL值
IP_ADD_MEMBERSHIP在指定接口上加入组播组
IP_DROP_MEMBERSHIP退出组播组
IP_MULTICAST_IF获取默认接口或设置接口
IP_MULTICAST_LOOP禁止组播数据回送

多播程序框架主要包含套接字初始化、设置多播超时时间、加入多播组、发送数据、接收数据以及从多播组中离开几个方面。其步骤如下:

(1)建立一个socket。

(2)然后设置多播的参数,例如超时时间TTL、本地回环许可LOOP等。

(3)加入多播组。

(4)发送和接收数据。

(5)从多播组离开。

服务端代码

 1 #include<iostream>
 2 #include<stdio.h>
 3 #include<sys/socket.h>
 4 #include<netdb.h>
 5 #include<sys/types.h>
 6 #include<arpa/inet.h>
 7 #include<netinet/in.h>
 8 #include<unistd.h>
 9 #include<stdlib.h>
10 #include<string.h>
11 #define MCAST_PORT 8888
12 #define MCAST_ADDR "224.0.0.88"  // 多播地址
13 #define MCAST_DATA "BROADCAST TEST DATA"  // 多播内容
14 #define MCAST_INTERVAL 5  //多播时间间隔
15 using namespace std;
16 
17 int main()
18 {
19         int sock;
20         struct sockaddr_in mcast_addr;
21         sock=socket(AF_INET,SOCK_DGRAM,0);
22         if(sock==-1)
23         {
24                 cout<<"socket error"<<endl;
25                 return -1;
26         }
27         memset(&mcast_addr,0,sizeof(mcast_addr));
28         mcast_addr.sin_family=AF_INET;
29         mcast_addr.sin_addr.s_addr=inet_addr(MCAST_ADDR);
30         mcast_addr.sin_port=htons(MCAST_PORT);
31         while(1)
32         {       //向局部多播地址发送多播内容
33                 int n=sendto(sock,MCAST_DATA,sizeof(MCAST_DATA),0,(struct sockaddr*)&mcast_addr,sizeof(mcast_addr));
34                 if(n<0)
35                 {
36                         cout<<"send error"<<endl;
37                         return -2;
38                 }
39                 else
40                 {
41                         cout<<"send message is going ...."<<endl;
42                 }
43                 sleep(MCAST_INTERVAL);
44 
45         }
46         return 0;
47 }

客户端代码

 1 #include<iostream>
 2 #include<stdio.h>
 3 #include<stdlib.h>
 4 #include<string.h>
 5 #include<sys/types.h>
 6 #include<unistd.h>
 7 #include<sys/socket.h>
 8 #include<netdb.h>
 9 #include<arpa/inet.h>
10 #include<netinet/in.h>
11 #define MCAST_PORT 8888
12 #define MCAST_ADDR "224.0.0.88" /*一个局部连接多播地址,路由器不进行转发*/
13 #define MCAST_INTERVAL 5  //发送时间间隔
14 #define BUFF_SIZE 256   //接收缓冲区大小
15 using namespace std;
16 int main()
17 {
18         int sock;
19         struct sockaddr_in local_addr;
20         int err=-1;
21         sock=socket(AF_INET,SOCK_DGRAM,0);
22         if(sock==-1)
23         {
24                 cout<<"sock error"<<endl;
25                 return -1;
26         }
27         /*初始化地址*/
28         local_addr.sin_family=AF_INET;
29         local_addr.sin_addr.s_addr=htonl(INADDR_ANY);
30         local_addr.sin_port=htons(MCAST_PORT);
31         /*绑定socket*/
32         err=bind(sock,(struct sockaddr*)&local_addr,sizeof(local_addr));
33         if(err<0)
34         {
35                 cout<<"bind error"<<endl;
36                 return -2;
37         }
38         /*设置回环许可*/
39         int loop=1;
40         err=setsockopt(sock,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop));
41         if(err<0)
42         {
43                 cout<<"set sock error"<<endl;
44                 return -3;
45         }
46         struct ip_mreq mreq;/*加入广播组*/
47         mreq.imr_multiaddr.s_addr=inet_addr(MCAST_ADDR);//广播地址
48         mreq.imr_interface.s_addr=htonl(INADDR_ANY); //网络接口为默认
49         /*将本机加入广播组*/
50         err=setsockopt(sock,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));
51         if(err<0)
52         {
53                 cout<<"set sock error"<<endl;
54                 return -4;
55         }
56         int times=0;
57         socklen_t addr_len=0;
58         char buff[BUFF_SIZE];
59         int n=0;
60         /*循环接受广播组的消息,5次后退出*/
61         for(times=0;;times++)
62         {
63                 addr_len=sizeof(local_addr);
64                 memset(buff,0,BUFF_SIZE);
65                 n=recvfrom(sock,buff,BUFF_SIZE,0,(struct sockaddr*)&local_addr,&addr_len);
66                 if(n==-1)
67                 {
68                         cout<<"recv error"<<endl;
69                         return -5;
70                 }
71                 /*打印信息*/
72                 printf("RECV %dst message from server : %s\n",times,buff);
73                 sleep(MCAST_INTERVAL);
74         }
75         /*退出广播组*/
76         err=setsockopt(sock,IPPROTO_IP,IP_DROP_MEMBERSHIP,&mreq,sizeof(mreq));
77         close(sock);
78         return 0;
79 }

关于一台主机多个套接字同时监听同一个端口的问题。

下面的解释思路:为什么UDP套接字n元组可以相同?

UDP 套接字本身是二元组。

它由以下两部分组成:

目标 IP 地址:指定数据包要发送到的目的主机的 IP 地址。

目标端口号:指定数据包要发送到的目的主机上的应用程序所使用的端口号。

为什么是二元组?

UDP 是一种无连接的协议,它不需要建立和维护端到端的状态。因此,UDP 套接字只需要知道目标地 址(IP 地址和端口号)即可发送数据。源 IP 地址和源端口号是在发送数据包时由操作系统自动填充的, 不需要在套接字中显式指定。

UDP套接字与五元组的关系

当 UDP 套接字发送数据时,操作系统会将源 IP 地址、源端口号、目标 IP 地址、目标端口号和协议类 型(UDP)组合成一个五元组,并将其添加到数据包头部。这个五元组用于在网络中路由和传递数据 包,并确保数据包能够正确地到达目的地。

UDP 套接字五元组在多播场景下可以相同

此时五元组为什么会出现冲突?

多播地址是共享的:多播通信中,所有加入同一个组播组的主机都共享同一个组播地址。

端口号也相同:如果多个客户端在同一主机上监听相同的端口,那么它们的源 IP 和端口号都相同。

五元组冲突:这导致多个客户端的五元组完全相同,操作系统无法区分它们,从而无法将数据包正确 地 传递给对应的客户端。

冲突解决方案 - SO_REUSEPORT

为了解决这个问题,我们需要使用 SO_REUSEPORT 套接字选项。这个选项允许多个套接字绑定到相同 的 IP 地址和端口号,从而避免五元组冲突。

SO_REUSEPORT 工作原理

当启用 SO_REUSEPORT 后,操作系统会将发送到该地址和端口的数据包分发给所有绑定到该地址和端 口的套接字。具体的分发策略可能因操作系统而异,但通常会采用某种负载均衡算法,确保数据包在多 个套接字之间公平分配。

关于为什么要广播方和接受方的端口号相同才能收到广播?

因为udp套接字是二元组,server端广播消息的时候,不知道客户端的信息,只知道要发送的局域网ip等 信息。

此时客户端要接受到消息,就要有约定俗成的规定使得客户端和服务端在同一频道上。这种约定就是同 一个端口

UDP 单播

例如在一个以太网上有3个主机,主机的配置如下表所示。

主 机ABC
IP地址192.168.1.150192.168.1.151192.168.1.158
MAC地址00:00:00:00:00:0100:00:00:00:00:0200:00:00:00:00:03

单播流程:主机A向主机B发送UDP数据报,发送的目的IP为192.168.1.151,端口为 80,目的MAC地址为00:00:00:00:00:02。此数据经过UDP层、IP层,到达数据链路层,数据在整个以太网上传播,在此层中其他主机会判断目的MAC地址。主机C的MAC地址为00:00:00:00:00:03,与目的MAC地址00:00:00:00:00:02不匹配,数据链路层不会进行处理,直接丢弃此数据。

PS

molecular.js中选择tcp作为通信协议的时候,服务之间是通过UDP协议互相感知到的。