Linux学习(二十五):网络超时检测

1、介绍

在网络通信中,很多操作会使得进程阻塞,TCP套接字中的recv/accept/connect,UDP套接字中的recvfrom。
超时检测的必要性:
1、避免进程在没有数据时无限制地阻塞
2、当设定的时间到时,进程从原操作返回继续运行

       网络超时检测的实质是:设定一定的时间,在时间到达之前,阻塞等待数据的到来,如果时间到时还没有数据,则变成非阻塞状态
当客户端连接服务器之后,服务器会开辟一块空间与客户端进行通信,如果在一定时间之内客户端没有雨服务器进行通信,则会浪费服务器空间,所以使用超时检测断开客户端的连接,减少服务器的资源的浪费。

2、使用setsocketopt实现超时检测


        这个函数功能还有很多,我们这一节只介绍和超时检测相关的,在后面的广播和组播中还会用到。设置超时时间时的optval的类型为
struct timeval
{
__time_t tv_sec;        秒
__suseconds_t tv_usec;  微秒
};

例程:
服务器设定5s接收超时时间,注意,设置后,网络相关的阻塞函数都有效accept和recv函数都会有5s的检测超时机制(accpet和recv其实都是一种,都是在接收数据)
#include <stdio.h>  //printf
#include <arpa/inet.h>  //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h>  //socket bind listen accept connect
#include <netinet/in.h>  //sockaddr_in
#include <stdlib.h>  //exit
#include <unistd.h>  //close
#include <string.h>
#include <errno.h>

#define N 128
#define errlog(errmsg) do{\
							perror(errmsg);\
							printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
						 }while(0)

int main(int argc, const char *argv[])
{
	int sockfd, acceptfd;
	struct sockaddr_in serveraddr, clientaddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};
	ssize_t bytes;

	if(argc < 3)
	{
		printf("您输入的参数太少了: %s <ip> <port>\n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));

	//第三步:将套接字域网络信息结构体绑定
	if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
	{
		errlog("fail to bind");
	}

	//第四步:将套接字设置为监听状态
	if(listen(sockfd, 5) < 0)
	{
		errlog("fail to listen");
	}

	//使用setsockopt实现网络超时检测
	//注意:使用setscokopt设置的超时时间,设置一次,永久有效,并且与网络相关的阻塞函数都有效
	
	struct timeval time_out;
	time_out.tv_sec = 5;
	time_out.tv_usec = 0;

	if(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &time_out, sizeof(time_out)) < 0)
	{
		errlog("fail to setsockopt");
	}

	while(1)
	{
		//第五步:阻塞等待客户端的连接请求
		if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
		{
			//printf("errno = %d\n", errno);
			if(errno == 11)
			{
				printf("accept timeout ...\n");
			}
			else 
			{
				errlog("fail to accept");
			}
		}
		else
		{
			//打印客户端的ip地址、端口号
			printf("%s --- %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

			while(1)
			{
				if((bytes = recv(acceptfd, buf, N, 0)) < 0)
				{
					if(errno == 11)
					{
						printf("recv timeout, client must be quited!!!\n");
						strcpy(buf, "TIMEO");
						send(acceptfd, buf, N, 0);

						close(acceptfd);
						break;
					}
					else
					{
						errlog("fail to recv");
					}
				}
				else if(bytes == 0)
				{
					printf("NO DATA\n");
					exit(1);
				}
				else 
				{
					if(strncmp(buf, "quit", 4) == 0)
					{
						printf("client is quited ...\n");
						break;
					}
					else
					{
						printf("client : %s\n", buf);

						strcat(buf, " *_*");

						if(send(acceptfd, buf, N, 0) < 0)
						{
							errlog("fail to send");
						}
					}
				}
			}
		}
	}

	close(acceptfd);
	close(sockfd);
	
	return 0;
}

客户端
#include <stdio.h>  //printf
#include <arpa/inet.h>  //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h>  //socket bind listen accept connect
#include <netinet/in.h>  //sockaddr_in
#include <stdlib.h>  //exit
#include <unistd.h>  //close
#include <string.h>

#define N 128
#define errlog(errmsg) do{\
							perror(errmsg);\
							printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
						 }while(0)

