• Home
  • About
    • Road to Coding photo

      Road to Coding

      只要那一抹笑容尚存, 我便心无旁骛

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags

非阻塞IO

14 Mar 2019

在讲到非阻塞IO的时候, 我们经常涉及的接口就是: (仅涉及网络编程范围内) 可能阻塞就分为四类:

  1. 输入操作: read, recv, recvfrom, readv, recvmsg
  2. 输出操作: write, send, sendto, sendv, sendmsg
  3. 发起对外连接: connect
  4. 接受外来连接: accept

输入输出操作, 如果能非阻塞的操作下去, 就会直接操作. 否则, 会立即返回EAGAIN/EWOULDBLOCK避免阻塞.

我们着重来分析, 对于connect和accept的非阻塞行为:

accept

大多数的情况下: 在我们使用多路复用配合 + 非阻塞IO的时候. 只要文件描述符返回, 它一定是不会阻塞的, 一定是可以直接接受连接的

但是, 历史上存在这样的问题: (现在好像还是有, 我测试了, 结果有问题)

客户端向服务端发起连接, IO复用记录该套接字就绪, 在返回之前, 客户端宕机,(或者其他情况, 都行) 这时候, 直接收到[FIN, ACK]连接断开了. 源自BSD实现的scoket API会将这个错误不进行报告, 直接在内核层面处理掉这个套接字fd 然后, 基于POSIX的实现, 现在会导致监听套接字, 阻塞在accept(2)上

虽然这个问题也不是很严重, 一旦有一个新的连接, 阻塞状态立即解除 一般返回的错误标志是ECONNABORTED (SysV),或者EPROTO (POSIX)

解决办法有很简单:

  1. 总是将监听套接字设置为非阻塞的.
  2. 同时忽略EWOULDBLOCK, ECONNABORTED, EPROTO, EINTR

设置为非阻塞监听套接字还有一个间接原因是: 连接套接字会直接继承监听套接字的属性, 减少了一次fcntl系统调用

当然,现在使用accept4(2)也可以直接避开这个问题.

connect

另一个比较重要的就是connect的非阻塞调用了. 我们回顾一下基础的网络知识: TCP三次握手的流程是这样的: [SYN,ACK] -> [SYN] -> [ACK] 三路分组交换, 客户端发送[SYN, ACK]之后, 它需要等待一个RTT, 而在这个RTT之间我们可以作很多事. 我们也是因此才会有使用Nonblock connect的理由

那么, 非阻塞connect的注意点在哪里呢?

  1. connect返回EAGIN, EWOULDBLOCK是真正的错误, 必须关闭套接字
  2. connect返回EINPROGRESS才是真正的非阻塞情况

我们进行非阻塞connect的判断需要结合IO复用,当connect无论是成功还是失败 都会触发读写事件, 此时我们查看套接字上发生的错误就可以判断, 是否connect成功

基本流程是这样的:

int ready = epoll_wait(ep_fd, &events, max_events, timeout);
for (...) {
  if (events[i].events & EPOLLIN &&
      events[i].events & EPOLLOUT) {
      if (getsockopt(events[i].data.fd, SOL_SOCKET, SO_ERROR, &error, sizeof(int)))
        if (error == 0)
          // connect successfully
	  }
}

被中断的connect

对于其他的系统调用, 如果被中断, 一般返回EINTR, 此时我们会尝试重启系统调用. 但是, 对于connect, 我们不能重启, 应该立即关闭套接字. 如果强行使用, 会导致EADDRINUSE



NetworkProgramming Share Tweet +1