Java NIO Buffer, Channel 及 Selector

更多 Java IO & NIO方面的文章,請參見文集《Java IO & NIO》


Java IO VS NIO

  • JDK 1.4 之前,java.io 包,面向流的I/O系統(tǒng)(字節(jié)流或者字符流)
    • 系統(tǒng)一次處理一個(gè)字節(jié)
    • 速度慢
  • JDK 1.4 提供,java.nio 包,面向塊的I/O系統(tǒng)
    • 系統(tǒng)一次處理一個(gè)塊
    • 速度快

Buffer 緩沖區(qū)

緩沖區(qū)實(shí)際上是一個(gè)容器對象,更直接的說,其實(shí)就是一個(gè)數(shù)組。
在 NIO 庫中,所有數(shù)據(jù)都是用緩沖區(qū)處理的:

  • 在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的;
  • 在寫入數(shù)據(jù)時(shí),它也是寫入到緩沖區(qū)中的;

在 NIO 中,所有的緩沖區(qū)類型都繼承于抽象類 Buffer。常見的緩沖區(qū) Buffer 包括:

  • ByteBuffer 存儲(chǔ)了字節(jié)數(shù)組 final byte[] hb;
  • CharBuffer final char[] hb;
    • ByteBuffer 與 CharBuffer 之間的轉(zhuǎn)換需要使用字符集 Charset
    • Charset 具體使用,參見 Java Charset 字符集
  • ShortBuffer final short[] hb;
  • IntBuffer final int[] hb;
  • LongBuffer final long[] hb;
  • FloatBuffer final float[] hb;
  • DoubleBuffer final double[] hb;

Buffer 類的屬性:

  • private int mark = -1; 記錄一個(gè)標(biāo)記位置
  • private int position = 0;

A buffer's <i>position</i> is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.
當(dāng)前操作的位置

  • private int limit;

A buffer's <i>limit</i> is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.
可以存放的元素的個(gè)數(shù)

  • private int capacity;

A buffer's <i>capacity</i> is the number of elements it contains. The capacity of a buffer is never negative and never changes.
數(shù)組容量

  • 大小關(guān)系:mark <= position <= limit <= capacity

Buffer 類的方法:

  • allocate(int capacity) 分配一個(gè)緩沖區(qū),默認(rèn) limit = capacity
  • put() 在當(dāng)前位置添加元素
  • get() 得到當(dāng)前位置的元素
  • clear() 將 Buffer 從 讀模式 切換到 寫模式 (該方法實(shí)際不會(huì)清空原 Buffer 的內(nèi)容)
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
  • flip() 將 Buffer 從 寫模式 切換到 讀模式
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

** clear() VS flip()**:

  • 在寫模式下,Buffer 的 limit 表示你最多能往 Buffer 里寫多少數(shù)據(jù)。
    • 因此寫之前,調(diào)用 clear(),使得 limit = capacity;
  • 在讀模式時(shí),Buffer 的 limit 表示你最多能從 Buffer 里讀多少數(shù)據(jù)。
    • 因此讀之前,調(diào)用 flip(),使得 limit = position;

IntBuffer 的使用:

public static void main(String[] args) throws Exception {
    // 創(chuàng)建 int 緩沖區(qū) capacity 為 4
    // 默認(rèn) limit = capacity
    IntBuffer buffer = IntBuffer.allocate(4);
    System.out.println("Capacity & Limit: " + buffer.capacity() + " " + buffer.limit());

    // 往 Buffer 中寫數(shù)據(jù)
    buffer.put(11);
    buffer.put(22);
    buffer.put(33);
    buffer.put(44);

    System.out.println("Position: " + buffer.position());

    // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
    buffer.flip();

    while (buffer.hasRemaining()) {
        System.out.print(buffer.get() + "  ");
    }
}

輸出:

Capacity & Limit: 4 4
Position: 4
11 22 33 44

