非阻塞i/o 上调用 connect 比非阻塞 i/o 上调用 read/write 要麻烦一点,一方面 connect 函数不能像 read/write 那样反复调用,它只能调用一次;另一方面,connect 函数返回错误,并不代表连接建立不成功。
1. 非阻塞 connect
对于 TCP 协议,在非阻塞 i/o 上调用 connect,意味着 connect 会发送 SYN 段给服务器:
- 如果在 connect 返回时,收到了服务器的 ACK,则 connect 返回 0,意味着连接建立成功,这通常只会发生在本机连接上。
- 如果在 connect 返回时,未收到服务器的 ACK,则 connect 返回 -1,同时 errno 置为 EINPROGRESS,这个错误表示“正在进行……”.
- 如果 connect 返回错误,errno 不是 EINPROGRESS 可以立即判断连接建立失败。
1.1 如何判断连接成功或失败?
对于 TCP 连接:
- 连接建立成功:套接字描述符可写。
- 连接建立失败:套接字描述符可读可写。
注1:上面说的可读可写的含义是 select 函数返回的读集合和写集合是否存在这个描述符。若描述符在读集合里,说明可读;如果在写集合里,说明可写。
注2:一般来说,新创建的描述符,在执行 connect 前既不可读也不可写,连接成功后,则变得可写(这是一定的),但是不一定可读。如果新创建的描述符变得可读了,大概率意味着出错,因为错误是通过 read 系统调用间接返回的。
根据上面两条规则,我们可以使用 select 来处理这两种情况。事先建立读写集合,然后使用 select 监听。
但是,反过来根据套接字描述符可读可写来判断连接成功是不可行的。换句话说,下面这样做是不对的:
- 如果可写不可读:连接建立成功(可以这样判断)
- 如果可写可读:连接建立失败(不可以这样判断)
原因很简单,可写可读,并不一定就是失败,也许是对端发来数据了呢?所以,得使用另外一种办法来判断——使用套接字选项 SO_ERROR. 这也是 SO_ERROR 极少能派上用场的地方之一。
只要套接字描述符变得可读或可写,直接使用 getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len)
取得套接字的状态。
注意:不同版本实现 getsockopt 行为有差异。如果真的产生错误:
- Berkeley 实现让 getsockopt 返回 0,同时 error 中保存错误码(linux 也是这样的)
- Solaris 实现让 getsockopt 返回 -1,同时 errno 中保存错误码(不是 error 参数)
1.2 伪代码
int nbioConnect(int sockfd, sockaddr *addr, socklent_t addrlen, int nsec) {
int ret, error;
socklent_t len;
fd_set rfds, wfds;
setNonblock(sockfd, 1); // 设置为非阻塞
error = 0;
// 发起连接
ret = connect(sockfd, addr, addrlen);
if (ret < 0) {
if (errno != EINPROGRESS) {
// 立即返回错误。
close(sockfd);
return -1;
}
}
else if (ret == 0) {
// 这种一般出现在本机连接上
setNonblock(sockfd, 0); // 重新设置为阻塞
return 0;
}
rfds = {sockfd};
wfds = {sockfd};
tv.tv_sec = nsec;
tv.tv_usec = 0;
// 在连接建立成功或失败前,或者超时时间未到,select 是不会返回的。
ret = select(sockfd + 1, &rfds, &wfds, NULL, nsec ? &tv : NULL);
if (ret < 0) {
// 小概率事件
close(sockfd);
return -1;
}
else if (ret == 0) {
// 超时
errno == ETIMEDOUT;
close(sockfd);
return -1;
}
// 如果执行到这里了,说明连接已经建立成功或者失败,也就是说套接字描述符一定是可写或可读的或两者兼有。
if (FD_ISSET(sockfd, &rfds) || FD_ISSET(sockfd, &wfds)) {
// 这个 if 判断显的多余
len = sizeof(error);
ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
if (ret < 0) {
// Solaris 版本可能会发生这种情况
close(sockfd);
return -1;
}
}
if (error) {
// Linux 版本如果错误会执行到这里
errno = error;
close(sockfd);
return -1;
}
setNonblock(sockfd, 0);
return 0;
}
2. 实验
这一次,只是将上篇写的时间获取客户端中的 connect 函数修改为了 nbioConnect 函数,并添加了一个超时参数进行控制。
- 程序路径
本文使用的程序托管在 gitos 上:http://git.oschina.net/ivan_allen/unp
如果你已经 clone 过这个代码了,请使用 git pull
更新一下。本节程序所使用的程序路径是 unp/program/nonblockio/nbiotimecli
.
- 实验结果
图1 设置超时时间为 10 s,结果超时
图2 连接成功
3. 总结
- 掌握非阻塞 i/o 上调用 connect 函数
- 知道如何判断非阻塞 connect 函数调用成功还是失败