下面接着学习套接字I/O超时设置方法以及用select实现超时——read_timeout函数封装、write_timeout函数封装、accept_timeout函数封装、connect_timeout函数封装。
1.套接字I/O超时设置方法
- alarm
- 套接字选项:SO_SNDTIMEO、SO_RCVTIMEO
- select
(1)alarm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void handler(int sig) { return 0; } signal(SIGALRM, handler); alarm(5); int ret = read(fd, buf, sizeof(buf)); if (ret == -1 && errno == EINTR) { errno = ETIMEDOUT; } else if (ret >= 0) { alarm(0); } |
1)比如有一个read读动作,在读之前先设置一个闹钟。如果超时,就会产生SIGALRM信号,将read函数打断。
2)被打断,意味超时,将errno错误码置为ETIMEDOUT。
3)否则如果读到数据,就将闹钟关闭。
4)但是闹钟容易产生冲突,所以一般不会采用这种方案
(2)套接字选项
- SO_SNDTIMEO:表示发送超时时间
- SO_RCVTIMEO:表示接收超时时间
123456setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, 5);int ret = read(sock, buf, sizeof(buf));if(ret == -1 && errno = EWOULDBLOCK){errno = ETIMEDOUT;}
1)调用setsockopt,再去读取数据的时候。如果超时时间到了,那么ret返回值-1并且错误码为EWOULDBLOCK。然后将错误码置为WTIMEDOUT。
2)这种方案也能控制超时,一般以很少采用该方案。
(3)用select实现时超
- read_timeout函数封装
- write_timeout函数封装
- accept_timeout函数封装
- connect_timeout函数封装
1)read_timeout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include "sysutil.h" /*伪代码,测试代码,如果未超时,就可以调用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 fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { fd_set read_fdset; struct timeval timeout; FD_ZERO(&read_fdset); FD_SET(fd, &read_fdset); timeout.tv_sec = wait_seconds;//时间,只关心秒,微妙tv_usec置为0 timeout.tv_usec = 0; do { ret = select(fd+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; } |
2)write_timeout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/** write_timeout-写超时检测函数,不包含写操作 fd:文件描述符 wait_seconds:等待超时秒数,如果为0表示不检测超时 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT **/ int write_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { fd_set write_fdset; struct timeval timeout; FD_ZERO(&write_fdset); FD_SET(fd, &write_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd+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; } |
3)accept_timeout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/** accept_timeout-超时的accept,包含accept操作 fd:套接字 addr:输出参数,返回对方地址 wait_seconds:等待超时秒数,如果为0表示正常模式 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT **/ int accept_timeout(int fd, 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(fd, &accept_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = selec(fd+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(fd, (struct sockaddr*)addr, &addrlen); else ret = accept(fd, NULL,NULL); if (ret == -1) ERR_EXIT("accept"); return ret; } |
4)connect_timeout
这个函数较其他三个要复杂一些:
a)首先我们不能直接connect,因为一旦调用connect就意味着阻塞了。
b)这时候我们希望以非阻塞的形式调用它,意味着我们需要将套接口fd设置为非阻塞模式。对此,我们封装了一个函数activate_nonblock。
c)接下来就可以调用connect了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
/** activate_nonblock - 设置I/O为非阻塞模式 fd:文件描述符 **/ void activate_nonblock(int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == -1) ERR_EXIT("fcntl"); flags |= O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) ERR_EXIT("fcntl"); } /** deactivate_nonblock - 设置I/O为阻塞模式 fd:文件描述符 **/ void deactivate_nonblock(int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == -1) ERR_EXIT("fcntl"); flags &= ~O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) ERR_EXIT("fcntl"); } /** connect_timeout - connect fd:套接字 addr:要连接的对方地址 wait_seconds:等待超时秒数如果为0表示正常模式 成功(未超时)返回0,失败返回-1, 超时返回-1并且errno = ETIMEDOUT **/ int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret = 0; socklen_t addrlen = sizeof(struct sockaddr_in); if (wait_seconds > 0) activate_nonblock(fd); ret = connect(fd, (struct sockaddr*)addr, addrlen); if (ret < 0 && errno == EINPROGRESS) { fd_set connect_fdset; struct timeval timeout; FD_ZERO(&connect_fdset); FD_SET(fd, &connect_fdset); timeout.tv_sec =wait_seconds; timeout.tv_usec = 0; do { //一旦连接建立,套接字就可写 ret = select(fd+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) { //ret返回1,可能有两种情况,一种是连接建立成功,一种是套接字产生错误 //此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取 int err; socklen_t socklen = sizeof(err); int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen); if (sockoptret == -1) { return -1; } if (err == 0) //表示没有错误,是第一种情况,连接建立 ret = 0; else //否则,产生了错误,将错误代码赋给errno { errno = err; ret = -1; } } } if (wait_seconds > 0) //将fd重新置为阻塞模式 { deactivate_nonblock(fd); } return ret; } |
小结:
介绍了套接字I/O超时设置的3中方法,其中用select实现的超时函数较为重要。