socket编程中套接字I/O超时设置的方法

本文介绍在Socket编程中处理超时的四种方法:使用alarm函数、套接字选项SO_SNDTIMEO/SO_RCVTIMEO、select函数以及通过非阻塞方式调用connect、accept、read和write函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、使用alarm函数设置超时

void handler(int signum){
	....
	return 0;
}

signal(SIGALRM, handler);
alarm(3);
int ret = read(sockfd, buf, sizeof(buf));
if(ret == -1 && errno == EINTR)
	errno = ETIMEDOUT;
else if(ret >= 0)
	alarm(0);

比如有一个read动作,在读之前先设置一个闹钟。如果超时就会产生SIGALRM信号,将read打断。被打断,就意味着超时,将errno错误码设置为ETIMEOUT。

否则,如果读到数据,就将闹钟关闭。

但是这种方法不常用,因为有时可能其他地方使用了alarm会造成混乱。

二、使用套接字选项SO_SNDTIMEO、SO_RCVTIMEO

SO_SNDTIMEO:表示设置发送超时时间
SO_RCVTIMEO: 表示设置接收超时时间

setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, 5);
int ret = read(sockfd, buf, sizeof(buf));
if(ret == -1 && errno == EWOULDBLOCK)
	errno = ETIMEDOUT;

调用setsockopt函数,再去读取数据。如果超时了,那么返回值ret为-1,并且错误码为EWOULDBLOCK。然后将错误码设置为ETIMEOUT。

移植性比较差,所以一般也很少用。

三、用select实现超时

select在socket编程中是比较重要的,可是对于初学socket的人来说都不太喜欢用select,只是习惯写诸如connect、accept、recv或者recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或者线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回。)

可是使用select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若时间没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能监视我们需要监视的文件描述符的变化情况----读写或异常。

下面程序分别是read_timeout、write_timeout、accept_timeout、connect_timeout。

read_timeout

/*伪代码,测试代码,如果未超时,就可以调用read进行操作了
 int ret;
 ret = read_timeout(fd, 5);
 if (ret ==0)
 {
	 read(fd, ...);
 }
 else if (ret == -1 && errno == ETIMEDOUT)
 {
	 //做相应的超时处理...
 }
 else//否则,错误
 {
	 ERR_EXIT("read_timeout");
 }
 */
 
 /**
 read_timeout - 读超时检测函数,不包含读操作
 fd:文件描述符
 wait_seconds:等待超时秒数,如果为0表示不检测超时
 成功(未超时)返回0;失败返回-1,超时返回-1并且errno=ETIMEDOUT
 **/
 int read_timeout(int sockfd, unsigned int wait_seconds){
	int ret = 0;
	if(wait_seconds > 0){
		fd_set read_fdset;
		struct timeval timeout;
		FD_ZERO(&read_fdset);
		FD_SET(sockfd, &read_fdset);

		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;
		do{
			ret = select(sockfd+1, &read_fdset, NULL, NULL, &timeout);
		}while(ret < 0 && errno == EINTR);
		
		if(ret == 0){    //超时
			ret = -1;
			errno = ETIMEDOUT;
		}  else if(ret == 1)   //未超时,产生了可读事件,接下来调用read就不会阻塞了
				ret = 0;
	}
	return ret;
}

如果read_timeout(sockfd, 0);则表示不检测超时,函数直接返回0,此时再调用read将会阻塞。

当wait_seconds参数大于0,则进入if括号执行,将超时时间设置为select函数的超时时间结构体,select会阻塞,直到检测到事件发生或者超时。

如果select返回-1并且errno为EINTR,说明是被信号中断,需要重启select;如果select返回0,则表示超时;如果select返回1,则表示检测到可读事件,否则返回-1表示出错。

write_timeout

 /**
 write_timeout-写超时检测函数,不包含写操作
 sockfd:文件描述符
 wait_seconds:等待超时秒数,如果为0表示不检测超时
 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 **/
 int write_timeout(int sockfd, unsigned int wait_seconds){
	int ret = 0;
	if(wait_seconds > 0){
		fd_set write_fdset;
		struct timeval timeout;

		FD_ZERO(&write_fdset);
		FD_SET(sockfd, &write_fdset);
		
			timeout.tv_sec = wait_seconds;
			timeout.tv_usec = 0;
			do{
				ret = select(sockfd+1, NULL, &write_fdset, NULL, &timeout);
			}while(ret < 0 && errno == EINTR);
			if(ret == 0){
					ret = -1;
					errno = ETIMEDOUT;
			}else if(ret == 1)
					ret = 0;
	}
	return ret;
}

跟read_timeout()函数类似,这里便不再赘述。

accept_timeout

