javaIO2--NIO

NIO

新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的,彌補了原來的 I/O 的不足,提供了高速的、面向塊的 I/O。java

1.1 流與塊

I/O 與 NIO 最重要的區別是數據打包和傳輸的方式,I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。數組

面向流的 I/O 一次處理一個字節數據:一個輸入流產生一個字節數據,一個輸出流消費一個字節數據。爲流式數據建立過濾器很是容易,連接幾個過濾器,以便每一個過濾器只負責複雜處理機制的一部分。不利的一面是,面向流的 I/O 一般至關慢。服務器

面向塊的 I/O 一次處理一個數據塊,按塊處理數據比按流處理數據要快得多。可是面向塊的 I/O 缺乏一些面向流的 I/O 所具備的優雅性和簡單性。網絡

I/O 包和 NIO 已經很好地集成了,java.io.* 已經以 NIO 爲基礎從新實現了,因此如今它能夠利用 NIO 的一些特性。例如,java.io.* 包中的一些類包含以塊的形式讀寫數據的方法,這使得即便在面向流的系統中,處理速度也會更快。app

1.2 通道與緩衝區

1. 通道

通道 Channel 是對原 I/O 包中的流的模擬,能夠經過它讀取和寫入數據。socket

通道與流的不一樣之處在於,流只能在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,能夠用於讀、寫或者同時用於讀寫。性能

通道包括如下類型:ui

  • FileChannel:從文件中讀寫數據;
  • DatagramChannel:經過 UDP 讀寫網絡中數據;
  • SocketChannel:經過 TCP 讀寫網絡中數據;
  • ServerSocketChannel:能夠監聽新進來的 TCP 鏈接,對每個新進來的鏈接都會建立一個 SocketChannel。

2. 緩衝區

發送給一個通道的全部數據都必須首先放到緩衝區中,一樣地,從通道中讀取的任何數據都要先讀到緩衝區中。也就是說,不會直接對通道進行讀寫數據,而是要先通過緩衝區。spa

緩衝區實質上是一個數組,但它不只僅是一個數組。緩衝區提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。操作系統

緩衝區包括如下類型:

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

1.3 緩衝區狀態變量

  • capacity:最大容量;
  • position:當前已經讀寫的字節數;
  • limit:還能夠讀寫的字節數。

狀態變量的改變過程舉例:

① 新建一個大小爲 8 個字節的緩衝區,此時 position 爲 0,而 limit = capacity = 8。capacity 變量不會改變,下面的討論會忽略它。

 

 

 

② 從輸入通道中讀取 5 個字節數據寫入緩衝區中,此時 position 移動設置爲 5,limit 保持不變。

 

 

 

③ 在將緩衝區的數據寫到輸出通道以前,須要先調用 flip() 方法,這個方法將 limit 設置爲當前 position,並將 position 設置爲 0。

 

 

 

④ 從緩衝區中取 4 個字節到輸出緩衝中,此時 position 設爲 4。

 

 

 

⑤ 最後須要調用 clear() 方法來清空緩衝區,此時 position 和 limit 都被設置爲最初位置。

 

 

1.4 文件 NIO 實例

如下展現了使用 NIO 快速複製文件的實例:

public static void fastCopy(String src, String dist) throws IOException {

 

    /* 得到源文件的輸入字節流 */

    FileInputStream fin = new FileInputStream(src);

 

    /* 獲取輸入字節流的文件通道 */

    FileChannel fcin = fin.getChannel();

 

    /* 獲取目標文件的輸出字節流 */

    FileOutputStream fout = new FileOutputStream(dist);

 

    /* 獲取輸出字節流的文件通道 */

    FileChannel fcout = fout.getChannel();

 

    /* 爲緩衝區分配 1024 個字節 */

    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

 

    while (true) {

 

        /* 從輸入通道中讀取數據到緩衝區中 */

        int r = fcin.read(buffer);

 

        /* read() 返回 -1 表示 EOF */

        if (r == -1) {

            break;

        }

 

        /* 切換讀寫 */

        buffer.flip();

 

        /* 把緩衝區的內容寫入輸出文件中 */

        fcout.write(buffer);

 

        /* 清空緩衝區 */

        buffer.clear();

    }

}

 

 

1.5 選擇器

NIO 經常被叫作非阻塞 IO,主要是由於 NIO 在網絡通訊中的非阻塞特性被普遍使用。

