IO:BIO NIO AIO網絡編程模型

參考視頻:https://www.bilibili.com/video/av76223318?p=5

 

I/O模型簡單的解釋:用什麼樣的通道進行數據的發送和接收,很大程度上決定了程序通訊的性能

Java共支持三種網絡編程模型:BIO,NIO,AIO

 

BIO:Blocking IO

同步並阻塞(傳統阻塞型),服務器實現模式爲一個連接一個線程,即客戶端有連接請求時,服務器端就需要啓動一個線程進行處理,如果這個連續不做任何事情會造成不必要的線程開銷。可以通過線程池機制改善(實現多個客戶連接服務器)。

放在java.io包下

適用場景:

連接數目小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中。jdk1.4之前的唯一選擇,但程序簡單易理解

BIO簡單流程:

1  服務器端啓動一個ServerSockert

2  客戶端啓動Socket對服務器進行通訊,默認情況下服務器端需要對每個客戶建立一個線程與之通訊

3  客戶端發送請求後,先諮詢服務器是否有線程響應

        3.1  如果沒有響應,則會等待,或者被拒絕

        3.2  如果有響應,客戶端線程會等待請求結束後,再繼續執行

 

public class BioServerSocket {
    public static void main(String[] args) throws Exception{
        //創建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        //用線程池來管理線程
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        System.out.println("===== 啓動ServerSocket");
        //啓用並監聽
        while (true){
            //等待並監聽
            Socket socket = serverSocket.accept();
            //獲得監聽後啓用線程來處理
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    byte[] bytes = new byte[1024];
                    try {
                        InputStream inputStream = socket.getInputStream();
                        int read;
                        while ((read = inputStream.read(bytes)) != -1){
                            System.out.println(new String(bytes,0,read));
                        }
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                }
            });
        }
    }
}

利用命令窗口的telnet模仿客戶端發送請求

telnet命令若需要啓動:https://jingyan.baidu.com/article/7908e85c6ec355af491ad265.html

連接命令:telnet 127.0.0.1 6666

作爲客戶端發送命令:Ctrl  ]

發送內容命令:send XXXXX

實際發送內容爲XXXX

 

NIO:Non-blocking/New IO

同步非阻塞,服務器實現爲一個線程處理多個請求,即客戶端發送的連接請求都會註冊到多路複用器(Selector選擇器)上,多路複用器輪詢到連接有I/O請求就進行處理

放在java.nio包下

適用場景:

連接數目多且連接時間短(輕操作)的架構,比如聊天服務器,彈屏系統,服務期間通訊等。編程比較複雜,jdk1.4之後開始

三大核心部分:Channel通道,Buffer緩衝區,Selector選擇器

NIO是面向緩衝區,或者面向塊編程,是Channel的事件Event驅動的

 

BIO  和  NIO 比較:

1  BIO以流的方式處理數據,NIO以塊的方式處理數據,塊IO的效率比流IO的效率要高很多

2  BIO是阻塞的,NIO是非阻塞的

3  BIO是基於字節流和字符流進行操作,而NIO基於Channel和Buffer進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector用於監聽多個通道的事件,因此使用單個線程可以監聽多個客戶端通道

 

Buffer緩衝區:

本質是一個可以讀寫數據的內存塊,可以理解成一個容器對象(含數組),該對象提供一組方法,可以更輕鬆的使用內存塊。緩衝區對象內置了一些機制,可以跟蹤和記錄緩衝區的狀態變化情況。Channel提供可以從文件,網絡讀取數據的渠道,但讀取和寫入的數據必須經由Buffer。

子類都有四個重要屬性:

    Capatity:  容量,即可以容納的最大數據量。在緩衝區創建時被設定並且不能改變。 

    Limit: 表示緩衝區的當前終點,不能對緩衝區超過極限的位置進行讀寫,且極限是可以修改的

    Position:  位置,下一個要被讀或者寫的元素的索引,每次讀寫緩衝區時都會改變該值,爲下次讀寫做準備

    Mark:標記

    static void Test(){
        ByteBuffer byteBuffer = ByteBuffer.allocate(5); //創建緩衝區
        ByteBuffer direct = ByteBuffer.allocateDirect(5);//創建直接緩衝區

        byteBuffer.put("h".getBytes()[0]);
        byteBuffer.put("s".getBytes()[0]);
        byteBuffer.put("s".getBytes()[0]);
        byteBuffer.put("n".getBytes()[0]);
        byteBuffer.put("j".getBytes()[0]);
        byteBuffer.flip(); //切換讀寫
        while (byteBuffer.hasRemaining()){
            System.out.println(byteBuffer.get());
        }
        System.out.println(" ================ ");
        byteBuffer.put(1,"k".getBytes()[0]);
        System.out.println("第一個元素:"+byteBuffer.get(1));
        System.out.println("第二個元素:"+byteBuffer.get(2));
        System.out.println("容量:"+byteBuffer.capacity());
        System.out.println("位置:"+byteBuffer.position());
        System.out.println(".. 具體諸多其他方法搜搜就好了");
    }

 

