轉載:https://www.ibm.com/developerworks/cn/java/j-nio2-1/index.html
NIO.2 入門,第 1 部分
異步通道 API
瞭解支持異步 I/O 的新通道
系列內容:
異步通道 提供支持連接、讀取、以及寫入之類非鎖定操作的連接,並提供對已啓動操作的控制機制。Java 7 中用於 Java Platform(NIO.2)的 More New I/O APIs,通過在 java.nio.channels
包中增加四個異步通道,從而增強了
Java 1.4 中的 New I/O APIs(NIO):
-
AsynchronousSocketChannel
-
AsynchronousServerSocketChannel
-
AsynchronousFileChannel
-
AsynchronousDatagramChannel
這些類在風格上與 NIO 通道 API 很相似。他們共享相同的方法與參數結構體,並且大多數對於 NIO 通道類可用的參數,對於新的異步版本仍然可用。主要區別在於新通道可使一些操作異步執行。
異步通道 API 提供兩種對已啓動異步操作的監測與控制機制。第一種是通過返回一個 java.util.concurrent.Future
對象來實現,它將會建模一個掛起操作,並可用於查詢其狀態以及獲取結果。第二種是通過傳遞給操作一個新類的對象,java.nio.channels.CompletionHandler
,來完成,它會定義在操作完畢後所執行的處理程序方法。每個異步通道類爲每個操作定義
API 副本,這樣可採用任一機制。
在本文中,關於 NIO.2 的 兩部分系列文章 中的第一部分,介紹了每個通道,並提供一些簡單的例子來演示它們的使用方法。這些例子都處於可運行狀態(見 下載),您可在 Oracle 以及 IBM®(在本文寫作期間,都還處於開發階段;見 參見資料) 所提供的 Java 7 版中運行這些例子。在 第二部分 中,您將有機會了解 NIO.2 文件系統 API。
異步套接字通道及特性
首先,我們將瞭解 AsynchronousServerSocketChannel
和 AsynchronousSocketChannel
類。我們將看到的第一個例子將演示如何利用這些新的類來實施簡單的客戶端/服務器。第一步,我們要設置服務器。
設置服務器
打開 AsychronousServerSocketChannel
並將其綁定到類似於 ServerSocketChannel
的地址:
1
2
|
AsynchronousServerSocketChannel
server = AsynchronousServerSocketChannel.open().bind(null); |
方法 bind()
將一個套接字地址作爲其參數。找到空閒端口的便利方法是傳遞一個 null
地址,它會自動將套接字綁定到本地主機地址,並使用空閒的 臨時 端口。
接下來,可以告訴通道接受一個連接:
1
|
Future< AsynchronousSocketChannel >
acceptFuture = server.accept(); |
這是與 NIO 的第一個不同之處。接受調用總會立刻返回,並且,—— 不同於 ServerSocketChannel.accept()
,它會返回一個 SocketChannel
——
它返回一個 Future<AsynchronousSocketChannel>
對象,該對象可在以後用於檢索 AsynchronousSocketChannel
。 Future
對象的通用類型是實際操作的結果。比如,讀取或寫入操作會因爲操作返回讀或寫的字節數,而返回一個 Future<Integer>
。
利用 Future
對象,當前線程可阻塞來等待結果:
1
|
AsynchronousSocketChannel
worker = future.get(); |
此處,其阻塞超時時間爲 10 秒:
1
|
AsynchronousSocketChannel
worker = future.get(10, TimeUnit.SECONDS); |
或者輪詢操作的當前狀態,還可取消操作:
1
2
3
|
if
(!future.isDone()) { future.cancel(true); } |
cancel()
方法可利用一個布爾標誌來指出執行接受的線程是否可被中斷。這是個很有用的增強;在以前的 Java
版本中,只能通過關閉套接字來中止此類阻塞 I/O 操作。
客戶端設置
接下來,要通過打開並連接與服務器之間的 AsynchronousSocketChannel
,來設置客戶端:
1
2
|
AsynchronousSocketChannel
client = AsynchronousSocketChannel.open(); client.connect(server.getLocalAddress()).get(); |
一旦客戶端與服務器建立連接,可通過使用字節緩存的通道來執行讀寫操作,如清單 1 所示:
清單 1. 使用讀寫字節緩存
1
2
3
4
5
6
7
|
//
send a message to the server ByteBuffer
message = ByteBuffer.wrap("ping".getBytes()); client.write(message).get(); //
read a message from the client worker.read(readBuffer).get(10,
TimeUnit.SECONDS); System.out.println("Message:
" + new String(readBuffer.array())); |
還支持異步地分散讀操作與寫操作,該操作需要大量字節緩存。
新異步通道的 API 完全從底層套接字中抽取掉:無法直接獲取套接字,而以前可以調用 socket()
,例如,SocketChannel
。引入了兩個新的方法
—— getOption
和 setOption
——
來在異步網絡通道中查詢並設置套接字選項。例如,可通過 channel.getOption(StandardSocketOption.SO_RCVBUF)
而不是 channel.socket().getReceiveBufferSize();
來檢索接收緩存大小。
完成處理程序
使用 Future
對象的替代機制,是向異步操作註冊一個 callback 。接口 CompletionHandler
有兩個方法:
-
void completed(V result, A attachment)
在任務完成結果中具有類型V
時執行。 -
void failed(Throwable e, A attachment)
在任務由於Throwable e
而失敗時執行。
兩個方法的附件參數都是一個傳遞到異步操作的對象。如果相同的對象用於多個操作,其可用於追蹤哪個操作已完成。
Open 命令
我們來看一個使用 AsynchronousFileChannel
類的例子。可通過將 java.nio.file.Path
對象傳遞到靜態 open()
方法中,來創建一個新的通道:
1
|
AsynchronousFileChannel
fileChannel = AsynchronousFileChannel.open(Paths.get("myfile")); |
Path
是 Java 7 中的新類,可在 第
2 部分 中找到更多細節。可利用 Paths.get(String)
實用方法,從代表文件名的 String
中創建 Path
。
默認情況下,該文件已打開以供讀取。open()
方法可利用附加選項來指定如何打開該文件。例如,此調用打開文件以供讀取或寫入,如果必要將創建該文件,並在通道關閉或者
JVM 終止時嘗試刪除文件:
1
2
3
|
fileChannel
= AsynchronousFileChannel.open(Paths.get("afile"), StandardOpenOption.READ,
StandardOpenOption.WRITE, StandardOpenOption.CREATE,
StandardOpenOption.DELETE_ON_CLOSE); |
替代方法,open()
提供了對通道的更好的控制,允許設置文件屬性。
實現一個完成處理程序
接下來,可將這些寫入文件,寫入完成後,就可執行一些操作。 首先要構造一個封裝了 “ something ” 的 CompletionHandler
,如清單
2 所示:
清單 2. 創建完成處理程序
1
2
3
4
5
6
7
8
9
10
11
12
|
CompletionHandler< Integer ,
Object> handler = new
CompletionHandler< Integer ,
Object>() { @Override public
void completed(Integer result, Object attachment) { System.out.println(attachment
+ " completed with " + result + " bytes written"); } @Override public
void failed(Throwable e, Object attachment) { System.err.println(attachment
+ " failed with:"); e.printStackTrace(); } }; |
現在可以進行寫入:
1
|
fileChannel.write(ByteBuffer.wrap(bytes),
0, "Write operation 1", handler); |
write()
方法採取:
-
包含要寫入內容的
ByteBuffer
- 文件中的絕對位置
- 要傳遞給完成處理程序方法的附件對象
- 完成處理程序
操作必須給出進行讀或寫的文件中的絕對位置。文件具有內部位置標記,來指出讀/寫發生的位置,這樣做沒有意義,因爲在上一個操作完成之前,就可以啓動新操作,它們的發生順序無法得到保證。由於相同的原因,在 AsynchronousFileChannel
API
中沒有用於設置或查詢位置的方法,在 FileChannel
中同樣也沒有。
除了讀寫方法之外,還支持異步鎖定方法,因此,如果當前有其他線程保持鎖定時,可對文件進行執行訪問鎖定,而不必在當前線程中鎖定(或者利用 tryLock
輪詢)。
異步通道組
每個異步通道都屬於一個通道組,它們共享一個 Java 線程池,該線程池用於完成啓動的異步 I/O 操作。這看上去有點像欺騙,因爲您可在自己的 Java 線程中執行大多數異步功能,來獲得相同的表現,並且,您可能希望能夠僅僅利用操作系統的異步 I/O 能力,來執行 NIO.2 ,從而獲得更優的性能。然而,在有些情況下,有必要使用 Java 線程:比如,保證 completion-handler 方法在來自線程池的線程上執行。
默認情況下,具有 open()
方法的通道屬於一個全局通道組,可利用如下系統變量對其進行配置:
-
java.nio.channels.DefaultThreadPoolthreadFactory
,其不採用默認設置,而是定義一個java.util.concurrent.ThreadFactory
-
java.nio.channels.DefaultThreadPool.initialSize
,指定線程池的初始規模
java.nio.channels.AsynchronousChannelGroup
中的三個實用方法提供了創建新通道組的方法:
-
withCachedThreadPool()
-
withFixedThreadPool()
-
withThreadPool()
這些方法或者對線程池進行定義,如 java.util.concurrent.ExecutorService
,或者是 java.util.concurrent.ThreadFactory
。例如,以下調用創建了具有線程池的新的通道組,該線程池包含
10 個線程,其中每個都構造爲來自 Executors
類的線程工廠:
1
2
|
AsynchronousChannelGroup
tenThreadGroup = AsynchronousChannelGroup.withFixedThreadPool(10,
Executors.defaultThreadFactory()); |
三個異步網絡通道都具有 open()
方法的替代版本,它們採用給出的通道組而不是默認通道組。例如,當有異步操作請求時,此調用告訴 channel
使用 tenThreadGroup
而不是默認通道組來獲取線程:
1
2
|
AsynchronousServerSocketChannel
channel = AsynchronousServerSocketChannel.open(tenThreadGroup); |
定義自己的通道組可更好地控制服務於操作的線程,並能提供關閉線程或者等待終止的機制。清單 3 展示了相關的例子:
清單 3. 利用通道組來控制線程關閉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//
first initiate a call that won't be satisfied channel.accept(null,
completionHandler); //
once the operation has been set off, the channel group can //
be used to control the shutdown if
(!tenThreadGroup.isShutdown()) { //
once the group is shut down no more channels can be created with it tenThreadGroup.shutdown(); } if
(!tenThreadGroup.isTerminated()) { //
forcibly shutdown, the channel will be closed and the accept will abort tenThreadGroup.shutdownNow(); } //
the group should be able to terminate now, wait for a maximum of 10 seconds tenThreadGroup.awaitTermination(10,
TimeUnit.SECONDS); |
AsynchronousFileChannel
在此處與其他通道不同,爲了使用定製的線程池,open()
方法採用 ExecutorService
而不是 AsynchronousChannelGroup
。
異步數據報通道與多播
最後的新通道是 AsynchronousDatagramChannel
。它與 AsynchronousSocketChannel
很類似,但由於
NIO.2 API 在該通道級別增加了對多播的支持,而在 NIO 中只在 MulticastDatagramSocket
級別才提供這一支持,因此有必要將其單獨提出。Java
7 中的 java.nio.channels.DatagramChannel
也能提供這一功能。
作爲服務器來使用的 AsynchronousDatagramChannel
可構建如下:
1
|
AsynchronousDatagramChannel
server = AsynchronousDatagramChannel.open().bind(null); |
接下來,可設置客戶端來接收發往一個多播地址的數據報廣播。首先,必須在多播地址範圍內選擇一個地址(從 224.0.0.0 到 239.255.255.255),還要選擇一個所有客戶端都可綁定的端口:
1
2
3
|
//
specify an arbitrary port and address in the range int
port = 5239; InetAddress
group = InetAddress.getByName("226.18.84.25"); |
我們也需要一個到所使用網絡接口的引用:
1
2
|
//
find a NetworkInterface that supports multicasting NetworkInterface
networkInterface = NetworkInterface.getByName("eth0"); |
現在,打開數據報通道並設置多播選項,如清單 4 所示:
清單 4. 打開數據報通道並設置多播選項
1
2
3
4
5
6
7
8
9
10
|
//
the channel should be opened with the appropriate protocol family, //
use the defined channel group or pass in null to use the default channel group AsynchronousDatagramChannel
client = AsynchronousDatagramChannel.open(StandardProtocolFamily.INET,
tenThreadGroup); //
enable binding multiple sockets to the same address client.setOption(StandardSocketOption.SO_REUSEADDR,
true); //
bind to the port client.bind(new
InetSocketAddress(port)); //
set the interface for sending datagrams client.setOption(StandardSocketOption.IP_MULTICAST_IF,
networkInterface); |
客戶端可通過如下方式加入多播組:
1
|
MembershipKey
key = client.join(group, networkInterface); |
java.util.channels.MembershipKey
是提供對組成員控制的新類。利用該鍵,您可丟棄組成員、阻塞或者取消阻塞來自特定地址的數據報、以及返回有關組和通道的消息。
服務器可以向特定地址和端口發送數據報,供客戶端接收,如清單 5 所示:
清單 5. 發送以及接收數據報
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//
send message ByteBuffer
message = ByteBuffer.wrap("Hello to all listeners".getBytes()); server.send(message,
new InetSocketAddress(group, port)); //
receive message final
ByteBuffer buffer = ByteBuffer.allocate(100); client.receive(buffer,
null, new CompletionHandler< SocketAddress ,
Object>() { @Override public
void completed(SocketAddress address, Object attachment) { System.out.println("Message
from " + address + ": " + new
String(buffer.array())); } @Override public
void failed(Throwable e, Object attachment) { System.err.println("Error
receiving datagram"); e.printStackTrace(); } }); |
可在同一端口上創建多個客戶端,它們可加入多播組來接收來自服務器的數據報。
結束語
NIO.2 的異步通道 APIs 提供方便的、平臺獨立的執行異步操作的標準方法。這使得應用程序開發人員能夠以更清晰的方式來編寫程序,而不必定義自己的 Java 線程,此外,還可通過使用底層 OS 所支持的異步功能來提高性能。如同其他 Java API 一樣,API 可利用的 OS 自有異步功能的數量取決於其對該平臺的支持程度。