广播的用途之一是在本地子网定位一个服务器主机,前提是已知或认定这个服务器主机位于本地子网,但是不知道它的单播IP地址。这种操作也称为资源发现。
源自Berkeley的内核不允许对广播数据报执行分片。对于目的地址是广播地址的IP数据报,如果其大小超过外出接口的MTU,发送它的系统调用将返回EMSGSIZE
错误。(这样的理由感觉上是由于既然广播已经施加给网络相当大的负担,再因分片而造成这个负担倍乘片段的数量就更不应该。)
使用记法{子网ID,主机ID}表示一个IPv4地址,其中子网ID表示由子网掩码覆盖的连续位,主机ID表示以外的位。例如IP为192.168.1.99/22的主机,其广播地址为192.168.3.255。
将文章UDP客户/服务器中的回射服务的客户端中的dg_cli
函数改写为使用广播,完整的客户端例子如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 8755
#define MAXLINE 32
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define error_exit(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
void* Signal(int signo, void (*func)(int)) {
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
#ifdef SA_INTERRUPT
if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART
if (signo != SIGALRM) act.sa_flags |= SA_RESTART;
#endif
if (sigaction(signo, &act, &oact) < 0)
return SIG_ERR;
return oact.sa_handler; // 返回信号的旧行为
}
static void sig_alrm(int signo) {
return; /* just interrupt the recvfrom() */
}
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) {
int n;
const int on = 1;
char sendline[MAXLINE], recvline[MAXLINE+1], buf[MAXLINE];
socklen_t len;
struct sockaddr *preply_addr;
/* 设置套接字选项、分配服务器地址空间、安装SIGALRM信号处理函数 */
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); // POSIX规范要求发送广播数据报必须设置该选项
preply_addr = malloc(servlen);
Signal(SIGALRM, sig_alrm);
/* 发送广播数据报 */
while (fgets(sendline, MAXLINE, fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
/* 5s内接收所有广播应答
* (如果某一时刻,程序刚好成功执行完recvfrom后,此时alarm触发SIGALRM信号,会导致程序永远阻塞在recvfrom上) */
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
error_exit("recvfrom error");
} else {
recvline[n] = 0;
printf("reply from %s:%d : %s",
inet_ntop(AF_INET, &((struct sockaddr_in *)preply_addr)->sin_addr, buf, sizeof(buf)),
ntohs(((struct sockaddr_in *)preply_addr)->sin_port),
recvline);
}
}
}
free(preply_addr);
}
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
error_exit("usage: udpcli " );
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
dg_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
exit(0);
}
结果:
test@test:~$ ./udpcli 192.168.3.255
123456
reply from 192.168.1.96:8755 : 123456
reply from 192.168.1.98:8755 : 123456
abcdefg
reply from 192.168.1.96:8755 : abcdefg
reply from 192.168.1.98:8755 : abcdefg
当涉及信号处理时,往往会出现另一种类型的竞争状态。发生问题的原因在于信号会在程序执行过程中由内核随时随地递交。例如上述程序中,如果某一时刻,程序刚好成功执行完recvfrom后,此时alarm触发SIGALRM信号,会导致程序永远阻塞在recvfrom上
此种方式改进仅仅是减少了上述问题出现的概率,但仍有可能出现。
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) {
int n;
const int on = 1;
char sendline[MAXLINE], recvline[MAXLINE+1], buf[MAXLINE];
socklen_t len;
struct sockaddr *preply_addr;
sigset_t sigset_alrm;
/* 初始化信号集 */
sigemptyset(&sigset_alrm);
sigaddset(&sigset_alrm, SIGALRM);
/* 设置套接字选项、分配服务器地址空间、安装SIGALRM信号处理函数 */
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); // POSIX规范要求发送广播数据报必须设置该选项
preply_addr = malloc(servlen);
Signal(SIGALRM, sig_alrm);
/* 发送广播数据报 */
while (fgets(sendline, MAXLINE, fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
/* 5s内接收所有广播应答
* (如果某一时刻,程序刚好成功执行完recvfrom后未执行sigprocmask,此时触发SIGALRM信号,程序仍会永远阻塞在recvfrom上) */
alarm(5);
for ( ; ; ) {
len = servlen;
/* 限制SIGALRM信号只能在“解阻塞”与“阻塞”之间执行时被内核递交,但仍存在程序永远阻塞在recvfrom上的可能性 */
sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL); // 解阻塞SIGALRM信号
n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
sigprocmask(SIG_BLOCK, &sigset_alrm, NULL); // 阻塞SIGALRM信号
if (n < 0) {
if (errno == EINTR)
break; /* waited long enough for replies */
else
error_exit("recvfrom error");
} else {
recvline[n] = 0;
printf("reply from %s:%d : %s",
inet_ntop(AF_INET, &((struct sockaddr_in *)preply_addr)->sin_addr, buf, sizeof(buf)),
ntohs(((struct sockaddr_in *)preply_addr)->sin_port),
recvline);
}
}
}
free(preply_addr);
}
pselect是一个POSIX函数,在调用进程看来,pselect的操作是一个原子操作。
#include
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) {
int n;
const int on = 1;
char sendline[MAXLINE], recvline[MAXLINE+1], buf[MAXLINE];
socklen_t len;
struct sockaddr *preply_addr;
sigset_t sigset_alrm, sigset_empty;
fd_set rset;
/* 初始化描述符集和信号集 */
FD_ZERO(&rset);
sigemptyset(&sigset_empty);
sigemptyset(&sigset_alrm);
sigaddset(&sigset_alrm, SIGALRM);
/* 设置套接字选项、分配服务器地址空间、安装SIGALRM信号处理函数 */
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); // POSIX规范要求发送广播数据报必须设置该选项
preply_addr = malloc(servlen);
Signal(SIGALRM, sig_alrm);
/* 发送广播数据报 */
while (fgets(sendline, MAXLINE, fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
/* 5s内接收所有广播应答,设置当前信号掩码中SIGALRM被阻塞 */
sigprocmask(SIG_BLOCK, &sigset_alrm, NULL); // 阻塞SIGALRM信号
alarm(5);
for ( ; ; ) {
FD_SET(sockfd, &rset);
/* pselect依次执行如下三个操作:设置信号掩码、测试描述符、恢复信号掩码。
* 1.设置信号掩码 : sigset_empty是一个没有任何信号被阻塞的信号集,意味着其所有信号都是解阻塞的。
* 2.测试描述符 : 等同于select。
* 3.恢复信号掩码 : pselect返回之前把进程信号掩码恢复成刚被调用时的值。 */
n = pselect(sockfd+1, &rset, NULL, NULL, NULL, &sigset_empty);
if (n < 0) {
if (errno == EINTR)
break; /* waited long enough for replies */
else
error_exit("pselect error");
} else if (n != 1)
error_exit("pselect error");
len = servlen;
n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
recvline[n] = 0;
printf("reply from %s:%d : %s",
inet_ntop(AF_INET, &((struct sockaddr_in *)preply_addr)->sin_addr, buf, sizeof(buf)),
ntohs(((struct sockaddr_in *)preply_addr)->sin_port),
recvline);
}
}
free(preply_addr);
}
解决竞争状态问题的另一个正确办法并非利用信号处理函数中断被阻塞系统调用的能力,而是从信号处理函数中调用siglongjmp(nonlocal goto 非局部跳转)。
#include
static sigjmp_buf jmpbuf; // 跳转缓冲区
static void sig_alarm_jmp(int signo) {
siglongjmp(jmpbuf, 1); // (nonlocal goto)跳转回sigsetjmp函数,并且使得sigsetjmp返回siglongjmp的第二个参数
}
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) {
int n;
const int on = 1;
char sendline[MAXLINE], recvline[MAXLINE+1], buf[MAXLINE];
socklen_t len;
struct sockaddr *preply_addr;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
preply_addr = malloc(servlen);
Signal(SIGALRM, sig_alarm_jmp);
while (fgets(sendline, MAXLINE, fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
alarm(5);
for ( ; ; ) {
if (sigsetjmp(jmpbuf, 1) != 0) // sigsetjmp在建立跳转缓冲区后返回0
break;
len = servlen;
n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
recvline[n] = 0; /* null terminate */
printf("reply from %s:%d : %s",
inet_ntop(AF_INET, &((struct sockaddr_in *)preply_addr)->sin_addr, buf, sizeof(buf)),
ntohs(((struct sockaddr_in *)preply_addr)->sin_port),
recvline);
}
}
free(preply_addr);
}
解决竞争状态的另一个办法如下:不是让信号处理函数简单的返回并期望该返回能够中断阻塞中的recvfrom,而是让信号处理函数使用管道通知主控函数dg_cli定时器已到时。
static int pipefd[2];
static void sig_alarm_pipe(int signo) {
write(pipefd[1], "", 1); // write one null byte to pipe
return;
}
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) {
int n, maxfdp1;
const int on = 1;
char sendline[MAXLINE], recvline[MAXLINE+1], buf[MAXLINE];
fd_set rset;
socklen_t len;
struct sockaddr *preply_addr;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
preply_addr = malloc(servlen);
Signal(SIGALRM, sig_alarm_pipe);
pipe(pipefd); // 创建一个Unix管道,返回两个描述符。pipefd[0]是读入端,pipefd[1]是写出端。
FD_ZERO(&rset);
maxfdp1 = MAX(sockfd, pipefd[0]) + 1;
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
error_exit("select error");
}
/* 从套接字读 */
if (FD_ISSET(sockfd, &rset)) {
len = servlen;
n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
recvline[n] = 0; /* null terminate */
printf("reply from %s:%d : %s",
inet_ntop(AF_INET, &((struct sockaddr_in *)preply_addr)->sin_addr, buf, sizeof(buf)),
ntohs(((struct sockaddr_in *)preply_addr)->sin_port),
recvline);
}
/* 从管道读 */
if (FD_ISSET(pipefd[0], &rset)) {
read(pipefd[0], &n, 1); /* timer expired */
break;
}
}
}
free(preply_addr);
}
单播地址标识单个IP接口,广播地址标识某个子网的所有IP接口,多播地址标识一组IP接口。多播数据报只应该由对它感兴趣的接口接收。另外,广播一般局限于局域网内使用,而多播则既可用于局域网,也可跨广域网使用。大多数系统不允许对广播数据报执行分片,对于多播数据报无此限制。
IPv4的D类地址(从224.0.0.0到239.255.255.255)是IPv4多播地址。D类地址的低序28位构成多播组ID(group ID),整个32位地址则称为组地址(group address)。
IPv4首部中的TTL字段兼用作多播范围字段:
0--接口局部;
1--链路局部;
2~32--网点局部;
33~64--地区局部;
65~128--大洲局部;
129~255--全球。
IPv6多播地址显式存在一个4位的范围(scope)字段,用于指定多播数据报能够游走的范围。下面是部分已经分配给范围字段的值:
1--接口局部(interface-local);
2--链路局部(link-local);
4--管区局部(admin-local);
5--网点局部(site-local);
8--组织机构局部(organization-local);
14--全球(global)。
将上述广播示例中dg_cli
函数中的setsockopt
调用去掉即可。如果外出接口、TTL和回馈选项的默认设置为可接受,那么发送多播数据报无需设置任何多播套接字选项。指定所有主机组为服务器地址的测试如下:
test@test:~$ ./udpcli 224.0.0.1
123456789
reply from 192.168.1.96:8755 : 123456789
reply from 192.168.1.98:8755 : 123456789
abcdefgh
reply from 192.168.1.96:8755 : abcdefgh
reply from 192.168.1.98:8755 : abcdefgh
下述程序是一个既发送又接收多播数据报的程序。该程序包含两个部分:第一部分每1s发送一个目的地为指定组的多播数据报,其中含有发送进程的主机信息和进程ID;第二部分是一个无限循环,先加入由第一部分发往的多播组,再显示接收到的每个数据报。下述测试在两台主机同时启动此程序。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 32
#define error_exit(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
void recv_all(int recvfd, socklen_t salen) {
int n;
char line[MAXLINE+1], buf[MAXLINE];
socklen_t len;
struct sockaddr *safrom;
safrom = malloc(salen);
for ( ; ; ) {
len = salen;
n = recvfrom(recvfd, line, MAXLINE, 0, safrom, &len);
line[n] = 0;/* null terminate */
printf("reply from %s:%d : %s",
inet_ntop(AF_INET, &((struct sockaddr_in *)safrom)->sin_addr, buf, sizeof(buf)),
ntohs(((struct sockaddr_in *)safrom)->sin_port),
line);
}
free(safrom);
}
void send_all(int sendfd, struct sockaddr *sadest, socklen_t salen) {
char line[MAXLINE];
struct utsname myname;
if (uname(&myname) < 0) // uname获取当前内核名称和其它信息
error_exit("uname error");
snprintf(line, sizeof(line), "%s, %d\n", myname.release, getpid());
for ( ; ; ) {
sendto(sendfd, line, strlen(line), 0, sadest, salen);
sleep(1);
}
}
int udp_client(const char *host, const char *serv, struct sockaddr **saptr, socklen_t *lenp) {
int sockfd, n;
struct addrinfo hints, *res, *ressave;
/* 获取所查找主机的所有IP地址信息。
* getaddrinfo可处理以下转换:1.名字(test-pc)到地址(192.168.1.66);2.服务(http)到端口(80)。 */
bzero(&hints, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC; // AF_UNSPEC意味着函数返回的是适合任何协议族的地址
hints.ai_socktype = SOCK_DGRAM; // SOCK_DGRAM意味着只返回数据报套接字信息
if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) //
printf("udp_client error for %s, %s: %s", host, serv, gai_strerror(n));
ressave = res;
/* 尝试在上述查找到的所有IP地址上创建套接字 */
do {
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd >= 0)
break; // success
} while ((res = res->ai_next) != NULL);
if (res == NULL) { // 创建套接字失败
printf("udp_client error for %s, %s", host, serv);
return (-1);
}
/* 保存创建的套接字地址信息并释放结果空间 */
*saptr = malloc(res->ai_addrlen);
memcpy(*saptr, res->ai_addr, res->ai_addrlen);
*lenp = res->ai_addrlen;
freeaddrinfo(ressave);
return sockfd;
}
/* 由协议族确定级别(level指定系统中解释选项的代码为“通用套接字代码”或为“IPv4协议代码”或为“IPv6协议代码”等) */
int family_to_level(int family) {
switch (family) {
case AF_INET:
return IPPROTO_IP; // IPv4中MCAST_JOIN_GROUP套接字选项对应的level为IPPROTO_IP
default:
return -1;
}
}
int mcast_join(int sockfd, const struct sockaddr *grp, socklen_t grplen, const char *ifname, u_int ifindex) {
#ifdef MCAST_JOIN_GROUP //内核是否支持MCAST_JOIN_GROUP套接字选项
/* 处理索引 */
struct group_req req;
if (ifindex > 0) { // 调用者给定接口索引
req.gr_interface = ifindex;
} else if (ifname != NULL) { // 调用者给定接口名字,需使用if_nametoindex把名字转换成索引
if ((req.gr_interface = if_nametoindex(ifname)) == 0) {
errno = ENXIO; /* i/f name not found */
return(-1);
}
} else // 把接口索引置为0,告知内核去选择接口
req.gr_interface = 0;
/* 复制地址并调用setsockopt通过MCAST_JOIN_GROUP套接字选项执行加入多播组 */
if (grplen > sizeof(req.gr_group)) { // gr_group成员是一个sockaddr_storage结构
errno = EINVAL;
return -1;
}
memcpy(&req.gr_group, grp, grplen);
return (setsockopt(sockfd, family_to_level(grp->sa_family), MCAST_JOIN_GROUP, &req, sizeof(req)));
#else
/* 根据协议族处理加入多播组操作 */
switch (grp->sa_family) {
case AF_INET: { // IPv4协议族
struct ip_mreq mreq;
struct ifreq ifreq;
/* 处理索引 */
memcpy(&mreq.imr_multiaddr, // 把IPv4多播地址复制到一个ip_mreg结构中
&((const struct sockaddr_in *)grp)->sin_addr,
sizeof(struct in_addr));
if (ifindex > 0) { // 调用者给定接口索引,那就用if_indextoname把接口名字存入一个ifreg结构中
if (if_indextoname(ifindex, ifreq.ifr_name) == NULL) {
errno = ENXIO; /* i/f index not found */
return(-1);
}
goto doioctl; // 接口名字存入ifreg结构后,发出ioctl请求获取单播地址
} else if (ifname != NULL) { // 调用者给定接口名字
strncpy(ifreq.ifr_name, ifname, IFNAMSIZ);
doioctl:
if (ioctl(sockfd, SIOCGIFADDR, &ifreq) < 0) // ioctl的SIOCGIFADDR请求返回与该接口名字关联的单播地址
return(-1);
memcpy(&mreq.imr_interface, // 把IPv4单播地址复制到一个ip_mreg结构中
&((struct sockaddr_in *) &ifreq.ifr_addr)->sin_addr,
sizeof(struct in_addr));
} else // 如果接口名字和接口索引都未给定,将接口设为通配地址,告知内核去选择接口
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
/* 通过IP_ADD_MEMBERSHIP IPv4套接字选项执行加入多播组 */
return(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)));
}
default:
errno = EAFNOSUPPORT;
return(-1);
}
#endif
}
/* 获取指定套接字的协议族 */
int sockfd_to_family(int sockfd) {
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if (getsockname(sockfd, (struct sockaddr *)&ss, &len) < 0)
return(-1);
return(ss.ss_family);
}
/* 设置指定套接字的IP_MULTICAST_LOOP多播选项 */
int mcast_set_loop(int sockfd, int onoff) {
u_char flag;
switch (sockfd_to_family(sockfd)) {
case AF_INET:
flag = onoff;
return(setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &flag, sizeof(flag)));
default:
errno = EAFNOSUPPORT;
return (-1);
}
}
int main(int argc, char **argv) {
int sendfd, recvfd;
const int on = 1;
socklen_t salen;
struct sockaddr *sasend, *sarecv;
if (argc != 3)
error_exit("usage: sendrecv " );
/* 创建两个数据报套接字,一个用于发送,一个用于接收。 */
sendfd = udp_client(argv[1], argv[2], &sasend, &salen); // 支持主机名或服务名用作参数
recvfd = socket(sasend->sa_family, SOCK_DGRAM, 0);
/* 给接收套接字捆绑多播组和端口,例如239.255.1.2:8888 */
setsockopt(recvfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); // 允许重用本地地址(即使存在连接也可重新绑定端口)
sarecv = malloc(salen);
memcpy(sarecv, sasend, salen);
bind(recvfd, sarecv, salen);
/* 接收套接字加入多播组 */
mcast_join(recvfd, sasend, salen, NULL, 0);
/* 禁止外出多播数据报的本地自环(回馈)。默认开启 */
mcast_set_loop(sendfd, 0);
/* 子进程不停接收数据报 */
if (fork() == 0)
recv_all(recvfd, salen);
/* 父进程不停发送数据报 */
send_all(sendfd, sasend, salen);
free(sarecv);
exit(0);
}
主机1结果:
host1@host1:~$ ./multicast 239.255.1.2 8888
reply from 192.168.1.98:56962 : 3.19.0-79-generic, 11410
reply from 192.168.1.98:56962 : 3.19.0-79-generic, 11410
reply from 192.168.1.98:56962 : 3.19.0-79-generic, 11410
reply from 192.168.1.98:56962 : 3.19.0-79-generic, 11410
reply from 192.168.1.98:56962 : 3.19.0-79-generic, 11410
主机2结果:
host2@host2:~$ ./multicast 239.255.1.2 8888
reply from 192.168.1.96:44145 : 3.13.0-96-generic, 10386
reply from 192.168.1.96:44145 : 3.13.0-96-generic, 10386
reply from 192.168.1.96:44145 : 3.13.0-96-generic, 10386
reply from 192.168.1.96:44145 : 3.13.0-96-generic, 10386
reply from 192.168.1.96:44145 : 3.13.0-96-generic, 10386