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

被折叠的 条评论
为什么被折叠?



