上文總結了Java NIO中的Buffer相關知識點,本文中我們來總結一下它的好兄弟:Channel。上文有說到,Java NIO中的Buffer一般和Channel配對使用,NIO中的所有IO都起始於一個Channel,一個Channel就相當於一個流,,可以從Channel中讀取數據到Buffer,或者寫數據到Channel中。
1. Channel簡介
Java NIO中的Channel類似流,但是有一些不同:
- Channel既可以支持寫也可以支持讀,而流則是單向的,只能支持寫或者讀;
- Channel支持異步讀寫;
- Channel一般和Buffer配套使用,從Channel中讀取數據到Buffer中,或從Buffer寫入到Channel中;
Channel的主要實現類有如下幾種:
- FileChannel,可以對文件讀/寫數據;
- DatagramChannel,通過UDP從網絡讀/寫數據;
- SocketChannel,通過TCP從網絡讀/寫數據;
- ServerSocketChannel,允許你監聽TCP連接,對於每個TCP連接都創建一個SocketChannel;
2. FileChannel
Java NIO FileChannel是一類文件相連的channel,通過它可以從文件讀取數據,或向文件寫數據。FileChannel類是Java NIO類庫提供的用於代替標准Java IO API來讀寫文件。FileChannel不能設置為非阻塞模式,只能工作在阻塞模式下。
2.1 打開FileChannel
在使用FileChannel之前先要打開它,就I/O類庫中有三個類被修改了,用以產生FileChannel,這三個類是:InputStream、OutputStream、RandomAccessFile,如下是如何從RandomAccessFile獲取FileChannel:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
2.2 從FileChannel讀取數據
首先需要分配一個Buffer,從FileChannel讀取的數據會讀到Buffer中(是不是有點繞)。然后調用FileChannel的read()方法來讀數據,這個方法會把數據讀到Buffer中,返回的int代表讀取了多少字節,返回-1代表文件末尾。
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
2.3 往FileChannel寫數據
通過調用FileChannel的write()方法可以往其中寫數據,參數是一個Buffer:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); }
這里write()方法也沒有保證一次寫入多少數據,我們只是不斷重復直到寫完。
2.4 關閉FileChannel
用完FileChannel之后記得要將其關閉:
channel.close();
2.5 FileChannel位置
調用FileChannel對象的position()方法可以獲取其position,也可以調用position(long pos)設置其position:
// 獲取position
long pos channel.position();
// 設置position
channel.position(pos +123);
如果將position設置到文件末尾后面,然后嘗試讀取文件,會返回-1;
如果將position設置到文件末尾后面,然后嘗試向文件中寫數據,則文件會自動擴展,並且從設置的position位置處開始寫數據,這會導致“file hole”,即物理文件會有間隙。
2.6 FileChannel尺寸
size()方法返回filechannel連接的文件的尺寸大小:
long fileSize = channel.size();
2.7 截短FileChannel
truncate()方法可以截短文件:
channel.truncate(1024);
如上,將文件截取為1024字節長度。
2.8 FileChannel Force
FileChannel.force()方法會刷新所有未寫入到磁盤的數據到磁盤上。因為操作系統會先將數據緩存到內存中,再一次性寫入磁盤,所以不能保證寫到channel中的數據是否寫入到磁盤上,所以可以調用flush()方法強制將數據寫入磁盤。
force()方法有一個boolean參數,代表是否要寫入文件的元數據(比如權限):
channel.force(true);
3. SocketChannel
Java NIO SocketChannel用於和TCP網絡socket相連,等同於Java網絡包中的Socket。可以通過兩種方式來創建一個SocketChannel:
- 打開一個SocketChannel並且將其和網絡上的某台服務器相連;
- 當有一個連接到達一個ServerSocketChannel時會自動創建一個SocketChannel;
3.1 打開Socket通道
如下示例說明了如何打開一個SocketChannel:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
3.2 關閉Socket通道
對於這種資源類的使用,是要記得及時關閉的:
socketChannel.close();
3.3 從Socket通道讀數據
調用read()方法可以讀取數據:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
3.4 往Socket通道寫數據
調用其寫方法write()可以向其中寫數據,使用一個Buffer作為其參數:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); }
3.5 工作在非阻塞模式下
可以將SocketChannel設置為非阻塞模式,設置之后可以異步地調用其connect()、read()和write()方法。
connect()
對於處於非阻塞模式下的SocketChannel,調用其connect()方法之后會立即返回,即使沒有成功建立連接。可以調用finishConnect()方法來獲知是否成功建立連接:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80)); while(! socketChannel.finishConnect() ){ //wait, or do something else... }
write()
對於處於非阻塞模式的SocketChannel,調用其write()也會返回,但是可能還沒有寫入數據。因此你需要在一個循環中調用它,就像前面的例子中看到的那樣。
read()
對於處於非阻塞模式的SocketChannel,調用其read()有可能出現返回int值,但是還沒有任何數據讀入到buffer中。因此需要關注返回int值,這個可以告訴我們有多少數據是已經讀取的。
非阻塞模式下和Selectors一起工作
非阻塞模式下的SocketChannel適合和Selector一起搭配使用。通過往Selector中注冊一個或多個SocketChannel,可以通過Selector選擇已經就緒的Channel。具體使用稍后會詳述。
4. ServerSocketChannel
Java NIO ServerSocketChannel可以監聽TCP連接,就像標准Java網絡庫中的ServerSocket。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999)); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... }
4.1 打開ServerSocketChannel
很簡單,調用其open()方法就可以開啟一個ServerSocketChannel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
4.2 關閉ServerSocketChannel
serverSocketChannel.close();
4.3 監聽連接
調用其accept()方法之后可以監聽連接。在一個新的連接到來之前,都處於阻塞狀態,一旦新的連接到來,accept()方法將返回一個和這個連接對應的SocketChannel。
將其放在一個while循環中就可以不斷監聽新的連接:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... }
當然實際中while循環應該使用其他的判斷條件而不是true。
4.4 工作在非阻塞模式下
ServerSocketChannel同樣可設置為非阻塞模式,此時調用其accept()會立即返回,如果沒有新的連接可能返回null,需要對返回值是否為空進行校驗:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999)); serverSocketChannel.configureBlocking(false); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ //do something with socketChannel... } }
5. DatagramChannel
DatagramChannel用於發送和接收UDP包。因為UDP是一個無連接協議,不是像其他channel一樣進行讀寫操作,而是通過數據包來交換數據。
5.1 打開DatagramChannel
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999));
這個例子中打開了一個DatagramChannel,可以通過端口9999接收UDP數據包。
5.2 接收數據
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
調用DatagramChannel的receive()方法可以將接收到的數據包中的數據復制到指定的Buffer中。如果收到的數據大於Buffer容量,默認將其拋棄。
5.3 發送數據
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
在這個例子中是向“jenkov.com”服務器的80端口發送UDP數據包。因為UDP不保證數據傳輸,所以也不會知道發送的數據包是否被收到。
5.4 連接到指定地址
可以將DatagramChannel“連接”到一個指定的網絡地址。因為UDP協議是無連接的,所以通過這種方式創建連接並不會像基於TCP的channel那樣創建一個真實的連接。但是,這樣可以“鎖定”這個DatagramChannel,使得它只能和一個指定的ip地址交換數據。
channel.connect(new InetSocketAddress("jenkov.com", 80));
在這種情況下(鎖定),也可以像其他Channel那樣調用read()和write()方法,只不過不能夠保證數據一定能夠收到。
int bytesRead = channel.read(buf); int bytesWritten = channel.write(buf);
6. 總結
本文主要總結了Channel的相關知識,Channel是通道,和Buffer進行交互數據,可以讀數據到Buffer中,也可以從Buffer往Channel寫數據。Channel主要有下面幾種:
- FileChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
其中FileChannel是和文件交互、SocketChannel和ServerSocketChannel是基於TCP的網絡Channel,DatagramChannel是基於UDP的網絡Channel。