Java NIO Blocking IO and Non-Blocking IO(阻塞式IO与非阻塞式IO)

阻塞式IO

阻塞式IO即在进行IO时,不能同时进行其它的计算任务。因此即使是在使用多线程的情况下,如果有多个IO操作同时进行,也可能导致CPU被占用且闲置,出现CPU利用率不高的情况。一个阻塞式多线程IO示例图如下:


多线程阻塞式IO

为了解决上述问题,加入了Selector(选择器)进行协调。通过将每一个Channel(通道)都注册到选择器上,选择器的作用即监视这些通道的IO情况。当某一个IO请求事件完全准备就绪时,选择器才会将其任务分配到服务端的一个或者多个线程上再去运行。

NIO非阻塞模式

使用阻塞式NIO单线程传递一张图片的示例代码如下:
客户端:

   @Test
   public void client() throws IOException {
        //1.获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));

        FileChannel inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
        //2.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);

        //3.读取本地文件,并发送到服务端
        while (inChannel.read(buf) != -1) {
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }

        //4.关闭通道
        inChannel.close();
        sChannel.close();
    }

服务端:

    @Test
    public void server() throws IOException {
        //1.获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();

        FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        //2.绑定连接
        ssChannel.bind(new InetSocketAddress(9898));

        //3.获取客户端连接的通道
        SocketChannel socketChannel = ssChannel.accept();

        //4.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);

        //5.结构客户端的数据,并保存到本地
        while (socketChannel.read(buf)!=-1) {
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        //6.关闭通道
        socketChannel.close();
        outChannel.close();
        ssChannel.close();
    }

若要在客户端接收服务端的反馈,需要客户端显式调用shutdownOutput()方法,告诉服务端数据已经传输完毕。改进的代码变化不大(客户端也可以使用通道之间使用直接内存进行传递的方式)。
客户端:

    @Test
    public void client() throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
        inChannel.transferTo(0, inChannel.size(), socketChannel); //没有声明缓冲区,可以直接使用通道传输(直接内存)

        socketChannel.shutdownOutput();//显式调用shutdownOutput()告诉服务端数据已经传输完毕

        //接收服务端的反馈
        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (socketChannel.read(buf) != -1) {
            buf.flip();
            System.out.println(new String(buf.array(), 0, buf.position()));
            buf.clear();
        }
        socketChannel.close();
        inChannel.close();
    }

服务端:

    @Test
    public void server() throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();

        FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        ssChannel.bind(new InetSocketAddress(9898));
        SocketChannel socketChannel = ssChannel.accept();

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (socketChannel.read(buf) != -1) {
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }

        //发送反馈给客户端
        buf.put("服务端接受数据成功".getBytes());
        buf.flip();
        socketChannel.write(buf);

        socketChannel.close();
        outChannel.close();
        ssChannel.close();
  }

非阻塞式IO

关于非阻塞式IO,使用的关键点如下:
1.对于一个通道,若要切换到非阻塞模式,需要调用方法configureBlocking(false),将通道置为非阻塞。
2.服务端需要声明一个Selector(选择器对象),可以调用Selector.open()方法获得。
3.需要将通道注册到选择器上,需要调用某通道对象的register方法。该方法需要指定注册到哪一个选择器,并且需要指定SelectionKey(注册哪一种事件)。可以监听的事件类型共有4种,在SelectionKey中用4个常量表示,可以用|连接多种状态。

  • 读:SelectionKey.OP_READ 1
  • 写:SelectionKey.OP_WRITE 4
  • 连接:SelectionKey.OP_CONNECT 8
  • 接收:SelectionKey.OP_ACCEPT 16

SelectionKey表示SelectableChannel和Selector之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。SelectionKey类提供了以下主要方法:


SelectionKey常用方法

客户端

    @Test
    public void client() throws IOException {
        //1.获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));

        //2.切换非阻塞模式
        socketChannel.configureBlocking(false);

        //3.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);

        //4.发送数据给服务端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String str = scanner.next();
            buf.put((new Date().toString()+":"+str).getBytes());
            buf.flip();
            socketChannel.write(buf);
            buf.clear();
        }

        //5.关闭通道
        socketChannel.close();
    }

服务端(和epoll的实现原理类似,可以理解为epoll的简化版本)

    @Test
    public void server() throws IOException {
        //1.获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //2.切换非阻塞模式
        serverSocketChannel.configureBlocking(false);

        //3.绑定连接
        serverSocketChannel.bind(new InetSocketAddress(9898));

        //4.获取选择器
        Selector selector = Selector.open();


        //5.将通道注册到选择器上,并且指定“监听事件”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//第二个参数时选择键,用于通道向选择器注册哪种事件,可以将多个事件使用|进行组合。

        //6.轮询式获取选择器上已经“准备就绪”的事件
        while (selector.select() > 0) {
            //7.获取当前选择器中所有注册的“选择键()已经就绪的事件”
            Iterator iterator = selector.selectedKeys().iterator();

            while (iterator.hasNext()) {
                //8.获取“准备就绪”的事件
                SelectionKey sk = iterator.next();

                //9.判断具体时什么事件准备就绪
                if (sk.isAcceptable()) {
                    //10.若“接收事件就绪”,获取客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();

                    //11.切换非阻塞模式
                    socketChannel.configureBlocking(false);

                    //12.将该通道注册到选择器上
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) {
                    //13.获取当前选择器上“读就绪”状态的通道
                    SocketChannel socketChannel = (SocketChannel) sk.channel();

                    //14.读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len;
                    while ((len = socketChannel.read(buf)) > 0) {
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                //15.取消选择键SelectionKey
                iterator.remove();
            }
        }
    }

上方使用的都是基于TCP协议的通道,在Java NIO中的DatagramChannel时一个能收发UDP包的通道。使用方式也很简单,只需要将网络通道声明为DatagramChannel即可,其它基本相同。
客户端

   @Test
    public void send() throws IOException {
        DatagramChannel dc = DatagramChannel.open();
        dc.configureBlocking(false);
        ByteBuffer buf = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String str = scanner.next();
            buf.put((new Date().toString() + ":\n" + str).getBytes());
            buf.flip();
            dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
            buf.clear();
        }
        dc.close();
    }

服务端

    @Test
    public void receive() throws IOException {
        DatagramChannel dc = DatagramChannel.open();
        dc.configureBlocking(false);
        dc.bind(new InetSocketAddress(9898));
        Selector selector = Selector.open();
        dc.register(selector, SelectionKey.OP_READ);

        while (selector.select() > 0) {
            Iterator iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey sk = iterator.next();

                if (sk.isReadable()) {
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    dc.receive(buf);
                    buf.flip();
                    System.out.println(new String(buf.array(), 0, buf.limit()));
                    buf.clear();
                }
            }
        }
    }

你可能感兴趣的:(Java NIO Blocking IO and Non-Blocking IO(阻塞式IO与非阻塞式IO))