NIO Reactor模式

  NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

  由于Java NIO属于基础部分,本文稍微讲解下,具体请自行了解更多NIO设计。

传统BIO模型分析

以下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模型:

public class BioServer {

    //initialize socket and input stream
    private Socket socket = null;
    private ServerSocket server = null;
    private DataInputStream in = null;

    public static void main(String args[]) {
        BioServer server = new BioServer(5000);
    }

    // constructor with port
    public BioServer(int port) {
        // starts server and waits for a connection
        try {
            server = new ServerSocket(port);
            System.out.println("Server started");

            System.out.println("Waiting for a client ...");

            socket = server.accept();
            System.out.println("Client accepted");

            // takes input from the client socket
            in = new DataInputStream(new BufferedInputStream(socket.getInputStream()));

            String line = "";

            // reads message from client until "Over" is sent
            while (!line.equals("Over")) {
                try {
                    line = in.readUTF();
                    System.out.println(line);

                } catch (IOException i) {
                    System.out.println(i);
                }
            }
            System.out.println("Closing connection");

            // close connection
            socket.close();
            in.close();
        } catch (IOException i) {
            System.out.println(i);
        }
    }

}

  这是一个经典的每连接每线程的模型,可以使用多线程(上面代码只有一个主现场,可以改成一个链接新建一个线程),主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。
  当然一般都使用线程池,可以让线程的创建和回收成本相对较低。
  不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源,主要表现在:
  1.线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
  2.线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
  3.线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
  4.容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

  对于像BAT,TMD这样的企业,每天面对千万次连接,传统的BIO模型是无能为力的,必然需要一种更高效的I/O处理模型。

NIO模型分析

我们先看一下 NIO 涉及到的关联类图,如下:


NIO Reactor模式_第1张图片
serversocketchannel.png

  上图中有两个关键类:Channel 和 Selector,它们是 NIO 中两个核心概念。我们还用前面的城市交通工具来继续比喻 NIO 的工作方式,这里的 Channel 要比 Socket 更加具体,它可以比作为某种具体的交通工具,如汽车或是高铁等,而 Selector 可以比作为一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态:是已经出战还是在路上等等,也就是它可以轮询每个 Channel 的状态。这里还有一个 Buffer 类,它也比 Stream 更加具体化,我们可以将它比作为车上的座位,Channel 是汽车的话就是汽车上的座位,高铁上就是高铁上的座位,它始终是一个具体的概念,与 Stream 不同。Stream 只能代表是一个座位,至于是什么座位由你自己去想象,也就是你在去上车之前并不知道,这个车上是否还有没有座位了,也不知道上的是什么车,因为你并不能选择,这些信息都已经被封装在了运输工具(Socket)里面了,对你是透明的。NIO 引入了 Channel、Buffer 和 Selector 就是想把这些信息具体化,让程序员有机会控制它.

  下面是典型的一段 NIO 代码:

public class PlainNioServer {

    public static void main(String[] args) throws Exception{
        int port = 9876;
        System.out.println("Listening for connections on port " + port);
        ServerSocketChannel serverChannel;
        Selector selector;

        serverChannel = ServerSocketChannel.open();
        // 开启非阻塞模式
        serverChannel.configureBlocking(false);
        ServerSocket ss = serverChannel.socket();
        InetSocketAddress address = new InetSocketAddress(port);
        //绑定相应IP及port
        ss.bind(address);
        selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());

        while (true) {
            try {
                selector.select();
            } catch (IOException ex) {
                ex.printStackTrace();
                // handle in a proper way
                break;
            }

            Set readyKeys = selector.selectedKeys();
            Iterator iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                try {
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();

                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, msg.duplicate());
                        System.out.println("Accept connection from " + client);
                    }
                    if (key.isWritable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        while (buffer.hasRemaining()) {
                            if (client.write(buffer) == 0) {
                                break;
                            }
                        }
                        System.out.println("服务器端写到客户端信息:" + buffer.toString());
                        client.close();
                    }
                    if (key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        client.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        client.read(buffer);
                        byte[] data = buffer.array();
                        System.out.println("服务器端收到客户端信息:" + data);
                    }
                }
                catch (IOException e) {
                    key.cancel();
                    try {
                        key.channel().close();
                    } catch (IOException ioe) {
                        System.out.println(ioe);
                    }
                }
            }
        }
    }
}

这个是NIO的服务端代码,调用 Selector 的EpollSelectorProvider创建一个选择器Selector, 创建的服务端的 ServerSocketChannel 绑定到一个 Socket 对象,并把这个通信channel注册到Selector上而且要监听OP_ACCEPT,客户端连接事件,再把channel设置为非阻塞模式并绑定9876端口。
代码可以看到,方法整体是一个死循环,轮询访问Selector,发生某些已经注册在Selector上的事件时,该方法返回。可以通过selector.selectedKeys获取发生的事件,我们通过key获取到注册它的那个Channel,在这里是ServerSocketChannel,通过server.accept()获取客户端连接,这里同样可以类比到传统的阻塞IO,在阻塞IO中我们可以通过ServerSocket.accept获取到socket,唯一不同的是,阻塞IO中的accept方法是阻塞操作,而NIO中是非阻塞的。
而轮询到的selectionKey是客户端的Writable,Readable的话,直接进行读写操作,利用NIOBuffer。


NIO Reactor模式_第2张图片
nio.png

上图中的 Selector 可以同时监听一组通信信道(Channel)上的 I/O 状态,选择器 Selector 可以调用 select() 方法检查已经注册的通信信道上的是否有 I/O 已经准备好,如果没有至少一个信道 I/O 状态有变化,那么 select 方法会阻塞等待或在超时时间后会返回 0。上图中如果有多个信道有数据,那么将会将这些数据分配到对应的数据 Buffer 中。所以关键的地方是有一个线程来处理所有连接的数据交互,每个连接的数据交互都不是阻塞方式,所以可以同时处理大量的连接请求。

