Java NIO深入詳解

想要學習Netty,NIO的瞭解必不可少。

什麼是NIO

IO的方式通常分爲幾種: 同步阻塞的BIO、同步非阻塞的NIO、異步非阻塞的AIO。

這裏簡單提及下

NIO的核心組件

NIO中涉及到核心三個組件:Selector (選擇器)、Channel (通道)、Buffer (緩衝區) 。傳統的IO是基於流進行操作的,包括字節流和字符流。而NIO是基於Buffer緩衝區進行數據傳輸。並且傳統IO的流都是單向的,比如各種輸入輸出流,只能用於輸入或者輸出。在NIO中數據可以由Buffer寫入到Channel(通道)中,也可以由Channel寫入到Buffer中。

Selector

NIO的核心處理器,屬於多路複用器 ,實現異步非阻塞IO操作。一個Selector能夠處理多個Channel,檢測多個Channel上的事件,因此不需要爲每一個channel分配一個線程。

Channel

Channel類似IO中的流,不過流是單向的,Channel是雙向的,Channel打開後可以讀取,寫入或這讀寫。既可以從通道中讀取數據,又可以寫數據到通道,並且通道中的數據必須讀到一個Buffer中,或者從一個Buffer中寫入。

Java NIO中channel的主要實現:

  • SocketChannel (TCP client)

  • ServerSocketChannel (TCP server)

  • DatagramChannel(UDP)

  • FileChannel (文件IO)

Buffer

Buffer是NIO中的緩衝區,主要和Channel交互,負責從Channel中讀取數據,或者寫入數據到Channel。

Buffer的實現有

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

分別對應基本類型中的byte, short, int, long, float, double 和 char。要注意上面這些Buffer都是抽象類,每一種都有各自的實現類或者繼承了這些類的其他抽象Buffer,類似HeapByteBuffer,HeapIntBuffer,還有Direct(XX)Buffer,Direct(XX)BufferR等。

一個Selector可以處理多個Channel,每個Channel上都可以有Buffer進行讀寫。需要注意這並不涉及對應關係,只是工作流程

在這裏插入圖片描述

NIO簡單示例

先看如下代碼,功能是將數據寫入到Buffer中再讀出來

public static void main(String[] args) {
    IntBuffer intBuffer = IntBuffer.allocate(10);
    //向buffer中寫入隨機數
    SecureRandom secureRandom = new SecureRandom();
    for (int i = 0; i < intBuffer.capacity(); i++) {
        int num = secureRandom.nextInt(10);
        intBuffer.put(num);
    }
    //切換模式
    intBuffer.flip();
    //輸出buffer中的內容
    while (intBuffer.hasRemaining()) {
        System.out.println(intBuffer.get());
    }
}

其中主要步驟:

  • 通過allocate方法創建一個Buffer
  • 向Buffer中寫入數據
  • 使用flip方法切換模式,寫狀態變爲讀狀態
  • 從Buffer中讀取數據

再看一個使用NIO讀取文件內容的代碼