Channel 通道

  • Java NIO 的核心概念,表示的是對支持 I/O 操作的實(shí)體的一個(gè)連接
  • 通過它可以讀取和寫入數(shù)據(jù)(并不是直接操作,而是通過 Buffer 來處理)
  • 雙向的

常用的 Channel 包括:

  • FileChannel 從文件中讀寫數(shù)據(jù)
  • DatagramChannel 從 UDP 中讀寫數(shù)據(jù)
  • SocketChannel 從 TCP 中讀寫數(shù)據(jù)
  • ServerSocketChannel 監(jiān)聽新進(jìn)來的 TCP 連接,每一個(gè)新進(jìn)來的連接都會(huì)創(chuàng)建一個(gè) SocketChannel。

FileChannel 連接到文件的通道

FileChannel 無法設(shè)置為非阻塞模式,只能運(yùn)行在阻塞模式下
常用方法:

  • int read(ByteBuffer dst) 從 Channel 中讀取數(shù)據(jù),寫入 Buffer
  • int write(ByteBuffer src) 從 Buffer 中讀取數(shù)據(jù),寫入 Channel
  • long size() 得到 Channel 中文件的大小
  • long position() 得到 Channel 中文件的當(dāng)前操作位置
  • FileChannel position(long newPosition) 設(shè)置 Channel 中文件的當(dāng)前操作位置

使用 FileChannel 來復(fù)制文件的例子:

public static void main(String[] args) throws Exception {
    // 通過 InputStream 或者 OutputStream 來構(gòu)造 FileChannel
    FileChannel in = new FileInputStream("a.txt").getChannel();
    FileChannel out = new FileOutputStream("b.txt").getChannel();

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    // 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
    while(in.read(buffer) != -1) {
        // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
        buffer.flip();
        // 從 Buffer 中讀數(shù)據(jù),寫入到 channel
        out.write(buffer);
        // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
        buffer.clear();
    }

    // 或者使用如下代碼
    // out.transferFrom(in, 0, in.size());
}

SocketChannel 連接到 TCP 套接字的通道

SocketChannel 可以設(shè)置為阻塞模式或非阻塞模式
使用 SocketChannel 來建立 TCP 連接,發(fā)送并接收數(shù)據(jù),默認(rèn)使用 阻塞模式

public static void main(String[] args) throws Exception {
    // 打開 SocketChannel
    SocketChannel channel = SocketChannel.open();
    // connect 方法會(huì)阻塞,直至連接建立成功
    channel.connect(new InetSocketAddress("127.0.0.1", 8080));

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    // 發(fā)送數(shù)據(jù)
    String msg = "This is client.";
    // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
    buffer.clear();
    buffer.put(msg.getBytes());

    // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
    buffer.flip();
    channel.write(buffer);

    // 接收數(shù)據(jù)
    // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
    buffer.clear();

    // 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
    channel.read(buffer);

    // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
    buffer.flip();

    // 從 Buffer 中讀數(shù)據(jù)
    while (buffer.hasRemaining()) {
        System.out.print(buffer.get());
    }
}

使用 SocketChannel 的 非阻塞模式 來建立 TCP 連接,發(fā)送并接收數(shù)據(jù):

public static void main(String[] args) throws Exception {
    // 打開 SocketChannel
    SocketChannel channel = SocketChannel.open();

    channel.configureBlocking(false);
    channel.connect(new InetSocketAddress("127.0.0.1", 8080));

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    while (!channel.finishConnect()) {
        // 發(fā)送數(shù)據(jù)
        String msg = "This is client.";
        // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
        buffer.clear();
        buffer.put(msg.getBytes());

        // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
        buffer.flip();
        channel.write(buffer);

        // 接收數(shù)據(jù)
        // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
        buffer.clear();

        // 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
        channel.read(buffer);

        // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
        buffer.flip();

        // 從 Buffer 中讀數(shù)據(jù)
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
    }
}

ServerSocketChannel 監(jiān)聽 TCP 連接的通道

