Java-NIO之Channel(通道)

竹根七發表於2022-04-11

1:Channel是什麼

通道表示與實體的開放連線,例如硬體裝置、檔案、網路套接字或能夠執行一個或多個不同 I/O 操作(例如讀取或寫入)的程式元件。

1.1:Channel與Stream的對比

Stream Channel 為什麼
是否支援非同步
是否同時支援輸入和輸出 Stream的輸入、輸出分別需要InputStream、OutputStream
是否必須結合Buffer使用 緩衝區是通道內部傳送資料和接收資料的端點
效能 通道是訪問IO服務的導管,通過通道,我們可以以最小的開銷來訪問作業系統的I/O服務

1.2:Channel的型別

檔案類:

  • FileChannel

可通過 FileInputStream/FileOutputStream 的getChannel方法獲取通道。


網路類:

基於socket協議:

  • SocketChannel
  • ServerSocketChannel

可通過 Socket/SocketServer 的getChannel方法獲取通道。

基於UDP協議:

  • DatagramChannel

可通過 DatagramSocket 的getChannel方法獲取通道。

1.3:作業系統IO演變史

早一代IO操作是由CPU負責IO介面:
image

新一代DMA授權處理IO介面:
image

通道(Channel)模式:
image

通道的產生是由於作業系統的升級而支援的。

2:Channel和作業系統的關係

在作業系統中對IO裝置的控制方式一共有四種,按時間線依次是 輪詢、中斷、DMA、和通道 方式。

  • 輪旋

輪詢就是進行IO時作業系統一直問控制器資料準備好了沒有。

  • 中斷

中斷就是非同步的方式進行了,CPU向裝置控制器傳送一條IO指令後接著返回繼續做原來的工作,而當裝置控制器從裝置中取出資料放到控制器的暫存器中後便向CPU傳送中斷訊號,CPU在檢查完資料後便向控制器傳送取走資料的訊號,將資料寫入記憶體,但仍是以位元組為單位的。

  • DMA

DMA則是CPU和裝置控制器之間的引入的一層加快速度的手段,由DMA代替CPU進行資料傳送,CPU將指令傳送給DMA,DMA向控制器傳送請求,裝置控制器將資料從緩衝區將資料直接寫入記憶體。完成後裝置控制器傳送一個訊號給DMA,DMA重複檢查資料是否傳送完成,確認完成後中斷讓CPU知道。

DMA比起中斷方式已經顯著減少了CPU的干預,但是CPU每發出一條IO指令,只能去讀寫一個連續的資料塊,當要讀多個資料塊並存放到不同的記憶體區域中去,CPU需要傳送多條IO指令及進行多次中斷。

  • 通道

IO通道方式是DMA方式的發展,把對一個資料塊的干預減少為對一組資料塊的干預。


IO通道有三種:

  • 位元組多路通道(Byte Multiplexor Channel)
  • 選擇通道(Block Selector Channel)
  • 陣列多路通道(Block Multiplexor Channel)

根據通道的工作方式分類,通道可以分為位元組多路通道、選擇通道、陣列多路通道
位元組多路通道是一種簡單的共享通道,主要用於連線大量的低速裝置。
由於外圍裝置的工作速度較慢,通道在傳送兩個位元組之間有很多空閒的時間,利用這段空閒時間位元組多路通道可以為其他外圍裝置服務。因此位元組多路通道採用分時工作方式,依賴它與CPU之間的高速匯流排分時為多臺外圍裝置服務。
資料選擇通道用於連線高速的外圍裝置。
高速外圍裝置需要很高的資料傳輸率,因此不能採用位元組多路通道那樣的控制方式。選擇通道在物理上可以連線多臺外圍裝置,但多臺裝置不能同事工作。也就是在同一段時間內,選擇通道只能為一臺外圍裝置服務,在不同的時間內可以選擇不同的外圍裝置。一旦選中某一裝置,通道就進入狀態,知道該裝置資料傳輸工作結束,才能為其他裝置服務。
陣列多路通道是位元組多路通道和選擇通道的結合。
其基本思想是:當某裝置進行資料傳輸時,通道只為該裝置服務;當裝置在進行定址等控制性操作時,通道暫時斷開與裝置的連線,掛起該裝置的通道程式,去為其他裝置服務,即執行其他裝置的通道程式。有數陣列多路通道既保持了選擇通道的告訴傳輸資料的有點,又充分利用了控制性操作偶讀時間間隔為其他裝置服務,使得通道效率充分得到發揮,因此資料多路通道在實際計算機系統中應用最多,適合於高速裝置的資料傳輸。

(以上引用內容來源於百度教育

至於JAVA的Channel和作業系統的的通道是如何選擇通道型別、如何互動的就沒法深入了,暫且理解JAVA的Channel是對作業系統的通道的一種抽象實現吧。

3:Channel檔案通道

上一篇已經介紹過Channel的檔案記憶體對映(map),就不做介紹了。

所謂的分散讀取、聚集寫入就是用多個buffer來接收資料、傳輸資料。
image

分散讀取、聚集寫入程式碼示例:

    @Test
    public void gatherWrite() {
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            File file = new File("src/test/java/com/loper/mine/SQLParserTest.java");
            inputStream = new FileInputStream(file);
            inChannel = inputStream.getChannel();

            ByteBuffer buffer1 = ByteBuffer.allocate(8);
            ByteBuffer buffer2 = ByteBuffer.allocate(15);
            ByteBuffer[] buffers = new ByteBuffer[]{buffer1, buffer2};

            // 分散讀取
            inChannel.read(buffers);
            for (ByteBuffer buffer : buffers) {
                buffer.flip();
                System.out.println(buffer.mark());
            }

            File outFile = new File("src/test/java/com/loper/mine/1.txt");
            outputStream = new FileOutputStream(outFile);
            outChannel = outputStream.getChannel();
            // 聚集寫入
            outChannel.write(buffers);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null)
                    inputStream.close();
                if (outputStream != null)
                    outputStream.close();
                if (inChannel != null)
                    inChannel.close();
                if (outChannel != null)
                    outChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

4:Channel網路通道

4.1:socket協議

這部分程式碼比較複雜,可以翻看我的github程式碼,這裡就不坐介紹了。
地址:https://github.com/zgq7/devloper-mine/tree/master/src/main/java/com/loper/mine/core/socket/nio

4.2:UDP協議

UDP傳送資料:

    @Test
    public void send() {
        DatagramChannel channel = null;
        try {
            channel = DatagramChannel.open();
            // 設定為非阻塞
            channel.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String nextLine = scanner.nextLine();
                buffer.put(nextLine.getBytes());
                buffer.flip();
                channel.send(buffer, new InetSocketAddress("127.0.0.1", 8056));
                buffer.clear();
                if ("over".equals(nextLine))
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

UDP接收資料:

    @Test
    public void receive() {
        DatagramChannel channel = null;
        try {
            channel = DatagramChannel.open();
            // 設定為非阻塞
            channel.configureBlocking(false);
            channel.bind(new InetSocketAddress(8056));

            Selector selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);

            while (true) {
                int select = selector.select();
                boolean exit = false;

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();

                    if (selectionKey.isReadable()) {
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.receive(buffer);
                        buffer.flip();
                        byte[] data = new byte[buffer.limit()];
                        buffer.get(data);
                        String str = new String(data);
                        System.out.println("收到:" + str);
                        if ("over".equals(str))
                            exit = true;
                    }
                    iterator.remove();
                }
                if (exit)
                    break;
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

接收端接收資料並退出:
image


以上即為本文理論知識+程式碼實戰全部內容。如有錯誤歡迎指正。

本文參考文章:

相關文章