美文网首页面试造飞机系列-NIO
Java NIO Blocking IO and Non-Blo

Java NIO Blocking IO and Non-Blo

作者: Chermack | 来源:发表于2020-06-08 17:08 被阅读0次

    阻塞式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<SelectionKey> 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<SelectionKey> 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-Blo

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