美文网首页
NIO应用浅析

NIO应用浅析

作者: 2389c57fa07b | 来源:发表于2020-03-16 12:42 被阅读0次

    作者:范平
    上海华瑞银行数字银行开发中心软件工程师
    目前负责华瑞银行移动银行、融资中台开发工作。

    本篇文章对NIO非阻塞IO在日常web容器中的使用分析,会从IO模型、Java的NIO包、Socket网络访问原理和web容器的常见核心NIO模型Reactor几方面循序渐进的进行一个讲解,阅读本篇文章需要对linux操作系统和网络有一定了解。

    1. IO模型

    同步阻塞IO

    用户线程发起IO请求到IO操作结束,用户线程会被一直挂起,下面是linux recvfrom的函数接口。

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    

    recvfrom函数是阻塞的,其作用是从指定的socket fd中,将数据读入到buffer里;在recvfrom的调用过程中,完成了kenel准备数据和从kernel复制数据到用户空间的过程。


    image
    同步非阻塞

    此场景中socket设置为非阻塞的(通过fcntl方法),调用recvfrom函数会返回EWOULDBLOCK,暗示kernel仍在准备数据中

    用户线程发起IO请求后会立即获得数据是否就绪的状态返回;在查询IO是否就绪的间隔,用户线程可以处理别的任务,其优势在于

    1. 使用较少的线程管理多个连接,减少内存管理和上下文切换所带来的开销

    2. 线程可以复用

    image
    IO复用模型

    常用函数:select(), poll(), epoll()

    在一个线程中使用select()或 poll()或epoll()函数,来轮询多个fd(e.g. socket)的就绪情况,如果有fd准备就绪,则返回,否则该线程会阻塞等待有fd就绪直到超时。就绪的fd,既可以放在当前现场处理,也可以创建线程池来处理。

    这种使用单线程来检测多个fd就绪情况的机制就是多路复用,其优势在于

    1)减少了线程数

    2)减少了内存开销

    3)减少了多线程之间上下文切换的开销

    image

    2. Java.NIO简介

    Channel

    Channel对象提供了对多种fd(socket, file等)写入/读取Buffer的实现

    image
    Buffer

    主要属性:capacity, limit, position, mark

    属性之间的关系:mark <= position <= limit <= capacity

    // 构造函数
    Buffer(int mark, int pos, int lim, int cap) {
      if (cap < 0)
        throw new IllegalArgumentException("Negative capacity: " + cap);
      this.capacity = cap;
      limit(lim);
      position(pos);
      if (mark >= 0) {
        if (mark > pos)
          throw new IllegalArgumentException("mark > position: ("
                                             + mark + " > " + pos + ")");
        this.mark = mark;
      }
    }
    
    Selector
    Selector selector = Selector.open(); // 创建selector
    channel.configureBlocking(false); // channel需要设置为非阻塞
    SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_CONNE);
    
    image

    selector会轮询已注册的channel,更具注册时登记的SelectionKey判断是否触发对应事件

    key 功能
    OP_ACCEPT 请求在接受新连接并创建Channel时获得通知
    OP_CONNECT 请求在建立一个连接时获得通知
    OP_READ 数据已经继续,请求可从Channel中读取数据时获得通知
    OP_WRITE 可以继续向Channel中写数据时,请求获取通知
    // e.g.
    socketChannle.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_CONNECT)
    
    方法 功能
    open() 新建一个selector
    keys() 返回selectionKeys
    select() 阻塞select操作
    selectNow() 非阻塞select操作
    wakeup() 在另外一个线程调用wakeup,被阻塞与select方法的线程就会立刻返回

    3. Socket是如何工作的?

    了解socket的工作原理,会有助于我们了解接下来的话题。

    在服务端创建一个socketfd(socket函数),绑定该服务的socket地址信息(bind函数),再将该socketfd转化为listenfd(listen函数)。这三个步骤是一般socket编程中所常见的。

    image

    当一个connection建立后,会获得一个connfd(accept函数),主进程创建子进程来处理该connfd的请求,伪代码如下:

    pid_t pid;
    int listenfd, connfd;
    listenfd = socket(...); // 创建socket
    bind(listenfd, ...); // 将该服务的socket地址信息绑定到listenfd上
    listen(listenfd, ...); // 转化为listenfd
    
    for ( ; ; ) {
       connfd = accept(listenfd, ...); // 阻塞至连接建立
       if ( (pid = fork()) == 0 ) { // 子进程部分
          close(listenfd); // 关闭listenfd 
          /*** 处理请求 ***/
            close(connfd); // 子进程关闭connfd,该fd引用计数减1
          exit(0);  // 关闭子进程
        }
        close(connfd);  // 父进程关闭connfd,该fd应用计数减1
      }
    }
    

    更近一步去优化这个过程。我们更倾向使用线程的方式来管理和调度。

    master线程专注于监听连接请求;buffer用于缓存那些tcp三次握手已经established的socketfd;每个worker线程将其需要处理的socketfd从buffer中取出(移除),调用accept函数生成对应的connfd和clientfd这对fd,即打通了一条两端均为socketfd的网络连接通道。每个work线程通过这条通道来实现对各自client的服务。

    image

    4. Reactor模式

    Reactor单线程模型

    Reactor单线程模型提供了一种解决思路。

    步骤一:开启Reactor主线程监听服务端端口;创建serverSocketChannel,并注册该channel到Selector中,关注OP_ACCEPT事件;selector的注册函数返回一个selectionKey, 设置selectionKey的attachment为Acceptor对象;

    步骤二:Selector轮询已注册的channel(目前只有一个serverSocketChannel),当listenfd(即serverSocketChannel的endpoint)准备好接收一个新的连接时,selector便会轮询到该channel所绑定的key,这个时候Reactor线程会调起之前绑定在这个key上的attachment(即Acceptor对象)

    步骤三:Accept对象将socketChannel注册到selector中,一般在这个阶段关注的事件为OP_READ或OP_WRITE事件,即当读或写就绪时,即可唤起一个线程来处理之

    image

    其中Reactor线程专注于listenfd用于监听客户端的connection请求;Acceptor线程被调起来之后会创建connfd(用于服务端和客户端搭建connection)。需要注意的是Acceptor线程是被动创建的,当有serverSocketChannel触发了OP_ACCEPT事件或socketChannel触发了OP_READ/OP_WRITE事件之后,才会由Reactor线程调用。Acceptor线程通过创建线程来处理对应请求,e.g.读取报文 => 处理 => 响应请求。

    Reactor线程池模型

    Reactor线程池模型和单线程模型的主要区别在于前者将对非IO的处理交给了线程池,其优势在于加快了React线程的处理速度。因为在单线程模型中从socket读取数据之后,必须等处理完成之后,才可以将该channel的interestSet从OP_READ变更为OP_WRITE;而在多线程模型中处理非IO的过程被丢给了thread pool,当处理结束之后由thread pool分配的线程变更interestSet即可,从而缩短了阻塞的时间。

    image
    Reactor主从模型

    在高并发的场景下,Reactor主从模型可以充分利用cpu核心数提升并发能力。Main Reactor线程专注于监听客户端的连接请求,并通过Main Acceptor线程分发OP_ACCEPT就绪的连接。

    比较来看,之前的两个模型都只有一个Selector,所有的OP_ACCEPT, OP_CONNECT, OP_READ和OP_WRITE都归这一个Selector去轮询;而主从模型中,Main Reactor线程持有的Selector中只关注OP_ACCEPT这一种selectionKey,其他SubReactor线程持有的Selector则管理注册到各自的serverChannel的interestSet事件的触发。因此Main Acceptor需要维护各个Sub Reactor与所持有Selector的关系。

    image

    总结

    Reactor模型将IO的处理和非IO的处理剥离开来,使IO事件可以更加及时得注册和分派,提升了事件驱动的效率。按照网络连接工作的特性:在接受请求阶段,将OP_ACCEPT单独监听并配合selector的使用,使单个线程不用阻塞在accept()中,而是线程直接被分配一个就绪的socketf;在处理请求阶段,IO线程在OP_READ触发后,通过channel将都就绪的socketfd指向的信息读入buffer,将报文信息交给线程池分配的一个线程来处理一些非IO的操作,此时刚才的IO线程又可以去处理别的IO事件了,当处理非IO的线程完成任务后,注册OP_WRITE给selector并由selector指派另一个空闲的IO线程将响应报文写给socketfd。

    总体来说Reactor模式为web服务提供了一套按照网络连接、IO操作定制化的多线程时序控制逻辑。

    相关文章

      网友评论

          本文标题:NIO应用浅析

          本文链接:https://www.haomeiwen.com/subject/zmtgehtx.html