第14章:多播与广播
假设我们要向10000名用户发送相同的数据,如果使用TCP提供服务,则需要10000个套接字连接,即使使用了UDP套接字提供服务,也需要10000次数据传输。
像这种需要向大量客户端发送相同数据时,会对服务器端和网络流量产生负面影响,这时可以使用多播技术解决。
14.1 多播(Multicast)
多播方式的数据传输基于UDP完成,区别在于,UDP数据传输以单一目标进行(一对一建立连接),而多播数据同时传递到加入特定组的大量主机,也就是说可以向多个主机传递数据。
14.1.1 多播数据传输方式及流量方面优势
多播数据传输特点:
- 服务器端针对特定多播组,只发送一次数据
- 即使只发送一次数据,该组内的所有客户端都会接收数据。
- 多播组数可在IP范围内任意增加
- 加入特定组即可接收发往该多播组的数据。
多播组是D类地址(224.0.0.0 ~ 239.225.225.225)
加入多播组:“在D类IP地址中,我希望接收发往目标239.234.218.234的多播数据”
多播基于UDP完成,但不同的是,向网络传递一个多播数据包时,路由器将复制该数据包并传递到多个主机。如下图所示:多播需要借住路由器完成。
从上图中可以看到,数据从发送端主句通过路由器一步一步传输到所有的组内成员。
“但是看起来这种方式并不利于网络流量”
从图中看到,路由器多次复制同一个文件,但是减少了发送数据包的次数。
使用多播的方式,当传输路径相似或者相同时,能够大大减少数据包的发送次数,只需要由路由器父子文件并传递到主句即可。
也正是因为这种特性,多播主要用于“多媒体数据的实时传输
(有些路由器并不支持多播通信,会使用隧道技术)
14.1.2 Routing(路由)和TTL(Time to Live 生存时间)以及加入组
1)TTL
为了传播多播数据包,必须设置TTL,因为TTL决定了数据包传递的距离。
TTL用整数表示,并且每经过一个路由器就 -1,当TTL变为 0 时,该数据包无法再被传递,只能销毁。(如下图所示)
因此,TTL的值设置过大将影响网络流量,过小则无法传递到目标。
那么如何设置TTL呢?
TTL设置是通过第九章的套接字可选项完成的。与TTL相关的协议层为IPPROTO_IP,选项名为IP_MULTICAST_TTL。因此可以通过类似下面的代码,将TTL设置为想要的数字。
int send_sock;
int time_live = 1234;
...
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST, (viod*)& time_live,
sizeof(time_live));
...
如何加入多播组呢?
同样通过设置套接字选项完成。加入多播组的协议层也是IPPROTO_IP
,选项名为IP_ADD_MEMBERSHIP
。可通过下面这种代码加入多播组。
int recv_sock;
struct ip_mreq join_adr;
...
recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
...
join_adr.imr_multiaddr.s_addr = "多播组地址信息";
join_adr.imr_interface.s_addr = "加入多播组的主机地址信息";
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (viod*)&join_adr,
sizeof(join_adr));
...
看起来有点不太清楚,但是如果之前认真学了,就会发现其实就是个结构体里面的信息,用来存储了我们想要加入的多播组的信息,再调用setsockopt函数吧这个信息设置到对应的套接字上,就搞定了。
我们看一下ip_mreq结构体的内容:
struct ip_mreq
{
struct in_addr imr_multiaddr; // 写入期望加入的组IP地址
struct in_addr imr_interface; // 该组套接字所属主机的 ip地址,也可使用INADDR_ANY
}
其中的in_addr
结构体类型我想大家应该不陌生,这在设置套接字时使用过了无数次了
(bind中的第二个参数是地址值,其结构体中包含 in_addr 结构体变量)
下面我们来通过一个示例感受一下
14.1.3 实现多播Sender 和 Receiver
多播中的“发送者”称为Sender,接受者称为“Receiver”,他俩替代了服务器端和客户端的称呼。
明显,Sender是数据发送主体,Receiver是需要加入多播组的数据接收者。
下面实现:
- Sender:向AAA组广播xxx文件中保存的信息。
- Receiver:接收传递到AAA组的信息。
先看Sender代码,记得创建的是UDP套接字哦~
关于UDP的相关函数用法,忘记的盆友可以看这篇文章~
基于UDP的服务器端/客户端
news_sender.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define TTL 200
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char *argv[])
{
/* code */
if(argc!= 3){
printf("Usage : %s <GROUPIP> <port>\n", argv[0]);
exit(1);
}
// 声明变量
int send_sock;
struct sockaddr_in mul_adr;
int time_live = TTL;
char buf[BUF_SIZE];
int count;
FILE *fp;
// 初始化UDP套接字
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&mul_adr, 0, sizeof(mul_adr));
mul_adr.sin_family = AF_INET;
mul_adr.sin_addr.s_addr = inet_addr(argv[1]); // 多播IP(目标IP)
mul_adr.sin_port = htons(atoi(argv[2])); // 多播端口(目标端口)
// 设置套接字的TTL
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_live, sizeof(time_live));
// 打开保存有待发送信息的文件
if((fp = fopen("news.txt","r") )== NULL){
error_handling("fopen() error");
}
// 开始多播循环
while(!feof(fp))
{
fgets(buf, BUF_SIZE, fp); // read数据
printf("%s\n", buf);
count = sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr*)&mul_adr, sizeof(mul_adr));
printf("%d\n", count);
sleep(2);
}
fclose(fp);
close(send_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
news_receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char *argv[])
{
/* code */
if(argc!= 3){ // 这里仍然需要两个参数
printf("Usage : %s <GROUPIP> <port>\n", argv[0]);
exit(1);
}
// 声明套接字相关变量
int recv_sock;
int str_len;
char buf[BUF_SIZE];
struct sockaddr_in adr;
struct ip_mreq join_adr;
// 初始化套接字、服务器地址啥的
recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr,0,sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = htonl(INADDR_ANY);
adr.sin_port = htons(atoi(argv[2]));
// 为接收端套接字分配地址信息
if(bind(recv_sock, (struct sockaddr*)&adr, sizeof(adr)) == -1){
error_handling("bind() error");
}
// 设置join_adr结构体中的多播组信息
join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]); // 输入的参数设置为多播组的IP地址信息
join_adr.imr_interface.s_addr = htonl(INADDR_ANY); // 设置本机地址为:期望加入多播组的 IP地址(新成员
// 设置接收端套接字的接收选项 增加新的多播组成员
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*)&join_adr, sizeof(join_adr));
// 下面开始循环接收信息
while(1)
{
str_len = recvfrom(recv_sock, buf, BUF_SIZE -1, 0, NULL, 0);
if(str_len < 0){
break;
}
buf[str_len] = 0;
fputs(buf, stdout);
}
close(recv_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
总结一下就是:
发送端的套接字在设置为UDP套接字并修改套接字属性为后,调用sendto
函数(同时已经分配了IP和端口号),向目标地址也就是多播组地址直接发送数据即可
接收端的套接字在设置为UDP套接字并分配当前接收端主机的地址给套接字,修改套接字属性后,将当前主机IP添加到多播组中,这样,去调用recvfrom函数就能够接收到数据了。
14.2 广播
广播与多播在一次性向多个主机发送数据这点上很像,但是传输数据的范围有区别。
多播即使在跨越不同网络的情况下,只要加入多播组就能接收到数据(通过路由器不断传播),而广播只能在同一网络中的主机传输数据。
14.2.1 广播的理解及实现方法
与多播相似,广播也是基于UDP完成的。根据传输数据时使用的IP地址的形式,广播分为下面两种:
- 直接广播
- 本地广播
两者的区别主要在IP地址。
直接广播的IP地址中除了网络地之外,其余主机地址全部设置为 1.
例如:希望向网络地址为 192.12.34中的所有主机传输数据时,可以向192.12.34.255传输。换言之,可以采用直接广播的方式向特定区域内所有主机传输数据。(192.12.34.255 代表了192.12.34这个网络地址下的所有主机)
本地广播中使用的IP地址限定为 255.255.255.255
例如:192.32.24网络中的主机向 255.255.255.255 传输数据时,数据将传递到192.32.24网络中的所有主机。
下面说说如何实现Sender和Receiver
广播中的示例通信和UDP真的像,主要区别就是IP地址的范围不同。
从上面我们知道,默认生成的套接字会阻止广播,因此我们只需要修改套接字选项。
int send_sock;
int bcast = 1; // 对变量进行初始化以将 SO_BROADCAST选项信息改为1
...
send_sock = socket(PF_INET,SOCK_DGRAM, 0);
...
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*)&bcast,
sizeof(bcast));
...
调用setsockopt函数,将SO_BROADCAST选项设置为bcast变量中的值 1.
则意味着可以进行数据广播。这个过程(修改套接字选项)只需要在Sender中进行修改。
14.2.2 实现广播数据的Sender和Receiver
下面进行实现。
news_sender_brd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char *argv[])
{
/* code */
if(argc!= 3){
printf("Usage : %s <Boradcast IP> <port>\n", argv[0]);
exit(1);
}
// 声明变量
int send_sock;
struct sockaddr_in broad_adr;
char buf[BUF_SIZE];
int so_brd = 1;
int count;
FILE *fp;
// 初始化UDP套接字 必须把地址设置为要传输目标的地址,也就是多播组地址
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
// 设置套接字的TTL
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*)&so_brd, sizeof(so_brd));
memset(&broad_adr, 0, sizeof(broad_adr));
broad_adr.sin_family = AF_INET;
broad_adr.sin_addr.s_addr = inet_addr(argv[1]); // (目标IP)本地广播和直接广播体现在这里
broad_adr.sin_port = htons(atoi(argv[2])); // (目标端口)
// 打开保存有待发送信息的文件
if((fp = fopen("news.txt","r") )== NULL){
error_handling("fopen() error");
}
// 开始广播循环
while(!feof(fp))
{
fgets(buf, BUF_SIZE, fp); // 先读数据
count = sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr*)&broad_adr, sizeof(broad_adr));
printf("%d\n", count);
if(count < 0){
error_handling("sendto error!");
}
sleep(2);
}
fclose(fp);
close(send_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
news_receiver_brd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char *argv[])
{
/* code */
if(argc!= 2){ // 这里仍然需要两个参数
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 声明套接字相关变量
int recv_sock;
int str_len;
char buf[BUF_SIZE];
struct sockaddr_in adr;
// 初始化套接字、接收端地址啥的
recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr,0,sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = htonl(INADDR_ANY);
adr.sin_port = htons(atoi(argv[1]));
// 为接收端套接字分配地址信息
if(bind(recv_sock, (struct sockaddr*)&adr, sizeof(adr)) == -1){
error_handling("bind() error");
}
// 这里并不需要为接收端套接字 设置套接字选项
// 下面开始循环接收信息
while(1)
{
str_len = recvfrom(recv_sock, buf, BUF_SIZE -1, 0, NULL, 0);
if(str_len < 0){
break;
}
buf[str_len] = 0;
fputs(buf, stdout);
}
close(recv_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}