int main(int argc, const char *argv[])
{
	int sockfd;
	struct sockaddr_in serveraddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};

	if(argc < 3)
	{
		printf("您输入的参数太少了: %s <ip> <port>\n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));



	//第三步:发送客户端的连接请求
	if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
	{
		errlog("fail to connect");
	}

	while(1)
	{
		fgets(buf, N, stdin);
		buf[strlen(buf) - 1] = '\0';

		if(send(sockfd, buf, N, 0) < 0)
		{
			errlog("fail to send");
		}

		if(strncmp(buf, "quit", 4) == 0)
		{
			printf("client quit ...\n");
			break;
		}
		else
		{
			if(recv(sockfd, buf, N, 0) < 0)
			{
				errlog("fail to recv");
			}

			printf("server : %s\n", buf);
		}
	}

	close(sockfd);
	
	return 0;
}
执行结果:
       服务器:


客户端

服务器在等待连接,客户端过一段时间再连接,可以看到服务器打印出超时信息,客户端连接后,不及时发送数据,服务器也会提示接收超时,客户端退出后,服务器再次进入accept超时检测。

2、select实现超时检测

        Linux学习(二十三):IO模型中我们学习了IO多路复用功能select函数,select函数中的最后一个参数就是设置超时时间,可以利用select函数实现网络超时检测。
我们用上一节Linux学习(二十四):服务器模型select函数章节的服务器代码稍作修改即可。
服务器:
#include <stdio.h>  //printf
#include <arpa/inet.h>  //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h>  //socket bind listen accept connect
#include <netinet/in.h>  //sockaddr_in
#include <stdlib.h>  //exit
#include <unistd.h>  //close
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>

#define N 128
#define errlog(errmsg) do{\
							perror(errmsg);\
							printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
						 }while(0)

int main(int argc, const char *argv[])
{
	int sockfd, acceptfd;
	struct sockaddr_in serveraddr, clientaddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};
	ssize_t bytes;

	if(argc < 3)
	{
		printf("您输入的参数太少了: %s <ip> <port>\n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	printf("scokfd = %d\n", sockfd);

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));

	//第三步:将套接字域网络信息结构体绑定
	if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
	{
		errlog("fail to bind");
	}

	//第四步:将套接字设置为监听状态
	if(listen(sockfd, 5) < 0)
	{
		errlog("fail to listen");
	}

	//使用select函数实现网络超时检测
	//注意:select函数返回后,会移除除当前文件描述符以外其他所有的
	
	//注意:使用select函数设置的超时时间,设置一次,只有效一次,所以需要每次都设置
	//      由于只考虑select函数的阻塞,所以没办法区分到底是哪个io操作引起的超时,一般
	//      用于一个io操作的select使用
	fd_set readfds, tempfds;
	int maxfd, i, ret;
	struct timeval time_out;

	//第一步:清空集合
	FD_ZERO(&readfds);

	//第二步:将需要的文件描述符添加到集合里面
	FD_SET(sockfd, &readfds);

	maxfd = sockfd;

	while(1)
	{	
		time_out.tv_sec = 5;
		time_out.tv_usec = 0;

		tempfds = readfds;

		//第三步:调用函数阻塞等待文件描述符准备就绪
		if((ret = select(maxfd + 1, &tempfds, NULL, NULL, &time_out)) < 0)
		{
			errlog("fail to select");
		}
		else if(ret == 0)
		{
			printf("timeout......\n");
		}
		else 
		{
			//由于返回之后集合里面只剩下一个文件描述符,所以判断是哪一个,执行相应的操作
			for(i = 0; i < maxfd + 1; i++)
			{
				if(FD_ISSET(i, &tempfds))
				{
					if(i == sockfd)
					{
						//第五步:阻塞等待客户端的连接请求
						if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
						{
							errlog("fail to accept");
						}

						printf("acceptfd = %d\n", acceptfd);
						//打印客户端的ip地址、端口号
						printf("%s --- %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

						//将需要的文件描述符添加到集合里面
						FD_SET(acceptfd, &readfds);

						//确定最大的文件描述符
						maxfd = maxfd > acceptfd ? maxfd : acceptfd;
					}
					else
					{
						if((bytes = recv(i, buf, N, 0)) < 0)
						{
							errlog("fail to recv");
						}
						else if(bytes == 0)
						{
							printf("NO DATA\n");

							FD_CLR(i, &readfds);
							close(i);

							break;

						}
						else 
						{
							if(strncmp(buf, "quit", 4) == 0)
							{
								printf("client is quited ...\n");

								FD_CLR(i, &readfds);
								close(i);

								break;
							}
							else
							{
								printf("client : %s\n", buf);

								strcat(buf, " *_*");

								if(send(i, buf, N, 0) < 0)
								{
									errlog("fail to send");
								}
							}
						}
					}
				}
			}
		}
	}

	close(acceptfd);
	close(sockfd);

	return 0;
}
       客户端都是一样的,这里还要注意一点,select函数返回之后,timeout的值也被清零了,要重新设置上,所以要将time_out.tv_sec = 5;time_out.tv_usec = 0;写入到while(1)当中

