UNP_20_广播

Posted on Mar 20, 2020

20.1 概述

         不同类型的寻址方式

类型 IPv4 IPv6 TCP UDP 所标识接口数 递送到接口数
单播 一个 一个
任播 尚没有 一组 一组中的一个
组播 可选 全体 一组中的全体
广播 全体 全体
  • 组播支持在 IPv4 中是可选的,在 IPv6 中却是必须的
  • IPv6 不支持广播。任何使用 IPv4 编写的广播程序,在移植到 IPV6 上要换成组播
  • 广播和组播要求用于 UDP 或原始 IP,他们不能用于 TCP

  IPv6 往寻址体系结构中增加了任播方式。任播允许从一组通常提供相同服务的主机中选择一个(一般是最近的一个)通过适当的配置路由,并在多个位置往路由协议中注入同一个地址,多个 IPv4 或 IPv6 主机可以提供该地址的任播服务,任播还在逐步地完善中。
  广播的用途之一是在本地子网定位一个服务器主机,前提是已经知道这台服务器主机位于本地子网,但并不知道具体的单播地址。这种操作也称为 资源发现 。另一个用途是在有多个客户端与单个服务器主机通信的局域网环境中尽量减少分组流通。处于这个目的使用广播的因特网应用有多个例子:

  • ARP:ARP 并不是一个用户应用,而是 IPv4 的基本组成之一。ARP 在本地子网上广播一个请获取想要的硬件地址。ARP 使用在链路层不是 IP 层
  • DHCP:在认定本地子网存在一个 DHCP 服务器主机的前提下,DCHP 客户主机向广播地址(通常是 255.255.255.255)发送自己的请求
  • NTP:NTR 的一种常见使用情形是客户主机配置上待使用的一个或多个服务器主机的 IP 地址,然后以某个频度轮询这些服务器主机。根据由服务器返回的当前时间和到达服务器的 RTT,客户使用更精妙的算法更新本地时钟
  • 路由守护进程:route 是最早实现且常用的路由守护进程之一,它在一个局域网上广播自己的路由表。这样一来该局域网所有路由器都可以接受这些路由通告,而无须事先为每个路由器配置其邻居路由的 IP 地址

组播可以代替广播的这两个用途(资源发现、减少网络分组流通)广播有一定的问题

20.2 广播地址

广播地址例子:

  • 子网 192.168.42/24 的所有接口的子网定向广播地址是 192.168.42.255 {子网 ID,-1}
  • 受限广播地址 {-1, -1} 或 255.255.255.255

路由器从不转发目的地址为 255.255.255.255 的 IP 数据报

20.3 单播和广播的比较

  单播局域网中其余的主机可以在比较了自己的以太网地址和数据报的目标以太网地址,发现不同就会忽略,只有相同才会沿着协议栈对其进行处理。所以忽略它的是接口而不是主机。但是对于广播,局域网中的所有主机都会对其沿着协议栈进行处理,所以忽略它的是主机
  广播存在的问题是:子网上未参与相应广播的所有主机也不得不沿协议栈一路向上完整的处理收取的 UDP 广播数据报,直到该数据报在 UDP 层时被丢弃。另外,子网所有非 IP 主机也不得不在数据链路层接受完整的帧,然后再丢弃它。如果又较高速率产生 IP 广播数据报,可能会严重影响子网其余主机的性能,使用组播来解决这个问题。

20.4 使用广播的 dg_cli 函数

修改 dg_cli 函数,使得可以向 UDP 标准 daytime 服务器广播发送请求,然后显示所有应答

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int				n;
	const int		on = 1;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	socklen_t		len;
	struct sockaddr	*preply_addr;
    // 给 recvfrom 收到的服务器地址分配空间
	preply_addr = Malloc(servlen);
    // 设置 SO_BROADCAST 套接字选项
	Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
    // 设置 SIGALRM 的信号处理函数 recvfrom_alarm
	Signal(SIGALRM, recvfrom_alarm);
    // 下面发送的是广播数据报,可能会获得多个应答,我们在一个循环中调用 recvfrom,并显示在 5 秒钟内收到应答
    // 5 秒后系统会产生 SIGLARM 信号,导致 recvfrom 返回 EINTR 错误
	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		alarm(5);
		for ( ; ; ) {
			len = servlen;
			n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
			if (n < 0) {
				if (errno == EINTR)
					break;		/* waited long enough for replies */
				else
					err_sys("recvfrom error");
			} else {
                // 打印收到的回答
				recvline[n] = 0;	/* null terminate */
				printf("from %s: %s",
						Sock_ntop_host(preply_addr, len), recvline);
			}
		}
	}
	free(preply_addr);
}

static void recvfrom_alarm(int signo)
{
	return;		/* just interrupt the recvfrom() */
}

  内核不允许对广播数据报进行分片,对于目的地址是广播地址的 IP 数据报,如果其大小超过外出接口 MTU 会返回 EMSGSIZE 错误。原因可能是广播已经给网络造成很大压力,再进行分片,局域网的压力会大大增加。

20.5 竞争状态

  对于上一小节的代码,其中存在着一个问题:我们在一个无限 for 循环处理打印程序,通过定时闹钟来 break。但是问题是在 for 循环的任何时候都可能执行信号处理,并不仅仅是我们期望的在 recvfrom 阻塞中进行。所以当正在打印时执行信号处理操作,我们就一直会阻塞在 recvfrom 上了,因为我们唯一可以通过信号处理在 recvfrom 上跳出循环的机会让我们错失在了打印时。