public static void main(String[] args) {
    try (FileInputStream fileInputStream = new FileInputStream("README.md")) {
        //獲取文件Channel
        FileChannel channel = fileInputStream.getChannel();
        //申請一個Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //將Channel中的內容讀到buffer中
        channel.read(byteBuffer);
        //將讀模式切換爲寫模式
        byteBuffer.flip();
        //讀取buffer中的內容
        while (byteBuffer.hasRemaining()) {
            byte b = byteBuffer.get();
            System.out.println((char) b);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

其中主要步驟:

  • 通過FileInputStream創建一個Channel
  • 創建一個ByteBuffer
  • 從Channel中向Buffer中寫入數據
  • 寫完後通過flip方法切換模式
  • 從Buffer中讀取數據

可以注意到每次Buffer中被寫入內容後再讀取內容都需要調用flip方法切換模式,這是爲什麼呢?這解決這些問題,必須深入瞭解Buffer的構成。

Buffer詳解

Buffer意爲緩衝區,實際上是一個容器,每種類型的Buffer,底層都是使用對應類型的數組存儲數據。向Buffer中寫入或讀取數據就是對底層數組中寫入或讀取數據。
Buffer中有幾個重要屬性,分別是:

  • capacity(容量)
  • limit(上限)
  • position(位置)
  • mark(標記)

capacity表示Buffer的最大容量,最大讀取存儲量,是allocate方法決定的,一個Buffer寫滿了,必須將其清空才能繼續寫入數據。

limit在初始情況下是和capacity的值一樣,當寫入數據後,如果Buffer沒寫滿,切換到讀模式,limit的就是寫入數據的容量,表示寫入的數據的容量。例如,初始capacity,limit都爲10的buffer,寫入6個數據後切換到讀模式,此時的limit就是6,表示數據的最大容量。limit的值始終是小於等於capacity的

position表示操作數據的當前位置。初始position的值是0,當數據寫入到Buffer中,position通過向前移動一位始終表示當前可寫的位置。當Buffer從寫模式切換到讀模式時position又會被置爲0,表示當前可讀的位置。position的值始終小於limit

mark表示標記的一個位置,默認是-1,記錄了當前position的前一個位置。可以通過reset方法回到mark標記的位置

它們三者的大小:0<=mark<=position<=limit<=capacity

在這裏插入圖片描述
示例:當使用一個小容量Buffer讀寫文件,需要不斷的切換讀寫模式,並清空Buffer的緩衝。

public static void main(String[] args) throws IOException {
    FileInputStream fileInputStream = new FileInputStream("input.txt");
    FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
    FileChannel inputChannel = fileInputStream.getChannel();
    FileChannel outputChannel = fileOutputStream.getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(4);
    while (true) {
        //一輪讀取後,需要使用clear方法清空緩衝區
        buffer.clear();
        int read = inputChannel.read(buffer);
        if (read == -1) {
            break;
        }
        //切換模式
        buffer.flip();
        outputChannel.write(buffer);
    }
    inputChannel.close();
    outputChannel.close();
    fileOutputStream.close();
    fileInputStream.close();
}

當數據讀取到Buffer中被寫入後,再次讀取數據需要調用clear方法,清除緩衝區。clear,flip方法都是涉及到這幾個屬性的變化。

查看flip和clear方法的源碼就知道爲何要調用這些方法,以及這幾個屬性的變化。

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

上面是flip方法的源碼,當數據被寫入到Buffer後,通過flip方法,limit的值爲寫入的position的值,position被重置爲0,即數據最開始的位置,mark的標記也被重置。例如,容量爲10的Buffer,寫入6個數據後,此時的position爲6(從0開始的),指向了下一個能被寫入的位置。調用flip方法後,limit的值爲6,position爲0,數據會從position的位置一直讀到limit。

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

在clear方法中,position被重置爲0,limit被重置爲capacity的大小。所以數據又會從頭寫入到Buffer中。

你可能會注意到雖然名字叫clear,但是實際上並沒有擦除數據,只是將一些索引值重新初始化了。查看源碼的Javadoc上也說明了

This method does not actually erase the data in the buffer, but it is named as if it did because it will most often be used in situations in which that might as well be the case.

這個方法實際上並不會清除buffer中數據,但是它被命名爲好像它刪除了一樣,因爲它常常用於這種情況(指清除數據)

因此調用clear方法後其實仍能從buffer中獲取到數據。

public static void main(String[] args) {
    IntBuffer intBuffer = IntBuffer.allocate(3);
    for (int i = 0; i < 3; i++) {
        intBuffer.put(i);
    }
    intBuffer.flip();
    while (intBuffer.hasRemaining()) {
        System.out.println(intBuffer.get());
    }
    intBuffer.clear();
    while (intBuffer.hasRemaining()) {
        System.out.println(intBuffer.get());
    }
}

輸出結果

0
1
2
0
1
2

思考一下,如果上述文件讀寫示例中input.txt的內容爲0123456789,註釋掉clear方法,文件output.txt中最後會是什麼內容。答案是會0123一直重複下去

ByteBuffer的實現類

前面說過ByteBuffer之類的都是抽象類,那他們的實現有哪些呢,在IDEA中繼承ByteBuffer的有5個類

ByteBuffer的實現類

查看UML類圖

查看UML類圖

其中HeapByteBuffer,HeapByteBufferR,DirectByteBuffer,DirectByteBufferR是實現類,MappedByteBuffer是抽象類。注意這些Buffer的實現類都是nio包下可見的,無法在自己的類中引入這些類的。

ByteBuffer中4個重要的屬性capacity,limit,position,mark就是在Buffer頂層抽象類中定義的。

查看ByteBuffer的allocate方法可以看到,返回的就是HeapByteBuffer

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

HeapByteBufferR是通過ByteBuffer的asReadOnlyBuffer返回的,這個方法顧名思義,返回一個只讀的Buffer。

HeapByteBufferR是隻讀Buffer。查看HeapByteBufferR的幾個put方法就發現,方法體中直接拋出異常。

public ByteBuffer put(byte x) {
    throw new ReadOnlyBufferException();
}
public ByteBuffer put(int i, byte x) {
    throw new ReadOnlyBufferException();
}
public ByteBuffer put(ByteBuffer src) {
    throw new ReadOnlyBufferException();
}

DirectByteBuffer是Buffer的allocateDirect方法生成的,意爲直接緩衝Buffer,它與HeapByteBuffer 非直接緩衝buffer有着較大的區別

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

首先簡單瞭解一下堆內內存和堆外內存的概念。

堆外內存是相對於堆內內存的一個概念。堆內內存是由JVM所管控的Java進程內存,我們平時在Java中創建的對象都處於堆內內存中,並且它們遵循JVM的內存管理機制,JVM會採用垃圾回收機制統一管理它們的內存。那麼堆外內存就是存在於JVM管控之外的一塊內存區域,因此它是不受JVM的管控。

DirectByteBuffer實現堆外內存的創建,HeapByteBuffer實現堆內內存的創建。關於這兩者的具體區別可以參考其他博客的詳細介紹

https://www.jianshu.com/p/007052ee3773

而DirectByteBufferR和HeapByteBufferR類似,也是屬於只讀Buffer。

MappedByteBuffer 直接使用內存映射(堆外內存),一般多用於操作大文件。他通過FileChannel的map方法創建,關於它的詳細使用可以參考這個

https://www.cnblogs.com/xubenben/p/4424398.html

https://www.jianshu.com/p/f90866dcbffc

以下是一個使用MappedByteBuffer的示例,input.txt的內容爲0123456789

 public static void main(String[] args) throws IOException {
     RandomAccessFile file = new RandomAccessFile("input.txt", "rw");
     FileChannel channel = file.getChannel();
     //讀寫模式,從0開始讀取5個數據到內存中
     MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
     //修改前兩個數據爲AB
     map.put(0, (byte) 'A');
     map.put(1, (byte) 'B');
     file.close();
 }

代碼運行後從新打開就會發現內存變成了AB23456789,注意在IDEA中打開input.txt會發現內存還是原來的,從資源管理器中打開文件會發現其實內容已經被修改了

PS 如果查看ByteBuffer的源碼可以發現源碼格式很亂,包含大量空行,並且在頂部註明// -- This file was mechanically generated: Do not edit! -- //,如果好奇爲什麼會是這樣的,可以參考這裏

Channel

在上面這些例子中已經見識到了FileChannel的使用,其餘的幾個Channel因爲都涉及到網絡,所有會和Selector一起講解。

Selector詳解

Selector是SelectableChannel對象的多路複用器,ServerSocketChannel,SocketChannel和DatagramChannel都繼承了SelectableChannel。

可以通過調用此類的open方法來創建選擇器,該方法將使用系統的默認值selector provider創建一個新的Selector。 還可以通過調用自定義選擇器提供程序的openSelector方法來創建Selector。 Selector保持打開,直到通過其close方法關閉。

創建一個SelectorSelector selector = Selector.open();

將Channel註冊到Selector上通過register方法:Channel.register(selector, Selectionkey);

例如

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//serverSocketChannel註冊一個accept事件到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

register方法的第二個參數表示interest set,可選值由4中,意爲channel註冊到selector上後對什麼事件有反應

  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT

Selector的可選Channel註冊是由SelectKey對象表示,Selector維持了三組SelectKey

  • key set 表示當前channel註冊到selector中所有SelectKey,該集合由keys方法返回
  • selected-key set 已註冊事件的Channel至少一個事件準備就緒
  • cancelled-key

在剛初始化的Selector對象中,這三個集合都是空的。 通過Selector的select()方法可以選擇已經準備好進行I/O操作通道 (這些通道包含你感興趣的的事件)。比如你對讀就緒的通道感興趣,那麼select()方法就會返回讀事件已經就緒的那些通道。

以下是一個socket server的示例,作用是接收到消息原文返回

public static void main(String[] args) throws IOException {
    //通過open方法獲取一個selector
    Selector selector = Selector.open();
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    //設置成非阻塞模式
    serverSocketChannel.configureBlocking(false);
    //綁定10000端口
    ServerSocket serverSocket = serverSocketChannel.socket();
    serverSocket.bind(new InetSocketAddress(10000));
    //serverSocketChannel註冊一個accept事件到selector上
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        System.out.println("重新進行select");
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        selectionKeys.forEach(selectionKey -> {
            try {
                if (selectionKey.isAcceptable()) {
                    //當接收到連接就會執行以下代碼
                    ServerSocketChannel socketChannel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel client = socketChannel.accept();
                    client.configureBlocking(false);
                    //建立連接後註冊READ事件
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("獲取客戶端連接:" + socketChannel);
                } else if (selectionKey.isReadable()) {
                    SocketChannel client = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    while (true) {
                        buffer.clear();
                        int read = client.read(buffer);
                        if (read <= 0) {
                            break;
                        }
                        buffer.flip();
                        client.write(buffer);
                    }
                    System.out.println("接收到客戶端消息:" + new String(buffer.array()));
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                selectionKeys.remove(selectionKey);
            }
        });
    }

}

關於更多selector的介紹可以參考這裏

https://www.cnblogs.com/snailclimb/p/9086334.html

以下是一個服務端與客戶端的示例,client端可以通過控制檯輸入發送消息到server上

Server

public static void main(String[] args) throws IOException {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);
    ServerSocket serverSocket = serverSocketChannel.socket();
    serverSocket.bind(new InetSocketAddress(8899));

    Selector selector = Selector.open();
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        selectionKeys.forEach(selectionKey -> {
            try {
                SocketChannel client;
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                    client = serverSocketChannel1.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    String key = "[" + UUID.randomUUID().toString() + "]";
                    map.put(key, client);
                } else if (selectionKey.isReadable()) {
                    client = (SocketChannel) selectionKey.channel();
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    int count = client.read(readBuffer);
                    if (count > 0) {
                        readBuffer.flip();
                        String receivedMessage = String.valueOf(StandardCharsets.UTF_8.decode(readBuffer).array());
                        System.out.println(client + ":" + receivedMessage);
                        String senderKey = null;
                        for (Map.Entry<String, SocketChannel> entry : map.entrySet()) {
                            String key = entry.getKey();
                            SocketChannel socketChannel = entry.getValue();
                            if (client == socketChannel) {
                                senderKey = key;
                                break;
                            }
                        }
                        for (Map.Entry<String, SocketChannel> entry : map.entrySet()) {
                            SocketChannel socketChannel = entry.getValue();
                            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                            byteBuffer.put((senderKey + ":" + receivedMessage).getBytes());
                            byteBuffer.flip();
                            socketChannel.write(byteBuffer);
                        }
                    }
                }
                System.out.println("length:" + selectionKeys.size());
                selectionKeys.clear();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
            }
        });
    }

}

