本文共 6850 字,大约阅读时间需要 22 分钟。
TCP连接的建立涉及到一个三次握手的过程,且SOCKET中connect函数需要一直等到客户接收到对于自己的SYN的ACK为止才返回,这意味着每个connect函数总会阻塞其调用进程至少一个到服务器的RTT时间,而RTT波动范围很大,从局域网的几个毫秒到几百个毫秒甚至广域网上的几秒。这段时间内,我们可以执行其他处理工作,以便做到并行。在此,需要用到非阻塞connect。本文主要介绍了非阻塞connect的编写方法以及应用场景。
1. 基础知识
(1) fcntl函数
fcntl函数可执行各种描述符的控制操作,对于socket描述符,常用应用是将其设置为阻塞式IO,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | int flags; if ((flags = fcntl(fd, F_GETFL)) < 0) //获取当前的flags标志 err_sys(“F_GETFL error!”); flags |= O_NONBLOCK; //修改非阻塞标志位 if (fcntl(fd, F_SETFL, flags) < 0) err_sys(“F_SETFL error!”); |
(2) connect函数
对于阻塞式套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或者出错时才返回;对于非阻塞式套接字,如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立,建立启动但是尚未完成;如果返回0,则表示连接已经建立,这通常是在服务器和客户在同一台主机上时发生。
1 2 3 4 5 6 7 8 9 10 11 12 | if (connect(fd, ( struct sockaddr*)&sa, sizeof (sa)) == -1) if ( errno != EINPROGRESS) { return -1; } if (n == 0) goto done; |
(3) select函数
select是一种IO多路复用机制,它允许进程指示内核等待多个事件的任何一个发生,并且在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。
connect本身并不具有设置超时功能,如果想对套接字的IO操作设置超时,可使用select函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | fd_set wfd; FD_ZERO(&wfd); FD_SET(fd, &wfd); if (select(FD_SETSIZE, NULL, &wfd, NULL, toptr) == -1) { __redisSetError(c,REDIS_ERR_IO, sdscatprintf(sdsempty(), "select(2): %s" , strerror ( errno ))); close(fd); return REDIS_ERR; } |
对于select和非阻塞connect,注意两点:
[1] 当连接成功建立时,描述符变成可写; [2] 当连接建立遇到错误时,描述符变为即可读,也可写,遇到这种情况,可调用getsockopt函数。
(4) getsockopt函数
可获取影响套接字的选项,比如SOCKET的出错信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | err = 0; errlen = sizeof (err); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) { sprintf ( "getsockopt(SO_ERROR): %s" , strerror ( errno ))); close(fd); return ERR; } if (err) { errno = err; close(fd); return ERR; } |
2. 实现非阻塞式connect
分以下几步:
(1) 创建socket,并利用fcntl将其设置为非阻塞
(2) 调用connect函数,如果返回0,则连接建立;如果返回-1,检查errno ,如果值为 EINPROGRESS,则连接正在建立。
(3) 为了控制连接建立时间,将该socket描述符加入到select的可写集合中,采用select函数设定超时。
(4) 如果规定时间内成功建立,则描述符变为可写;否则,采用getsockopt函数捕获错误信息
(5) 恢复套接字的文件状态并返回。
3. 应用实例
(1)实例一
《unix网络编程》卷1的16.5节有一个Netscape 的web客户端的程序实例,客户端先建立一个与某个web服务器的HTTP连接,然后获取该网站的主页。该主页往往含有多个对于其他网页的引用,客户可以使用非阻塞connect同时获取多个网页,以此取代每次只获取一个网页的串行获取手段。
(2)实例二
Redis客户端CLI (command line interface),位于源代码的src/deps/hiredis下面。实际上,不仅是Redis客户端,其他类似的client/server架构中,client均可采用非阻塞式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 | int redisContextConnectTcp(redisContext *c, const char *addr, int port, struct timeval *timeout) { int s; int blocking = (c->flags & REDIS_BLOCK); struct sockaddr_in sa; if ((s = redisCreateSocket(c,AF_INET)) < 0) return REDIS_ERR; if (redisSetBlocking(c,s,0) != REDIS_OK) return REDIS_ERR; sa.sin_family = AF_INET; sa.sin_port = htons(port); if (inet_aton(addr, &sa.sin_addr) == 0) { struct hostent *he; he = gethostbyname(addr); if (he == NULL) { __redisSetError(c,REDIS_ERR_OTHER, sdscatprintf(sdsempty(), "Can't resolve: %s" ,addr)); close(s); return REDIS_ERR; } memcpy (&sa.sin_addr, he->h_addr, sizeof ( struct in_addr)); } if (connect(s, ( struct sockaddr*)&sa, sizeof (sa)) == -1) { if ( errno == EINPROGRESS && !blocking) { /* This is ok. */ } else { if (redisContextWaitReady(c,s,timeout) != REDIS_OK) return REDIS_ERR; } } /* Reset socket to be blocking after connect(2). */ if (blocking && redisSetBlocking(c,s,1) != REDIS_OK) return REDIS_ERR; if (redisSetTcpNoDelay(c,s) != REDIS_OK) return REDIS_ERR; c->fd = s; c->flags |= REDIS_CONNECTED; return REDIS_OK; } static int redisContextWaitReady(redisContext *c, int fd, const struct timeval *timeout) { struct timeval to; struct timeval *toptr = NULL; fd_set wfd; int err; socklen_t errlen; /* Only use timeout when not NULL. */ if (timeout != NULL) { to = *timeout; toptr = &to; } if ( errno == EINPROGRESS) { FD_ZERO(&wfd); FD_SET(fd, &wfd); if (select(FD_SETSIZE, NULL, &wfd, NULL, toptr) == -1) { __redisSetError(c,REDIS_ERR_IO, sdscatprintf(sdsempty(), "select(2): %s" , strerror ( errno ))); close(fd); return REDIS_ERR; } if (!FD_ISSET(fd, &wfd)) { errno = ETIMEDOUT; __redisSetError(c,REDIS_ERR_IO,NULL); close(fd); return REDIS_ERR; } err = 0; errlen = sizeof (err); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) { __redisSetError(c,REDIS_ERR_IO, sdscatprintf(sdsempty(), "getsockopt(SO_ERROR): %s" , strerror ( errno ))); close(fd); return REDIS_ERR; } if (err) { errno = err; __redisSetError(c,REDIS_ERR_IO,NULL); close(fd); return REDIS_ERR; } return REDIS_OK; } __redisSetError(c,REDIS_ERR_IO,NULL); close(fd); return REDIS_ERR; } |
原创文章,转载请注明: 转载自
本文链接地址:
在一个TCP套接口被设置为非阻塞之后调用connect,connect会立即返回EINPROGRESS错误,表示连接操作正在进行中,但是仍未完成;同时TCP的三路握手操作继续进行;在这之后,我们可以调用select来检查这个链接是否建立成功;非阻塞connect有三种用途: 1.我们可以在三路握手的同时做一些其它的处理.connect操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网.在这段时间内我们可能有一些其他的处理想要执行; 2.可以用这种技术同时建立多个连接.在Web浏览器中很普遍; 3.由于我们使用select来等待连接的完成,因此我们可以给select设置一个时间限制,从而缩短connect的超时时间.在大多数实现中,connect的超时时间在75秒到几分钟之间.有时候应用程序想要一个更短的超时时间,使用非阻塞connect就是一种方法; 非阻塞connect听起来虽然简单,但是仍然有一些细节问题要处理: 1.即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况; 2.源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则: A:当连接建立成功时,套接口描述符变成可写; B:当连接出错时,套接口描述符变成既可读又可写; 注意:当一个套接口出错时,它会被select调用标记为既可读又可写;
非阻塞connect有这么多好处,但是处理非阻塞connect时会遇到很多可移植性问题;
处理非阻塞connect的步骤: 第一步:创建socket,返回套接口描述符; 第二步:调用fcntl把套接口描述符设置成非阻塞; 第三步:调用connect开始建立连接; 第四步:判断连接是否成功建立; A:如果connect返回0,表示连接简称成功(服务器可客户端在同一台机器上时就有可能发生这种情况); B:调用select来等待连接建立成功完成; 如果select返回0,则表示建立连接超时;我们返回超时错误给用户,同时关闭连接,以防止三路握手操作继续进行下去; 如果select返回大于0的值,则需要检查套接口描述符是否可读或可写;如果套接口描述符可读或可写,则我们可以通过调用getsockopt来得到套接口上待处理的错误(SO_ERROR),如果连接建立成功,这个错误值将是0,如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比如:ECONNREFUSED,ETIMEDOUT等). "读取套接口上的错误"是遇到的第一个可移植性问题;如果出现问题,getsockopt源自Berkeley的实现是返回0,等待处理的错误在变量errno中返回;但是Solaris会让getsockopt返回-1,errno置为待处理的错误;我们对这两种情况都要处理;
这样,在处理非阻塞connect时,在不同的套接口实现的平台中存在的移植性问题,首先,有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来.在这种情况下,连接成功时套接口将既可读又可写.这和连接失败时是一样的.这个时候我们还得通过getsockopt来读取错误值;这是第二个可移植性问题; 移植性问题总结: 1.对于出错的套接口描述符,getsockopt的返回值源自Berkeley的实现是返回0,待处理的错误值存储在errno中;而源自Solaris的实现是返回0,待处理的错误存储在errno中;(套接口描述符出错时调用getsockopt的返回值不可移植) 2.有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写;这与套接口描述符出错时是一样的;(怎样判断连接是否建立成功的条件不可移植)
这样的话,在我们判断连接是否建立成功的条件不唯一时,我们可以有以下的方法来解决这个问题: 1.调用getpeername代替getsockopt.如果调用getpeername失败,getpeername返回ENOTCONN,表示连接建立失败,我们必须以SO_ERROR调用getsockopt得到套接口描述符上的待处理错误; 2.调用read,读取长度为0字节的数据.如果read调用失败,则表示连接建立失败,而且read返回的errno指明了连接失败的原因.如果连接建立成功,read应该返回0; 3.再调用一次connect.它应该失败,如果错误errno是EISCONN,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的;
被中断的connect: 如果在一个阻塞式套接口上调用connect,在TCP的三路握手操作完成之前被中断了,比如说,被捕获的信号中断,将会发生什么呢?假定connect不会自动重启,它将返回EINTR.那么,这个时候,我们就不能再调用connect等待连接建立完成了,如果再次调用connect来等待连接建立完成的话,connect将会返回错误值EADDRINUSE.在这种情况下,应该做的是调用select,就像在非阻塞式connect中所做的一样.然后,select在连接建立成功(使套接口描述符可写)或连接建立失败(使套接口描述符既可读又可写)时返回;