Socket编程实践(9) --套接字IO超时设置方法
引:超时设置3种方案
1. alarm超时设置方法
//代码实现: 这种方式较少用 void sigHandlerForSigAlrm(int signo) { return ; } signal(SIGALRM, sigHandlerForSigAlrm); alarm(5); int ret = read(sockfd, buf, sizeof(buf)); if (ret == -1 && errno == EINTR) { // 超时被时钟打断 errno = ETIMEDOUT; } else if (ret >= 0) { // 正常返回(没有超时), 则将闹钟关闭 alarm(0); }
2. 套接字选项: SO_SNDTIMEO, SO_RCVTIMEO
调用setsockopt设置读/写超时时间
//示例: read超时 int seconds = 5; if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &seconds, sizeof(seconds)) == -1) err_exit("setsockopt error"); int ret = read(sockfd, buf, sizeof(buf)); if (ret == -1 && errno == EWOULDBLOCK) { // 超时,被时钟打断 errno = ETIMEDOUT; }
3. select方式[重点]
_timeout函数封装
1. read_timeout封装
/** *read_timeout - 读超时检测函数, 不包含读操作 *@fd: 文件描述符 *@waitSec: 等待超时秒数, 0表示不检测超时 *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT **/ int read_timeout(int fd, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set readSet; FD_ZERO(&readSet); FD_SET(fd,&readSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //将微秒设置为0(不进行设置),如果设置了,时间会更加精确 do { returnValue = select(fd+1,&readSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信号)打断的情况, 重启select if (returnValue == 0) //在waitTime时间段中一个事件也没到达 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime时间段中有事件产生 returnValue = 0; //返回0,表示成功 // 如果(returnValue == -1) 并且 (errno != EINTR), 则直接返回-1(returnValue) } return returnValue; }
2. write_timeout封装
/** *write_timeout - 写超时检测函数, 不包含写操作 *@fd: 文件描述符 *@waitSec: 等待超时秒数, 0表示不检测超时 *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT **/ int write_timeout(int fd, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set writeSet; FD_ZERO(&writeSet); //清零 FD_SET(fd,&writeSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { returnValue = select(fd+1,NULL,&writeSet,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信号)打断的情况 if (returnValue == 0) //在waitTime时间段中一个事件也没到达 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime时间段中有事件产生 returnValue = 0; //返回0,表示成功 } return returnValue; }
3. accept_timeout函数封装
/** *accept_timeout - 带超时的accept *@fd: 文件描述符 *@addr: 输出参数, 返回对方地址 *@waitSec: 等待超时秒数, 0表示不使用超时检测, 使用正常模式的accept *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT **/ int accept_timeout(int fd, struct sockaddr_in *addr, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set acceptSet; FD_ZERO(&acceptSet); FD_SET(fd,&acceptSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { returnValue = select(fd+1,&acceptSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); if (returnValue == 0) //在waitTime时间段中没有事件产生 { errno = ETIMEDOUT; return -1; } else if (returnValue == -1) // error return -1; } /**select正确返回: 表示有select所等待的事件发生:对等方完成了三次握手, 客户端有新的链接建立,此时再调用accept就不会阻塞了 */ socklen_t socklen = sizeof(struct sockaddr_in); if (addr != NULL) returnValue = accept(fd,(struct sockaddr *)addr,&socklen); else returnValue = accept(fd,NULL,NULL); return returnValue; }
4. connect_timeout函数封装
(1)我们为什么需要这个函数?
TCP/IP在客户端连接服务器时,如果发生异常,connect(如果是在默认阻塞的情况下)返回的时间是RTT(相当于客户端阻塞了这么长的时间,客户需要等待这么长的时间,显然这样的客户端用户体验并不好(完成三次握手需要使用1.5RTT时间));会造成严重的软件质量下降.
(2)怎样实现connect_timeout?
1)sockfd首先变成非阻塞的; 然后试着进行connect,如果网络状况良好,则立刻建立链接并返回,如果网络状况不好,则链接不会马上建立,这时需要我们的参与:调用select,设置等待时间,通过select管理者去监控sockfd,一旦能够建立链接,则马上返回,然后建立链接,这样就会大大提高我们的软件质量.
2)需要注意:select机制监控到sockfd可写(也就是可以建立链接时),并不代表调用connect就一定能够成功(造成sockfd可写有两种情况: a.真正的链接可以建立起来了; b.建立链接的过程中发生错误,然后错误会回写错误信息,造成sockfd可写);
通过调用getsockopt做一个容错即可(见下例)!
(3)代码实现:
/**设置文件描述符fd为非阻塞/阻塞模式**/ bool setUnBlock(int fd, bool unBlock) { int flags = fcntl(fd,F_GETFL); if (flags == -1) return false; if (unBlock) flags |= O_NONBLOCK; else flags &= ~O_NONBLOCK; if (fcntl(fd,F_SETFL,flags) == -1) return false; return true; } /** *connect_timeout - connect *@fd: 文件描述符 *@addr: 要连接的对方地址 *@waitSec: 等待超时秒数, 0表示使用正常模式的accept *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT **/ int connect_timeout(int fd, struct sockaddr_in *addr, long waitSec) { if (waitSec > 0) //设置为非阻塞模式 setUnBlock(fd, true); socklen_t addrLen = sizeof(struct sockaddr_in); //首先尝试着进行链接 int returnValue = connect(fd,(struct sockaddr *)addr,addrLen); //如果首次尝试失败(并且errno == EINPROGRESS表示连接正在处理当中),则需要我们的介入 if (returnValue < 0 && errno == EINPROGRESS) { fd_set connectSet; FD_ZERO(&connectSet); FD_SET(fd,&connectSet); struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { /*一旦建立链接,则套接字可写*/ returnValue = select(fd+1, NULL, &connectSet, NULL, &waitTime); } while (returnValue < 0 && errno == EINTR); if (returnValue == -1) //error return -1; else if (returnValue == 0) //超时 { returnValue = -1; errno = ETIMEDOUT; } else if (returnValue == 1) //正确返回,有一个套接字可写 { /**由于connectSet只有一个文件描述符, 因此FD_ISSET的测试也就省了**/ /**注意:套接字可写有两种情况: 1.连接建立成功 2.套接字产生错误(但是此时select是正确的, 因此错误信息没有保存在errno中),需要调用getsockopt获取 */ int err; socklen_t errLen = sizeof(err); int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&errLen); if (sockoptret == -1) return -1; // 测试err的值 if (err == 0) //确实是链接建立成功 returnValue = 0; else //连接产生了错误 { errno = err; returnValue = -1; } } } if (waitSec > 0) setUnBlock(fd, false); return returnValue; }
/**测试:使用connect_timeout的client端完整代码(server端如前)**/ int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) err_exit("socket error"); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8001); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect_timeout(sockfd, &serverAddr, 5); if (ret == -1 && errno == ETIMEDOUT) { cerr << "timeout..." << endl; err_exit("connect_timeout error"); } else if (ret == -1) err_exit("connect_timeout error"); //获取并打印对端信息 struct sockaddr_in peerAddr; socklen_t peerLen = sizeof(peerAddr); if (getpeername(sockfd, (struct sockaddr *)&peerAddr, &peerLen) == -1) err_exit("getpeername"); cout << "Server information: " << inet_ntoa(peerAddr.sin_addr) << ", " << ntohs(peerAddr.sin_port) << endl; close(sockfd); }
附-RTT(Round-Trip Time)介绍:
RTT往返时延:在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。
RTT由三个部分决定:即链路的传播时间、末端系统的处理时间以及路由器的缓存中的排队和处理时间。其中,前面两个部分的值作为一个TCP连接相对固定,路由器的缓存中的排队和处理时间会随着整个网络拥塞程度的变化而变化。所以RTT的变化在一定程度上反映了网络拥塞程度的变化。简单来说就是发送方从发送数据开始,到收到来自接受方的确认信息所经历的时间。