Client

public static void main(String[] args) throws IOException {
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.configureBlocking(false);
    Selector selector = Selector.open();
    socketChannel.register(selector, SelectionKey.OP_CONNECT);
    socketChannel.connect(new InetSocketAddress("localhost", 8899));
    while (true) {
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        System.out.println("selectionKeys length:" + selectionKeys.size());
        selectionKeys.forEach(selectionKey -> {
            try {
                if (selectionKey.isConnectable()) {
                    SocketChannel client = (SocketChannel) selectionKey.channel();
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                        ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
                        writeBuffer.put((LocalDateTime.now() + " 連接成功").getBytes());
                        writeBuffer.flip();
                        client.write(writeBuffer);
                        ExecutorService executorService = Executors.newSingleThreadExecutor();
                        executorService.submit(() -> {
                            while (true) {
                                writeBuffer.clear();
                                InputStreamReader reader = new InputStreamReader(System.in);
                                BufferedReader bufferedReader = new BufferedReader(reader);
                                String sendMessage = bufferedReader.readLine();
                                writeBuffer.put(sendMessage.getBytes());
                                writeBuffer.flip();
                                client.write(writeBuffer);
                            }
                        });
                    }
                    client.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    SocketChannel client = (SocketChannel) selectionKey.channel();
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    int read = client.read(readBuffer);
                    if (read > 0) {
                        String receiveMessage = new String(readBuffer.array(), 0, read);
                        System.out.println(receiveMessage);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                selectionKeys.clear();
            }
        });
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章