一、网络超时检测
在网络通信过程中,经常会出现不可预知的各种情况。例如网络线路突发故障、通信一方异常结束等。一旦出现上述情况,很可能长时间都不会收到数据,而且无法判断是没有数据还是数据无法到达。如果使用的是TCP协议,可以检测出来;但如果使用UDP协议的话,需要在程序中进行相关检测。所以,为避免进程在没有数据时无限制的阻塞,使用网络超时检测很有必要。
1、套接字接收超时检测
这里先介绍设置套接字选项的函数 setsockopt() 函数:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
函数原型 | int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen ); |
函数参数 | sockfd:套接字描述符 level:选项所属协议层 optval:保存选项值的缓冲区 optlen:选项值的长度 |
函数返回值 | 成功:0 出错:-1,并设置 errno |
下面是套接字常用选项及其说明:
LEVEL:SOL_SOCKET
选项名称 | 说明 | 数据类型 |
SO_BROADCAST | 允许发送广播数据 | int |
SO_DEBUG | 允许调试 | int |
SO_DONTRUOTE | 不查找路由 | int |
SO_ERROR | 获得套接字错误 | int |
SO_KEEPALIVE | 保持连接 | int |
SO_LINGER | 延迟关闭连接 | struct linger |
SO_OOBINLINE | 带外数据放入正常数据流 | int |
SO_RCVBUF | 接收缓冲区大小 | int |
SO_SNDBUF | 发送缓冲区大小 | int |
SO_RCVTIMEO | 接收超时 | struct timeval |
SO_SNDTIMEO | 发送超时 | struct timeval |
SO_REUSERADDR | 允许重用本地地址和端口 | int |
SO_TYPE | 获得套接字类型 | int |
下面利用SO_RCVTIMEO的选项实现套接字的接收超时检测:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define N 64
- #define PORT 8888
- int main()
- {
- int sockfd;
- char buf[N];
- struct sockaddr_in seraddr;
- struct timeval t = {6, 0};
- if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("sockfd:%d\n",sockfd);
- }
- memset(&seraddr, 0, sizeof(seraddr));
- seraddr.sin_family = AF_INET;
- seraddr.sin_port = htons(PORT);
- seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
- if(bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1)
- {
- perror("bind error");
- exit(-1);
- }
- else
- {
- printf("bind successfully!\n");
- printf("PORT:%d\n",PORT);
- }
- if(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(t)) < 0)
- {
- perror("setsockopt error");
- exit(-1);
- }
- if(recvfrom(sockfd, buf, N, 0, NULL, NULL) < 0)
- {
- perror("fail to recvfrom");
- exit(-1);
- }
- else
- {
- printf("recv data: %s\n",buf);
- }
- return 0;
- }
执行结果如下:
- fs@ubuntu:~/qiang/socket/time$ ./setsockopt
- socket successfully!
- sockfd:3
- bind successfully!
- PORT:8888
- fail to recvfrom: Resource temporarily unavailable
- fs@ubuntu:~/qiang/socket/time$
注意:套接字一旦设置了超时之后,每一次发送或接收时都会检测,如果要取消超时检测,重新用setsockopt函数设置即可(把时间值指定为 0)。
2、定时器超时检测
这里利用定时器信号SIGALARM,可以在程序中创建一个闹钟。当到达目标时间后,指定的信号处理函数被执行。这样同样可以利用SIGALARM信号实现检测,下面分别介绍相关数据类型和函数。
struct sigaction 是 Linux 中用来描述信号行为的结构体类型,其定义如下:
- struct sigaction
- {
- void (*sa_handler) (int);
- void (*sa_sigaction)(int, siginfo_t *, void *);
- sigset_t sa_mask;
- int sa_flags;
- void (*sa_restorer) (void);
- }
① sa_handler:此参数和signal()的参数handler相同,此参数主要用来对信号旧的安装函数signal()处理形式的支持;
② sa_sigaction:新的信号安装机制,处理函数被调用的时候,不但可以得到信号编号,而且可以获悉被调用的原因以及产生问题的上下文的相关信息。
③ sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置;
④ sa_restorer: 此参数没有使用;
⑤ sa_flags:用来设置信号处理的其他相关操作,下列的数值可用。可用OR 运算(|)组合:
ŸA_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来
SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction。
所需头文件 | #include <signal.h> |
函数原型 | int sigaction(int signum, const struct sigaction *act , struct sigaction *oldact ); |
函数传入值 | signum:可以指定SIGKILL和SIGSTOP以外的所有信号 act :act 是一个结构体,里面包含信号处理函数的地址、 处理方式等信息; oldact :参数oldact 是一个传出参数,sigaction 函数调用成功后, oldact 里面包含以前对 signum 信号的处理方式的信息; |
函数返回值 | 成功:0 出错:-1 |
使用定时器信号检测超时的示例代码如下:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define N 64
- #define PORT 8888
- void handler(int signo)
- {
- printf("interrupted by SIGALRM\n");
- }
- int main()
- {
- int sockfd;
- char buf[N];
- struct sockaddr_in seraddr;
- struct sigaction act;
- if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("sockfd:%d\n",sockfd);
- }
- memset(&seraddr, 0, sizeof(seraddr));
- seraddr.sin_family = AF_INET;
- seraddr.sin_port = htons(PORT);
- seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
- if(bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1)
- {
- perror("bind error");
- exit(-1);
- }
- else
- {
- printf("bind successfully!\n");
- printf("PORT:%d\n",PORT);
- }
- sigaction(SIGALRM, NULL, &act);
- act.sa_handler = handler;
- act.sa_flags &= ~SA_RESTART;
- sigaction(SIGALRM, &act, NULL);
- alarm(6);
- if(recvfrom(sockfd, buf, N, 0, NULL, NULL) < 0)
- {
- perror("fail to recvfrom");
- exit(-1);
- }
- printf("recv data: %s\n",buf);
- alarm(0);
- return 0;
- }
- fs@ubuntu:~/qiang/socket/time$ ./alarm
- socket successfully!
- sockfd:3
- bind successfully!
- PORT:8888
- interrupted by SIGALRM
- fail to recvfrom: Interrupted system call
- fs@ubuntu:~/qiang/socket/time$
前面的网络通信中,采用的都是单播(唯一的发送方和接收方)的方式。很多时候,需要把数据同时发送给局域网中的所有主机。例如,通过广播ARP包获取目标主机的MAC地址。
1、广播地址
IP地址用来标识网络中的一台主机。IPv4 协议用一个 32 位的无符号数表示网络地址,包括网络号和主机号。子网掩码表示 IP 地址中网络和占几个字节。对于一个 C类地址来说,子网掩码为 255.255.255.0。
每个网段都有其对应的广播地址。以 C 类地址网段 192.168.1.x为例,其中最小的地址 192.168.1.0 代表该网段;而最大的地址192.168.1.255 则是该网段中的广播地址。当我们向这个地址发送数据包时,该网段中所以的主机都会接收并处理。
注意:发送广播包时,目标IP 为广播地址而目标 MAC 是 ff:ff:ff:ff:ff。
2、广播包的发送和接收
广播包的发送和接收通过UDP套接字实现。
1)广播包发送流程如下:
创建udp 套接字
指定目标地址和端口
设置套接字选项允许发送广播包
发送数据包
发送广播包的示例如下:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define N 64
- #define PORT 8888
- int main()
- {
- int sockfd;
- int on = 1;
- char buf[N] = "This is a broadcast package!";
- struct sockaddr_in dstaddr;
- if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("sockfd:%d\n",sockfd);
- }
- memset(&dstaddr, 0, sizeof(dstaddr));
- dstaddr.sin_family = AF_INET;
- dstaddr.sin_port = htons(PORT);
- dstaddr.sin_addr.s_addr = inet_addr("192.168.1.255"); // 192.168.1.x 网段的广播地址
- if(setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) < 0) //套接字默认不允许发送广播包,通过修改 SO_BROADCAST 选项使能
- {
- perror("setsockopt error");
- exit(-1);
- }
- while(1)
- {
- sendto(sockfd, buf, N, 0,(struct sockaddr *)&dstaddr, sizeof(dstaddr));
- sleep(1);
- }
- return 0;
- }
2)、广播包接收流程
广播包接收流程如下:
创建UDP套接字
绑定地址
接收数据包
接收包示例如下:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define N 64
- #define PORT 8888
- int main()
- {
- int sockfd;
- char buf[N];
- struct sockaddr_in seraddr;
- socklen_t peerlen = sizeof(seraddr);
- if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("sockfd:%d\n",sockfd);
- }
- memset(&seraddr, 0, sizeof(seraddr));
- seraddr.sin_family = AF_INET;
- seraddr.sin_port = htons(PORT);
- seraddr.sin_addr.s_addr = inet_addr("192.168.1.255"); //接收方绑定广播地址
- if(bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1)
- {
- perror("bind error");
- exit(-1);
- }
- else
- {
- printf("bind successfully!\n");
- printf("PORT:%d\n",PORT);
- }
- while(1)
- {
- if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&seraddr, &peerlen) < 0)
- {
- perror("fail to recvfrom");
- exit(-1);
- }
- else
- {
- printf("[%s:%d]",inet_ntoa(seraddr.sin_addr),ntohs(seraddr.sin_port));
- printf("%s\n",buf);
- }
- }
- return 0;
- }
执行结果如下
- fs@ubuntu:~/qiang/socket/guangbo$ ./guangbore
- socket successfully!
- sockfd:3
- bind successfully!
- PORT:8888
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- [127.0.0.1:56195]This is a broadcast package!
- ...
3、组播
通过广播可以很方便地实现发送数据包给局域网中的所有主机。但广播同样存在一些问题,例如,频繁地发送广播包造成所以主机数据链路层都会接收并交给上层 协议处理,也容易引起局域网的网络风暴。
下面介绍一种数据包发送方式成为组播或多播。组播可以看成是单播和广播的这种。当发送组播数据包时,至于加入指定多播组的主机数据链路层才会处理,其他主机在数据链路层会直接丢掉收到的数据包。换句话说,我们可以通过组播的方式和指定的若干主机通信。
1、组播地址
IPv4 地址分为以下5类。
A类地址:最高位为0,主机号占24位,地址范围从 1.0.0.1到 126.255.255.254。
B类地址:最高两位为10,主机号占16位,地址范围从 128.0.0.1 到 191.254.255.254。
C类地址:最高3位为110,主机号占8位,地址范围从 192.0.1.1 到 223.255.254.254。
D类地址:最高4位为1110,地址范围从192.0.1.1到 223.255.254.254。
E类地址保留。
其中D类地址呗成为组播地址。每一个组播地址代表一个多播组。
2、组播包的发送和接收
组播包的发送和接收也通过UDP套接字实现。
1))组播发送流程如下:
创建UDP套接字
指定目标地址和端口
发送数据包
程序中,紧接着bind有一个setsockopt操作,它的作用是将socket加入一个组播组,因为socket要接收组播地址224.0.0.1的数据,它就必须加入该组播组。
结构体struct ip_mreq mreq是该操作的参数,下面是其定义:
- struct ip_mreq
- {
- struct in_addr imr_multiaddr; // 组播组的IP地址。
- struct in_addr imr_interface; // 本地某一网络设备接口的IP地址。
- };
发送组播包的示例代码如下:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define N 64
- #define PORT 8888
- int main()
- {
- int sockfd;
- char buf[N] = "This is a multicast package!";
- struct sockaddr_in dstaddr;
- if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("sockfd:%d\n",sockfd);
- }
- memset(&dstaddr, 0, sizeof(dstaddr));
- dstaddr.sin_family = AF_INET;
- dstaddr.sin_port = htons(PORT);
- dstaddr.sin_addr.s_addr = inet_addr("224.10.10.1"); //绑定组播地址
- while(1)
- {
- sendto(sockfd, buf, N, 0,(struct sockaddr *)&dstaddr, sizeof(dstaddr));
- sleep(1);
- }
- return 0;
- }
2)组播包接收流程
组播包接收流程如下
创建UDP套接字
加入多播组
绑定地址和端口
接收数据包
组播包接收流程如下:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define N 64
- #define PORT 8888
- int main()
- {
- int sockfd;
- char buf[N];
- struct ip_mreq mreq;
- struct sockaddr_in seraddr,myaddr;
- socklen_t peerlen = sizeof(seraddr);
- if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("sockfd:%d\n",sockfd);
- }
- memset(&mreq, 0, sizeof(mreq));
- mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.1"); //加入多播组,允许数据链路层处理指定组播包
- mreq.imr_interface.s_addr = htonl(INADDR_ANY);
- if(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
- {
- perror("fail to setsockopt");
- exit(-1);
- }
- memset(&seraddr, 0, sizeof(myaddr));//为套接字绑定组播地址和端口
- myaddr.sin_family = AF_INET;
- myaddr.sin_port = htons(PORT);
- myaddr.sin_addr.s_addr = inet_addr("224.10.10.1");
- if(bind(sockfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1)
- {
- perror("bind error");
- exit(-1);
- }
- else
- {
- printf("bind successfully!\n");
- printf("PORT:%d\n",PORT);
- }
- while(1)
- {
- if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&seraddr, &peerlen) < 0)
- {
- perror("fail to recvfrom");
- exit(-1);
- }
- else
- {
- printf("[%s:%d]",inet_ntoa(seraddr.sin_addr),ntohs(seraddr.sin_port));
- printf("%s\n",buf);
- }
- }
- return 0;
- }
执行结果如下:
- fs@ubuntu:~/qiang/socket/zubo$ ./zubore
- socket successfully!
- sockfd:3
- bind successfully!
- PORT:8888
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- [192.168.1.2:53259]This is a multicast package!
- ....