本节来将客户端的connect
阻塞式改为非阻塞式. 这样可以同时向服务端发起多个连接并一起进行处理, 非阻塞connect
一般用来测试服务端的抗压能力.
connect 非阻塞用途
connect
设置为非阻塞之后会立即返回 设置errno为 EINPROGRESS
错误, 表示连接操作正在进行中, 但是仍未完成连接; 同时TCP的三次握手操作继续进行. 之后, 可以调用 select
来检查连接是否成功.
其非阻塞connect
有三种用途 :
- 可以在三路握手的同时做一些其它的处理. 这段时间可能几毫秒但也可能几百毫秒.
- 可以同时建立多个连接. 在Web浏览器中很普遍.
- 可以使用
select
(或者IO复用其他函数) 来等待连接的完成, 因此可以给select
设置时间限制, 从而缩短connect
的超时时间. 毕竟大多数中,connect
的超时时间在75秒到几分钟之间. 我们就可以通过connect
非阻塞来设置更短的超时时间.
connect 连接注意的细节
connect
的细节还有一些需要注意哦, 不然很容易处理错误 :
- 即使套接口是非阻塞的, 如果连接的服务器在同一台主机上, 那么调用
connect
建立连接时, 连接通常会立即建立成功. - 源自Berkeley 的实现 (和POSIX) 有两条与 select 和非阻塞 connect 相关的两条规则:
- 当连接建立成功时, 套接口描述符变成可写;
- 当连接出错时, 套接口描述符变成既可读又可写;
注意:当一个套接口出错时,它会被select调用标记为既可读又可写.
connect 超时设置
connect
没有超时设置, 但是我们可以将其设置为非阻塞式, 由select
来设置超时即可. 在写代码时要注意上述的细节处理哦.
完整代码 :
客服端 : timeout_client.c
服务端 : service.c
客服端部分代码 :
// connect 超时封装
int timeout_connect(int sockfd, struct sockaddr *addr, socklen_t socklen, int nsec){
int oldfd;
int ret;
int err = 0; // 通过 getsockopt 获取 connect 是否错误
struct timeval tval; // 设置定时时间
// 设置为非阻塞
oldfd = setnonblock(sockfd);
if((ret = connect(sockfd, addr, socklen)) < 0){
if(errno != EINPROGRESS)
return -1;
}
// 本机连接, 会立即建立连接
if (ret == 0){
goto done;
}
// rset : 用于判断可读; wset : 用于判断可写.
fd_set rset, wset;
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = nsec;
tval.tv_usec = 0;
if(select(sockfd+1, &rset, &wset, NULL, nsec ? &tval : 0) == 0){
// 超时
close(sockfd);
errno = ETIMEDOUT;
return -1;
}
// 可读 | 可写
if(FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)){
socklen_t len = sizeof(err);
// 通过 getsockopt 不返回0, 表示连接出错
if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
return -1;
}
else{
fprintf(stderr, "select error");
exit(1);
}
done:
// sockfd 恢复到进函数进前的状态. 因为只有 connect 需要非阻塞
fcntl(sockfd, F_SETFL, oldfd);
if(err){
close(sockfd);
errno = err;
return -1;
}
return 0;
}
函数中, 注意connect
返回0代表本机间能快速连接成功. 连接成功后需要将socket
恢复到最初的状态, 在该封装中我们只用关心非阻塞状态.
connect 移植性问题
关于connect
非阻塞还有一个问题, 就是不可移植性. 下面我将移植的问题罗列出来.
- 出错的套接口描述符,
getsockopt
的返回值在 Berkeley 的实现是返回0, 待处理的错误值存储在errno中; 而 Solaris 的实现是返回-1, 待处理的错误存储在errno中.( 套接口描述符出错时调用getsockopt的返回值不可移植) - 有可能在调用select之前, 连接就已经建立成功, 而且对方的数据已经到来, 在这种情况下, 套接口描述符是既可读又可写;这与套接口描述符出错时是一样的.
移植性的解决办法
那么上述问题怎么解决呢? 在我们判断连接是否建立成功的条件不唯一时, 可用以下的方法来解决这个问题:
-
调用
getpeername
代替getsockopt
.如果调用
getpeername
失败,getpeername
返回 ENOTCONN, 表示连接建立失败, 必须以 SO_ERROR 调用 getsockopt 得到套接口描述符上的待处理错误. -
调用
read
, 读取长度为0字节的数据.-
如果read调用失败, 则表示连接建立失败, 而且read返回的 errno 指明了连接失败的原因.
-
如果连接建立成功, read应该返回0.
-
-
调用一次
connect
. 它应该失败, 如果错误errno = EISCONN
, 就表示套接口已经建立, 而且第一次连接是成功的; 否则, 连接就是失败的.
总结
- connect 非阻塞的用途
- connect 非阻塞细节
- connect 非阻塞实现