上文總結了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。