深刻分析Java IO機制

1、IO介紹:

1.1 Java IO分類:

  1. IO按照處理的數據類型 可分爲:(1)面向字節操做的I/O接口:inputStream,outputStream (2)面向字符操做的接口:Reader,Writer
  2. IO按照數據的傳輸方式 可分爲:(1)面向磁盤操做的I/O接口:File (2)面向網絡操做的I/O接口:Socket
  3. 因此I/O主要的操做能夠總結爲將什麼類型的數據以何種傳輸方式傳到什麼地方去。

1.2 Unix中IO的五種模型:

以網絡IO爲例:html

當客戶端發送的網絡包通過路由器和交換器的轉發後到達對應服務端的網絡適配器(網卡),並存儲在對應網絡I/O的套接字文件中,而後操做系統會將該文件中的數據通常經過DMA複製到內存中供應用程序使用;java

Unix網絡編程這本書中概述了完成上述操做的幾種模型:編程

  1. 首先解釋兩組名詞: 這兩組名詞其實只是對同一個場景的兩種不一樣的描述方式:

(1)阻塞與非阻塞: 阻塞與非阻塞主要是從 CPU 的消耗上來講的,阻塞就是 CPU 停下來等待一個慢的操做完成後CPU 才接着完成其它的事。非阻塞就是在這個慢的操做在執行時 CPU 去幹其它別的事,等這個慢的操做完成時,CPU 再接着完成後續的操做。bash

(2)同步與非同步: 同步與非同步主要是從程序方面來講的,同步指程序發出一個功能調用後,沒有結果前不會返回。非同步是指程序發出一個功能調用後,程序便會返回,而後再經過回調機制通知程序進行處理。服務器

  1. 同步阻塞IO(BIO):

注意,此時客戶端與服務端已經經過三次握手創建了鏈接,便可以經過套接字文件進行數據的交換,因此在此模型下的服務端的用戶進程阻塞在 recvfrom方法等待客戶端發送的數據發送到內存並返回;

這個模型最大的問題就是操做系統中最典型的CPU速度與外設速度不匹配的問題,網絡適配器的速度相對於CPU的速度是極慢的,而且此時CPU卻一直在阻塞。網絡

  1. 同步非阻塞IO:

當用戶線程調用 recvfrom 方法後,若是此時套接字文件尚未準備好,則直接返回一個錯誤信息,而後CPU就會去作其餘事情,而該線程會不斷獲取CPU時間片進行輪詢,因此該模式下雖然是非阻塞,但其線程切換確實很頻繁的,因此經過該方式增長的CPU使用時間與線程切換的成本仍是須要好好評估的;

而且當數據準備好後,而且線程獲取到時間片再次調用recvfrom 時,線程仍是須要等待數據拷貝至內存的。異步

  1. 多路複用IO:(Java NIO原理) socket

    該模型經過一個方法select,該方法一直會阻塞到IO事件的到來(即套接字文件準備好)再返回,這個時候咱們再調用recvfrom方法就只須要等待數據拷貝至內存便可;而且select方法能夠監聽多個事件,因此聯繫到Java NIO中時,就是多個線程能夠向同一個Selector註冊多個事件,從而達到了多路複用的效果。

  2. 異步IO(AIO):post

該模型經過操做系統提供的異步IO方法 aio_read,應用程序調用後便直接返回,而且不須要像前幾種模型同樣須要等待數據拷貝至內存;

但其內在的實現仍是很複雜的,底層仍是使用BIO實現的,就不展開描述了,由於對編程人員好像並無太大的做用。性能

  1. 信號驅動IO:

其實籠統點講,AIO和多路複用IO其實也是某種信號進行驅動的IO,即都不須要應用程序阻塞在 網絡適配器(網卡)的數據準備好的這個過程當中,而都是通發出種信號進行通知應用程序,雖然信號的實現方式或是用 select 或是用更底層的方式,但本質上仍是很類似的;但信號驅動IO也是須要線程等待數據拷貝至用戶空間的。

2、Java BIO:

2.1 簡介:

