Java NIO 之 Channel(通道)

SnailClimb發表於2018-05-15

歷史回顧:

Java NIO 概覽

Java NIO 之 Buffer(緩衝區)

其他高贊文章:

面試中關於Redis的問題看這篇就夠了

一文輕鬆搞懂redis叢集原理及搭建與使用

超詳細的Java面試題總結(三)之Java集合篇常見問題

一 Channel(通道)介紹

通常來說NIO中的所有IO都是從 Channel(通道) 開始的。

  • 從通道進行資料讀取 :建立一個緩衝區,然後請求通道讀取資料。

  • 從通道進行資料寫入 :建立一個緩衝區,填充資料,並要求通道寫入資料。

資料讀取和寫入操作圖示:

資料讀取和寫入操作圖示

Java NIO Channel通道和流非常相似,主要有以下幾點區別:

  • 通道可以讀也可以寫,流一般來說是單向的(只能讀或者寫,所以之前我們用流進行IO操作的時候需要分別建立一個輸入流和一個輸出流)。
  • 通道可以非同步讀寫。
  • 通道總是基於緩衝區Buffer來讀寫。

Java NIO中最重要的幾個Channel的實現:

  • FileChannel: 用於檔案的資料讀寫
  • DatagramChannel: 用於UDP的資料讀寫
  • SocketChannel: 用於TCP的資料讀寫,一般是客戶端實現
  • ServerSocketChannel: 允許我們監聽TCP連結請求,每個請求會建立會一個SocketChannel,一般是伺服器實現

類層次結構:

下面的UML圖使用Idea生成的。

java.nio.channels類的層次結構

二 FileChannel的使用

使用FileChannel讀取資料到Buffer(緩衝區)以及利用Buffer(緩衝區)寫入資料到FileChannel:

package filechannel;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelTxt {
    public static void main(String args[]) throws IOException {
        //1.建立一個RandomAccessFile(隨機訪問檔案)物件,
        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
        //通過RandomAccessFile物件的getChannel()方法。FileChannel是抽象類。
        FileChannel inChannel=raf.getChannel();
        //2.建立一個讀資料緩衝區物件
        ByteBuffer buf=ByteBuffer.allocate(48);
        //3.從通道中讀取資料
        int bytesRead = inChannel.read(buf);
        //建立一個寫資料緩衝區物件
        ByteBuffer buf2=ByteBuffer.allocate(48);
        //寫入資料
        buf2.put("filechannel test".getBytes());
        buf2.flip();
        inChannel.write(buf);
        while (bytesRead != -1) {

            System.out.println("Read " + bytesRead);
            //Buffer有兩種模式,寫模式和讀模式。在寫模式下呼叫flip()之後,Buffer從寫模式變成讀模式。
            buf.flip();
           //如果還有未讀內容
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            //清空快取區
            buf.clear();
            bytesRead = inChannel.read(buf);
        }
        //關閉RandomAccessFile(隨機訪問檔案)物件
        raf.close();
    }
}


複製程式碼

執行效果:

執行效果

通過上述例項程式碼,我們可以大概總結出FileChannel的一般使用規則:

1. 開啟FileChannel

使用之前,FileChannel必須被開啟 ,但是你無法直接開啟FileChannel(FileChannel是抽象類)。需要通過 InputStreamOutputStreamRandomAccessFile 獲取FileChannel。

我們上面的例子是通過RandomAccessFile開啟FileChannel的:

        //1.建立一個RandomAccessFile(隨機訪問檔案)物件,
        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
        //通過RandomAccessFile物件的getChannel()方法。FileChannel是抽象類。
        FileChannel inChannel=raf.getChannel();
複製程式碼

2. 從FileChannel讀取資料/寫入資料

從FileChannel中讀取資料/寫入資料之前首先要建立一個Buffer(緩衝區)物件,Buffer(緩衝區)物件的使用我們在上一篇文章中已經詳細說明了,如果不瞭解的話可以看我的上一篇關於Buffer的文章。

使用FileChannel的read()方法讀取資料:

        //2.建立一個讀資料緩衝區物件
        ByteBuffer buf=ByteBuffer.allocate(48);
        //3.從通道中讀取資料
        int bytesRead = inChannel.read(buf);
複製程式碼

