Java NIO 通道

一、通道基礎

通道(Channel)是 java.nio 的第二個主要創新。他們既不是一個擴展也不是一個增強,而是全新、極好的Java IO 示例,提供與 IO 服務的直接連接。Channel 用於在字節緩衝區和位於通道另一側的實體(通常是一個文件或套接字)之間有效地數據傳輸。

通道可以形象地比喻爲銀行出納窗口使用的氣動導管。你的薪水支票就是你要傳送的信息,載體(Carrier)就好比一個緩衝區。你先填充緩衝區(將你的支票放到載體上),接着將緩衝寫到通道中(將載體丟進導管中),然後信息負載就被傳遞到通道另一側 IO 服務。

通道是一種途徑,藉助該途徑,可以用最小的總開銷來訪問操作系統本身的 IO 服務。緩衝區則是通道內部用來發送和接收數據的端點。

與緩衝區不同,通道 API 主要由接口指定。不同的操作系統上實現(ChannelImplementation)會有根本性的差異,所以通道 API 僅僅描述了可以做什麼。因此很自然的,通道實現經常使用操作系統本地代碼。通道接口允許你以一種受控且可移植的方式來訪問底層 IO 服務。

你可以從頂層的 Channel 接口看到,對所有通道來說只有兩種共同的操作:檢查一個通道是否打開(IsOpen())和關閉一個打開的通道(close())。所有有趣的東西都是那些實現Channel接口以及它的子接口的類。

通道是訪問 IO 服務的導管。IO 可以分爲廣義的兩大類別:File IO 和 Stream IO。那麼相應的有兩種類型的通道也就不足爲怪。他們是文件(file)通道和套接字(socket)通道。你會發現有一個 FileChannel 類和三個 socket 通道類:SocketChannel、ServerSocketChannel 和 DatagramChannel。

通道可以以多種方式創建。Socket 通道有可以直接創建新 socket 通道的工廠方法。但是一個 FileChannel 對象卻只能通過一個打開的 RandomAccessFile、FileInputStream 或 FileOutputStream 對象上調用 getChannel() 方法來獲取。你不能直接創建一個 FileChannel 對象。

使用通道

通道是可以單向(undirectional)或者雙向的(bidirectional)。一個 channel 類可能實現定義 read() 方法的 ReadableByteChannel 接口,而另一個 channel 類也去實現 WritableByteChannel 接口已提供 write() 方法。實現這兩種接口其中之一的類都是單向的,只能在一個方向上傳輸數據。如果一個類同時實現這兩個接口,那麼它是雙向的,可以雙向傳輸數據。

通道會連接一個特定的 IO 服務且通道實例(channel instance)的性能受它所連接的 IO 服務的特徵限制,記住這很重要。一個連接到只讀文件的 Channel 實例不能進行寫操作,即使該實例所屬的類可能有 write() 方法。基於此,程序員需要知道通道是如何打開的,避免試圖嘗試一個底層 IO 服務不允許的操作。