优化线程模型

  上面铺垫了这么多BIO,NIO知识,终于来到了我们今天的主题,Reactor模式。NIO虽然大大改善了BIO中,原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

  但是现代计算机都是多核心的,如果能够利用多核心进行I/O,无疑对效率会有更大的提高。所以引入了Reactor 模式。

Reactor模式思想:分而治之+事件驱动

一. 分而治之

  一个连接里完整的网络处理过程一般分为accept、read、decode、process、encode、send这几步。

  Reactor模式将每个步骤映射为一个Task,服务端线程执行的最小逻辑单元不再是一次完整的网络请求,而是Task,且采用非阻塞方式执行。

二. 事件驱动

  每个Task对应特定网络事件。当Task准备就绪时,Reactor收到对应的网络事件通知,并将Task分发给绑定了对应网络事件的Handler执行。

三. 几个角色

  Reactor:负责响应事件,将事件分发给绑定了该事件的Handler处理;

  Handler:事件处理器,绑定了某类事件,负责执行对应事件的Task对事件进行处理;

  Acceptor:Handler的一种,绑定了connect事件。当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。

单线程Reactor

NIO Reactor模式_第3张图片
单线程Reactor.png

1. 优点:

  不需要做并发控制,代码实现简单清晰。

2. 缺点:

a. 不能利用多核CPU;

b. 一个线程需要执行处理所有的accept、read、decode、process、encode、send事件,当其中某个 handler 阻塞时, 会导致其他所有的 client 的 handler 都得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了). 因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少。

下面是单线程Reactor的代码:

public class Reactor implements Runnable {

    final Selector selector;
    final ServerSocketChannel serverSocketChannel;
    int port;

    public Reactor(int port) throws IOException {
        this.port = port;
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //利用sk的attache功能绑定Acceptor 如果有事情,触发Acceptor
        sk.attach(new Acceptor(this));
        System.out.println("-->attach(new Acceptor()!");
    }

    @Override
    public void run() {
        while (true) {
            try {
                selector.select();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Set readyKeys = selector.selectedKeys();
            Iterator it = readyKeys.iterator();
            while (it.hasNext()) {
                dispatch((SelectionKey) (it.next()));
            }
            readyKeys.clear();
        }
    }

    //运行Acceptor或handler
    private void dispatch(SelectionKey sk) {
        Runnable task = (Runnable) sk.attachment();
        if (task != null) {
            task.run();
        }
    }
}
public class Acceptor implements Runnable {

    private Reactor reactor;

    public Acceptor(Reactor reactor) {
        this.reactor = reactor;
    }

    @Override
    public void run() {
        try {
            SocketChannel socketChannel = reactor.serverSocketChannel.accept();
            if (socketChannel != null) {
                // 调用Handler来处理channel, 并向Reactor注册此Handler
                new NioHandler(reactor.selector, socketChannel);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class NioHandler implements Runnable {
    final SocketChannel socketChannel;
    final SelectionKey selectionKey;
    ByteBuffer input = ByteBuffer.allocate(1024);
    static final int READING = 0, SENDING = 1;
    int state = READING;
    String clientName = "";

    NioHandler(Selector selector, SocketChannel c) throws IOException {
        socketChannel = c;
        c.configureBlocking(false);
        selectionKey = socketChannel.register(selector, 0);
        selectionKey.attach(this);
        selectionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override
    public void run() {
        ByteBuffer inputBuffer = ByteBuffer.allocate(1024);
        inputBuffer.clear();
        try {
            int n = socketChannel.read(inputBuffer);
            if (n != -1) {
                byte[] data = new byte[n];
                System.arraycopy(inputBuffer.array(), 0, data, 0, n);
                System.out.println("Output: " + new String(data) + "\n");
            }
            System.out.println("Finish......");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

多线程Reactor

NIO Reactor模式_第4张图片
多线程Reactor.png

特点:

a. Reactor 的多线程模型与单线程模型的区别就是 acceptor 是一个单独的线程处理, 并且有一组特定的 NIO 线程来负责各个客户端连接的 IO 操作

b. 有专门一个线程, 即 Acceptor 线程用于监听客户端的TCP连接请求.

c. 客户端发送的IO操作由一个worker reactor线程池负责,由线程池中的NIO线程负责监听客户端SocketChannel事件,进行消息的读取、解码、编码和发送。

d. 一个NIO线程可以同时处理N条链路,但是一个链路只注册在一个NIO线程上处理,防止发生并发操作问题。

4、主从多线程Reactor

NIO Reactor模式_第5张图片
主从多线程Reactor.png

  在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是在极个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。

特点:

a. 服务端用于接收客户端连接的不再是个1个单独的reactor线程,而是一个boss reactor线程池;

b. 服务端启用多个ServerSocketChannel监听不同端口时,每个ServerSocketChannel的监听工作可以由线程池中的一个NIO线程完成。


二、Netty线程模型

NIO Reactor模式_第6张图片
Netty线程模型.png

a. netty线程模型采用“服务端监听线程”和“IO线程”分离的方式,与多线程Reactor模型类似。

b. netty服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时, 实际上是在一个线程中的, 所以对只有一个服务的应用来说, bossGroup 设置多个线程是没有什么作用的, 反而还会造成资源浪费.

c. Netty线程模型是多Reactor模型,bossGroup一个线程,workerGroup多个线程(cpu核数*2)。

你可能感兴趣的:(NIO Reactor模式)