使用FileChannel的write()方法寫入資料:

        //建立一個寫資料緩衝區物件
        ByteBuffer buf2=ByteBuffer.allocate(48);
        //寫入資料
        buf2.put("filechannel test".getBytes());
        buf2.flip();
        inChannel.write(buf);
複製程式碼

3. 關閉FileChannel

完成使用後,FileChannel您必須關閉它。

channel.close();    
複製程式碼

三 SocketChannel和ServerSocketChannel的使用

利用SocketChannel和ServerSocketChannel實現客戶端與伺服器端簡單通訊:

SocketChannel 用於建立基於tcp協議的客戶端物件,因為SocketChannel中不存在accept()方法,所以,它不能成為一個服務端程式。通過 connect()方法 ,SocketChannel物件可以連線到其他tcp伺服器程式。

客戶端:

package socketchannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class WebClient {
    public static void main(String[] args) throws IOException {
        //1.通過SocketChannel的open()方法建立一個SocketChannel物件
        SocketChannel socketChannel = SocketChannel.open();
        //2.連線到遠端伺服器(連線此通道的socket)
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
        // 3.建立寫資料快取區物件
        ByteBuffer writeBuffer = ByteBuffer.allocate(128);
        writeBuffer.put("hello WebServer this is from WebClient".getBytes());
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        //建立讀資料快取區物件
        ByteBuffer readBuffer = ByteBuffer.allocate(128);
        socketChannel.read(readBuffer);
        //String 字串常量,不可變;StringBuffer 字串變數(執行緒安全),可變;StringBuilder 字串變數(非執行緒安全),可變
        StringBuilder stringBuffer=new StringBuilder();
        //4.將Buffer從寫模式變為可讀模式
        readBuffer.flip();
        while (readBuffer.hasRemaining()) {
            stringBuffer.append((char) readBuffer.get());
        }
        System.out.println("從服務端接收到的資料:"+stringBuffer);

        socketChannel.close();
    }

}
複製程式碼

ServerSocketChannel 允許我們監聽TCP連結請求,通過ServerSocketChannelImpl的 accept()方法 可以建立一個SocketChannel物件使用者從客戶端讀/寫資料。

服務端:

package socketchannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class WebServer {
    public static void main(String args[]) throws IOException {
        try {
            //1.通過ServerSocketChannel 的open()方法建立一個ServerSocketChannel物件,open方法的作用:開啟套接字通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //2.通過ServerSocketChannel繫結ip地址和port(埠號)
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
            //通過ServerSocketChannelImpl的accept()方法建立一個SocketChannel物件使用者從客戶端讀/寫資料
            SocketChannel socketChannel = ssc.accept();
            //3.建立寫資料的快取區物件
            ByteBuffer writeBuffer = ByteBuffer.allocate(128);
            writeBuffer.put("hello WebClient this is from WebServer".getBytes());
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            //建立讀資料的快取區物件
            ByteBuffer readBuffer = ByteBuffer.allocate(128);
            //讀取快取區資料
            socketChannel.read(readBuffer);
            StringBuilder stringBuffer=new StringBuilder();
            //4.將Buffer從寫模式變為可讀模式
            readBuffer.flip();
            while (readBuffer.hasRemaining()) {
                stringBuffer.append((char) readBuffer.get());
            }
            System.out.println("從客戶端接收到的資料:"+stringBuffer);
            socketChannel.close();
            ssc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

執行效果:

客戶端:

客戶端

服務端:

服務端

通過上述例項程式碼,我們可以大概總結出SocketChannel和ServerSocketChannel的使用的一般使用規則:

考慮到篇幅問題,下面只給出大致步驟,不貼程式碼,可以結合上述例項理解。

客戶端

1.通過SocketChannel連線到遠端伺服器

2.建立讀資料/寫資料緩衝區物件來讀取服務端資料或向服務端傳送資料

3.關閉SocketChannel

服務端

1.通過ServerSocketChannel 繫結ip地址和埠號

2.通過ServerSocketChannelImpl的accept()方法建立一個SocketChannel物件使用者從客戶端讀/寫資料

3.建立讀資料/寫資料緩衝區物件來讀取客戶端資料或向客戶端傳送資料

4. 關閉SocketChannel和ServerSocketChannel

四 ️DatagramChannel的使用

DataGramChannel,類似於java 網路程式設計的DatagramSocket類;使用UDP進行網路傳輸, UDP是無連線,面向資料包文段的協議,對傳輸的資料不保證安全與完整 ;和上面介紹的SocketChannel和ServerSocketChannel的使用方法類似,所以這裡就簡單介紹一下如何使用。

1.獲取DataGramChannel

        //1.通過DatagramChannel的open()方法建立一個DatagramChannel物件
        DatagramChannel datagramChannel = DatagramChannel.open();
        //繫結一個port(埠)
        datagramChannel.bind(new InetSocketAddress(1234));
複製程式碼

上面程式碼表示程式可以在1234埠接收資料包。

2.接收/傳送訊息

接收訊息:

先建立一個快取區物件,然後通過receive方法接收訊息,這個方法返回一個SocketAddress物件,表示傳送訊息方的地址:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
複製程式碼

傳送訊息:

由於UDP下,服務端和客戶端通訊並不需要建立連線,只需要知道對方地址即可發出訊息,但是是否傳送成功或者成功被接收到是沒有保證的;傳送訊息通過send方法發出,改方法返回一個int值,表示成功傳送的位元組數:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put("datagramchannel".getBytes());
buf.flip();
int send = channel.send(buffer, new InetSocketAddress("localhost",1234));
複製程式碼

這個例子傳送一串字元:“datagramchannel”到主機名為”localhost”伺服器的埠1234上。

五 Scatter / Gather

Channel 提供了一種被稱為 Scatter/Gather 的新功能,也稱為本地向量 I/O。Scatter/Gather 是指在多個緩衝區上實現一個簡單的 I/O 操作。正確使用 Scatter / Gather可以明顯提高效能。

大多數現代作業系統都支援本地向量I/O(native vectored I/O)操作。當您在一個通道上請求一個Scatter/Gather操作時,該請求會被翻譯為適當的本地呼叫來直接填充或抽取緩衝區,減少或避免了緩衝區拷貝和系統呼叫;

Scatter/Gather應該使用直接的ByteBuffers以從本地I/O獲取最大效能優勢。

Scatter/Gather功能是通道(Channel)提供的 並不是Buffer。

  • Scatter: 從一個Channel讀取的資訊分散到N個緩衝區中(Buufer).

  • Gather: 將N個Buffer裡面內容按照順序傳送到一個Channel.

Scattering Reads

"scattering read"是把資料從單個Channel寫入到多個buffer,如下圖所示:

scattering read
示例程式碼:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);
複製程式碼

read()方法內部會負責把資料按順序寫進傳入的buffer陣列內。一個buffer寫滿後,接著寫到下一個buffer中。

舉個例子,假如通道中有200個位元組資料,那麼header會被寫入128個位元組資料,body會被寫入72個位元組資料;

注意:

無論是scatter還是gather操作,都是按照buffer在陣列中的順序來依次讀取或寫入的;

Gathering Writes

"gathering write"把多個buffer的資料寫入到同一個channel中,下面是示意圖:

Gathering Writes

示例程式碼:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);
複製程式碼

write()方法內部會負責把資料按順序寫入到channel中。

注意:

並不是所有資料都寫入到通道,寫入的資料要根據position和limit的值來判斷,只有position和limit之間的資料才會被寫入;

舉個例子,假如以上header緩衝區中有128個位元組資料,但此時position=0,limit=58;那麼只有下標索引為0-57的資料才會被寫入到通道中。

六 通道之間的資料傳輸

在Java NIO中如果一個channel是FileChannel型別的,那麼他可以直接把資料傳輸到另一個channel。

  • transferFrom() :transferFrom方法把資料從通道源傳輸到FileChannel
  • transferTo() :transferTo方法把FileChannel資料傳輸到另一個channel

參考:

官方JDK相關文件

谷歌搜尋排名第一的Java NIO教程

《Java NIO》

《Java 8程式設計官方參考教程(第9版)》

歡迎關注我的微信公眾號:"Java面試通關手冊"(一個有溫度的微信公眾號,期待與你共同進步~~~堅持原創,分享美文,分享各種Java學習資源):

Java NIO 之 Channel(通道)

相關文章