1. 程式人生 > >Java NIO 學習筆記(四)----檔案通道和網路通道

Java NIO 學習筆記(四)----檔案通道和網路通道

目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----檔案通道和網路通道

FileChannel 檔案通道

FileChannel 是連線到檔案的通道,可以從檔案中讀取資料,並將資料寫入檔案,可以替代使用標準 IO 讀寫檔案的操作。

注意 FileChannel 無法設定為非阻塞模式。 它始終以阻塞模式執行。

開啟 FileChannel

在使用 FileChannel 之前必須先將其開啟。 無法直接開啟 FileChannel ,必須通過 InputStream,OutputStream 或 RandomAccessFile 獲取 FileChannel 。 以下是通過 RandomAccessFile 開啟 FileChannel 的方法:

RandomAccessFile aFile = new RandomAccessFile("D:\\test\\input.txt", "rw");
FileChannel inChannel = aFile.getChannel();

從 FileChannel 讀寫資料

這是通過呼叫 read()/write() 方法之一完成的,讀和寫都是針對一個 ByteBuufer 物件的。
這是一個例子:

// 這裡省略 2 個 Channel 的定義...

ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear(); // 清除緩衝區,準備寫入資料

int bytesRead = inChannel.read(buffer); // 將 inChannel 的資料讀入緩衝區

buffer.flip(); // 反轉緩衝區,就是把指標放到開頭,並設定 limit 標記結尾

while(buffer.hasRemaining()) { // 只要緩衝區還有資料
    outChannel.write(buffer); // 就將緩衝區資料寫入通道
}

分配緩衝區後,呼叫 FileChannel 物件的 read() 方法將 FileChannel 中的資料讀入 Buffer ,返回的 int 代表讀取的位元組數。 如果返回 -1,則到達檔案結尾。

使用 write() 方法將資料寫入 FileChannel ,該方法同樣將 Buffer 作為引數,注意在 while 迴圈中呼叫 write() 方法。 這是因為無法保證 write() 方法寫入 FileChannel 的位元組數。 因此,我們重複呼叫 write() 方法,直到 Buffer 中沒有要寫入通道的位元組。

關閉 FileChannel

使用 FileChannel 後,必須將其關閉:

channel.close();    

FileChannel Position 指定操作位置

可以呼叫 position() 方法獲取 FileChannel 物件的當前位置,呼叫 position(long pos) 方法來設定 FileChannel 的位置,這樣就可以在特定位置開始讀取或寫入 FileChannel 。
一個例子:

// 獲取當前位置
long pos = channel.position();
// 指定通道的位置,0 <= position <= limit
channel.position(pos +123);

如果位置設定在檔案結束後面:

  1. 讀取操作將得到 -1 ---- 檔案結束標記。
  2. 寫入操作,檔案將擴大到指定位置並寫入資料。 這可能導致“檔案漏洞”,磁碟上的物理檔案資料中存在間隙。(比如開始順序寫入123後,此時 position=3,手動指定 position = 6,寫入4,那此時磁碟物理檔案應該是['1', '2', '3', '~', '~', '~', '4'],中間有3個位置是空的)

獲取 FileChannel 的大小資訊

FileChannel 物件的 size() 方法返回通道所連線檔案的檔案大小。 這是一個簡單的例子:

long fileSize = channel.size();    

FileChannel Truncate(截斷)

可以通過呼叫 FileChannel 物件的 truncate() 方法截斷檔案。 截斷檔案時,會以給定長度將其剪下掉,即指定長度後面部分被刪除。 這是一個例子,將檔案長度截斷為 1024 位元組:

channel.truncate(1024);

將 FileChannel 資料強制儲存到磁碟

FileChannel 物件的 void force(boolean metaData) 方法強制將所有未寫入磁碟的資料從通道重新整理到磁碟。 出於效能原因,作業系統可能會將資料快取在記憶體中,因此在呼叫 force() 方法之前,無法保證寫入通道的資料實際寫入磁碟。
boolean 引數指定是否應該重新整理檔案元資料(許可權資訊等)到磁碟。

channel.force(true);
channel.force(flase);

SocketChannel 套接字通道

SocketChannel 是連線到 TCP 網路套接字的通道,相當於 Java 網路程式設計的套接字。可以通過兩種方式建立:

  1. 開啟 SocketChannel 並連線到 Internet 上的某個伺服器,即向伺服器發出連線。
  2. 當傳入連線到達 ServerSocketChannel 時,建立 SocketChannel ,即接收客戶端發來的連線。

開啟和關閉 SocketChannel

以下是開啟SocketChannel的方法:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://baidu.com", 80));

socketChannel.close();    

InetSocketAddress 是 SocketAddress 類的子類,為(IP地址+埠號)型別,也就是埠地址型別,可以使用靜態方法 createUnresolved(String host, int port) 獲取物件,另外也能由建構函式 InetSocketAddress(InetAddress addr, int port) 建立,其中 InetAddress 物件可省略,也可用字串代替。

通過 SocketChannel 讀寫資料

