Java NIO 之 Channel(通道)

一 Channel(通道)介紹

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

  • 從通道進行數(shù)據(jù)讀取 :創(chuàng)建一個緩沖區(qū),然后請求通道讀取數(shù)據(jù)。

  • 從通道進行數(shù)據(jù)寫入 :創(chuàng)建一個緩沖區(qū),填充數(shù)據(jù),并要求通道寫入數(shù)據(jù)。

數(shù)據(jù)讀取和寫入操作圖示:

數(shù)據(jù)讀取和寫入操作圖示

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

  • 通道可以讀也可以寫,流一般來說是單向的(只能讀或者寫,所以之前我們用流進行IO操作的時候需要分別創(chuàng)建一個輸入流和一個輸出流)。
  • 通道可以異步讀寫。
  • 通道總是基于緩沖區(qū)Buffer來讀寫。

Java NIO中最重要的幾個Channel的實現(xiàn):

  • FileChannel: 用于文件的數(shù)據(jù)讀寫
  • DatagramChannel: 用于UDP的數(shù)據(jù)讀寫
  • SocketChannel: 用于TCP的數(shù)據(jù)讀寫,一般是客戶端實現(xiàn)
  • ServerSocketChannel: 允許我們監(jiān)聽TCP鏈接請求,每個請求會創(chuàng)建會一個SocketChannel,一般是服務器實現(xiàn)

類層次結構:

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


java.nio.channels類的層次結構

二 FileChannel的使用

使用FileChannel讀取數(shù)據(jù)到Buffer(緩沖區(qū))以及利用Buffer(緩沖區(qū))寫入數(shù)據(jù)到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.創(chuàng)建一個RandomAccessFile(隨機訪問文件)對象,
        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
        //通過RandomAccessFile對象的getChannel()方法。FileChannel是抽象類。
        FileChannel inChannel=raf.getChannel();
        //2.創(chuàng)建一個讀數(shù)據(jù)緩沖區(qū)對象
        ByteBuffer buf=ByteBuffer.allocate(48);
        //3.從通道中讀取數(shù)據(jù)
        int bytesRead = inChannel.read(buf);
        //創(chuàng)建一個寫數(shù)據(jù)緩沖區(qū)對象
        ByteBuffer buf2=ByteBuffer.allocate(48);
        //寫入數(shù)據(jù)
        buf2.put("filechannel test".getBytes());
        buf2.flip();
        inChannel.write(buf);
        while (bytesRead != -1) {

            System.out.println("Read " + bytesRead);
            //Buffer有兩種模式,寫模式和讀模式。在寫模式下調(diào)用flip()之后,Buffer從寫模式變成讀模式。
            buf.flip();
           //如果還有未讀內(nèi)容
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            //清空緩存區(qū)
            buf.clear();
            bytesRead = inChannel.read(buf);
        }
        //關閉RandomAccessFile(隨機訪問文件)對象
        raf.close();
    }
}

運行效果:

運行效果

通過上述實例代碼,我們可以大概總結出FileChannel的一般使用規(guī)則:

1. 開啟FileChannel

使用之前,F(xiàn)ileChannel必須被打開 ,但是你無法直接打開FileChannel(FileChannel是抽象類)。需要通過 InputStream , OutputStreamRandomAccessFile 獲取FileChannel。

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

        //1.創(chuàng)建一個RandomAccessFile(隨機訪問文件)對象,
        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
        //通過RandomAccessFile對象的getChannel()方法。FileChannel是抽象類。
        FileChannel inChannel=raf.getChannel();

2. 從FileChannel讀取數(shù)據(jù)/寫入數(shù)據(jù)

從FileChannel中讀取數(shù)據(jù)/寫入數(shù)據(jù)之前首先要創(chuàng)建一個Buffer(緩沖區(qū))對象,Buffer(緩沖區(qū))對象的使用我們在上一篇文章中已經(jīng)詳細說明了,如果不了解的話可以看我的上一篇關于Buffer的文章。

使用FileChannel的read()方法讀取數(shù)據(jù):

        //2.創(chuàng)建一個讀數(shù)據(jù)緩沖區(qū)對象
        ByteBuffer buf=ByteBuffer.allocate(48);
        //3.從通道中讀取數(shù)據(jù)
        int bytesRead = inChannel.read(buf);