注:《深刻理解計算機系統》中定義,Linux將全部外設抽象成文件,與外設的通訊被抽象成文件的讀寫;而網絡也只是外設的一種;客戶端與服務器端創建鏈接時互相交換了彼此的文件描述符,以後兩端進行通訊即爲向這兩個文件描述符對應的套接字文件中寫值


Java中的Socket是對進行通訊的兩端的抽象,其封裝了一系列TCP/IP層面的底層操做; 代碼以下:

  1. 客戶端:
//經過一個IP:PORT套接字新建一個Socket對象,肯定要鏈接的服務器的位置和端口
            Socket socket = new Socket("127.0.0.1", 8089);
            //經過Socket對象拿到OutputStream,能夠將其理解經過其向服務器端對應的套接字文件寫入數據
            OutputStream outputStream = socket.getOutputStream();
            //使用默認的字符集去解析outputStream的字節流
            PrintWriter printWriter = new PrintWriter(outputStream, true);
            /*向服務器發送一個HTTP1.1的請求*/
            printWriter.println("GET /index.html HTTP/1.1");
            printWriter.println("Host: localhost:8080");
            printWriter.println("Connection Close");
            printWriter.println();
複製代碼
  1. 服務端:
//ServerSocket在該套接字上監聽鏈接事件
            ServerSocket serverSocket = new ServerSocket(8089, 1, InetAddress.getByName("127.0.0.1"));
            //服務端阻塞在accept()方法上,直到客戶端的connect()請求,並返回一個Socket對象
            socket = serverSocket.accept();
            //從返回的Socket對象中獲取該Socket對應的套接字文件的內容並進行讀取
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            int i = 0;
            while (i != -1) {
            i = bufferedReader.read();
            System.out.println("拿到的數據爲:"+(char)i);
            }
            socket.close();
複製代碼

其實Java BIO 即爲對系統提供的網絡I/O方法的封裝;

2.2 Java BIO 帶來的問題:

咱們通常都是適用Acceptor模型來進行BIO服務端的建立,即經過一個ServerSocket()監聽來自客戶端的鏈接,而後經過三次握手創建鏈接後便會建立一個子線程並經過線程池進行相應的邏輯處理;

而上述邏輯帶來了一系列問題:

  1. Acceptor是一個單線程,即全部鏈接的請求都是串行處理的,而ServerSocket是經過backlog這個參數來代表在服務端拒絕鏈接請求以前,能夠排隊的請求數量,因此這樣的模型註定了BIO性能的侷限性(排隊的通訊線程可能要阻塞一段時間),處理量的侷限性;
  2. 阻塞IO天生的問題,即須要一個線程對應一個鏈接,因此對資源的要求比較高;
  3. 一些特殊的應用場景,如多個線程須要共享資源的時候,而BIO模型下每一個線程之間是不共享資源的。

3、 Java NIO:

3.1 與BIO對比,改變了什麼,又爲何要這麼改變?

圖片及實例代碼參考來自: juejin.im/post/5d1acd…

  1. Java NIO經過多路複用IO的模型實現了單個Selector線程管理了多個鏈接,解決了BIO最致命的一個問題;

  2. 不管是In/OutputStream仍是Java NIO中的通道channel 本質上都是對網絡I/O文件的抽象,與前者不一樣,channel是雙通道的,既能夠讀又能夠寫。

因此按照I/O多路複用 的模型,當channel中的數據準備好了的時候會返回一個可讀的事件,而且經過selector進行處理,安排相應的Socket進行相應數據的讀取,這是一個數據可讀的事件,而Selector可監聽的事件有四種:

SelectionKey.OP_CONNECT // 鏈接事件
SelectionKey.OP_ACCEPT //接收事件
SelectionKey.OP_READ //數據可讀事件
SelectionKey.OP_WRITE //可寫事件
複製代碼
  1. 爲何要引入Buffer機制? 在BIO的時候咱們通常是經過相似於socket.getInputStream.write()方法來直接進行讀寫的,而NIO中向channel中寫入數據必須從buffer中獲取,而channel也只能向buffer寫入數據,這樣使得這樣的操做更爲接近操做系統執行I/O的方式;細一點講,是由於在向OutputStream中write()數據即爲向接收方Socket對象中的InputStream中的RecvQ隊列中,而若是write()的數據大於隊列中每一個數據對象限定的長度,就須要進行拆分,而這個過程,咱們是不能夠控制的,並且涉及到用戶空間與內核空間地址的轉換;可是當咱們使用Buffer後,咱們能夠控制Buffer的長度,是否擴容以及如何擴容咱們均可以掌握。 參考文章:www.ibm.com/developerwo…

