Java〖NIO篇〗看這一篇就夠了 緩衝區 通道 阻塞式非阻塞式IO

參考課程

PS: 之前一直想了解這個NIO到底是什麼東西,奈何目前用不到,聽說現在許多框架都在用,而且面試的時候也有被問道,感覺還是去多瞭解瞭解底層怎麼實現的~~這是我的第100篇博客!!!

一. NIO與IO區別

在這裏插入圖片描述
NIO主要有三大核心部分:Channel(通道),Buffer(緩衝區), Selector

二. 緩衝區

在這裏插入圖片描述

在這裏插入圖片描述

緩衝區(Buffer) :一個用於特定基本數據類型的容器。由 java.nio 包定義的,所有緩衝區都是 Buffer 抽象類的子類。
Java NIO 中的 Buffer 主要用於與 NIO 通道進行交互,數據是從通道讀入緩衝區,從緩衝區寫入通道中的。

2.1 直接緩衝區與非直接緩衝區

  • 非直接緩衝區:通過allocate()方法分配緩衝區,將緩衝區建立在JVM的內存中
  • 直接緩衝區:通過allocateDirect()方法分配直接緩衝區,將緩衝區建立在物理內存中。可以提高效率

在這裏插入圖片描述
這裏探究驗證了一下底層Buffer的原理

  //分配指定大小的緩衝區
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        String s=new String("abcde");
        //緩衝區存數據
        System.out.println("PUT操作");
        byteBuffer.put(s.getBytes());
        System.out.println(byteBuffer.position()); //5
        System.out.println(byteBuffer.limit());  //1024
        System.out.println(byteBuffer.capacity()); //1024
        System.out.println("***************************");

        System.out.println("filp操作");
        byteBuffer.flip();
        System.out.println(byteBuffer.position()); //0
        System.out.println(byteBuffer.limit());    //5
        System.out.println(byteBuffer.capacity()); //1024
        System.out.println("***************************");

        System.out.println("get操作");
        byte[] b=new byte[byteBuffer.limit()];
        byteBuffer.get(b,0,4);
        if(byteBuffer.hasRemaining()){ //判斷緩衝區是否有剩餘
            System.out.println("緩衝區還有幾個剩餘的: "+byteBuffer.remaining());
        }
        System.out.println(byteBuffer.position()); //5
        System.out.println(byteBuffer.limit());    //5
        System.out.println(byteBuffer.capacity()); //1024
        System.out.println("***************************");

        System.out.println("rwind操作可重複讀"); //又把position置於開頭
        byteBuffer.rewind();
        System.out.println(byteBuffer.position()); //0
        System.out.println(byteBuffer.limit());    //5
        System.out.println(byteBuffer.capacity()); //1024
        System.out.println("***************************");


        System.out.println("mark操作記錄當前的position位置"); //又把position置於開頭
        byteBuffer.mark();
        byte[] bytes=new byte[1024];
        byteBuffer.get(bytes,0,2); //取走前兩個,position爲2
        System.out.println("取走前兩個,position爲"+byteBuffer.position()); //2
        byteBuffer.reset(); //恢復到mark位置的position 爲0
        System.out.println("恢復到mark位置的position 爲"+byteBuffer.position()); //0
        System.out.println("***************************");

        System.out.println("clear操作清空(非真清空,相當於把position和limit恢復開始狀態)數組中的數據依然存在"); //又把position置於開頭
        byteBuffer.clear();
        System.out.println(byteBuffer.position()); //0
        System.out.println(byteBuffer.limit());    //1024
        System.out.println(byteBuffer.capacity()); //1024
        System.out.println("***************************");

        //獲取第一個bytez轉化爲char還是能輸出
        System.out.println("獲取第一個bytez轉化爲char還是能輸出"+(char)byteBuffer.get());

        if(byteBuffer.hasRemaining()){ //判斷緩衝區是否有剩餘
            System.out.println("緩衝區還有幾個剩餘的: "+byteBuffer.remaining());
        }

在這裏插入圖片描述

ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1024);//創建一個直接緩衝區
System.out.println(byteBuffer.isDirect());//判斷是否爲直接緩衝區

