UNP Chapter 18 - 广播
18.1. 概述
广播的用途之一是将定服务器主机在本地子网上,但不知到它的单播IP地址时,对它进行定位,这就是资源发现(resouce discovery)。另一用途是当有多个客户和单个服务器通信时,减少局域网上数据流量。实际例子有: ARP(地址解析协议, Address Resolution Protocol),BOOTP(引导协议,Bootstrap Protocol),NTP(网络时间协议,Network)。
18.2 广播地址
如果用{netid, subnetid, hostid}( {网络ID,子网ID,主机ID})表示IPv4地址。那么有四种类型的广播地址,我们用-1表示所有比特位均为1的字段:
1. 子网广播地址:{netid, subnetid, -1}。这类地址编排指定子网上的所有接口。
2. 全部子网广播地址:{netid, -1, -1}。这类广播地址编排指定网络上的所有子网。
3. 网络广播地址:{netid, -1}。这类地址用于不进行子网划分的网络。
4. 受限广播地址:{-1, -1, -1}。路由器从不转发目的地址为255.255.255.255的IP数据报
18.3. 单播和广播的比较
子网上所有未参与广播应用系统的主机也必须完成对数据报的协议处理,直至在UDP层将它丢弃
18.4. 使用广播的dg_cli函数
再次修改我们的dg_cli函数,让它向标准UDP时间/日期服务器广播请求数据报,并输出所有的应答。
#include "unp.h"
static void recvfrom_alarm(int);
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socken_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); /* malloc为由recvfrom返回的服务器地址分配空间 */
Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
Signal(SIGALRM, recvfrom_alarm); /* 设置SO_BROADCAST套接口选项,并为SIGALRM信号安装信号处理程序 */
while(Fgets(sendline, MAXLINE, fp) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 9, 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);
}
}
}
}
static void recvfrom_alarm(int signo)
{
return; /* just interrupt the recvfrom() */
}
18.5. 竞争状态
多个进程访问共享数据,但正确结构依赖于进程的执行顺序,这种情况我们称之为竞争状态(race condition)。竞争状态通常是线程编程中始终要注意的一个重要问题,因为在线程中有非常之多的数据需要共享,例如所有的全程变量。
在进行信号处理时,通常会出现各种类型的竞争状态。这是因为在我们的程序执行过程中,内核随时都会递交信号。
1. 用pselect阻塞和解阻塞信号
#include "unp.h"
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], recvfrom[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, fg) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
Sigprocmask(SIG_BLOCK, &sigset_alrm, NULL);
alarm(5);
for( ; ; )
{
FD_SET(sockfd, &rset);
n = pselect(sockfd+1, &rset, NULL, NULL, NULL, &sigset_empty); /* sigset_empty 是一个没有任何信号阻塞的信号集,也就是说,所有的信号都是解阻塞的 */
if(n < 0)
{
if(errno == EINTR)
break;
else
err_sys("pselect error");
}
else if(n != 1)
err_sys("pselect erro: returned %d", n);
len = servlen;
n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len); /* 如果套接口是可读的,则调用recvfrom,这时我们知道它不会阻塞 */
recvline[n] = 0; /* null terminate */
printf("from %s: %s", Sock_ntop_host(preply_addr, len), recvline);
}
}
}
static void recvfrom_alarm(int signo)
{
return; /* just interrupt the recvfrom() */
}
2. 使用sigsetjmp和siglongjmp
从信号处理程序调用siglongjmp,我们称之为非本地跳转(nonlocal goto),因为使用它可以从一个函数跳转到另一个函数
#include "unp.h"
#include <setjmp.h>
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( ; ; )
{
if(sigsetjmp(jmpbuf, 1) != 0) /* 从dg_cli函数直接调用sigsetjmp,它在建立了跳转缓冲区后返回0,接着嗲用recvfrom */
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);
}
}
}
static void recvfrom_alarm(int signo)
{/* 当SIGALRM信号递交时,我们调用siglongjmp。这将引起dg_cli函数中的sigsetjmp返回,返回值为第二个参数(1),它必须为非0值。sigsetjmp返回导致dg_cli中的for循环的结束。 */
siglongjmp(jmpbuf, 1);
}
以这种方式使用sigsetjmp和siglongjmp保证了不会出现由于信号递交时间不当而引起的recvfrom永远阻塞现象。
3. 使用从信号处理程序到函数的IPC
使用进程内部的管道,当定时器到时时,信号处理程序就向管道中写1个字节。函数dg_cli读取该字节并以此决定什么时候终止for循环。使该方法成为一个好方法的原因是我们使用了select来测试管道是否可读。我们测试要么套接口可读,要么管道可读。
#include "unp.h"
static void recvfrom_alarm(int);
static int pipefd[2];
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socklen_t servlen)
{
int n, maxfdp1;
const int on = 1;
char sendline[MAXLINE], recvline[MAXLINE+1];
fd_set rset;
socklen_t len;
struct sockaddr * preply_addr;
preply_addr = Malloc(servlen);
Setsockopt(sockfd, SOL_SOCKET, SO_BORADCAST, &on, sizeof(on));
Pipe(pipefd); /* 创建一个普通的Unix管道,并返回两个描述字,pipefd[0]是管道的读端,pipefd[1]是管道的写端 */
maxfdp1 = max(sockfd, pipefd[0]) + 1;
FD_ZERO(&rset);
Signal(SIGALRM, recvfrom_alarm);
while(Fgets(sendline, MAXLINE, fp) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
alarm(5);
for( ; ; )
{/* 对套接口和管道读端进行select操作 */
FD_SET(sockfd, &rset);
FD_SET(pipefd[0], &rset);
if((n = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0)
{
if(errno == EINTR)
continue;
else
err_sys("select error");
}
if(FD_ISSET(sockfd, &rset))
{
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);
}
if(FD_ISSET(pipefd[0], &rset))
{/* 当管道的读端可读时,我们从管道中read信号处理程序写入的空字节,并将它忽略。但这告诉我们定时器到时,于是我们break出这个无限的for循环 */
Read(pipefd[0], &n, 1); /* timer expired */
break;
}
}
}
}
static void recvfrom_alarm(int signo)
{/* 当SIGALRM信号递交时,信号处理程序向管道中写入1个字节,使读端可读。由于信号处理程序返回时可能中断select,因此,如果select返回EINTR错误,我们就忽略它,并且由此可知管道的读端是可读的,这样一来将终结for循环 */
Write(pipefd[1], "", 1); /* write 1 null byte to pipe */
return;
}
18.6. 小结