Java NIO Selector 介紹
1. 前言
前面小節(jié)介紹 Java TCP Socket 編程時,我們遇到了“創(chuàng)建的 TCP server 不能同時接收多個客戶端請求”的問題。我們的解決方案是采用多線程模型,即每線程模型或者是線程池模型。然而多線程模型會創(chuàng)建大量的線程,消耗大量系統(tǒng)資源。后來進(jìn)入了 Java NIO 編程的學(xué)習(xí),我們說編寫高性能服務(wù)器必須采用非阻塞式 Socket 編程。然而,通過 Java NIO 的編程示例可以發(fā)現(xiàn):相比阻塞式 Socket 編程,非阻塞式 Socket 編程的難度大了一個數(shù)量級。我們需要應(yīng)用好 I/O 多路事件處理機(jī)制,需要處理好數(shù)據(jù)收發(fā)的各種情況,而 I/O 事件多路復(fù)用機(jī)制是整個非阻塞 Socket 編程的核心。
其實,I/O 多路復(fù)用機(jī)制(I/O Multiplex)最早是由操作系統(tǒng)提供的,有一套專用的系統(tǒng) API。目前主流操作系統(tǒng)提供的 I/O 多路復(fù)用 API 如下:
- select,是通用機(jī)制,Windows、Unix-like 系統(tǒng)都支持。
- poll, 是 UNIX-like 系統(tǒng)支持。
- devpoll,是 SUN Solaris 系統(tǒng)支持。當(dāng)然,SUN 公司已經(jīng)不存在了。
- epoll, 是 Linux 系統(tǒng)支持的主流機(jī)制。
- Kqueue,是 freebsd 內(nèi)核支持的機(jī)制,Mac OS、IOS 系統(tǒng)也支持。
- IOCP,是 Windows 系統(tǒng)支持的機(jī)制。
對于 Java 來說,也有自己的 I/O 多路復(fù)用機(jī)制,那就是 Java NIO Selector。
2. Java NIO Selector 工作原理
Java NIO 四個核心的組件分別是 Selector、SocketChannel、ServerSocketChannel、SelectionKey。Selector 是 I/O 事件反應(yīng)器,是動力源。SocketChannel、ServerSocketChannel、SelectionKey 都是功能組件,它們之間互相配合,如下:
- 首先創(chuàng)建一個 Selector 對象,然后調(diào)用它的 select 方法,進(jìn)入事件等待狀態(tài)。
- 對于服務(wù)器來說,需要創(chuàng)建 ServerSocketChannel 對象,然后調(diào)用它的 register 方法,將 SelectionKey.OP_ACCEPT 事件注冊到 Selector,準(zhǔn)備監(jiān)聽新的客戶端連接。
- 如果 Selector 監(jiān)聽到新的客戶端連接請求,SelectionKey.OP_ACCEPT 事件就會產(chǎn)生。調(diào)用 ServerSocketChannel 的 accept 方法,返回一個 SocketChannel 對象,需要將 SocketChannel 的 SelectionKey.OP_READ 事件注冊到 Selector。
- 在上面兩步中, ServerSocketChannel 和 SocketChannel 都提供了 register 方法,返回值是 SelectionKey。SelectionKey 中綁定了上下文信息。
- 如果 Selector 監(jiān)聽到 I/O 事件,它的 select 方法就會返回??梢哉{(diào)用 Selector 的 selectedKeys 方法,返回一個 SelectionKey 數(shù)組,包含了所有產(chǎn)生了 I/O 事件的 SocketChannel。遍歷這個數(shù)組,逐個處理相應(yīng)的 I/O 事件。
3. Java Selector API 介紹
3.1 創(chuàng)建實例
Selector 只聲明了一個 protected 構(gòu)造方法,構(gòu)造 Selector 實例只能通過它的工廠方法 open,聲明如下:
public static Selector open() throws IOException
3.2 注冊事件
Selector 接口并沒有聲明 register 方法,而是通過它的抽象實現(xiàn)類提供了一個 abstract protected 方法,也沒有對外暴露。聲明如下:
protected abstract SelectionKey register(AbstractSelectableChannel ch,int ops, Object att);
Selector 的 register 機(jī)制最終委派給了 AbstractSelectableChannel 類。為此,我們要想將 Channel 注冊到 Selector,需要調(diào)用 AbstractSelectableChannel 的 register 方法。聲明如下:
public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException
參數(shù)說明:
- sel 是預(yù)先創(chuàng)建的 Selector 對象。
- ops 表示需要注冊的具體事件。支持的事件類型如下:
- SelectionKey.OP_ACCEPT 表示監(jiān)聽客戶端的連接,用于服務(wù)器 - SelectionKey.OP_CONNECT 表示非阻塞式客戶端連接過程,用于客戶端 - SelectionKey.OP_READ 表示監(jiān)聽讀事件 - SelectionKey.OP_WRITE 表示監(jiān)聽寫事件
- att 用于保存上下文對象。
3.3 監(jiān)聽事件
Selector 提供了 select 方法用于監(jiān)聽 I/O 事件,聲明如下:
public abstract int select() throws IOException
public abstract int select(long timeout) throws IOException
當(dāng)沒有 I/O 事件產(chǎn)生時,調(diào)用 select 方法的線程會被阻塞。如果你調(diào)用無參 select 方法,線程進(jìn)入等待狀態(tài),直到有 I/O 事件發(fā)生才返回。如果你調(diào)用包含了 timeout 參數(shù)的 select 方法,線程會在 timeout 超時,或者是有 I/O 事件發(fā)生返回。select 的返回值表示產(chǎn)生了 I/O 事件的 SelectionKey 的個數(shù)。
3.4 遍歷事件
當(dāng)有 I/O 事件發(fā)生,Selector 的 select 方法會返回??梢酝ㄟ^ Selector 的 selectedKeys 方法,獲取所有產(chǎn)生了 I/O 事件的 SelectionKey。聲明如下:
public abstract Set<SelectionKey> selectedKeys()
方法的返回值是一個 SelectionKey 類型的集合,我們需要遍歷此集合,逐個處理。遍歷的方法如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key != null) {
if (key.isAcceptable()) {
// ServerSocketChannel 接收了一個新連接
} else if (key.isConnectable()) {
// 表示一個新連接建立
} else if (key.isReadable()) {
// Channel 有準(zhǔn)備好的數(shù)據(jù),可以讀取
} else if (key.isWritable()) {
// Channel 有空閑的 Buffer,可以寫入數(shù)據(jù)
}
}
keyIterator.remove();
}
3.5 SelectionKey 介紹
SelectionKey 是由 AbstractSelectableChannel 的 register 方法返回的,主要包含一個事件類型和上下文對象。SelectionKey 提供了一組方法,用以識別 I/O 事件類型。聲明如下:
public final boolean isAcceptable()
public final boolean isConnectable()
public final boolean isReadable()
public final boolean isWritable()
可以通過 SelectionKey 的 channel 方法,獲取關(guān)聯(lián)的 Channel,聲明如下:
public abstract SelectableChannel channel()
可以通過 SelectionKey 的 attachment 方法,獲取關(guān)聯(lián)的上下文對象。
public final Object attachment()
SelectionKey 的各個方法相對簡單,容易理解,我們在前面小節(jié)多次提到,不再贅述。
4. 總結(jié)
本小結(jié)主要是介紹 Java NIO Selector 機(jī)制的工作原理。關(guān)于 Selector 機(jī)制,不僅 Java 支持,各大操作系統(tǒng)都有支持,是編寫高性能服務(wù)器的利器。一般在依賴倒轉(zhuǎn)模型中,充當(dāng)動力源、反應(yīng)器的角色。