/**
 accept_timeout-超时的accept,包含accept操作
 sockfd:套接字
 addr:输出参数,返回对方地址
 wait_seconds:等待超时秒数,如果为0表示正常模式
 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT
 **/
 int accept_timeout(int sockfd, struct sockaddr_in *addr, unsigned int wait_seconds){
	int ret = 0;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	if(wait_seconds > 0){
		fd_set accept_fdset;
		struct timeval timeout;
		FD_ZERO(&accept_fdset);
		FD_SET(sockfd, &accept_fdset);
		timeout.tv_sec = wait_seconds;
		timeout.tv.usec = 0;
		do{
			ret = select(sockfd+1, &accept_fdset, NULL, NULL, &timeout);
		}while(ret < 0 && errno == EINTR);
		if(ret == -1)
			return -1;
		else if(ret == 0){
			errno = ETIMEDOUT;
			return -1;
		}
	}
	//如果进行到这一步,表明wait_seconds不大于0,或者未超时,accept不再阻塞
	if(addr != NULL)
		ret = accept(sockfd, (struct sockaddr*) addr, &addrlen);
	else
		ret = accept(sockfd, NULL, NULL);
	if(ret == -1)
		perror("accept");
	return ret;
}

此函数是带超时的accept函数,如果能从if(wait_seconds > 0)括号执行后向下执行,说明select返回为1,检测到已连接队列不为空,此时再调用accept,不再阻塞,当然如果wait_seconds == 0则像正常模式一样,accept阻塞等待。

connect_timeout
这个函数较其他三个要复杂一些,

首先,不能直接connect,因为一旦调用connect就意味着阻塞了。这时候,我们希望以非阻塞的形式调用它,意味着我们需要将套接口sockfd设置为非阻塞模式。对此,我们封装了一个函数activate_nonblock。

接下来就可以调用connect了。

 /**
 activate_nonblock - 设置I/O为非阻塞模式
 sockfd:文件描述符
 **/
 void activate_nonblock(int sockfd){
	int ret;
	int flags = fcntl(sockfd, F_GETFL);
	if(flags == -1)
		perror("fcntl");
	flags |= O_NONBLOCK;
	ret = fcntl(sockfd, F_SETFL, flags);
	if(ret == -1)
		perror("fcntl");
}

 /**
 deactivate_nonblock - 设置I/O为阻塞模式
 sockfd:文件描述符
 **/
 void deactivate_nonblock(int sockfd){
	int ret;
	int flags = fcntl(sockfd, F_GETFL);
	if(ret == -1)
		perror("fcntl");
	flags &= ~O_NONBLOCK;
	ret = fcntl(sockfd, F_SETFL, flags);
	if(ret == -1)
		perror("fcntl");
}

 /**
 connect_timeout - connect
 sockfd:套接字
 addr:要连接的对方地址
 wait_seconds:等待超时秒数如果为0表示正常模式
 成功(未超时)返回0,失败返回-1, 超时返回-1并且errno = ETIMEDOUT
 **/
 int connect_timeout(int sockfd, struct sockaddr_in *addr, unsigned int wait_seconds){
	int ret = 0;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	
	if(wait_seconds > 0)
		activate_nonblock(sockfd);
	
	ret = connect(sockfd, (struct sockaddr*) addr, addrlen);
	if(ret < 0 && errno == EINPROGRESS){
		fd_set connect_fdset;
		struct timeval timeout;
		FD_ZERO(&connect_fdset);
		FD_SET(sockfd, &connect_fdset);
		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;

		do{
			//一旦建立连接,套接字就可写
			ret = select(sockfd+1, NULL, &connect_fdset, NULL, &timeout);
		}while(ret < 0 && errno == EINTR);

		if(ret == 0){
			ret = -1;
			errno = ETIMEDOUT;
		}else if(ret < 0)
			return -1;
		else if(ret == 1){
			//select返回1,可能有两种情况:
			//一种是连接建立成功,另一种是套接字产生错误
			//此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取
			int err;
			socklen_t socklen = sizeof(err);
			int sockoptret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &socklen);
			if(sockoptret == -1)
				return -1;
			if(err == 0)   //表示没有错误,是第一种情况,连接建立
				return 0;
			else{
				errno = err;
				ret = -1;
			}
		}
	}
	if(wait_seconds > 0)    //将sockfd重新设置阻塞模式
		deactivate_nonblock(sockfd);
	return ret;
}

在调用connect前需要使用fcntl函数将套接字标志设置为非阻塞,如果网络环境很好,则connect立即返回0,不进入if大括号执行;如果网络环境拥塞,则connect返回-1,且errno = EINPROGRESS,表示正在处理。

此后调用select与前面3个函数类似,但这里关注的是可写事件,因为一旦连接建立,套接字就可写。

connect 只是完成发送 syn 的过程,后续的两次握手由协议栈完成。如果sockfd 是阻塞的,则 connect 会一直等到超时或者连接成功返回;如果 sockfd 是非阻塞的,则 connect 会立刻返回,但此时协议栈是否已经完成连接要判断下返回值和 errno;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值