Java NIO Channel 通道


Channel 類(通道)

  • Channel(通道):Channel 是一個對象,可以通過它讀取和寫入數據。可以把它看做是 IO 中的流,不同的是:
    • 爲所有的原始類型提供(Buffer)緩存支持;
    • 字符集編碼解決方案(Charset);
    • Channel : 一個新的原始 I/O 抽象;
    • 支持鎖和內存映射文件的文件訪問接口;
    • 提供多路(non-bloking)非阻塞式的高伸縮性網路 I/O;
  • 正如上面提到的,所有數據都通過 Buffer 對象處理,所以,你永遠不會將字節直接寫入到 Channel 中,相反,您是將數據寫入到 Buffer 中;同樣,您也不會從 Channel 中讀取字節,而是將數據從 Channel 讀入 Buffer,再從 Buffer 獲取這個字節;
  • 因爲 Channel 是雙向的,所以 Channel 可以比流更好地反映出底層操作系統的真實情況。特別是在 Unix 模型中,底層操作系統通常都是雙向的;
  • 在 Java NIO 中的 Channel 主要有如下幾種類型:
    • FileChannel:從文件讀取數據;
    • DatagramChannel:讀寫 UDP 網絡協議數據;
    • SocketChannel:讀寫 TCP 網絡協議數據;
    • ServerSocketChannel:可以監聽 TCP 連接;

1. FileChannel 類的基本使用

  • java.nio.channels.FileChannel:是用於讀、寫文件的通道;
  • FileChannel 是抽象類,可以通過 FileInputStream 和 FileOutputStream 的 getChannel() 方法方便的獲取一個它的子類對象;
    FileInputStream fi = new FileInputStream(new File(src));
    FileOutputStream fo = new FileOutputStream(new File(dst));
    //獲得傳輸通道channel
    FileChannel inChannel = fi.getChannel();
    FileChannel outChannel = fo.getChannel();
  • 通過 CopyFile 的例子可以更好體會 NIO 的操作過程。CopyFile 執行三個基本的操作:創建一個 Buffer,然後從源文件讀取數據到緩衝區,然後再將緩衝區寫入目標文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        //聲明源文件和目標文件
        FileInputStream fi = new FileInputStream("C:\\Users\\80626\\Desktop\\1.png");
        FileOutputStream fo = new FileOutputStream("C:\\Users\\80626\\Desktop\\1_copy.png");
        //獲得傳輸通道channel
        FileChannel inChannel = fi.getChannel();
        FileChannel outChannel = fo.getChannel();
        //獲得容器buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int eof = 0;
        while ((eof = inChannel.read(buffer)) != -1) {//讀取的字節將會填充buffer的position到limit位置
            //重設一下buffer:limit=position , position=0
            buffer.flip();
            //開始寫
            outChannel.write(buffer);//只輸出position到limit之間的數據
            //寫完要重置buffer,重設position=0,limit=capacity,用於下次讀取
            buffer.clear();
        }
        inChannel.close();
        outChannel.close();
        fi.close();
        fo.close();
    }
}

2. FileChannel 結合 MappedByteBuffer 實現高效讀寫

  • 上例直接使用 FileChannel 結合 ByteBuffer 實現的管道讀寫,但並不能提高文件的讀寫效率,ByteBuffer 有個子類:MappedByteBuffer,它可以創建一個“直接緩衝區”,並可以將文件直接映射至內存,可以提高大文件的讀寫效率;
  • ByteBuffer 是抽象類,MappedByteBuffer 也是抽象類;
  • 可以調用 FileChannel 的 map() 方法獲取一個 MappedByteBuffer,map() 方法的原型:MappedByteBuffer map(MapMode mode, long position, long size);:將節點中從 position 開始的 size 個字節映射到返回的 MappedByteBuffer 中;