NIO 實現了 IO 多路複用中的 Reactor 模型,一個線程 Thread 使用一個選擇器 Selector 經過輪詢的方式去監聽多個通道 Channel 上的事件,從而讓一個線程就能夠處理多個事件。

經過配置監聽的通道 Channel 爲非阻塞,那麼當 Channel 上的 IO 事件還未到達時,就不會進入阻塞狀態一直等待,而是繼續輪詢其它 Channel,找到 IO 事件已經到達的 Channel 執行。

由於建立和切換線程的開銷很大,所以使用一個線程來處理多個事件而不是一個線程處理一個事件,對於 IO 密集型的應用具備很好地性能。

應該注意的是,只有套接字 Channel 才能配置爲非阻塞,而 FileChannel 不能,爲 FileChannel 配置非阻塞也沒有意義。

 

 

 

1. 建立選擇器

Selector selector = Selector.open();

2. 將通道註冊到選擇器上

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

 

通道必須配置爲非阻塞模式,不然使用選擇器就沒有任何意義了,由於若是通道在某個事件上被阻塞,那麼服務器就不能響應其它事件,必須等待這個事件處理完畢才能去處理其它事件,顯然這和選擇器的做用背道而馳。

在將通道註冊到選擇器上時,還須要指定要註冊的具體事件,主要有如下幾類:

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

它們在 SelectionKey 的定義以下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

 

能夠看出每一個事件能夠被當成一個位域,從而組成事件集整數。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

 

3. 監聽事件
int num = selector.select();

 

使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。

4. 獲取到達的事件

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

 

5. 事件循環

由於一次 select() 調用不能處理完全部的事件,而且服務器端有可能須要一直監聽事件,所以服務器端處理事件的代碼通常會放在一個死循環內。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}

 

1.6 套接字 NIO 實例

public class NIOServer {

 

    public static void main(String[] args) throws IOException {

 

        Selector selector = Selector.open();

 

        ServerSocketChannel ssChannel = ServerSocketChannel.open();

        ssChannel.configureBlocking(false);

        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

 

        ServerSocket serverSocket = ssChannel.socket();

        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);

        serverSocket.bind(address);

 

        while (true) {

 

            selector.select();

            Set<SelectionKey> keys = selector.selectedKeys();

            Iterator<SelectionKey> keyIterator = keys.iterator();

 

            while (keyIterator.hasNext()) {

 

                SelectionKey key = keyIterator.next();

 

                if (key.isAcceptable()) {

 

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

 

                    // 服務器會爲每一個新鏈接建立一個 SocketChannel

                    SocketChannel sChannel = ssChannel1.accept();

                    sChannel.configureBlocking(false);

 

                    // 這個新鏈接主要用於從客戶端讀取數據

                    sChannel.register(selector, SelectionKey.OP_READ);

 

                } else if (key.isReadable()) {

 

                    SocketChannel sChannel = (SocketChannel) key.channel();

                    System.out.println(readDataFromSocketChannel(sChannel));

                    sChannel.close();

                }

 

                keyIterator.remove();

            }

        }

    }

 

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

 

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        StringBuilder data = new StringBuilder();

 

        while (true) {

 

            buffer.clear();

            int n = sChannel.read(buffer);

            if (n == -1) {

                break;

            }

            buffer.flip();

            int limit = buffer.limit();

            char[] dst = new char[limit];

            for (int i = 0; i < limit; i++) {

                dst[i] = (char) buffer.get(i);

            }

            data.append(dst);

            buffer.clear();

        }

        return data.toString();

    }

}

public class NIOClient {

 

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1", 8888);

        OutputStream out = socket.getOutputStream();

        String s = "hello world";

        out.write(s.getBytes());

        out.close();

    }

}

 

1.7 內存映射文件

內存映射文件 I/O 是一種讀和寫文件數據的方法,它能夠比常規的基於流或者基於通道的 I/O 快得多。

向內存映射文件寫入多是危險的,只是改變數組的單個元素這樣的簡單操做,就可能會直接修改磁盤上的文件。修改數據與將數據保存到磁盤是沒有分開的。

下面代碼行將文件的前 1024 個字節映射到內存中,map() 方法返回一個 MappedByteBuffer,它是 ByteBuffer 的子類。所以,能夠像使用其餘任何 ByteBuffer 同樣使用新映射的緩衝區,操做系統會在須要時負責執行映射。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

1.8 對比

NIO 與普通 I/O 的區別主要有如下兩點:

  • NIO 是非阻塞的;
  • NIO 面向塊,I/O 面向流
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息