Java NIO學習系列二:Channel

木瓜芒果發表於2019-07-01

  上文總結了Java NIO中的Buffer相關知識點,本文中我們來總結一下它的好兄弟:Channel。上文有說到,Java NIO中的Buffer一般和Channel配對使用,NIO中的所有IO都起始於一個Channel,一個Channel就相當於一個流,,可以從Channel中讀取資料到Buffer,或者寫資料到Channel中。

  Channel簡介

  FileChannel

  SocketChannel

  ServerSocketChannel

  DatagramChannel

  總結

 

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。

相關文章