// 這裡省略 2 個 Channel 的定義...

ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear(); // 清除緩衝區,準備寫入資料
int bytesRead = socketChannel.read(buffer); // 將 socketChannel 的資料讀入緩衝區

buffer.flip(); // 反轉緩衝區,就是把指標放到開頭,並設定 limit 標記結尾

while(buffer.hasRemaining()) { // 只要緩衝區還有資料
    channel.write(buffer);// 就將緩衝區資料寫入通道
}

可以看到,基本和 FileChannel 的讀寫方式一致,首先分配緩衝區,然後呼叫 read() 方法。 此方法將資料從 SocketChannel 讀入 Buffer 。 read() 方法返回的 int 代表寫入了多少位元組資料。 如果返回 -1 ,則到達流的末尾(連線已關閉)。呼叫 SocketChannel 物件的 write(Buffer buffer) 方法則可以將 Buffer 的資料寫入 SocketChannel。

SocketChannel 的非阻塞模式

可以將 SocketChannel 設定為非阻塞模式,可以在非同步模式下呼叫 connect(),read() 和 write() 方法。

connect()

如果 SocketChannel 處於非阻塞模式呼叫 connect() 方法,則可能會在建立連線之前返回。 要確定是否成功建立了連線,可以呼叫 finishConnect() 方法,它當且僅當已連線此通道的套接字時才返回 true 。
如下所示:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://baidu.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...    
}
write()

在非阻塞模式下,write() 方法可能在沒有寫入任何內容的情況下返回。 因此,需要在迴圈中呼叫 write() 方法。

while(buffer.hasRemaining()) { // 只要緩衝區還有資料
    channel.write(buffer);// 就將緩衝區資料寫入通道
}
read()

在非阻塞模式下,read() 方法可能在沒有讀取任何資料的情況下返回。 因此,需要注意返回的 int ,它代表讀取了多少位元組。

帶有選擇器的非阻塞模式

使用 Selector 時,SocketChannel 的非阻塞模式效果更好。 通過使用選擇器註冊一個或多個 SocketChannel ,可以向選擇器詢問已準備好進行讀取,寫入的通道。後面會有更詳細的提起,這裡先不講。

ServerSocketChannel

ServerSocketChannel 是一個可以偵聽傳入 TCP 連線的通道,就像標準 ServerSocket 一樣。 這是一個例子:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept(); // 監聽傳入的連線
    //do something with socketChannel...
}

serverSocketChannel.close();

ServerSocketChannel 可以設定為非阻塞模式。 在非阻塞模式下,accept() 方法呼叫後會立即返回,如果有連線傳入,則返回 SocketChannel 物件,如果沒有連線傳入,則返回 null。 因此,必須檢查返回的 SocketChannel 是否為空。如下:

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...
        }
}

DatagramChannel 資料包通道

DatagramChannel 是可以傳送和接收 UDP 資料包的通道。 由於 UDP 是一種無連線的網路協議,因此不能像在其他通道中那樣讀取和寫入 DatagramChannel 。 而是通過傳送和接收資料包的方式通訊。

接收資料

DatagramChannel 是通過 receive() 方法接收資料的。 receive() 方法將接收到的資料包的內容複製到給定的 Buffer 中。 如果接收的資料包包含的資料多於緩衝區可以包含的資料,則會悄悄丟棄多出的資料。一個示例如下:

ByteBuffer receiveBuffer = ByteBuffer.allocate(128);
DatagramChannel serverChannel = DatagramChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9999));

receiveBuffer.clear(); // 清除緩衝區,準備寫入資料
serverChannel.receive(receiveBuffer);

receiveBuffer.flip(); // 反轉緩衝區以準備被讀取
System.out.println(new String(receiveBuffer.array(), 0, receiveBuffer.limit()));

傳送資料

ByteBuffer sendBuffer = ByteBuffer.allocate(128);
sendBuffer.clear(); // 清除緩衝區,準備寫入資料
byte[] sendData = "string from cilent".getBytes();
sendBuffer.put(sendData);

DatagramChannel clientChannel = DatagramChannel.open();
int sendSuccess = clientChannel.send(sendBuffer, new InetSocketAddress("127.0.0.1", 9999));
System.out.println("sendSuccess: " + sendSuccess);
clientChannel.close();

此示例將字串傳送到 UDP 本機的 9999 埠, 由於 UDP 不對資料傳送做出任何保證,因此不會通知對方是否收到了傳送的資料包。

連線到特定地址

可以將 DatagramChannel“連線”到網路上的特定地址。 由於 UDP 是無連線的,因此這種連線到地址的方式不會像 TCP 通道那樣建立真正的連線。 它只會鎖定 DatagramChannel ,讓其只能從一個特定地址傳送和接收資料包。一個示例:

channel.connect(new InetSocketAddress("baidu.com", 80));    

連線後,還可以使用 read() 和 write() 方法,就像使用傳統通道一樣,只是在資料傳送方面沒有任何保證。 例子:

int bytesRead = channel.read(buf);    
int bytesWritten = channel.write(buf);