使用FileChannel的write()方法寫入數(shù)據(jù):

        //創(chuàng)建一個寫數(shù)據(jù)緩沖區(qū)對象
        ByteBuffer buf2=ByteBuffer.allocate(48);
        //寫入數(shù)據(jù)
        buf2.put("filechannel test".getBytes());
        buf2.flip();
        inChannel.write(buf);

3. 關閉FileChannel

完成使用后,F(xiàn)ileChannel您必須關閉它。

channel.close();    

三 SocketChannel和ServerSocketChannel的使用

利用SocketChannel和ServerSocketChannel實現(xiàn)客戶端與服務器端簡單通信:

SocketChannel 用于創(chuàng)建基于tcp協(xié)議的客戶端對象,因為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()方法創(chuàng)建一個SocketChannel對象
        SocketChannel socketChannel = SocketChannel.open();
        //2.連接到遠程服務器(連接此通道的socket)
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
        // 3.創(chuàng)建寫數(shù)據(jù)緩存區(qū)對象
        ByteBuffer writeBuffer = ByteBuffer.allocate(128);
        writeBuffer.put("hello WebServer this is from WebClient".getBytes());
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        //創(chuàng)建讀數(shù)據(jù)緩存區(qū)對象
        ByteBuffer readBuffer = ByteBuffer.allocate(128);
        socketChannel.read(readBuffer);
        //String 字符串常量,不可變;StringBuffer 字符串變量(線程安全),可變;StringBuilder 字符串變量(非線程安全),可變
        StringBuilder stringBuffer=new StringBuilder();
        //4.將Buffer從寫模式變?yōu)榭勺x模式
        readBuffer.flip();
        while (readBuffer.hasRemaining()) {
            stringBuffer.append((char) readBuffer.get());
        }
        System.out.println("從服務端接收到的數(shù)據(jù):"+stringBuffer);

        socketChannel.close();
    }

}

ServerSocketChannel 允許我們監(jiān)聽TCP鏈接請求,通過ServerSocketChannelImpl的 accept()方法 可以創(chuàng)建一個SocketChannel對象用戶從客戶端讀/寫數(shù)據(jù)。

服務端:

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()方法創(chuàng)建一個ServerSocketChannel對象,open方法的作用:打開套接字通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //2.通過ServerSocketChannel綁定ip地址和port(端口號)
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
            //通過ServerSocketChannelImpl的accept()方法創(chuàng)建一個SocketChannel對象用戶從客戶端讀/寫數(shù)據(jù)
            SocketChannel socketChannel = ssc.accept();
            //3.創(chuàng)建寫數(shù)據(jù)的緩存區(qū)對象
            ByteBuffer writeBuffer = ByteBuffer.allocate(128);
            writeBuffer.put("hello WebClient this is from WebServer".getBytes());
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            //創(chuàng)建讀數(shù)據(jù)的緩存區(qū)對象
            ByteBuffer readBuffer = ByteBuffer.allocate(128);
            //讀取緩存區(qū)數(shù)據(jù)
            socketChannel.read(readBuffer);
            StringBuilder stringBuffer=new StringBuilder();
            //4.將Buffer從寫模式變?yōu)榭勺x模式
            readBuffer.flip();
            while (readBuffer.hasRemaining()) {
                stringBuffer.append((char) readBuffer.get());
            }
            System.out.println("從客戶端接收到的數(shù)據(jù):"+stringBuffer);
            socketChannel.close();
            ssc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行效果:

客戶端:


客戶端

服務端:


服務端

通過上述實例代碼,我們可以大概總結出SocketChannel和ServerSocketChannel的使用的一般使用規(guī)則:

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

客戶端

1.通過SocketChannel連接到遠程服務器

2.創(chuàng)建讀數(shù)據(jù)/寫數(shù)據(jù)緩沖區(qū)對象來讀取服務端數(shù)據(jù)或向服務端發(fā)送數(shù)據(jù)

3.關閉SocketChannel

服務端

1.通過ServerSocketChannel 綁定ip地址和端口號

2.通過ServerSocketChannelImpl的accept()方法創(chuàng)建一個SocketChannel對象用戶從客戶端讀/寫數(shù)據(jù)

3.創(chuàng)建讀數(shù)據(jù)/寫數(shù)據(jù)緩沖區(qū)對象來讀取客戶端數(shù)據(jù)或向客戶端發(fā)送數(shù)據(jù)

4. 關閉SocketChannel和ServerSocketChannel

四 ?DatagramChannel的使用

