87-非阻塞 connect

非阻塞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 函数调用成功还是失败
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值