3、利用信号实现超时检测

       在学习进程通信时,我们学了一个唯一的异步通信方式--信号,我们可以使用alarm闹钟信号的方式实现超时检测。要注意以下几点:
        1、闹钟信号响的时候,默认会退出进程,我们使用signal函数改变闹钟信号的执行结果,输出打印超时信息即可。
2、信号响应时会中断当前的系统调用,信号函数执行完毕后,默认会继续执行之前的系统调用,这是Linux的自重启属性。例如accept函数被alarm信号打断,执行响应handler完毕后,仍会回到accetp继续一直等待,无法实现重复超时检测,需要取消自重启属性,然后通过错误标识号errno来实现超时检测。这里要用到sigaction函数

参数中struct sigaction如下:
 struct sigaction {
void     (*sa_handler)(int); 信号处理函数
void     (*sa_sigaction)(int, siginfo_t *, void *); 信号处理函数,sa_flag为SA_SIGINFO时使用
sigset_t   sa_mask; 掩码(关于阻塞)
int        sa_flags; 标志位( SA_RESTART 自重启属性
void     (*sa_restorer)(void); 没有用
};
一般设置第一个参数sa_handler和标志位参数sa_flags,注意标志位都是用位来表征的,所以我们要用与或来操作。
服务器:

#include <stdio.h>  //printf
#include <arpa/inet.h>  //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h>  //socket bind listen accept connect
#include <netinet/in.h>  //sockaddr_in
#include <stdlib.h>  //exit
#include <unistd.h>  //close
#include <string.h>
#include <errno.h>
#include <signal.h>

#define N 128
#define errlog(errmsg) do{\
							perror(errmsg);\
							printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
						 }while(0)

void handler(int sig)
{
	//printf("timeout.......\n");
}