ServerSocketChannel 可以設(shè)置為阻塞模式或非阻塞模式
使用 ServerSocketChannel 來監(jiān)聽 TCP 連接,默認(rèn)使用 阻塞模式

public static void main(String[] args) throws Exception {
    // 打開 SocketChannel
    ServerSocketChannel channel = ServerSocketChannel.open();
    // 綁定端口
    channel.socket().bind(new InetSocketAddress(8080));

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    while (true) {
        // accept 方法會(huì)阻塞,直至監(jiān)聽到 TCP 連接
        SocketChannel socketChannel = channel.accept();
        System.out.println("A new connection...");

        // 接收數(shù)據(jù)
        // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
        buffer.clear();

        // 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
        socketChannel.read(buffer);

        // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
        buffer.flip();

        // 從 Buffer 中讀數(shù)據(jù)
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }

        // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
        // 發(fā)送數(shù)據(jù)
        String msg = "This is server.";
        // 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
        buffer.clear();
        buffer.put(msg.getBytes());

        // 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
        buffer.flip();
        socketChannel.write(buffer);
    }
}

Selector 選擇器

Selector 允許單個(gè)進(jìn)程可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的 IO,即監(jiān)聽多個(gè)端口的 Channel

關(guān)于 IO 模式,參見 Linux IO 模型 中對多路復(fù)用 IO Multiplexing IO 的說明。

引用:


多路復(fù)用 IO Multiplexing IO

  • 單個(gè)進(jìn)程可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的 IO,即監(jiān)聽多個(gè)端口的 IO
  • 適用于連接數(shù)很高的情況
  • 實(shí)現(xiàn)方式:select,poll,epoll 系統(tǒng)調(diào)用
    • 注冊多個(gè)端口的監(jiān)聽 Socket,比如 8080,8081
    • 當(dāng)用戶進(jìn)程調(diào)用 select 方法后,整個(gè)用戶進(jìn)程被阻塞,OS 內(nèi)核會(huì)監(jiān)聽所有注冊的 Socket
    • 當(dāng)任何一個(gè)端口的 Socket 中的數(shù)據(jù)準(zhǔn)備好了( 8080 或者 8081),select 方法就會(huì)返回
    • 隨后用戶進(jìn)程再調(diào)用 read 操作,將數(shù)據(jù)從 OS 內(nèi)核緩存區(qū)拷貝到應(yīng)用程序的地址空間。
  • 多路復(fù)用 IO 類似于 多線程結(jié)合阻塞 IO
    • 要實(shí)現(xiàn)監(jiān)聽多個(gè)端口的 IO,還可以通過多線程的方式,每一個(gè)線程負(fù)責(zé)監(jiān)聽一個(gè)端口的 IO
    • 如果處理的連接數(shù)不是很高的話,使用 多路復(fù)用 IO 不一定比使用 多線程結(jié)合阻塞 IO 的服務(wù)器性能更好,可能延遲還更大
    • 多路復(fù)用 IO 的優(yōu)勢并不是對于單個(gè)連接能處理得更快,而是在于能處理更多的連接

