Loading

Java NIO

想学Netty,所以先学下一直也没开始学的NIO。

传统的同步阻塞模型

下面是类似Java的伪代码编写的基于Java传统BIO的socket服务程序:

while(true) {
  sock = serverSocket.accept(); // 请求到来前阻塞
  ins = sock.getInputStream();
  ins.read(buf); // 当有数据读取之前阻塞
  result = handle(buf);
  sock.getOutputStream().write(result); // 可写之前,阻塞
}
  1. 当调用accept时,产生阻塞,在请求到来之前,当前线程啥都不能再干了,只能干等着
  2. 当调用read时,如果流中并没有可读数据也会阻塞
  3. 调用write时,如果写缓冲满了,该方法也会阻塞

对于第三点,可以这样测试,不想看代码直接忽略即可。

public class ClientBIO {
    public static void main(String[] args) throws IOException, InterruptedException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("127.0.0.1", 11441));
        // 循环写入大量数据,占满缓冲区
        for (int i=0;i < 100; i++) {
            socket.getOutputStream().write(new byte[1024 * 1024]);
            System.out.println("send"+i);
        }
        Thread.sleep(10000);
        socket.close();
    }
}

public class ServerBIO {
    public static void main(String[] args) throws IOException, InterruptedException {
        ServerSocket ssock = new ServerSocket(11441);
        while (true) {
            Socket sock = ssock.accept();
            // 暂停5s,不进行读取
            Thread.sleep(5000);
            InputStream ins = sock.getInputStream();
            byte[] buf = new byte[1024];
            long l = 0;
            while ((l = ins.read(buf)) != -1) {

            }
        }
    }
}

在我的机器上,write会在第三次循环时被阻塞,稍后当Server开始读取,程序开始正常运作。

可以看到传统的BIO在accept、read和write上都会阻塞,其过程如下:

  1. 发起IO调用
  2. 线程阻塞等待IO事件发生
  3. IO事件发生后的某个时间,之前阻塞的线程被唤醒
  4. 唤醒后继续执行下面的代码

所以传统的BIO都采用调度线程+工作线程+线程池的方式来调度

while(true) {
  // dispatcher,不断接收新请求
  sock = server.accept();
  // 通过线程池开启一个请求处理器
  executor.submit(new RequestHandler(sock));
}

但是线程创建销毁切换成本高,占用空间大这是一直被诟病的,大家都在寻求更好的办法。还有个最重要的问题,当线程因为外部IO事件阻塞时,这个线程就丧失了继续工作的能力

NIO模型

NIO是非阻塞IO,它不再阻塞在acceptread这种IO操作上,它向系统注册一类事件,然后当注册的事件发生时,当前线程会接收到事件。

NIO有三种组件

  1. Buffer
  2. Channel
  3. Selector

Buffer就是传统意义上的字节缓冲区,Channel代表某种连接的通道,比如Socket,Selector中可以注册一系列Channel的事件,然后调用select轮询是否有事件发生。

// 注册serverSocketChannel,ACCEPT事件
serverSocketChannel.register(selector, OP_ACCEPT);
while(true) {
  selector.select(); // 当没有事件要处理时就阻塞,有就被唤醒
  // 判断事件类型,处理...
}

下面是使用NIO编写SocketServer的一个示例:

public class ServerNIO {
    public static void main(String[] args) {
        new ServerNIO().serveForever();
    }
    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        System.out.println("accept " + sc.getLocalAddress().toString());
        sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
    }

    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer buf = (ByteBuffer) key.attachment();
        long br;
        while ((br = sc.read(buf)) > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
        }
        if (br == -1) Utils.closeClosable(sc);
    }

    public void serveForever() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try {
            selector = Selector.open();
            ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
            ssc.bind(new InetSocketAddress("0.0.0.0", 11441));
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                System.out.println("SELECT");
                if (selector.select(10000) == 0) continue;
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            Utils.closeClosable(selector, ssc);
        }
    }
}
posted @ 2022-03-16 16:09  yudoge  阅读(36)  评论(0编辑  收藏  举报