第一次听人用男女关系讲 N(Non-Blocking)I(进)O(出)

987 阅读7分钟

BIO:80 年代屌丝追妹

80 年代屌丝男买了一个 BP 机用来追妹,男士使用传呼台给女生留言:

男:下午一起看个电影?[早晨 10 点]

这是男生唯一心动的女生,所以一直守着自己的 BP 机,等待女生回复,就这样一天过去了,直到:

男:BP 没电,自动关机。

名词解释

  1. BP 机和传呼台指的是 BIO 中的的流单向传输的特性,屌丝男士通过传呼台给 BP 机发送消息是单向的,如果是女生通过传呼台回复屌丝男士,也是单向的。
  2. 这个女生是男士唯一心动的女生,所以他傻等着 BP 机回复,即便可能一直不会有消息,这就是同步阻塞 IO,中 B 的概念。

BIO 的缺点

  1. 同步阻塞 IO,如果存在多个请求的时候,服务端必须通过多线程处理,增加了服务端的压力和创建销毁线程的开销。
  2. 如果连接一直没有响应,服务端也需要一直监听端口等待,浪费了服务端资源。

NIO:80 年代公子哥把妹

80 年代的公子哥买了一个大哥大,关键是这个公子哥太花心,同时中意了两个妹子,于是他就开始了把妹过程。
公子哥用大哥大给女 A 打电话:

公子哥 -> 女 A:下午一起看个电影?[早晨 10 点]
女 A -> 公子哥:我正在吃饭,你晚点再打过来?

公子哥用大哥大给女 B 打电话:

公子哥 -> 女 B:下午一起看个电影?[早晨 10 点 1 分]
女 B -> 公子哥:我正在吃饭,你晚点再打过来?

过了 10 分钟公子哥再次打电话询问

公子哥 -> 女 A:怎么样,有空吗?[早晨 10 点 10 分]
女 A -> 公子哥:我下午陪爸爸打马球,不去了。
公子哥 -> 女 B:怎么样,有空吗?[早晨 10 点 11 分]
女 B -> 公子哥:好呀,下午 3 点来我家接我吧。
公子哥 -> 女 B:好嘞,我开车去接你。

最终公子哥成功了追求到了女 B,这个故事告诉我们,成功的前提是有钱。(你怎么看?)

名词解释

  1. 公子哥用上了大哥大,可以实现双向的通话,这就是 NIO 中的 Channel,可以实现双向的数据流传输。
  2. 公子哥不用像 BP 机小哥一样死等着回复,每次打电话都能得到回复,挂断电话一会儿再来询问即可,这就是 Channel 的非阻塞特性,也就是 “N” 的体现。
  3. 公子哥可以同时撩两个妹子,这就是 NIO 的 IO 多路复用,也就是 Selector。
  4. 公子哥只能同时和一个人通话,这就是同步,所以 NIO 的全称叫做同步非阻塞 IO。

优缺点

  1. 非阻塞 IO 模型,不需要阻塞在特定的请求。
  2. 一个线程可以处理多个请求,不需要客户端和服务器端一比一的对应,没有多线程创建和销毁带来的系统开销。
  3. 服务端不需要死等请求,减少了服务端压力。

关键名词

  1. Channel,双向传输,非阻塞的通道,有FileChannel,DatagramChannel,ServerSocketChannel/SocketChannel 等。
  2. Buffer,数据块的读写,可以理解字节数组,效率高。四个重要属性:capacity 容量,position 位置, limit 上限,用户切换读写时候的游标,mark 标记,标记 position 的位置,分为堆内内存和堆外内存,也是 NIO 性能的关键内容。
  3. Selector,IO 多路复用的关键,实现了循环查看可以使用的 Channel,解决死等问题。
  4. Selector 实现有多种方式,自己写一个数组循环也是方式,也可以实现 IO 多路复用,只是性能好坏而已,所以基于底层 poll、select和epoll 也是实现“遍历”可用通道的方式不同而已。select 使用轮询的方式,有 1024 个连接的限制,poll 去掉了这个限制依然是轮询的方式,epoll 是基于系统的注册回调的方式,监听系统的事件实现。
  5. NIO 引入了 Buffer 的概念,每次使用 Buffer 拷贝数据其实是一次从用户空间(JVM) 向系统空间(系统内存) 的一次拷贝, Java 里面提供了 DirectByteBuffer 堆外内存,如果使用使用堆外内存,可以减少一次系统空间和用户空间的拷贝,这种现象叫做零拷贝。强调一下,并不是操作系统不能直接操作 HeapByteBuffer(对内内存),而且在 GC 的作用下,内存地址可能随时变化,操作的内存数据不一定准备。
  6. IO 多路复用性能更好,针对的 I/O 密集型应用程序,如果是 CPU 密集型应用程序,还是通过多线程的方案。所以很多写 IO 多路复用的文章都会说“多线程的创建,必然存在创建销毁和切换的开销,在高并发系统中,会拖慢整个系统”,其实并不是非常的准确,虽然是想说明 I/O 多路复用的利好,但是确实有点以偏概全。