DataGramChannel,類似于java 網(wǎng)絡編程的DatagramSocket類;使用UDP進行網(wǎng)絡傳輸, UDP是無連接,面向數(shù)據(jù)報文段的協(xié)議,對傳輸?shù)臄?shù)據(jù)不保證安全與完整 ;和上面介紹的SocketChannel和ServerSocketChannel的使用方法類似,所以這里就簡單介紹一下如何使用。

1.獲取DataGramChannel

        //1.通過DatagramChannel的open()方法創(chuàng)建一個DatagramChannel對象
        DatagramChannel datagramChannel = DatagramChannel.open();
        //綁定一個port(端口)
        datagramChannel.bind(new InetSocketAddress(1234));

上面代碼表示程序可以在1234端口接收數(shù)據(jù)報。

2.接收/發(fā)送消息

接收消息:

先創(chuàng)建一個緩存區(qū)對象,然后通過receive方法接收消息,這個方法返回一個SocketAddress對象,表示發(fā)送消息方的地址:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);

發(fā)送消息:

由于UDP下,服務端和客戶端通信并不需要建立連接,只需要知道對方地址即可發(fā)出消息,但是是否發(fā)送成功或者成功被接收到是沒有保證的;發(fā)送消息通過send方法發(fā)出,改方法返回一個int值,表示成功發(fā)送的字節(jié)數(shù):

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

這個例子發(fā)送一串字符:“datagramchannel”到主機名為”localhost”服務器的端口1234上。

五 Scatter / Gather

Channel 提供了一種被稱為 Scatter/Gather 的新功能,也稱為本地矢量 I/O。Scatter/Gather 是指在多個緩沖區(qū)上實現(xiàn)一個簡單的 I/O 操作。正確使用 Scatter / Gather可以明顯提高性能。

大多數(shù)現(xiàn)代操作系統(tǒng)都支持本地矢量I/O(native vectored I/O)操作。當您在一個通道上請求一個Scatter/Gather操作時,該請求會被翻譯為適當?shù)谋镜卣{(diào)用來直接填充或抽取緩沖區(qū),減少或避免了緩沖區(qū)拷貝和系統(tǒng)調(diào)用;

Scatter/Gather應該使用直接的ByteBuffers以從本地I/O獲取最大性能優(yōu)勢。

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

  • Scatter: 從一個Channel讀取的信息分散到N個緩沖區(qū)中(Buufer).

  • Gather: 將N個Buffer里面內(nèi)容按照順序發(fā)送到一個Channel.

Scattering Reads

“scattering read”是把數(shù)據(jù)從單個Channel寫入到多個buffer,如下圖所示:

scattering read

示例代碼:

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

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

read()方法內(nèi)部會負責把數(shù)據(jù)按順序?qū)戇M傳入的buffer數(shù)組內(nèi)。一個buffer寫滿后,接著寫到下一個buffer中。

舉個例子,假如通道中有200個字節(jié)數(shù)據(jù),那么header會被寫入128個字節(jié)數(shù)據(jù),body會被寫入72個字節(jié)數(shù)據(jù);

注意:

無論是scatter還是gather操作,都是按照buffer在數(shù)組中的順序來依次讀取或?qū)懭氲模?/p>

Gathering Writes

“gathering write”把多個buffer的數(shù)據(jù)寫入到同一個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()方法內(nèi)部會負責把數(shù)據(jù)按順序?qū)懭氲絚hannel中。

注意:

并不是所有數(shù)據(jù)都寫入到通道,寫入的數(shù)據(jù)要根據(jù)position和limit的值來判斷,只有position和limit之間的數(shù)據(jù)才會被寫入;

舉個例子,假如以上header緩沖區(qū)中有128個字節(jié)數(shù)據(jù),但此時position=0,limit=58;那么只有下標索引為0-57的數(shù)據(jù)才會被寫入到通道中。

六 通道之間的數(shù)據(jù)傳輸

在Java NIO中如果一個channel是FileChannel類型的,那么他可以直接把數(shù)據(jù)傳輸?shù)搅硪粋€channel。

  • transferFrom() :transferFrom方法把數(shù)據(jù)從通道源傳輸?shù)紽ileChannel
  • transferTo() :transferTo方法把FileChannel數(shù)據(jù)傳輸?shù)搅硪粋€channel

參考:

官方JDK相關文檔

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

《Java NIO》

《Java 8編程官方參考教程(第9版)》

Java NIO 之 Channel(通道)

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容