a. 複製 2 GB 以下的文件

  • 此例不能複製大於 2 G 的文件,因爲 map 的第三個參數被限制在 Integer.MAX_VALUE(字節) = 2G
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        try {
            //java.io.RandomAccessFile類,可以設置讀、寫模式的IO流類。
            //"r"表示:只讀--輸入流,只讀就可以。
            RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
            //"rw"表示:讀、寫--輸出流,需要讀、寫。
            RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
            //分別獲取FileChannel通道
            FileChannel in = source.getChannel();
            FileChannel out = target.getChannel();
            //獲取文件大小
            long size = in.size();
            //調用Channel的map方法獲取MappedByteBuffer
            MappedByteBuffer mbbi = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer mbbo = out.map(FileChannel.MapMode.READ_WRITE, 0, size);
            long start = System.currentTimeMillis();
            System.out.println("開始...");
            for (int i = 0; i < size; i++) {
                byte b = mbbi.get(i);//讀取一個字節
                mbbo.put(i, b);//將字節添加到mbbo中
            }
            long end = System.currentTimeMillis();
            System.out.println("用時: " + (end - start) + " 毫秒");
            source.close();
            target.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
輸出
開始...
用時: 2 毫秒
 */
  • map() 方法的第一個參數 mode:映射的三種模式,在這三種模式下得到的將是三種不同的 MappedByteBuffer:三種模式都是 Channel 的內部類 MapMode 中定義的靜態常量,這裏以 FileChannel 舉例:
    (1)FileChannel.MapMode.READ_ONLY:得到的鏡像只能讀不能寫(只能使用 get 之類的讀取 Buffer 中的內容);
    (2)FileChannel.MapMode.READ_WRITE:得到的鏡像可讀可寫(既然可寫了必然可讀),對其寫會直接更改到存儲節點;
    (3)FileChannel.MapMode.PRIVATE:得到一個私有的鏡像,其實就是一個 (position, size) 區域的副本罷了,也是可讀可寫,只不過寫不會影響到存儲節點,就是一個普通的 ByteBuffer 了;
  • 爲什麼使用 RandomAccessFile?
    (1) 使用 InputStream 獲得的 Channel 可以映射,使用 map 時只能指定爲 READ_ONLY 模式,不能指定爲 READ_WRITE 和 PRIVATE,否則會拋出運行時異常;
    (2) 使用 OutputStream 得到的 Channel 不可以映射!並且 OutputStream 的 Channel 也只能 write不能 read;
    (3) 只有 RandomAccessFile 獲取的 Channel 才能開啓任意的這三種模式;

b. 複製 2 GB 以上的文件

  • 下例使用循環,將文件分塊,可以高效的複製大於 2 G 的文件;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String[] args) throws Exception {
        try {
            RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
            RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
            FileChannel in = source.getChannel();
            FileChannel out = target.getChannel();
            long size = in.size();//獲取文件大小
            long count = 1;//存儲分的塊數,默認初始化爲:1
            long copySize = size;//每次複製的字節數,默認初始化爲:文件大小
            long everySize = 1024 * 1024 * 512;//每塊的大小,初始化爲:512M
            if (size > everySize) {//判斷文件是否大於每塊的大小
                //判斷"文件大小"和"每塊大小"是否整除,來計算"塊數"
                count = (int) (size % everySize != 0 ? size / everySize + 1 : size / everySize);
                //第一次複製的大小等於每塊大小。
                copySize = everySize;
            }

            MappedByteBuffer mbbi = null;//輸入的MappedByteBuffer
            MappedByteBuffer mbbo = null;//輸出的MappedByteBuffer
            long startIndex = 0;//記錄複製每塊時的起始位置
            long start = System.currentTimeMillis();
            System.out.println("開始...");
            for (int i = 0; i < count; i++) {
                mbbi = in.map(FileChannel.MapMode.READ_ONLY, startIndex, copySize);
                mbbo = out.map(FileChannel.MapMode.READ_WRITE, startIndex, copySize);

                for (int j = 0; j < copySize; j++) {
                    byte b = mbbi.get(i);
                    mbbo.put(i, b);
                }
                startIndex += copySize;//計算下一塊的起始位置
                //計算下一塊要複製的字節數量。
                copySize = in.size() - startIndex > everySize ? everySize : in.size() - startIndex;
            }


            long end = System.currentTimeMillis();
            source.close();
            target.close();
            System.out.println("用時: " + (end - start) + " 毫秒");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
輸出
開始...
用時: 4 毫秒
 */

3. ServerSocketChannel 和 SocketChannel 創建連接

a. 服務器端 ServerSocketChannel

  • 該類用於連接的服務器端,它相當於:ServerSocket;(1) 調用 ServerSocketChannel 的靜態方法 open():ServerSocketChannel serverChannel = ServerSocketChannel.open();,打開一個通道,新頻道的套接字最初未綁定;必須通過其套接字的 bind 方法將其綁定到特定地址,才能接受連接;
    (2) 調用 ServerSocketChannel 的實例方法 bind(SocketAddress add):serverChannel.bind(new InetSocketAddress(8888));,綁定本機監聽端口,準備接受連接。注意,java.net.SocketAddress 是一個抽象類,代表一個 Socket 地址,可以使用它的子類:java.net.InetSocketAddress 類,其構造方法 InetSocketAddress(int port) 可以指定本機監聽端口;
    (3) 調用 ServerSocketChannel 的實例方法 accept():SocketChannel accept = serverChannel.accept(); System.out.println("後續代碼...");,等待連接;

i. 示例:服務器端等待連接(默認-阻塞模式)

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

public class Test {
    public static void main(String[] args) {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("【服務器】等待客戶端連接...");
            SocketChannel accept = serverChannel.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/*
輸出
【服務器】等待客戶端連接...
 */

ii. 通過 ServerSocketChannel 的 configureBlocking(boolean b) 方法設置 accept() 是否阻塞

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

public class Test {
    public static void main(String[] args) throws Exception {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("【服務器】等待客戶端連接...");
            //   serverChannel.configureBlocking(true);//默認--阻塞
            serverChannel.configureBlocking(false);//非阻塞
            SocketChannel accept = serverChannel.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/*
輸出
【服務器】等待客戶端連接...
 */
  • 可以看到,accept() 方法並沒有阻塞,而是直接執行後續代碼,返回值爲 null;
  • 這種非阻塞的方式,通常用於"客戶端"先啓動,"服務器端"後啓動,來查看是否有客戶端連接,有,則接受連接;沒有,則繼續工作;

b. 客戶端 SocketChannel

  • 該類用於連接的客戶端,它相當於:Socket;
    (1) 先調用 SocketChannel 的 open() 方法打開通道:ServerSocketChannel serverChannel = ServerSocketChannel.open();
    (2) 調用 SocketChannel 的實例方法 connect(SocketAddress add) 連接服務器:socket.connect(new InetSocketAddress("localhost", 8888));

i. 示例:客戶端連接服務器

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

public class Test {
    public static void main(String[] args) {
        try (SocketChannel socket = SocketChannel.open()) {
            socket.connect(new InetSocketAddress("localhost", 8888));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("客戶端完畢!");
    }
}
/*
輸出
客戶端完畢!
 */

4. ServerSocketChannel 和 SocketChannel 收發信息

  • 模擬客戶端和服務器端實現信息交互的過程:

a. 創建服務器端

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

public class Server {
    public static void main(String[] args) {
        try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
            serverChannel.bind(new InetSocketAddress("localhost", 8888));
            System.out.println("【服務器】等待客戶端連接...");
            SocketChannel accept = serverChannel.accept();
            System.out.println("【服務器】有連接到達...");
            //1.先發一條
            ByteBuffer outBuffer = ByteBuffer.allocate(100);
            outBuffer.put("你好客戶端,我是服務器".getBytes());
            outBuffer.flip();//limit設置爲position,position設置爲0
            accept.write(outBuffer);//輸出從position到limit之間的數據

            //2.再收一條,不確定字數是多少,但最多是100字節。先準備100字節空間
            ByteBuffer inBuffer = ByteBuffer.allocate(100);
            accept.read(inBuffer);
            inBuffer.flip();//limit設置爲position,position設置爲0
            String msg = new String(inBuffer.array(), 0, inBuffer.limit());
            System.out.println("【服務器】收到信息:" + msg);
            accept.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

b. 創建客戶端

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

public class Client {
    public static void main(String[] args) {
        try (SocketChannel socket = SocketChannel.open()) {
            socket.connect(new InetSocketAddress("localhost", 8888));

            //1.先發一條
            ByteBuffer buf = ByteBuffer.allocate(100);
            buf.put("你好服務器,我是客戶端".getBytes());
            buf.flip();//limit設置爲position,position設置爲0
            socket.write(buf);//輸出從position到limit之間的數據

            //2.再收一條,不確定字數是多少,但最多是100字節。先準備100字節空間
            ByteBuffer inBuffer = ByteBuffer.allocate(100);
            socket.read(inBuffer);
            inBuffer.flip();//limit設置爲position,position設置爲0
            String msg = new String(inBuffer.array(), 0, inBuffer.limit());
            System.out.println("【客戶端】收到信息:" + msg);
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("客戶端完畢!");
    }
}

c. 效果展示

  • 服務器端打印結果:
    在這裏插入圖片描述
  • 客戶端打印結果:
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章