3.2 咱們來看一段實例代碼(服務端):

/**
 * @CreatedBy:CVNot
 * @Date:2020/2/21/15:30
 * @Description:
 */
public class NIOServer {
    public static void main(String[] args) {
        try {
            //建立一個多路複用選擇器
            Selector selector = Selector.open();
            //建立一個ServerSocket通道,並監聽8080端口
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(8080));
            //設置爲非阻塞
            serverSocketChannel.configureBlocking(false);
            //監聽接收數據的事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true){
                selector.select();
                //拿到Selector關心的已經到達事件的SelectionKey集合
                Set keys = selector.selectedKeys();
                Iterator iterator = keys.iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = (SelectionKey)iterator.next();
                    iterator.remove();
                    //由於咱們只註冊了ACCEPT事件,因此這裏只寫了當鏈接處於這個狀態時的處理程序
                    if(selectionKey.isAcceptable()){
                        //拿到產生這個事件的通道
                        ServerSocketChannel serverChannel = (ServerSocketChannel)selectionKey.channel();
                        SocketChannel clientChannel = serverChannel.accept();
                        clientChannel.configureBlocking(false);
                        //併爲這個通道註冊一個讀事件
                        clientChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
                    }

                    else if(selectionKey.isReadable()){
                        SocketChannel clientChannel = (SocketChannel)selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        long bytesRead = clientChannel.read(byteBuffer);
                        while(bytesRead > 0){
                            byteBuffer.flip();
                            System.out.printf("來自客戶端的數據" + new String(byteBuffer.array()));
                            byteBuffer.clear();
                            bytesRead = clientChannel.read(byteBuffer);
                        }

                        byteBuffer.clear();
                        byteBuffer.put("客戶端你好".getBytes("UTF-8"));
                        byteBuffer.flip();
                        clientChannel.write(byteBuffer);
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

複製代碼

客戶端:

/**
 * @CreatedBy:CVNot
 * @Date:2020/2/21/16:06
 * @Description:
 */
public class NIOClient {
    public static void main(String[] args) {
        try {
            Selector selector = Selector.open();
            SocketChannel clientChannel = SocketChannel.open();
            clientChannel.configureBlocking(false);
            clientChannel.connect(new InetSocketAddress(8080));
            clientChannel.register(selector, SelectionKey.OP_CONNECT);
            while (true) {
                //若是事件沒到達就一直阻塞着
                selector.select();
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isConnectable()) {
                        /**
                         * 鏈接服務器端成功
                         *
                         * 首先獲取到clientChannel,而後經過Buffer寫入數據,而後爲clientChannel註冊OP_READ事件
                         */
                        clientChannel = (SocketChannel) key.channel();
                        if (clientChannel.isConnectionPending()) {
                            clientChannel.finishConnect();
                        }
                        clientChannel.configureBlocking(false);
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        byteBuffer.clear();
                        byteBuffer.put("服務端你好,我是客戶端".getBytes("UTF-8"));
                        byteBuffer.flip();
                        clientChannel.write(byteBuffer);
                        clientChannel.register(key.selector(), SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        //通道能夠讀數據
                        clientChannel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        long bytesRead = clientChannel.read(byteBuffer);
                        while (bytesRead > 0) {
                            byteBuffer.flip();
                            System.out.println("server data :" + new String(byteBuffer.array()));
                            byteBuffer.clear();
                            bytesRead = clientChannel.read(byteBuffer);
                        }
                    } else if (key.isWritable() && key.isValid()) {
                        //通道能夠寫數據
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
複製代碼

3.3 可用一張圖大概總結流程:

相關文章
相關標籤/搜索