Java NIO - Channel 與 Selector

LZC發表於2020-06-16

Java NIO有三個核心的元件:Buffer、Channel和Selector。

在上一篇文章中,我們已經介紹了Buffer,這篇文章主要介紹剩下兩個元件:Channel和Selector。

Selector與Channel的關係

Channel翻譯過來是“通道”的意思,所有的Java NIO都要經過Channel。一個Channel物件其實就對應了一個IO連線。Java NIO中主要有以下Channel實現:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

分別用於處理檔案IO、UDP、TCP客戶端、TCP服務端。

這裡以ServerSocketChannel和SocketChannel為例,介紹一些常用的方法。

// server:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080));
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
int readBytes = socketChannel.read(buffer);
if (readBytes > 0) {
    buffer.flip();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    String body = new String(bytes, StandardCharsets.UTF_8);
    System.out.println("server 收到:" + body);
}

對於服務端來說,先用open方法建立一個物件,然後使用bind方法繫結埠。

繫結以後,使用accept方法等待新的連線進來,這個方法是阻塞的。一旦有了新的連線,才會解除阻塞。再次呼叫可以阻塞等待下一個連線。

與Buffer配合,使用read方法可以把資料從Channel讀到Buffer裡面,然後做後續處理。

// Client:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.put("hi, 這是client".getBytes(StandardCharsets.UTF_8));
buffer.flip();
socketChannel.write(buffer);

對於客戶端來說,有一些微小的區別。客戶端不需要bind監聽埠,而是直接connect去嘗試連線服務端。

同樣與Buffer配合,Channel使用write方法可以把資料從Buffer寫到Channel裡,然後後續就可以做網路傳輸了。

Selector翻譯過來叫做“選擇器”,Selector允許一個執行緒處理多個Channel。Selector的應用場景是:如果你的應用開啟了多個連線(Channel),但每個連線的流量都很低。比如:聊天伺服器或者HTTP伺服器。

使用Selector很簡單。使用open方法建立一個Selector物件,然後把Channel註冊到Selector上。

// 建立一個Selector
Selector selector = Selector.open();

// 把一個Channel註冊到Selector
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);

需要注意的是,一定要使用configureBlocking(false)把Channel設定成非阻塞模式,否則會丟擲IllegalBlockingModeException異常。

Channel的register有兩個過載方法:

SelectionKey register(Selector sel, int ops) {
    return register(sel, ops, null);
}
SelectionKey register(Selector sel, int ops, Object att);

對於ops引數,即selector要關心這個Channel的事件型別,在SelectionKey類裡面有這樣幾個常量:

  • OP_READ 可以從Channel讀資料
  • OP_WRITE 可以寫資料到Channel
  • OP_CONNECT 連線上了伺服器
  • OP_ACCEPT 有新的連線進來了

如果你對不止一種事件感興趣,使用或運算子即可,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

需要注意的是,FileChannel只有阻塞模式,不支援非阻塞模式,所以它是沒有register方法的!

第三個引數attattachment的縮寫,代表可以傳一個“附件”進去。在返回的SelectionKey物件裡面,可以獲取以下物件:

  • channel():獲取Channel
  • selector():獲取Selector
  • attachment():獲取附件
  • attach(obj):更新附件

除此之外,還有一些判斷當前狀態的方法:

  • isReadable()
  • isWritable()
  • isConnectable()
  • isAcceptable()

一般來說,我們很少直接使用單個的SelectionKey,而是從Selector裡面輪詢所有的SelectionKey,比如:

輪詢

 while (selector.select() > 0) {
     Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
     while(keyIterator.hasNext()) {
         SelectionKey key = keyIterator.next();
         if (key.isReadable()) {
             SocketChannel socketChannel = (SocketChannel) key.channel();
             // read
         } else if(key.isAcceptable()) {
             // accept
         }
         // 其它條件
         keyIterator.remove();
     }
 }

Selector可以返回兩種SelectionKey集合:

  • keys():已註冊的鍵的集合
  • selectedKeys():已選擇的鍵的集合

並不是所有註冊過的鍵都仍然有效,有些可能已經被cancel()方法被呼叫過的鍵。所以一般來說,我們輪詢selectedKeys()方法。

以下是一個完整的Server-Client Demo:

Server:

public class Server {
    public static void main(String[] args) {
        try (
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                Selector selector = Selector.open();
        ) {
            serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080));
            serverSocketChannel.configureBlocking(false);
            System.out.println("server 啟動...");
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (selector.select() > 0) {
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while(keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isReadable()) {
                        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        int readBytes = socketChannel.read(buffer);
                        if (readBytes > 0) {
                            buffer.flip();
                            byte[] bytes = new byte[buffer.remaining()];
                            buffer.get(bytes);
                            String body = new String(bytes, StandardCharsets.UTF_8);
                            System.out.println("server 收到:" + body);
                        }
                    } else if(key.isAcceptable()) {
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    keyIterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client:

public class Client {
    public static void main(String[] args) {
        try (
                SocketChannel socketChannel = SocketChannel.open();
        ) {
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
            System.out.println("client 啟動...");

            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            buffer.put("hi, 這是client".getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            socketChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

作者:公眾號_xy的技術圈

連結:www.imooc.com/article/289136

來源:慕課網

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章