网络编程之非阻塞connect编写
一、connect非阻塞编写
TCP连接的建立涉及到一个三次握手的过程,且socket中connect函数需要一直等到客户接收到对于自己的SYN的ACK为止才返回,
这意味着每 个connect函数总会阻塞其调用进程至少一个到服务器的RTT时间,而RTT波动范围很大,从局域网的几个毫秒到几百个毫秒甚至广域网上的几秒。
这段 时间内,我们可以执行其他处理工作,以便做到并行。在此,需要用到非阻塞connect。
将一个阻塞connect变为非阻塞大概有如下几步:
第一步:将套接字设置为非阻塞模式
设置套接字可以用ioctl()或者fcntl()
(1)用ioctl设置
1 /*创建套接字*/ 2 int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 3 //................. 4 /*step 1: 将套接字设置为非阻塞*/ 5 flags = 1; 6 ioctl(sockfd, FIONBIO, &flags);
(2)用fcntl函数设置
1 #include <unistd.h> 2 #include <fcntl.h> 3 4 int fcntl(int fd, int cmd, ... /* arg */ ); 5 6 //int fcntl(int fd, int cmd, long arg);
参数fd
参数cmd
返回值
fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列四个命令有特定返回值:
F_DUPFD、F_GETFD、F_GETFL、F_GETOWN.
第一个返回新的文件描述符,接下来的两个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
1 /*创建套接字*/ 2 int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 3 //................. 4 /*获取flags标志*/ 5 int flags; 6 flags = fcntl(sockfd, F_GETFL, 0); 7 /*设置套接字为非阻塞*/ 8 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
第二步:调用connect连接判断返回值
1 ret = connect(sockfd, addr, addrlen); 2 if (ret == 0) { 3 return TURE; 4 } 5 if (ret < 0 && errno == EINPROGRESS) { 6 7 /*如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立,建立启动但是尚未完成,*/ 8 } 9 else 10 // connect error
实现非阻塞 connect ,首先把 sockfd 设置成非阻塞的。这样调用 connect 可以立刻返回,根据返回值和 errno 处理三种情况
(1) 如果返回 0,表示 connect 成功。
(2) 如果返回值小于 0, errno 为 EINPROGRESS, 表示连接建立已经启动但是尚未完成。这是期望的结果,不是真正的错误
(3) 如果返回值小于0,errno 不是 EINPROGRESS,则连接出错了。
建立connect连接,此时socket设置为非阻塞,connect调用后,无论连接是否建立立即返回-1,同时将errno(包含 errno.h就可以直接使用)设置为EINPROGRESS, 表示此时tcp三次握手仍旧进行,如果errno不是EINPROGRESS,则说明连接错误,程序结束。
当客户端和服务器端在同一台主机上的时候,connect回马上结束,并返回0;无需等待,所以使用goto函数跳过select等待函数,直接进入连接后的处理部分
第三步:将套接字添加到select集合中,判断是否可读可写
ret = connect(sockfd, addr, addrlen); if (ret < 0 && errno == EINPROGRESS) { /*如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立,建立启动但是尚未完成;*/ FD_ZERO(&connectfd); FD_SET(0, &connectfd); FD_SET(sockfd, &connectfd);//将套接字加入到集合中 /*设置超时时间*/ timeout.tv_sec = 2; timeout.tv_usec = 0; if (select(sockfd+1, NULL, &connectfd, NULL, &timeout) > 0) { len = sizeof(uint32); /*获取error值,并进行判断*/ getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len); if (err == 0) { ret = TURE; } else ret = ERR; } else ret = ERR; }
设置等待时间,使用select函数等待正在后台连接的connect函数,这里需要说明的是使用select监听socket描述符是否可读或者可写,如果只可写,说明连接成功,可以进行下面的操作。如果描述符既可读又可写,分为两种情况,第一种情况是socket连接出现错误,第二种情况是connect连接成 功,socket读缓冲区得到了远程主机发送的数据。需要通过connect连接后返回给errno的值来进行判定,或者通过调用 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len); 函数返回值来判断是否发生错误.当网络发生错误的时候,getsockopt返回-1,return -1,程序结束。网络正常时候返回0,程序继续执行
第四步:恢复套接字描述符状态为非阻塞,并返回
1 (1) 2 /* 恢复套接字非阻塞*/ 3 flags = 0; 4 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); 5 6 (2) 7 /* 恢复套接字阻塞*/ 8 flags = 0; 9 ioctl(sockfd, FIONBIO, &flags);
下面是整个connect流程代码:
(1) 创建socket,并利用fcntl将其设置为非阻塞
(2) 调用connect函数,如果返回0,则连接建立;如果返回-1,检查errno ,如果值为 EINPROGRESS,则连接正在建立
(3) 为了控制连接建立时间,将该socket描述符加入到select的可写集合中,采用select函数设定超时
(4) 如果规定时间内成功建立,则描述符变为可写;否则,采用getsockopt函数捕获错误信息
(5) 恢复套接字的文件状态并返回
1 int Connect(uint32 sockfd, struct sockaddr *addr, socklen_t addrlen) 2 { 3 struct timeval timeout; 4 uint32 flags, len; 5 int err, ret; 6 fd_set connectfd; 7 8 if (NULL == addr) 9 return ERR; 10 11 /*step 1: 将套接字设置为非阻塞*/ 12 flags = 1; 13 ioctl(sockfd, FIONBIO, &flags); 14 15 /*step 2: 调用connect连接*/ 16 if ((ret = connect(sockfd, addr, addrlen)) < 0) 17 { 18 if (errno != EINPROGRESS) 19 ret = ERR; 20 21 FD_ZERO(&connectfd); 22 FD_SET(0, &connectfd); 23 FD_SET(sockfd, &connectfd); 24 25 timeout.tv_sec = 2; 26 timeout.tv_usec = 0; 27 if (select(sockfd+1, NULL, &connectfd, NULL, &timeout) > 0) 28 { 29 len = sizeof(uint32); 30 getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len); 31 if (err == 0) 32 { 33 ret = TURE; 34 } 35 else 36 ret = ERR; 37 } 38 else 39 ret = ERR; 40 } 41 42 /*恢复套接字为阻塞模式*/ 43 flags = 0; 44 ioctl(sockfd, FIONBIO, &flags); 45 46 return ret; 47 }