Java NIO的原理和使用


  NIO是面向缓存的非阻塞IO模型,其有三大核心组件:Buffer、Channel、Selector,如下图:

  原理都好理解,接下来从Java api来看下三大核心组件的简单使用。

1、Buffer

  Buffer有几大子类:ByteBuffer(最常用)、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。

  Buffer底层维护一个数组,由四个重要参数:

    position(数组中下一个可读或可写的位置)

    mark(标记)

    limit(最大可读或可写的位置)

    capacity(数组容量)

  以ByteBuffer为例看一下常用的api:

      

  看一段简单代码:

public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(5);

        for (int i=0;i){
            intBuffer.put(i*2);
        }

        //读写切换
        intBuffer.flip();

        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer.get());
        }
}

  输出:0、2、4、6、8

Buffer特性:

(1)Buffer支持类型化的put和get

private static void type(){
        ByteBuffer byteBuffer = ByteBuffer.allocate(64);

        byteBuffer.putInt(100);
        byteBuffer.putLong(3);
        byteBuffer.putChar('哈');

        byteBuffer.flip();

        System.out.println(byteBuffer.getInt());
        System.out.println(byteBuffer.getLong());
        System.out.println(byteBuffer.getChar());
}

  但是如果put或是get时超出的Buffer的容量,会抛出java.nio.BufferOverflowException异常。

(2)Buffer可以设置为只读模式

(3)MappedByteBuffer

  MappedByteBuffer可以使文件直接在内存(堆外内存)中修改,减少了内核空间和用户空间之间的数据拷贝来提升效率。

private static void mappingBuffer(){
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/jijingyi/Desktop/test.txt","rw");
            FileChannel channel = randomAccessFile.getChannel();

            /**
             * 参数1:FileChannel.MapMode.READ_WRITE 表示使用读写模式
             * 参数2:文件可以直接在内存中修改的起始位置
             * 参数3:这个5指的是可修改的字节长度,即从下标0-4,修改超出下标4会报 IndexOutOfBoundsExpection
             */
            MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE,0,5);

            mappedByteBuffer.put(0,(byte)'H');
            mappedByteBuffer.put(3,(byte)'L');

            randomAccessFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
}

(4)Buffer的分散和聚集