Channel通道:

類似於流,但有區別

1  通道能同時進行讀寫,而流只能進行讀或者只能寫

2  通道能異步進行讀寫數據

3  通道能從緩衝讀取數據,也能寫入緩衝

Channel在java.nio中是接口,具體常用的實現類:FileChannel(文件數據讀寫),  DatagramChannel(UDP的數據讀寫),  ServerSocketChannel(TCP數據讀寫),  SocketChannel(TCP數據讀寫)

FileChannel:

方法:

      read(ByteBuffer des);     通道讀取數據,放到緩存區

      write(ByteBuffer tar);      讀取緩衝區數據,放到通道

      transferFrom(....);          從目標通道複製數據到當前通道

      transferTo(....);               從當前通道複製數據到目標通道

static void test2() throws Exception {
        //發送數據
        String str = "Hi,女孩";
        //封裝的Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put(str.getBytes());
        //buffer反轉
        byteBuffer.flip();

        //最終存儲地方
        FileOutputStream fileInputStream = new FileOutputStream("d://hiGirl.text");
        //獲得通道
        FileChannel channel = fileInputStream.getChannel();

        //緩衝區讀取數據到通道
        channel.write(byteBuffer);

        //關閉流
        fileInputStream.close();
    }


    static void test4() throws Exception{
        File file = new File("d://hiGirl.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir2.txt");

        FileChannel channel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());

        while (true){
            byteBuffer.clear();
            int read = channel.read(byteBuffer);
            if (read == -1) break;
            byteBuffer.flip();
            outputStreamChannel.write(byteBuffer);
            byteBuffer.flip();
        }


        fileInputStream.close();
        fileOutputStream.close();
    }

    static void test5() throws Exception{
        File file = new File("d://hiGirl.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir4.txt");
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();

        inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);
        // 或 outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());

        inputStreamChannel.close();
        outputStreamChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }

MapperedByteBuffer:

可以讓文件直接在內存(對外內存)中進行修改(操作系統不需要copy),而如何同步到文件由NIO完成

 static void test6() throws Exception{
        RandomAccessFile randomAccessFile = new RandomAccessFile("d://hiGirl.txt","rw");
        FileChannel channel = randomAccessFile.getChannel();
        /**
         * FileChannel.MapMode
         *  READ_ONLY: 只讀
         *  READ_WRITE:讀寫
         *  PRIVATE: private (copy-on-write)
         *
         * 定義可以修改的範圍
         * position: 可以修改的起始位置
         * size: 映射內存大小
         */
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        map.put(0,(byte) 'H');
        map.put(2,(byte) 'L');

        randomAccessFile.close();
    }

scattering:將數據寫入到Buffer時,可以採用Buffer數組,依次寫入

gatthering:將數據讀取到Buffer時,可以採用Buffer數組,依次讀取

 static void test7() throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        SocketChannel accept = serverSocketChannel.accept();

        //設置數組Buffer
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(3);
        byteBuffers[1] = ByteBuffer.allocate(5);

        int mesLength = 3+5;

        while (true){
            long byteRead = 0;

            while (byteRead < mesLength){
                long read = accept.read(byteBuffers);
                byteRead += read;
                System.out.println("byteRead: "+byteRead);
                Arrays.asList(byteBuffers).stream().map(buffer -> "position: "+buffer.position()+" ,limit: "+buffer.limit())
                        .forEach(System.out::println);
            }

            //將所有buffer進行反轉可以進行其他操作
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());

            //將數據讀出顯示到客戶端
            long byteWrite = 0;
            while (byteWrite < mesLength){
                long write = accept.write(byteBuffers);
                byteWrite += write;
            }

            //復位
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());

            System.out.println("byteRead = "+byteRead + "  byteWrite = "+byteWrite);
        }
    }

 

selector:

selector能夠檢測多個註冊的通道上是否有事件發生(多個Channel可以以事件的方式註冊到註冊到同一個Selector),如果有事件發生便獲取事件,然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。

1  Netty的IO線程聚合了Selector選擇器,可以同時併發處理成百上千個併發請求

2  當線程從某客戶端Socket通道進行讀寫數據時,若沒有線程可用時,可進行其他操作

3  線程通常將阻塞IO的空閒時間用於其他通道上執行IO操作,所以單個線程可以管理多個輸入和輸出通道

4  由於讀寫操作都是非阻塞的,這就可以充分提升IO線程的運行效率,避免由於頻繁IO阻塞導致的線程掛起

5  一個IO線程可以併發處理N個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞IO一連接一線程模型,架構的性能,彈性伸縮能力和可靠性都得到了極大的提升

Selector抽象類

public abstract class Selector implements Closeable {
    