下面有 4 种解决办法,第一种是错误的:

1.阻塞和解阻塞信号(错误版本):

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int				n;
	const int		on = 1;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	sigset_t		sigset_alrm;
	socklen_t		len;
	struct sockaddr	*preply_addr;
 
	preply_addr = Malloc(servlen);

	Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
    // 声明一个信号集,把它初始化为空集,再打开 SIGALRM 对应的位
	Sigemptyset(&sigset_alrm);
	Sigaddset(&sigset_alrm, SIGALRM);

	Signal(SIGALRM, recvfrom_alarm);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		alarm(5);
		for ( ; ; ) {
            // 调用 recvfrom 前,解阻塞 SIGALRM 信号
			len = servlen;
			Sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL);
			n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
            // recvfrom 函数结束后,立即阻塞 SIGALRM 信号,直到等到下一次解阻塞,也就是下一次循环开始
			Sigprocmask(SIG_BLOCK, &sigset_alrm, NULL);
			if (n < 0) {
				if (errno == EINTR)
					break;		/* waited long enough for replies */
				else
					err_sys("recvfrom error");
			} else {
				recvline[n] = 0;	/* null terminate */
				printf("from %s: %s",
						Sock_ntop_host(preply_addr, len), recvline);
			}
		}
	}
	free(preply_addr);
}

static void 
recvfrom_alarm(int signo)
{
	return;		/* just interrupt the recvfrom() */
}

  这种方式仍然存在问题,就是 recvfrom 和 阻塞 SIGALRM 信号操作都是相互独立的操作,万一,信号在 recvfrom 调用完但是信号还没有阻塞之前进行递交,那么内核就不能阻塞该信号,虽然出错拆给你扣已经很小了,但是问题依然是存在的。

2.用 pselect 阻塞和解阻塞信号:

使用 pselect 来解决这个问题:

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int				n;
	const int		on = 1;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	fd_set			rset;
	sigset_t		sigset_alrm, sigset_empty;
	socklen_t		len;
	struct sockaddr	*preply_addr;
 
	preply_addr = Malloc(servlen);

	Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));

	FD_ZERO(&rset);

	Sigemptyset(&sigset_empty);
	Sigemptyset(&sigset_alrm);
	Sigaddset(&sigset_alrm, SIGALRM);

	Signal(SIGALRM, recvfrom_alarm);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {
		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
        // 阻塞 SIGALRM 并调用 pselect
		Sigprocmask(SIG_BLOCK, &sigset_alrm, NULL);
		alarm(5);
		for ( ; ; ) {
			FD_SET(sockfd, &rset);
            // 调用 pselect 时,信号都是非阻塞的。调用完成,再恢复成被调用时的状态
			n = pselect(sockfd+1, &rset, NULL, NULL, NULL, &sigset_empty);
			if (n < 0) {
				if (errno == EINTR)
					break;
				else
					err_sys("pselect error");
			} else if (n != 1)
				err_sys("pselect error: returned %d", n);
            // 套接字变为可读,直接调用 recvfrom
			len = servlen;
			n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
			recvline[n] = 0;	/* null terminate */
			printf("from %s: %s",
					Sock_ntop_host(preply_addr, len), recvline);
		}
	}
	free(preply_addr);
}

static void
recvfrom_alarm(int signo)
{
	return;		/* just interrupt the recvfrom() */
}

3.使用 sigsetjmp 和 siglongjmp:

  解决静态问题还可以不使用信号来进行中断被阻塞系统调用并跳出循环。在是在信号处理函数中调用 siglongjmp 函数,siglongjmp 是非局部跳转,使用它可以从一个函数跳转到另一个函数

static void			recvfrom_alarm(int);
// 本函数和信号处理函数使用的跳转缓冲区
static sigjmp_buf	jmpbuf;

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int				n;
	const int		on = 1;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	socklen_t		len;
	struct sockaddr	*preply_addr;
 
	preply_addr = Malloc(servlen);

	Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));

	Signal(SIGALRM, recvfrom_alarm);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		alarm(5);
		for ( ; ; ) {
            // 从 dg_cli 函数中直接调用 sigsetjmp 时,它在建立跳转缓冲区后返回 0,接着调用 recvfrom。
			if (sigsetjmp(jmpbuf, 1) != 0)
				break;
			len = servlen;
			n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
			recvline[n] = 0;	/* null terminate */
			printf("from %s: %s",
					Sock_ntop_host(preply_addr, len), recvline);
		}
	}
	free(preply_addr);
}

static void
recvfrom_alarm(int signo)
{
    // 调用 siglongjmp 使 dg_cli 进行跳转
	siglongjmp(jmpbuf, 1);
}

4.使用从信号处理函数到主控函数的 IPC:

  通过信号通知系统 IPC 再通过 IPC 管道通知 dg_cli 中循环跳出,这里不贴代码了

小结

  广播发送的数据报由子网上所有终端主机接收。广播的劣势在于同一个子网上所有主机都必须处理数据报,若是 UDP 数据报则需要沿着协议栈一直处理到 UDP 层,即使不参与广播也需要处理,运行大量广播的应用,系统负担较大。使用组播解决这个问题,因为组播发送的数据只会对相应组播应用感兴趣的主机接收处理。

 解决竞争状态的几种方式:

  • 使用 pselect
  • 使用 sigsetjmp 和 siglongjmp
  • 使用从信号处理函数到主循环的 IPC(典型为管道)