天天看點

Java NIO系列之[前世今生]

這節重點探讨兩個概念性的問題:什麼是NIO?為什麼要用NIO?

在引入NIO之前,有必要聊下BIO,因為NIO是相對于BIO所提出的新的Java IO api,但這裡不會深入,每本java書籍都會介紹javaIO的。

BIO:blocking IO,即阻塞IO,是java的傳統IO api,以流的方式處理資料,一般可分為檔案IO(處理檔案)和網絡IO(Socket網絡程式設計),這裡重點探讨網絡IO,通過一個小的例子,讓大家了解阻塞IO的具體含義。

服務端代碼:(代碼中用到java8中的try-with-resources寫法,可以自動關閉流和socket連接配接,jdk1.7就有了try-with-resources)

public class BIOServer {
    public static void main(String[] args) {

        while (true){
            try(ServerSocket serverSocket = new ServerSocket(9000); //建立ServerSocket對象
                //監聽用戶端的連接配接請求
                final Socket socket= serverSocket.accept();  //當沒有用戶端連接配接時,就會阻塞到這裡,一直等待用戶端的連接配接請求,下面的代碼不會執行
                //從連接配接中取出輸入流來接收用戶端發來的消息
                InputStream is = socket.getInputStream(); //阻塞
                //從連接配接中取出輸出流回應用戶端
                OutputStream os = socket.getOutputStream()) {
                System.out.println("我是風清揚"); //在用戶端啟動之前,不會列印輸出,因為前面的accept()方法已經使線程阻塞,等待用戶端連接配接,用戶端啟動後,該語句執行
                //讀取消息
                byte[] b = new byte[1024];
                int len;
                String clientIP = socket.getInetAddress().getHostAddress();
                while ((len = is.read(b)) != -1){ //當沒有讀取到用戶端發送的消息時,就會阻塞到這裡,InputStream.read()方法下面的代碼不會執行
                    System.out.println(clientIP + "說:" + new String(b,0,len));
                }
                System.out.println("我是令狐沖");   //該語句在用戶端沒有發送消息前不會執行,隻有上面的read()方法接收到服務端發送的消息時才會執行該語句
                //發送消息
                Scanner scanner = new Scanner(System.in);
                String msg = scanner.nextLine();
                os.write(msg.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           

用戶端代碼:

public class BIOClient {
    public static void main(String[] args) {
        while (true){
            try(Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9000); // 建立Socket對象
                //從連接配接中取出輸入流讀消息
                final InputStream is = socket.getInputStream();  //阻塞
                //從連接配接中取出輸出流發消息
                final OutputStream os = socket.getOutputStream()) {

                System.out.println("請輸入:");
                Scanner scanner = new Scanner(System.in);
                String msg = scanner.nextLine();
                os.write(msg.getBytes());
                socket.shutdownOutput();
                byte[] b = new byte[1024];
                int len;
                while ((len = is.read(b)) != -1){ //當沒有讀取到服務端發送的消息時,就會阻塞到這裡,InputStream.read()方法下面的代碼不會執行
                    System.out.println("服務端說:" + new String(b));
                }
                System.out.println("我是逍遙子"); //該語句在服務端沒有發送消息前不會執行,隻有上面的read()方法接收到服務端發送的消息時才會執行該語句
            } catch (UnknownHostException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           

執行結果分析:

BIOServer控制台輸出:

Java NIO系列之[前世今生]

BIOClient控制台輸出:

Java NIO系列之[前世今生]

一、什麼是NIO:

百度百科對NIO的解釋如下:

Java NIO系列之[前世今生]

NIO:non-blocking IO,即非阻塞IO,以塊的方式處理資料,相比于BIO用流的方式處理資料,塊IO的效率要比流IO高很多,有的書籍也叫做新IO(New IO),這裡我們與BIO進行比較,non-blocking IO更加語義化,能夠突出對比性。JDK1.4中,新增了許多新的用于處理輸入輸出的類,這些類放在java.nio包下(java11是放在java.base子產品下的java.nio包中):

Java NIO系列之[前世今生]

NIO提出了三大核心的概念:Channel(通道)、Buffer(緩沖區)、Selector(選擇器),後面的内容也是主要圍繞這三個核心概念展開,再适當加一些charset字元集的東西。相對于BIO基于 位元組流和字元流 對資料進行操作,NIO是基于Channel和Buffer對資料進行操作。讀資料的時候先将資料從通道讀取到緩沖區,寫資料的時候要将資料從緩沖區寫如到通道,Selector用于監聽多個通道的事件,進而使單個線程就可以監聽多個用戶端。下面通過安倍和特朗普通電話的事例代碼(本人屬于段子手類型,平時掃地,偶爾開車,如果例子舉的有不當的地方,還請大家包涵),讓大家了解NIO相對于BIO的非阻塞的特性,涉及到的api不了解也沒關系,後面會詳細介紹,可以把代碼運作一遍,體會下NIO的非阻塞特性。

用戶端代碼:

public class NioClient {
    public static void main(String[] args) throws IOException {

         //1.得到一個通道
        final SocketChannel channel = SocketChannel.open();

        //2.設定非阻塞方式,預設是阻塞方式,是以需要手動設定為非阻塞方式
        channel.configureBlocking(false);

        //3.設定服務端的ip和端口号
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",9000);

        //4.連接配接伺服器端
        if(!channel.connect(address)){ //當connect連接配接沒有成功時,嘗試再次連接配接服務端,但不能再用connect方法連接配接,此方法隻能連接配接一次,再次連接配接需要用finishConnect()方法
            while (!channel.finishConnect()){ //當連接配接服務端時,用戶端不會處于阻塞狀态,還可以繼續執行下面的代碼
                System.out.println("安倍:在特朗普老兄的電話還未接通之前,小弟我可以幹些其他的事情:哎呀,我大日本的網絡有點令人擔憂啊,半天了," +
                        "特朗普老兄的電話還是沒有接通,這老家夥,不會在做些羞羞的事情吧,不行我還是先看個蒼老師或是波多老師的劇情片吧,真有意思,先看再說!");
            }
        }

        //5.提供一個緩沖區并存入資料
        String msg = "您好,特朗普兄長,我是安倍,可否賞臉陪小弟喝個便茶,小弟最近有點迷茫啊,突然感覺沒有了高潮!";
        final ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

        //6..發送資料
        channel.write(buffer);

        //7.當程式執行到這裡,已經執行完,channel就會關閉,伺服器端就會抛出異常,是以讓程式阻塞到這裡
        System.in.read();
    }
}
           

服務端代碼:

public class NioServer {
    public static void main(String[] args) throws IOException {

        //1.得到一個通道
        final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //2.得到一個Selector對象
        final Selector selector = Selector.open();

        //3.綁定端口号
        serverSocketChannel.bind(new InetSocketAddress(9000));

        //4.設定非阻塞方式
        serverSocketChannel.configureBlocking(false);

        //5.把ServerSocketChannel對象注冊給Selector對象
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //6.實作業務
        while (true){

            //6,1 一直監控用戶端,直到有用戶端連接配接上來
            if(selector.select(2000) == 0){
                System.out.println("特朗普:安倍這小子好幾天就越好今天和我通話,現在也沒來,不會幹火星上去了吧,不管他了,我還是幹别的事情吧:82年拉菲," +
                        "加拿大總理特魯多小老弟送我的,我去找我的merry女秘書喝兩杯,cheers,大爺我要嫖了!");
                continue;
            }

            //6.2 得到SelectionKey
            final Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()){
                final SelectionKey selectionKey = selectionKeyIterator.next();

                if(selectionKey.isAcceptable()){ //用戶端連接配接事件
                    System.out.println("連接配接...............................................");
                    final SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if(selectionKey.isReadable()){ //讀取用戶端資料事件
                    final SocketChannel channel = (SocketChannel) selectionKey.channel();
                    final ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(buffer);
                    System.out.println("用戶端發來資料:" +new String(buffer.array()));
                }

                //6.3 手動從目前集合中移除key,防止重複處理
                selectionKeyIterator.remove();
            }

        }
    }
}
           

運作結果分析:

1.隻啟動服務端,不啟動用戶端

Java NIO系列之[前世今生]

2.隻啟動用戶端,不啟動服務端

Java NIO系列之[前世今生]

3.啟動服務端和用戶端

Java NIO系列之[前世今生]

通過以上的代碼舉例,想必大家對NIO的非阻塞特性應該有所了解。下面聊下NIO的今生,AIO。

AIO: asynchronous IO,即異步IO,并且是非阻塞的,也被稱作NIO2,算是對NIO的增強和補充吧,在JDK1.7中新加入的,AIO最大的特性就是異步能力,jdk1.7新增三個異步通道,用于對異步IO的支援:

1.AsynchronousFileChannel: 用于檔案異步讀寫;

2.AsynchronousSocketChannel: 用戶端異步socket;

3.AsynchronousServerSocketChannel: 伺服器異步socket。

二、為什麼要用NIO:

NIO比普通的BIO提供了功能更加強大,處理資料更快的解決方案,大大提升IO的吞吐量,常用在高性能伺服器上,在大多數涉及java高性能應用軟體中,NIO是必不可少的技術之一,例如Netty就是封裝了NIO,而網際網路微服務架構中常用的Dubbo,Elasticsearch等中間件底層網絡通信都用Netty實作,可見NIO對于高性能網絡通信的作用。

三、總結:

這個小節由BIO引出了NIO,通過将NIO與BIO相比較,突出NIO非阻塞IO的優勢,簡單介紹了什麼是NIO,回答了為什麼要用NIO的疑問。