    //得到一個選擇器對象
     public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    //監控所有註冊通道,當其中有IO操作可進行時,將對應的SelectionKey加入到內部集合中並返回,參數用來設置超時時間
    //select()爲阻塞方法,至少有一個事件發生纔會返回
    //select(long timeout) 非阻塞,若無事件發生,超時時間後也會返回
    //selectNow() 非阻塞,有事件發生立馬返回
    //wakeup() 喚醒selector
    public abstract int select(long timeout)
        throws IOException;
    //從內部集合中得到所有SelectionKey
    public abstract Set<SelectionKey> selectedKeys();

}

原理

1 當有客戶端連接時,會通過ServerSocketChannel得到對應的SocketChannel

2 對應的SocketChannel註冊倒Selector上,一個Selector上可以註冊多個SocketChannel

   -----  SelectionKey register(Selector sel, int ops,Object att)

3 註冊後返回一個SelectionKey,會和該Selector關聯

4 Selector進行監聽,用select()方法 ,會返回有事件發生的通道的個數

5 進一步得到各個SelectionKey

6 再通過SelectionKey反向獲取SocketChannel channel()

7 可以通過得到的channel,完成業務處理

 

OP_ACCEPT: 有新的網絡連接可以accept, 1<<4 = 16

OP_CONNECT:代表連接已經建立:1<<3=8

OP_READ:代表讀操作:1<<0 = 1

OP_WRITE:代表寫操作:1<<2=4

 

零拷貝:零拷貝不是不拷貝,而是沒有CPU拷貝

  零拷貝是網絡編程的關鍵,很多性能優化都離不開。在java程序中,常用的零拷貝mmap(內存映射)和sendFile。那麼它們在OS裏到底是一個怎樣的設計?

1  我們說的零拷貝,是從操作系統角度來說。因爲內核緩衝區之間,沒有數據是重複的(只有kernel buffer有一份數據)

2  零拷貝不僅帶來更少的數據複製,還能帶來其他的性能優勢,例如更少的上下文切換,更少的CPU緩存僞共享以及無CPU校驗和計算

(用戶態,kernel[內核態],硬件)

mmap:

文件映射,將文件映射到內核緩衝區,同時,用戶空間可以共享內核空間的數據。這樣在進行網絡傳輸時,就可以減少內核空間到用戶控件的拷貝次數

 

SendFile:

Linux2.1提供了sendFile函數,基本原理:數據不經過用戶態,直接從內核緩衝區進入到SocketBuffer,同時由於和用戶態完全無關,就減少了一次上下文切換

Linux2.4對sendFile函數進行了優化,避免了從內核緩衝區拷貝到SocketBuffer的操作,直接拷貝到協議棧,從而再一次減少了數據拷貝

mmap 和sendfile區別

1 mmap適合小數據量讀寫,sendfile適合大文件傳輸

2 mmap需要3次上下文切換,3次數據拷貝;sendfile需要2次上下文切換,最少2次數據拷貝

3 sendfile可以利用DMA方式,減少CPU拷貝,mmap則不能(必須從內核拷貝到Socket緩衝區)

傳統拷貝:

    4次拷貝:   

        硬件 -> DMA拷貝到 -> 內核態(kernelBuffer)

        內核態(kernelBuffer) -> cpu拷貝到 ->用戶buffer

        用戶buffer -> cpu拷貝到 -> socket buffer

        socket buffer -> DMA拷貝到 -> 協議棧

    3次切換:硬件,內核態, 用戶

MMAP拷貝:

    3次拷貝:   

        硬件 -> DMA拷貝到 -> 內核態(kernelBuffer)

        內核態(kernelBuffer) -> cpu拷貝到 -> socket buffer

        socket buffer -> DMA拷貝到 -> 協議棧

    3次切換:硬件,內核態, 用戶

SendFile拷貝:

    linux2.1版的sendfile中還有一次CPU拷貝:3次拷貝,2次切換

    linux2.4版的纔是零拷貝:2次拷貝,2次切換

 

 

零拷貝代碼Test:

server:

public class NewIOServiceSocket {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(7000));
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true){
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readLength = 0;
            while (readLength != -1){
                readLength = socketChannel.read(byteBuffer);
                byteBuffer.rewind();//倒帶
            }
        }
    }
}

client

public class NewIOClient {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",7000));
        FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\chinaoly\\Desktop\\12345.txt"));
        FileChannel fileChannel = fileInputStream.getChannel();
        long start = System.currentTimeMillis();
        /**
         * linux 環境下 transferTo 一次即可完成
         * windows環境下,transferTo 一次最多傳8M,大於8M分段傳輸,需要記住傳輸時的位置
         */
        long count = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println(" 時間 : "+(System.currentTimeMillis() - start));
    }
}

 

AIO(NIO.2):

異步非阻塞,AIO引入異步通道的概念,採用Proactor模式,簡化了程序編寫,有校的請求才啓用線程,它的特點是先有操作系統完成後才通知服務器端啓動線程去處理,一般適用於連接數較多且連接時間較長的應用。jdk1.7以後引入,但目前還未得到廣泛運用

適用場景:

連接數目多且連接時間長(重操作)的架構。比如相冊服務器,充分調用OS參與併發操作。編程負責,jdk7開始

 

 

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