三. 通道

在這裏插入圖片描述

通道:由java.nio.channels包定義。
Channel表示IO源與目標打開的連接。
Channel類似於傳統的“流”。但其自身不能直接訪問數據,Channel只能與Buffer進行交互。

java.nio.channels.Channel 接口

  • FileChannel:用於讀取、寫入、映射和操作文件的通道。
    
  • SocketChannel:通過 TCP 讀寫網絡中的數據。
    
  • ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。
    
  • DatagramChannel:通過 UDP 讀寫網絡中的數據通道。
    

3.1 java針對支持通道的類提供了getChannel()方法

  • 本地IO:
FileInputStream/FileOutputStream
long start=System.currentTimeMillis();

        FileInputStream fis=null;
        FileOutputStream fos=null;
        //獲取通道
        FileChannel inchannel = null;
        FileChannel outchannel = null;

        try {
            fis = new FileInputStream("E:\\image/1.jpg");
            fos = new FileOutputStream("E:\\image/3.jpg");
            inchannel = fis.getChannel();
            outchannel = fos.getChannel();
            //設置非直接緩衝區大小
            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
            while(inchannel.read(byteBuffer)!=-1){ //將通道里的數據存入緩衝區
                byteBuffer.flip(); //切換讀取模式;
                outchannel.write(byteBuffer); // //將緩衝區中的數據寫入通道中
                byteBuffer.clear(); ////清空緩衝區
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally{
            if(outchannel!=null){
                try {
                    outchannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(inchannel!=null){
                try {
                    inchannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fos!=null){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        long end=System.currentTimeMillis();
        System.out.println("耗費時間:"+(end-start));//耗費時間:1094
RandomAccessFile

在這裏插入圖片描述

      RandomAccessFile raf=new RandomAccessFile("E:\\image/1234.txt","rw");
        //建立通道
        FileChannel inChannel = raf.getChannel();
        //創建緩存區數組 ->分散讀取
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        ByteBuffer byteBuffer1=ByteBuffer.allocate(100);
        ByteBuffer[] buffers={byteBuffer,byteBuffer1};
        //讀取到緩存區
        inChannel.read(buffers);
        for (ByteBuffer buffer : buffers) {
            buffer.flip(); //切換爲讀模式
        }
        //查看各自通道有多少
        System.out.println(new String(buffers[0].array(),0,buffers[0].limit()));
        System.out.println("****************************************************");
        System.out.println(new String(buffers[1].array(),0,buffers[1].limit()));

        //聚集寫入
        RandomAccessFile raf2=new RandomAccessFile("E:\\image/12345.txt","rw");
        FileChannel outChannel = raf2.getChannel();
        outChannel.write(buffers);
        inChannel.close();
        outChannel.close();
  • 網絡IO:
    Socket
    ServerSocket
    DatagramSocket

3.2 在JDK 1.7 中的NIO.2 針對各個通道提供了靜態方法 open()

   		long start=System.currentTimeMillis();
        //通過靜態方法 open()創建通道
        FileChannel inChannel=FileChannel.open(Paths.get("E:\\image/1.jpg"), StandardOpenOption.READ); //讀模式
        FileChannel outChannel=FileChannel.open(Paths.get("E:\\image/4.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); //讀寫模式;存在就報錯,不存在就創建
        //內存映射文件
        MappedByteBuffer inMappedBuf=inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
        MappedByteBuffer outMappedBuf=outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
        //進行寫操作
        byte[] bytes=new byte[inMappedBuf.limit()];
        inMappedBuf.get(bytes);
        outMappedBuf.put(bytes);
        //關閉通道
        inChannel.close();
        outChannel.close();
        long end=System.currentTimeMillis();
        System.out.println("耗時: "+(end-start));

直接通過通道複製

 	    long start=System.currentTimeMillis();
        FileChannel inChannel=FileChannel.open(Paths.get("E:\\image/1.jpg"), StandardOpenOption.READ); //讀模式
        FileChannel outChannel=FileChannel.open(Paths.get("E:\\image/4.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); //讀寫模式;存在就覆蓋,不存在就創建
        inChannel.transferTo(0,inChannel.size(),outChannel);
        inChannel.close();
        outChannel.close();
        long end=System.currentTimeMillis();
        System.out.println("耗時: "+(end-start));

3.3 在JDK 1.7 中的NIO.2 的Files工具類的newByteChannel()

3.4 測試結果

在這裏插入圖片描述

四. NIO 的阻塞式與非阻塞式網絡通信

傳統的 IO 流都是阻塞式的。也就是說,當一個線程調用 read() 或 write()時,該線程被阻塞,直到有一些數據被讀取或寫入,該線程在此期間不能執行其他任務。因此,在完成網絡通信進行 IO 操作時,由於線程會阻塞,所以服務器端必須爲每個客戶端都提供一個獨立的線程進行處理,當服務器端需要處理大量客戶端時,性能急劇下降。

Java NIO 是非阻塞模式的。當線程從某通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道。因此,NIO 可以讓服務器端使用一個或有限幾個線程來同時處理連接到服務器端的所有客戶端。

選擇器(Selector)
選擇器(Selector) 是 SelectableChannle 對象的多路複用器,Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的線程管理多個 Channel。Selector 是非阻塞 IO 的核心。

使用NIO 完成網絡通信的三個核心:

1. 通道(Channel):負責連接
 *      java.nio.channels.Channel 接口:
 *           |--SelectableChannel
 *               |--SocketChannel
 *               |--ServerSocketChannel
 *               |--DatagramChannel
 *               
 *               |--Pipe.SinkChannel
 *               |--Pipe.SourceChannel
 *               
2. 緩衝區(Buffer):負責數據的存取
3. 選擇器(Selector):是 SelectableChannel 的多路複用器。用於監控SelectableChannel的IO狀況

4.1 阻塞式IO

   //阻塞式通信客戶端
    @Test
    public void client() throws IOException {
        //獲取通道
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9777));
        //文件通道
        FileChannel fileChannel=FileChannel.open(Paths.get("E:\\image/1.jpg"), StandardOpenOption.READ);
        //緩衝區
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //讀取緩衝區
        while (fileChannel.read(byteBuffer)!=-1){
            byteBuffer.flip(); //切換讀模式
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        //關閉寫模式
        socketChannel.shutdownOutput();
        //獲取服務端信息
        int len=0;//記錄長度
        while((len=socketChannel.read(byteBuffer))!=-1){
            byteBuffer.flip(); //切換讀模式
            System.out.println(new String(byteBuffer.array(),0,len));// 輸出
            byteBuffer.clear(); //清空緩衝區(復位)
        }

        socketChannel.close();
        fileChannel.close();

    }

    //阻塞式通信服務端
    @Test
    public void server() throws IOException {
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); //打開服務端通道
        serverSocketChannel.bind( new InetSocketAddress(9777)); //綁定端口

        FileChannel outChannel=FileChannel.open(Paths.get("E:\\image/19.jpg"),StandardOpenOption.WRITE,StandardOpenOption.CREATE); //文件寫入

        //服務端處於阻塞狀態
        SocketChannel socketChannel=serverSocketChannel.accept();
        //定義緩衝區
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);

        while (socketChannel.read(byteBuffer)!=-1){
            byteBuffer.flip();
            outChannel.write(byteBuffer);
            byteBuffer.clear();
        }

        //發送反饋給客戶端
        byteBuffer.put("服務端接收完畢".getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);//寫回去給客戶端

        socketChannel.close();
        outChannel.close();
        serverSocketChannel.close();
    }

4.2 非阻塞式IO(ServerSocketChannel/SocketChannel)

在這裏插入圖片描述

//非阻塞式通信客戶端
    @Test
    public void Nonclient() throws IOException{
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9777));
        //設置非阻塞式模式
        socketChannel.configureBlocking(false);
        //設置緩衝區
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //緩衝區設置當前時間
        byteBuffer.put(LocalDateTime.now().toString().getBytes());
        byteBuffer.flip();//轉換讀模式
        socketChannel.write(byteBuffer); //將緩衝區數據發送給客戶端
        byteBuffer.clear();
        //關閉通道
        socketChannel.close();
    }

    //非阻塞式通信服務端
    @Test
    public void Nonserver() throws IOException{
        //獲取通道
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        //切換非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //綁定鏈接
        serverSocketChannel.bind(new InetSocketAddress(9777));
        //獲取選擇器
        Selector selector=Selector.open();
        //將通道註冊到選擇器上,並監聽接受事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //輪詢式的選擇 選擇器中的事件
        while (selector.select()>0){
            //用迭代器迭代
            Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();

            while(iterator.hasNext()){
                //獲取準備就緒的事件
                SelectionKey sk=iterator.next();
                //判斷具體的是什麼事件
                if(sk.isAcceptable()){
                    //若接受就緒,獲取客戶端連接
                     SocketChannel channel = serverSocketChannel.accept();
                    //切換非阻塞模式
                     channel.configureBlocking(false);
                    //註冊到選擇器上
                     channel.register(selector,SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    SocketChannel socketChannel= (SocketChannel) sk.channel();//獲取通道
                    ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//開闢緩衝區
                    int len=0;
                    while((len=socketChannel.read(byteBuffer))>0){
                        byteBuffer.flip();//切換讀模式
                        System.out.println(new String(byteBuffer.array(),0,len));//輸出數據
                        byteBuffer.clear();//復位
                    }
                }
                iterator.remove();//取消選擇selectionKey
            }
        }
    }

4.3 非阻塞式IO(DatagramChannel)

    //DatagramChannel 數據報通道客戶端
    @Test
    public void DatagramChannelClient() throws IOException{
        DatagramChannel datagramChannel=DatagramChannel.open();
        //設置非阻塞通道
        datagramChannel.configureBlocking(false);
        //開闢緩衝區
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //輸入信息(帶時間格式)
        Scanner scanner=new Scanner(System.in);
        while (scanner.hasNext()){
            String msg=scanner.next();
            byteBuffer.put((LocalDateTime.now().toString()+"\n"+msg).getBytes());
            byteBuffer.flip();
            datagramChannel.send(byteBuffer,new InetSocketAddress("127.0.0.1",9777));//向目的地發送數據
            byteBuffer.clear(); //清空復位
        }
        datagramChannel.close();//關閉
    }

    //DatagramChannel 數據報通道服務端
    @Test
    public void DatagramChannelServer() throws IOException{
        DatagramChannel datagramChannel=DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        datagramChannel.bind(new InetSocketAddress(9777));

        Selector selector=Selector.open();

        datagramChannel.register(selector,SelectionKey.OP_READ);

        while (selector.select()>0){
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                if(selectionKey.isReadable()){
                    ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                    datagramChannel.receive(byteBuffer);
                    byteBuffer.flip();
                    System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
                    byteBuffer.clear();
                }

                iterator.remove();
            }

        }
    }

4.4 測試

如果 IDEA 使用@Test單元測試控制檯輸入不了

首先找到你IDEA 安裝目錄
在這裏插入圖片描述
最底下增加一句,然後重啓IDEA 即可

-Deditable.java.test.console=true

測試結果

客戶端1號
在這裏插入圖片描述
客戶端2號
在這裏插入圖片描述
服務端接受結果
在這裏插入圖片描述

五. 管道

在這裏插入圖片描述

public class TestPipe {
    @Test
    public void test1()throws IOException{
        //1.獲取管道
        Pipe pipe=Pipe.open();
        //2.將緩衝區中的數據寫入管道
        ByteBuffer buf=ByteBuffer.allocate(1024);
        Pipe.SinkChannel sinkChannel=pipe.sink();
        buf.put("通過單向管道發送數據".getBytes());
        buf.flip();
        sinkChannel.write(buf);

        //3.讀取緩衝區中的數據
        Pipe.SourceChannel sourceChannel=pipe.source();
        buf.flip();
        int len=sourceChannel.read(buf);
        System.out.println(new String(buf.array(),0,len));

        sourceChannel.close();
        sinkChannel.close();
    }
}

製作不易,轉載請標註~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章