AIO:21 世纪非智能时代大学生把妹

21 世纪初期,还没有智能机,不过诺基亚 1110 砸核桃神机已经普及了,下面就是新时代大学生小王用自己的诺基亚 1110 的把妹过程。
小王给中意的两个女生直接发短信留言(群发):

小王 -> 女 A:下午一起看个电影?[早晨 10 点]
小王 -> 女 B:下午一起看个电影?[早晨 10 点]

发完短信小王去看《西游记》去了。10分钟以后电话响起,收到了妹子的短信,小王拿起了手机阅读了消息并进行回复。

女 B -> 小王:好呀,下午 3 点来我家接我吧 [早晨 10 点 10 分]
小王 -> 女 B:好的,我去接你不见不散。

名词解释

  1. 小王发完短信不需要盯着手机看,也不需要时不时看一下手机,有短信回复会有通知,再来阅读就好了。这就是 AIO 中的 AsynchronousServerSocketChannel,可以注册一个回调 CompletionHandler,等待有消息的时候直接通知回调处理即可。

优缺点

AIO 包括了 NIO 的所有优缺点的同时,增加了异步回调的能力,由此解决的问题是不需要同步的等待非阻塞 IO 的反馈,所以 NIO 叫做异步非阻塞 IO 模型。

是时候展示真正的技术了

说了这么多,用 NIO 实现一个把妹聊天程序呗?

服务器端

public class NioServer {
    public void start() throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(6789));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器启动成功");
        while (true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {
                    iterator.remove();
                    handleAccept(serverSocketChannel, selector);
                } else if (selectionKey.isReadable()) {
                    handleRead(selectionKey);
                } else {
                    System.out.println("其他请求");
                }
            }
        }
    }

    private void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        StringBuilder request = new StringBuilder();
        while (socketChannel.read(byteBuffer) > 0) {
            byteBuffer.flip();
            request.append(StandardCharsets.UTF_8.decode(byteBuffer));
        }
        if (request.length() > 0) {
            System.out.println("服务端收到消息:" + request.toString());
        }
    }

    private void handleAccept(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("有新人进入聊天室");
        socketChannel.write(StandardCharsets.UTF_8.encode("进入聊天室,现在可以聊天了"));
    }

    public static void main(String[] args) throws IOException {
        new NioServer().start();
    }
}

客户端

public class NioClient {
    public void start() throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(6789));
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        new Thread(new NioClientHandler(selector)).start();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String request = scanner.nextLine();
            if (request != null && request.length() > 0) {
                socketChannel.write(StandardCharsets.UTF_8.encode(request));
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new NioClient().start();
    }

    private class NioClientHandler implements Runnable {
        private Selector selector;

        public NioClientHandler(Selector selector) {
            this.selector = selector;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    selector.select();
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();

                    Iterator iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = (SelectionKey) iterator.next();
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        StringBuilder response = new StringBuilder();
                        while (socketChannel.read(byteBuffer) > 0) {
                            byteBuffer.flip();
                            response.append(StandardCharsets.UTF_8.decode(byteBuffer));
                        }
                        if (response.length() > 0) {
                            System.out.println("接收服务端消息:" + response);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

参考文档

  1. 图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!
  2. 解锁网络编程之NIO的前世今生
  3. NIO如何实现多路复用?
  4. 深入理解 Java IO
  5. 高并发专题之 IO 多路复用:Select、Poll、Epoll