/**
     * scatter(分散):将数据写入到buffer时,可以使用数组,依次写入
     * gatter(聚集):从buffer读取数据时,可以使用数组,依次读取
     */
    private static void serverBoost(){
        try {
            //创建服务端
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
            //绑定端口
            serverSocketChannel.socket().bind(inetSocketAddress);

            //监听连接请求
            SocketChannel socketChannel = serverSocketChannel.accept();

            //缓存数组
            ByteBuffer[] byteBuffers = new ByteBuffer[2];
            byteBuffers[0] = ByteBuffer.allocate(6);
            byteBuffers[1] = ByteBuffer.allocate(4);

            int msgLength = 10; //设定最多写10个字节
            while (true){
                int readByte = 0;
                while (readByte < msgLength){
                    long l = socketChannel.read(byteBuffers);
                    readByte += l;
                    //输出一下每个buffer的position和limit
                    Arrays.asList(byteBuffers).stream().map(buffer -> "position="+buffer.position()+",limit="+buffer.limit()).forEach(System.out::println);
                }

                //buffer读写反转
                Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());

                //再将客服端发送的数据写回去
                int writeByte = 0;
                while (writeByte < msgLength){
                    long l = socketChannel.write(byteBuffers);
                    writeByte += l;
                }

                //清空缓存,这里并不是把缓存中的数据删除了,而是重置position和limit
                Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
}

2、Channel

  NIO的channel类似于BIO的stream,但是区别是:

    ? channel既可以从buffer中读,也可以向buffer中写,是双向的,而stream是单向的

    ? channel可以实现异步读写

  常用的Channel类:FileChannel(文件读写)、DatagramChannel(UDP协议)、ServerSocketChannel和SocketChannel(TCP协议)

  我们来看一下FileChannel的几个api案例,ServerSocketChannel和SocketChannel在介绍netty的时候再说。

(1)文件读取

private static void readData(){
        try {
            File file = new File("/Users/jijingyi/Desktop/test.txt");
            FileInputStream inputStream = new FileInputStream(file);

            FileChannel fileChannel = inputStream.getChannel();

            ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
            fileChannel.read(byteBuffer);

            System.out.println(new String(byteBuffer.array()));
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
}

(2)文件写入

private static void writeData(){
        String str = "hello world";

        //创建一个输出流
        try {
            //FileOutputStream中持有一个channel属性
            FileOutputStream outputStream = new FileOutputStream("/Users/jijingyi/Desktop/test.txt");

            FileChannel fileChannel = outputStream.getChannel();

            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put(str.getBytes());
            byteBuffer.flip();
            //注意读和写是相对缓冲区来说的
            fileChannel.write(byteBuffer);
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
}

(3)实现文件拷贝

private static void copyFile(){
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream("/Users/jijingyi/Desktop/test.txt");
            FileChannel fileChannel01 = inputStream.getChannel();

            outputStream = new FileOutputStream("test.txt");
            FileChannel fileChannel02 = outputStream.getChannel();

            ByteBuffer byteBuffer = ByteBuffer.allocate(5);

            while (true){
                //如果文件读完返回-1
                int read = fileChannel01.read(byteBuffer);
                if (read == -1){
                    break;
                }
                //读写反转
                byteBuffer.flip();
                fileChannel02.write(byteBuffer);
                //这里写完要重置channel,否则会由于position=limit出现死循环(因为再次读取时read会一直等于0)
                byteBuffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null){
                    inputStream.close();
                }
                if (outputStream != null){
                    outputStream.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
}

(4)使用transfFrom实现文件拷贝

private static void copyFileByTransf(){
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream("/Users/jijingyi/Desktop/17岁.mp3");
            FileChannel channelSrc = inputStream.getChannel();

            outputStream = new FileOutputStream("/Users/jijingyi/Desktop/100岁.mp3");
            FileChannel channelDest = outputStream.getChannel();

            channelDest.transferFrom(channelSrc,0,channelSrc.size());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null){
                    inputStream.close();
                }
                if (outputStream != null){
                    outputStream.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
}

3、Selector

  Selector就是选择器,也叫多路复用器,可以同时并发处理多个连接(就是监听连接对应的channel,通过事件机制来触发响应的操作)。

3.1、Selector常用的方法

    public static Selector open();  //获得一个选择器对象

    public int select(); //监听所有注册的通道,将有事件发生的channel对应的selectionKey加入到集合中,返回有事件触发的通道的个数。这个方法是阻塞的。

    public int select(long timeout); //作用同select,但是带超时时间

    public int selectNow(); //作用同select,但是不阻塞,不管是否有可处理的channel都返回

    public Selector wakeup(); //立刻唤醒选择器对象

    public Set selectedKeys();  //返回所有有事件触发的selectionKey的集合

3.2、NIO非阻塞网络编程原理  

   NIO非阻塞网络编程主要涉及4个核心类:Selector、SelectionKey、ServerSocketChannel、SocketChannel,原理如下图

  对上图的几点说明:

    ? 当有客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel

    ? Selector 通过 select() 进行监听,返回有事件发生的通道个数

    ? 将 SocketChannel 通过 register(Selector sel, int ops) 注册到Selector上,一个 Selector 上可以注册多个通道

    ? 注册后返回一个 selectionKey ,它会和该 Selector 关联(Selector 维护一个 selectionKey 的集合)

    ? 进一步得到各个有事件发生的 selectionKey

    ? SelectionKey 通过 channel() 反向获取 SocketChannel

    ? 最后通过得到的 channel 处理相应的事件

  NIOServer:

private static void startNioServer(){
        try {
            //创建服务端
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
            serverSocketChannel.socket().bind(inetSocketAddress);
            //设置为非阻塞,否则会报 IllegalBlockingModeException 异常
            serverSocketChannel.configureBlocking(false);

            //创建选择器对象
            Selector selector = Selector.open();

            //将服务端通道注册到Selector
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true){
                //Selector监听所有注册的通道,这里选择带超时的监听(这里是阻塞的),返回的是有事件触发的通道个数
                if (selector.select(5000) == 0){
                    System.out.println("服务器等待5秒,没有连接");
                    continue;
                }
                //获取有事件触发的通道关联的 SelectionKey,然后通过 SelectionKey 反向获取 SocketChannel
                Set selectionKeys = selector.selectedKeys();

                //遍历集合
                Iterator iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //客户端请求连接事件
                    if (key.isAcceptable()){
                        //注意这里的 accept() 虽然是阻塞的,但是因为已经明确了是连接事件,所以会立刻执行
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("客户端连接成功,生成SocketChannel=" + socketChannel.hashCode());
                        //设置为非阻塞
                        socketChannel.configureBlocking(false);
                        //将客户端通道注册到 Selector,并关联一个 buffer
                        socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    }
                    //读事件
                    else if (key.isReadable()){
                        //通过 SelectionKey 反向获取 SocketChannel
                        SocketChannel socketChannel = (SocketChannel)key.channel();
                        //获取关联的 buffer
                        ByteBuffer buffer = (ByteBuffer)key.attachment();
                        //读取数据客户端发送的数据
                        socketChannel.read(buffer);
                        System.out.println("客户端发送数据:" + new String(buffer.array()));
                    }
                    //手动从集合中删除 SelectionKey,避免重复操作
                    iterator.remove();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
}

  NIOClient:

private static void nioClient(){
        try {
            //创建一个 SocketChannel
            SocketChannel socketChannel = SocketChannel.open();
            //设置为非阻塞
            socketChannel.configureBlocking(false);
            //提供服务端 ip和port,并连接服务端
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8888);
            socketChannel.connect(inetSocketAddress);

            //这里是非阻塞的,如果还没有连接成功,可以处理其他业务
            if (!socketChannel.isConnected()){
                while (!socketChannel.finishConnect()){
                    System.out.println("客户端还未完成连接,处理其他业务");
                }
            }

            //向服务端发送数据
            String data = "哈哈哈";
            ByteBuffer byteBuffer = ByteBuffer.wrap(data.getBytes());
            socketChannel.write(byteBuffer);
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
}

  这段程序有一个问题:客户端如果不加 System.in.read(),服务端会一直触发读事件,一直打印客户端发送的数据,应该是服务端代码有点问题。