int main(int argc, const char *argv[])
{
	int sockfd, acceptfd;
	struct sockaddr_in serveraddr, clientaddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};
	ssize_t bytes;

	if(argc < 3)
	{
		printf("您输入的参数太少了: %s <ip> <port>\n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));

	//第三步:将套接字域网络信息结构体绑定
	if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
	{
		errlog("fail to bind");
 	}

	//第四步:将套接字设置为监听状态
	if(listen(sockfd, 5) < 0)
	{
		errlog("fail to listen");
	}

	//使用alarm闹钟实现网络超时检测
	
	//第一步:获取旧的行为
	struct sigaction act;
	if(sigaction(SIGALRM, NULL, &act) < 0)
	{
		perror("fail to sigaction");
		return -1;
	}

	//第二步:修改行为
	act.sa_handler = handler;
	act.sa_flags = act.sa_flags & (~SA_RESTART);
	//act.sa_flags |= SA_RESTART;

	//第三步:将新的行为写回去
	if(sigaction(SIGALRM, &act, NULL) < 0)
	{
		perror("fail to sigaction");
		return -1;
	}

	while(1)
	{
		alarm(5);

		//第五步:阻塞等待客户端的连接请求
		if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
		{
			//printf("errno = %d\n", errno);
			if(errno == 4)
			{
				printf("accept timeout ...\n");
			}
			else 
			{
				errlog("fail to accept");
			}
		}
		else
		{
			//打印客户端的ip地址、端口号
			printf("%s --- %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

			while(1)
			{
				alarm(5);

				if((bytes = recv(acceptfd, buf, N, 0)) < 0)
				{
					if(errno == 4)
					{
						printf("recv timeout, client must be quited!!!\n");
						strcpy(buf, "TIMEO");
						send(acceptfd, buf, N, 0);

						close(acceptfd);
						break;
					}
					else
					{
						errlog("fail to recv");
					}
				}
				else if(bytes == 0)
				{
					printf("NO DATA\n");
					exit(1);
				}
				else 
				{
					if(strncmp(buf, "quit", 4) == 0)
					{
						printf("client is quited ...\n");
						break;
					}
					else
					{
						printf("client : %s\n", buf);

						strcat(buf, " *_*");

						if(send(acceptfd, buf, N, 0) < 0)
						{
							errlog("fail to send");
						}
					}
				}
			}
		}
	}

	close(acceptfd);
	close(sockfd);
	
	return 0;
}

客户端:
#include <stdio.h>  //printf
#include <arpa/inet.h>  //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h>  //socket bind listen accept connect
#include <netinet/in.h>  //sockaddr_in
#include <stdlib.h>  //exit
#include <unistd.h>  //close
#include <string.h>

#define N 128
#define errlog(errmsg) do{\
							perror(errmsg);\
							printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
						 }while(0)

int main(int argc, const char *argv[])
{
	int sockfd;
	struct sockaddr_in serveraddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};

	if(argc < 3)
	{
		printf("您输入的参数太少了: %s <ip> <port>\n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));

#if 0
	//客户端也可以自己指定自己的信息
	struct sockaddr_in clientaddr;
	clientaddr.sin_family = AF_INET;
	clientaddr.sin_addr.s_addr = inet_addr(argv[3]);
	clientaddr.sin_port = htons(atoi(argv[4]));

	if(bind(sockfd, (struct sockaddr *)&clientaddr, addrlen) < 0)
	{
		errlog("fail to bind");
	}
#endif

	//第三步:发送客户端的连接请求
	if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
	{
		errlog("fail to connect");
	}

	while(1)
	{
		fgets(buf, N, stdin);
		buf[strlen(buf) - 1] = '\0';

		if(send(sockfd, buf, N, 0) < 0)
		{
			errlog("fail to send");
		}

		if(strncmp(buf, "quit", 4) == 0)
		{
			printf("client quit ...\n");
			break;
		}
		else
		{
			if(recv(sockfd, buf, N, 0) < 0)
			{
				errlog("fail to recv");
			}

			if(strncmp(buf, "TIMEO", 5) == 0)
			{
				printf("timeout, quit...\n");

				break;
			}

			printf("server : %s\n", buf);
		}
	}

	close(sockfd);
	
	return 0;
}

服务器在接收超时后向客户端发送超时信息,客户端再次输入数据时将会被退出,表明已经断开连接,需要从新连接
执行结果,服务器:


客户端:








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值