Selector 使用步驟:

  • 創(chuàng)建 Selector
  • 創(chuàng)建 Channel,可以創(chuàng)建多個(gè) Channel,即監(jiān)聽多個(gè)端口,比如 8080,8081
  • 將 Channel 注冊到 Selector 中
    • 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的, 即 channel.configureBlocking(false);
    • 因此 FileChannel 是不能夠使用 Selector 的, 因?yàn)?FileChannel 都是阻塞的
    • 注冊時(shí),需要指定了對 Channel 的什么事件感興趣,包括:
      • SelectionKey.OP_CONNECT:TCP 連接 static final int OP_CONNECT = 1 << 3;
      • SelectionKey.OP_ACCEPT:確認(rèn) static final int OP_ACCEPT = 1 << 4;
      • SelectionKey.OP_READ:讀 static final int OP_READ = 1 << 0;
      • SelectionKey.OP_WRITE:寫 static final int OP_WRITE = 1 << 2;
      • 可以使用或運(yùn)算 | 來組合,例如 SelectionKey.OP_READ | SelectionKey.OP_WRITE
    • register 方法返回一個(gè) SelectionKey 對象,包括:
      • int interestOps():調(diào)用 register 注冊 channel 時(shí)所設(shè)置的 interest set.
      • int readyOps():Channel 所準(zhǔn)備好了的操作
        • selectionKey.isAcceptable();
        • selectionKey.isConnectable();
        • selectionKey.isReadable();
        • selectionKey.isWritable();
      • public abstract SelectableChannel channel();: 得到 Channel
      • public abstract Selector selector();:得到 Selector
      • public final Object attachment:得到附加對象
  • 不斷重復(fù):
    • 調(diào)用 Selector 對象的 select() 方法,該方法會(huì)阻塞,直至注冊的事件發(fā)生
    • 事件發(fā)生,調(diào)用 Selector 對象的 selectedKeys() 方法獲取 selected keys
    • 遍歷每個(gè) selected key:
      • 從 selected key 中獲取對應(yīng)的 Channel 并處理
      • 在 OP_ACCEPT 事件中, 從 key.channel() 返回的是 ServerSocketChannel
      • 在 OP_WRITE 和 OP_READ 事件中, 從 key.channel() 返回的是 SocketChannel
  • 關(guān)閉 Selector

示例:

public static void main(String args[]) throws Exception {
    // 創(chuàng)建 Selector
    Selector selector = Selector.open();

    // 創(chuàng)建 Server Socket,監(jiān)聽端口 8080
    ServerSocketChannel serverChannel1 = ServerSocketChannel.open();
    serverChannel1.socket().bind(new InetSocketAddress(8080));
    // 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的
    serverChannel1.configureBlocking(false);

    // 創(chuàng)建 Server Socket,監(jiān)聽端口 8081
    ServerSocketChannel serverChannel2 = ServerSocketChannel.open();
    serverChannel2.socket().bind(new InetSocketAddress(8081));
    // 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的
    serverChannel2.configureBlocking(false);

    // 將 Channel 注冊到 Selector 中
    serverChannel1.register(selector, SelectionKey.OP_ACCEPT);
    serverChannel2.register(selector, SelectionKey.OP_ACCEPT);

    // 不斷重復(fù)
    while (true) {
        // 調(diào)用 Selector 對象的 select() 方法,該方法會(huì)阻塞,直至注冊的事件發(fā)生
        selector.select();

        // 事件發(fā)生,調(diào)用 Selector 對象的 selectedKeys() 方法獲取 selected keys
        Iterator<SelectionKey> it = selector.selectedKeys().iterator();

        // 遍歷每個(gè) selected key:
        while (it.hasNext()) {
            SelectionKey key = it.next();

            if (key.isAcceptable()) {
                // 在 OP_ACCEPT 事件中, 從 key.channel() 返回的是 ServerSocketChannel
                ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();

                // 調(diào)用 accept 方法獲取 TCP 連接 SocketChanne
                SocketChannel clientChannel = serverChannel.accept();
                clientChannel.configureBlocking(false);

                // 注冊 SocketChannel
                clientChannel.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE);

                System.out.println("Accept event");
            }

            if (key.isReadable()) {
                // 在 OP_WRITE 和 OP_READ 事件中, 從 key.channel() 返回的是 SocketChannel
                SocketChannel clientChannel = (SocketChannel) key.channel();
                System.out.println("Read event");
                // 可以從 clientChannel 中讀數(shù)據(jù),通過 ByteBuffer
                // TO DO
            }

            if (key.isWritable()) {
                // 在 OP_WRITE 和 OP_READ 事件中, 從 key.channel() 返回的是 SocketChannel
                SocketChannel clientChannel = (SocketChannel) key.channel();
                System.out.println("Write event");
                // 可以向 clientChannel 中寫數(shù)據(jù),通過 ByteBuffer
                // TO DO
            }
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容