Part 1 start network programming:chapter 14:多播与广播

第14章:多播与广播

假设我们要向10000名用户发送相同的数据,如果使用TCP提供服务,则需要10000个套接字连接,即使使用了UDP套接字提供服务,也需要10000次数据传输。

像这种需要向大量客户端发送相同数据时,会对服务器端和网络流量产生负面影响,这时可以使用多播技术解决。

14.1 多播(Multicast)

多播方式的数据传输基于UDP完成,区别在于,UDP数据传输以单一目标进行(一对一建立连接),而多播数据同时传递到加入特定组的大量主机,也就是说可以向多个主机传递数据。

14.1.1 多播数据传输方式及流量方面优势

多播数据传输特点:

  1. 服务器端针对特定多播组,只发送一次数据
  2. 即使只发送一次数据,该组内的所有客户端都会接收数据。
  3. 多播组数可在IP范围内任意增加
  4. 加入特定组即可接收发往该多播组的数据。

多播组是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是需要加入多播组的数据接收者。

下面实现:

  1. Sender:向AAA组广播xxx文件中保存的信息。
  2. 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地址的形式,广播分为下面两种:

  1. 直接广播
  2. 本地广播

两者的区别主要在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);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值