通道可以以阻塞(blocking)或非阻塞(nonblocking)模式運行。非阻塞模式的通道永遠不會讓調用的線程休眠。請求的操作要麼立即完成,要麼返回一個結果表明未進行任何操作。只有面向流(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。

關閉通道

通過調用通道的 close 方法進行關閉,但是可能會導致關閉底層IO服務時發生阻塞(非阻塞模式和阻塞模式都一樣)。通過 isopen 方法來測試通道的開放狀態,如果返回 true,那麼說明通道可以使用。反之,說明通道已經關閉,不能使用。

二、FileChannel 類

文件通道總是阻塞式的,因此不能被置於非阻塞模式。現代操作系統都有複雜的緩存和預取機制,使得本地磁盤 IO 操作延遲很少。網絡文件系統一般而言延遲會多些,不過卻也因該優化而受益。面向流的 IO 的非阻塞範例對於面向文件的操作並無多大意義。這是由文件 IO 本質上的不同性質造成的。對於文件 IO,最強大之處在於異步 IO(asynchronous IO),它允許一個進程可以從操作系統請求一個或多個 IO 操作而不必等待這些操作的完成。發起請求的進程之後會收到它請求的 IO 操作已完成的通知。異步 IO 是一種高級性能,當前的很多操作系統都還不具備。

文件通道創建

FileChannel 對象不能直接創建。一個FileChannel 實例只能通過一個打開的 file 對象(RandomAccessFile、FileInputStream 或 FileOutputStream)上調用 getChannel() 方法來獲取。調用 getChannel() 方法會返回一個連接到相同文件的 FileChannel 對象且該 FileChannel 對象具有與 file 對象相同的訪問權限,然後你就可以使用該通道對象來利用強大的 FileChannel API 了。

FileChannel 對象是線程安全(thread-safe)的。多個進程可以在同一個實例上併發調用方法而不會引起任何問題,不過並非所有操作都是多線程的(multithreaded)。影響通道位置或者影響文件大小的操作都是單線程的(single-threaded)。如果有一個線程已經執行會影響通道位置或者文件大小的操作,那麼其他嘗試進行此類操作之一的線程必須等待。併發行爲也會受到底層操作系統或文件系統影響。

同大多數 IO 相關的類一樣,FileChannel 是一個反應 Java 虛擬機外部一個具體對象的抽象。FileChannel 類保證同一個 Java 虛擬機上所有實例看到的某個文件的視圖均是一致的,但是 Java 虛擬機卻不能對超出它控制範圍的因素提供擔保。通過一個 FileChannel 實例看到的某個文件的視圖同通過一個外部的非 Java 進程看到的該文件的視圖可能一致,也可能不一致。多個進程發起的併發文件的語義高度取決於底層的操作系統和(或)文件系統。一般而言,由運行在不同 Java 虛擬機上的 FileChannel對象發起的對某個文件的併發訪問和由非 Java 進程發起的對該文件的併發訪問是一致的。

訪問文件

在通道這塊我們可以使用 FileChannel 的 read 和 write 方法進行文件的訪問,以及配合 position() 進行文件操作。

FileChannel 位置(position)是從底層的文件描述符獲得的,該 position 同時被作爲通道引用獲取來源的文件對象共享。這也就意味着一個對象對該 position 的更新可以被另一個對象看到。

position 能夠決定文件中哪一處的數據接下來將被讀或者寫。類似於緩衝區的 get() 和 put() 方法,當字節被 read() 或 write() 方法傳輸時,文件 position 會自動更新。如果 position 值達到了文件大小的值(文件大小的值可以通過 size() 方法返回),read() 方法返回一個文件尾條件值(-1)。可是,不同於緩衝區的是,如果實現 write() 方法時, position 前進到超過文件大小的值,該文件會擴展以容納新寫入的字節。

三、Socket 通道

新的 socket 通道類可以運行非阻塞模式並且是可選擇的。這兩個性能可以激活大程序(如網絡服務器和中間件組件)巨大的可伸縮性和靈活性。本節中我們會看到,再也沒有爲每個 socket 連接使用一個線程的必要了,也避免了管理大量線程所需的上下文交換總開銷。藉助新的 NIO 類,一個或幾個線程就可以管理成百上千的活動 socket 連接了,並且只有很少甚至可能沒有性能損失。

全部 socket 通道類(DatagramChannel、SocketChannel 和 ServerSocketChannel)在被實例化時都會創建一個對等 socket 對象。這些是我們所熟悉的來自 java.net 的類(Socket、ServerSocket 和 DatagramSocket),它們已經被更新已識別通道。對等 socket 可以通過調用 socket() 方法從一個通道上獲取。此外,這三個 java.net 類現在都有 getChannel() 方法。

雖然每個 socket 通道(在 java.nio.channels 包中)都有一個關聯的 java.net socket 對象,卻並非所有的 socket 都有一個關聯的通道。如果用傳統方式(直接實例化)創建一個 socket 對象,它就不會有關聯的 SocketChannel 並且它的 getChannel() 方法將總是返回 null。

非阻塞模式

Socket 通道可以在非阻塞模式下運行。這個說法雖然簡單卻有着深遠的含義。傳統 Java socket 的阻塞性質性質曾經是 Java 程序可伸縮性的最重要制約之一。非阻塞 IO 是許多複雜的、高性能的程序構建的基礎。

設置或重新設置一個通道的阻塞模式是很簡單的,只要調用 configureBlocking() 方法即可,傳遞參數值爲 true 則設爲阻塞模式,參數值爲 false 值設爲非阻塞模式。可以通過調用 isBlocking() 方法來判斷某個 socket 通道當前處於哪種模式。

服務器端的使用經常會考慮到非阻塞 socket 通道,因爲它們使管理很多 socket 通道變得更容易。但是,在客戶端使用一個或幾個非阻塞模式的 socket 通道也是有益處的,例如,在客戶端使用一個或幾個非阻塞模式的 socket 通道也是有益處的,例如,藉助非阻塞 socket 通道,GUI 程序可以傳與用戶請求並且同時維護與一個或多個服務器的會話。在很多程序上,非阻塞模式都是有用的。

ServerSocketChannel

它是一個基於通道的 socket 監聽器。它同我們所熟悉的 java.net.ServerSocket 執行相同的基本任務,不過它增加了通道語義,因此能夠在非阻塞模式下運用靜態的 open() 工廠方法創建一個新的 ServerSocketChannel 對象,將會返回同一個未綁定的 java.net.ServerSocket 關聯的通道。該對等 ServerSocket 可以通過在返回的 ServerSocketChannel 上調用 socket() 方法來獲取。作爲 ServerSocketChannel 的對等體被創建的 ServerSocket 對象依賴通道實現。這些 socket 關聯的 SocketImpl 能識別通道。

通道不能被封裝在隨意的 socket 對象外面。由於ServerSocketChannel 沒有 bind() 方法,因此有必要取出對等的 socket 並使用它來綁定到一個端口以開始監聽連接。我們也是使用對等 ServerSocket 的 API 來根據需要設置其他的 socket 選項。

和 java.net.ServerSocket 一樣,ServerSocektChannel 也有 socket() 方法。一旦創建了一個 ServerSocketChannel 並用對等 socket 幫i當了它,就可以在其中一個上調用 accept()。如果選擇在 ServerSocekt 上調用 accept() 方法,那麼它會同任何其他的 ServerSocket 表現一樣的行爲:總是阻塞並返回一個 java.net.Socket 對象。如果選擇在 ServerSocketChannel 上調用 accept() 方法則會返回 SocketChannel 類型的對象,返回的對象能夠在非阻塞模式下運行。假設系統已經有一個安全管理器(security manager),兩種形式的方法調用都執行相同的安全檢查。

如果以非阻塞模式被調用,當沒有傳入連接在等待時,ServerSocketChannel.accept() 會立即返回 null。正是這種檢查連接而不阻塞的能力實現了可伸縮性並降低了複雜性。可選擇性也因此得到實現。我們可以使用一個選擇器實例來註冊一個 ServerSocektChannel 對象以實現新連接到達時自動通知的功能。

SocketChannel

Socket 和 SocketChannel 類封裝點對點、有序的網絡連接,類似於我們所熟知並喜愛的 TCP/IP 連接。SocketChannel 扮演客戶端發起同一個監聽服務器的連接。直到連接成功,它才能收到數據並且只會從連接到的地址接收。

每個 SocketChannel 對象創建時都是同一個對等的 java.net.Socket 對象串聯的。靜態的 open() 方法可創建一個新的 SocketChannel 對象,而在新創建的 SocketChannel 上調用 socket() 方法能返回它對等的 Socket 對象;在該 Socket 上調用 getChannel() 方法則能返回最初的哪個 SocketChannel。

新創建的 SocketChannel 雖已打開卻是未連接的。在一個未連接的 SocketChannel 對象上嘗試一個 I/O 操作會導致 NotYetConnectedException 異常。我們可以通過在通道上直接調用 connect() 方法或在通道關聯的 Socket 對象上調用 connect() 來將該 socket 通道連接。一旦一個 socket 通道被連接,它將保持連接狀態直到被關閉。可以通過調用布爾型的 isConnected() 方法來測試某個 SocketChannel 當前是否已連接。

如果選擇使用傳統方法進行連接——通過在對等 Socket 對象上調用 connect() 方法,那麼傳統的連接語義將適用於此。線程在連接建立好或超時過期之前都將保持阻塞。如果選擇通過在通道上直接使用 connect() 方法來建立連接並且通道處於阻塞模式(默認模式),那麼連接過程實際上是一樣的。

在 SocketChannel 上並沒有一種 connect() 方法可以讓你指定超時(timeout)值,當 connect() 方法在非阻塞模式下被調用時, SocketChannel 提供併發連接:它發起對請求地址的連接並且立即返回值。如果返回值是 true,說明連接立即建立了(這可能是本地環回連接);如果連接不能立即建立,connect() 方法會返回 false 且併發地繼續連接建立過程。

面向流的 socket 建立連接狀態需要一定的時間,因爲兩個待連接系統之間必須進行包對話以建立維護流 socket 所需的狀態信息。跨越開放互聯網連接到遠程系統會特別耗時。假如某個 SocketChannel 上當前正有一個併發連接,isConnectPeding() 方法就會返回 true 值。

調用 finishConnect() 方法來完成連接過程,該方法任何時候都可以安全地進行調用。假如在一個非阻塞模式的 SocketChannel 對象上調用 finishConnect() 方法,將可能出現下列情形之一:
1. connect() 方法尚未被調用。那麼將產生 NoConnectionPendingException 異常。
2. 連接建立過程正在進行,尚未完成。那麼什麼都不會發生,finishConnect() 方法會立即返回 false 值。
3. 在非阻塞模式下調用 connect() 方法之後,SocketChannel 又被切換回阻塞模式。那麼如果有必要的話,調用線程會阻塞直到連接建立完 finishConnect() 方法接着就會返回 true 值。
4. 在初次調用 connect() 或最後一次調用 finishConnect() 之後,連接建立過程已經完成。那麼 SocketChannel 對象的內部狀態將被更新到已連接狀態,finishConnect() 方法會返回 true 值,然後SocketChannel 對象就可以被用來傳輸數據了。
5. 連接已經建立,那麼什麼都不會發生,finishConnect() 方法會返回 true 值。

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