《UNIX网络编程 卷1》 笔记:广播

时间:2021-05-22 22:27:51

之前几节我们展示的客户服务器通信方式都是单播的通信方式,也就是一台主机上的进程和另一台主机上的进程通信。

本节我们讲述广播通信,这种通信方式只有UDP协议支持,它使得一台主机能够和主机所在子网上的其他所有主机同时进行通信。

为了方便,这里我们只讨论IPv4地址,后面所指的IP都是IPv4地址。

我们知道IP地址长度为32位,一般使用的单播IP地址由{子网ID,主机号}组成,子网ID的位数由子网掩码指定,当主机号所占的位全为1时,这个IP地址就是广播地址,更确切地说法是子网定向广播地址。

举个例子,我的机器的一个网口IP地址是192.168.36.128,子网掩码是24位,主机号占8位,也就是说我的机器在子网192.168.36上,主机号是128。将主机号所占的位全部取值为1得255,就得出了这个网口的广播地址,也就是192.168.36.255。和我在同一个子网的其他主机的(一个)广播地址也是192.168.36.255。

下面我们看看书中讲述的UDP单播和广播的通信原理。

《UNIX网络编程 卷1》 笔记:广播

单播通信,UDP客户指定通信的目的IP地址是192.168.42.3(单播地址,服务器的IP地址),这样网卡发送出去的以太网帧的目的MAC地址是服务器的MAC地址00:0a:95:79:bc:b4。然后同一个子网192.168.42上所有主机的网卡都会收到这个以太网帧,但是网卡驱动忽略目的MAC地址(不是我们下文要讲到的以太网广播地址)和自己网卡MAC地址不相同的帧,因此,只有在服务器上收到的帧才会被网卡驱动交给上层协议栈处理。


《UNIX网络编程 卷1》 笔记:广播

广播通信,UDP客户指定通信的目的IP地址是192.168.42.255(广播地址),这样网卡发送出去的以太网帧的目的MAC地址是以太网广播地址ff:ff:ff:ff:ff:ff。子网上所有收到这个以太网帧的主机的网卡驱动程序都会将帧交给上层协议栈处理。

上述就是广播和多播的通信原理以及它们的区别,关键就在于广播通信的以太网帧的目的MAC地址是以太网广播地址。

接下来我们实现一个UDP回射客户程序,它同时与多个回射服务器交互,在收到回应时打印出回射的数据和服务器的IP地址。运行这个程序时我们指定一个子网广播地址。假设这个子网上有其它5台主机,每个主机都运行对应的回射服务程序,那么我们期待从标准输入获取一行数据发送后就会看到5行回射的数据。

这里我们只给出dg_cli函数,主函数使用基本UDP套接字编程一节中的主函数。

static void recvfrom_alarm(int);

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));

Signal1(SIGALRM, recvfrom_alarm);

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;
else
err_sys("recvfrom error");
} else {
recvline[n] = 0;
printf("from %s: %s",
Sock_ntop_host(preply_addr, len), recvline);
}
}
}
free(preply_addr);
}

static void recvfrom_alarm(int signo)
{
    return;
}
 
从代码可以看到开始多播通信之前要使用setsockopt函数为套接字开启SO_BROADCAST选项。另外由于是多播通信,在发送数据之后我们不知道会收到多少回应,而我们又不能一直阻塞在recvfrom调用(这样就无法再次发送数据),因此我们设置了一个5秒的定时器,期待定时器超时后产生SIGALRM信号打断recvfrom调用,退出for循环,这样我们又可以再次发送数据。

然而,这段代码在运行中可能会发生我们意想不到的情况,因为这段代码包含有竞争条件。我们将在下节详细讲述代码中的竞争条件。