java架構之路-(netty專題)初步認識BIO、NIO、AIO

  本次咱們主要來講一下咱們的IO阻塞模型,只是很少,可是必定要理解,對於後面理解netty很重要的java

IO模型精講 編程

  IO模型就是說用什麼樣的通道進行數據的發送和接收,Java共支持3種網絡編程IO模式:BIO,NIO,AIO。數組

BIO服務器

  BIO(Blocking IO) 同步阻塞模型,一個客戶端鏈接對應一個處理線程。也是咱們熟悉的同步阻塞模型,先別管那個同步的概念,咱們先來看一下什麼是阻塞,簡單來一段代碼。網絡

  服務端:多線程

package com.xiaocai.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待鏈接。。");
            //阻塞方法
            Socket socket = serverSocket.accept();
            System.out.println("有客戶端鏈接了。。");
            handler(socket);
        }
    }

    private static void handler(Socket socket) throws IOException {
        System.out.println("thread id = " + Thread.currentThread().getId());
        byte[] bytes = new byte[1024];

        System.out.println("準備read。。");
        //接收客戶端的數據,阻塞方法,沒有數據可讀時就阻塞
        int read = socket.getInputStream().read(bytes);
        System.out.println("read完畢。。");
        if (read != -1) {
            System.out.println("接收到客戶端的數據:" + new String(bytes, 0, read));
            System.out.println("thread id = " + Thread.currentThread().getId());

        }
        socket.getOutputStream().write("HelloClient".getBytes());
        socket.getOutputStream().flush();
    }
}

  客戶端異步

package com.xiaocai.bio;

import java.io.IOException;
import java.net.Socket;

public class SocketClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 9000);
        //向服務端發送數據
        socket.getOutputStream().write("HelloServer".getBytes());
        socket.getOutputStream().flush();
        System.out.println("向服務端發送數據結束");
        byte[] bytes = new byte[1024];
        //接收服務端回傳的數據
        socket.getInputStream().read(bytes);
        System.out.println("接收到服務端的數據:" + new String(bytes));
        socket.close();
    }
}

  這個就是一個簡單的BIO服務端代碼,就是要準備接受線程訪問的代碼段。這一個單線程版本什麼意思呢?socket

  咱們先開啓一個端口爲9000的socket服務,而後運行Socket socket = serverSocket.accept();意思就是等待線程的出現,咱們來接收客戶端的請求,這個方法時阻塞的,也是隻有在阻塞狀態才能夠接收到咱們的請求。當有請求進來時,運行handler(socket);方法,中間是打印線程ID的方法不解釋,int read = socket.getInputStream().read(bytes);準備讀取咱們的客戶端發送數據。read和write可能會混淆,我畫個圖來講一下。ide

  咱們也能夠看到咱們的客戶端也是先拿到socket鏈接(Socket socket = new Socket("127.0.0.1", 9000)),而後要往服務端寫入數據(socket.getOutputStream().write("HelloServer".getBytes());)以byte字節形式寫入。這時咱們的服務端等待read咱們的客戶端weite的數據,會進入阻塞狀態,若是咱們的客戶端遲遲不寫數據,咱們的客戶端一直是阻塞狀態,也就沒法接收到新的請求,由於阻塞了,無法回到咱們的Socket socket = serverSocket.accept();去等待客戶端請求,只要在serverSocket.accept阻塞時才能夠接收新的請求。因而咱們採起了多線程的方式來解決這個問題,咱們來看一下代碼。性能

package com.xiaocai.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待鏈接。。");
            //阻塞方法
            Socket socket = serverSocket.accept();
            System.out.println("有客戶端鏈接了。。");
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    private static void handler(Socket socket) throws IOException {
        System.out.println("thread id = " + Thread.currentThread().getId());
        byte[] bytes = new byte[1024];

        System.out.println("準備read。。");
        //接收客戶端的數據,阻塞方法,沒有數據可讀時就阻塞
        int read = socket.getInputStream().read(bytes);
        System.out.println("read完畢。。");
        if (read != -1) {
            System.out.println("接收到客戶端的數據:" + new String(bytes, 0, read));
            System.out.println("thread id = " + Thread.currentThread().getId());

        }
        socket.getOutputStream().write("HelloClient".getBytes());
        socket.getOutputStream().flush();
    }
}

  咱們這時每次有客戶端來新的請求時,咱們就會開啓一個線程來處理這個請求,及時你的客戶端沒有及時的write數據,雖然咱們的服務端read進行了阻塞,也只是阻塞了你本身的線程,不會形成其它請求沒法接收到。

   這樣的處理方式貌似好了不少不少,其實否則,想一個實例,咱們的看小妹直播時,一句歡迎榜一大哥,彈幕不少,加入一次性來了100彈幕還好,咱們開啓100個線程來處理,若是一塊兒來了十萬彈幕呢?難道你要開啓十萬個線程來處理這些彈幕嘛?很顯然BIO仍是有弊端的,BIO仍是有優勢的(代碼少,不容易出錯)。

NIO

  NIO(Non Blocking IO) 同步非阻塞,服務器實現模式爲一個線程能夠處理多個請求(鏈接),客戶端發送的鏈接請求都會註冊到多路複用器selector上,多路複用器輪詢到鏈接有IO請求就進行處理。 可能概念太抽象了,我來舉個例子吧,如今有兩個小區都有不少的房子出租,BIO小區和NIO小區,都有一個門衛,BIO小區,來了一個租客,門衛大爺就拿着鑰匙,帶這個租客去看房子了,後面來的租客都暫時沒法看房子了,尷尬...想同時多人看房子,必須增長門衛大爺的數量,而咱們的NIO小區就很聰明,仍是一個門衛大媽,來了一個租客要看房子,門衛大媽,給了那個租客一把鑰匙,而且告訴他哪房間是空的,你本身進去看吧,及時這個租客看房子慢,耽誤了不少時間也不怕了,由於門衛大媽一直在門衛室,即便又來了新的租客,門衛大媽也是如此,只給鑰匙和空房間地址就能夠了。這個例子反正我記得很清楚,也以爲很貼切,這裏提到了一個鑰匙的概念,一會告訴大家是作什麼的,咱們先看一下代碼。

  服務端

package com.xiaocai.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer {

    //public static ExecutorService pool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws IOException {
        // 建立一個在本地端口進行監聽的服務Socket通道.並設置爲非阻塞方式
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //必須配置爲非阻塞才能往selector上註冊,不然會報錯,selector模式自己就是非阻塞模式
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress(9000));
        // 建立一個選擇器selector
        Selector selector = Selector.open();
        // 把ServerSocketChannel註冊到selector上,而且selector對客戶端accept鏈接操做感興趣
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            System.out.println("等待事件發生。。");
            // 輪詢監聽channel裏的key,select是阻塞的,accept()也是阻塞的
            int select = selector.select();

            System.out.println("有事件發生了。。");
            // 有客戶端請求,被輪詢監聽到
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                //刪除本次已處理的key,防止下次select重複處理
                it.remove();
                handle(key);
            }
        }
    }

    private static void handle(SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            System.out.println("有客戶端鏈接事件發生了。。");
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            //NIO非阻塞體現:此處accept方法是阻塞的,可是這裏由於是發生了鏈接事件,因此這個方法會立刻執行完,不會阻塞
            //處理完鏈接請求不會繼續等待客戶端的數據發送
            SocketChannel sc = ssc.accept();
            sc.configureBlocking(false);
            //經過Selector監聽Channel時對讀事件感興趣
            sc.register(key.selector(), SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            System.out.println("有客戶端數據可讀事件發生了。。");
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //NIO非阻塞體現:首先read方法不會阻塞,其次這種事件響應模型,當調用到read方法時確定是發生了客戶端發送數據的事件
            int len = sc.read(buffer);
            if (len != -1) {
                System.out.println("讀取到客戶端發送的數據:" + new String(buffer.array(), 0, len));
            }
            ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
            sc.write(bufferToWrite);
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        } else if (key.isWritable()) {
            SocketChannel sc = (SocketChannel) key.channel();
            System.out.println("write事件");
            // NIO事件觸發是水平觸發
            // 使用Java的NIO編程的時候,在沒有數據能夠往外寫的時候要取消寫事件,
            // 在有數據往外寫的時候再註冊寫事件
            key.interestOps(SelectionKey.OP_READ);
            //sc.close();
        }
    }
}

  客戶端

package com.xiaocai.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NioClient {
    //通道管理器
    private Selector selector;

    /**
     * 啓動客戶端測試
     *
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        NioClient client = new NioClient();
        client.initClient("127.0.0.1", 9000);
        client.connect();
    }

    /**
     * 得到一個Socket通道,並對該通道作一些初始化的工做
     *
     * @param ip   鏈接的服務器的ip
     * @param port 鏈接的服務器的端口號
     * @throws IOException
     */
    public void initClient(String ip, int port) throws IOException {
        // 得到一個Socket通道
        SocketChannel channel = SocketChannel.open();
        // 設置通道爲非阻塞
        channel.configureBlocking(false);
        // 得到一個通道管理器
        this.selector = Selector.open();

        // 客戶端鏈接服務器,其實方法執行並無實現鏈接,須要在listen()方法中調
        //用channel.finishConnect() 才能完成鏈接
        channel.connect(new InetSocketAddress(ip, port));
        //將通道管理器和該通道綁定,併爲該通道註冊SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    /**
     * 採用輪詢的方式監聽selector上是否有須要處理的事件,若是有,則進行處理
     *
     * @throws IOException
     */
    public void connect() throws IOException {
        // 輪詢訪問selector
        while (true) {
            selector.select();
            // 得到selector中選中的項的迭代器
            Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                // 刪除已選的key,以防重複處理
                it.remove();
                // 鏈接事件發生
                if (key.isConnectable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 若是正在鏈接,則完成鏈接
                    if (channel.isConnectionPending()) {
                        channel.finishConnect();
                    }
                    // 設置成非阻塞
                    channel.configureBlocking(false);
                    //在這裏能夠給服務端發送信息哦
                    ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
                    channel.write(buffer);
                    //在和服務端鏈接成功以後,爲了能夠接收到服務端的信息,須要給通道設置讀的權限。
                    channel.register(this.selector, SelectionKey.OP_READ);                                            // 得到了可讀的事件
                } else if (key.isReadable()) {
                    read(key);
                }
            }
        }
    }

    /**
     * 處理讀取服務端發來的信息 的事件
     *
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key) throws IOException {
        //和服務端的read方法同樣
        // 服務器可讀取消息:獲得事件發生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 建立讀取的緩衝區
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int len = channel.read(buffer);
        if (len != -1) {
            System.out.println("客戶端收到信息:" + new String(buffer.array(), 0, len));
        }
    }
}

  代碼看到了不少不少,我來解釋一下大概什麼意思吧,這個NIO超級重要後面的netty就是基於這個寫的,必定要搞懂,首先咱們建立了一個ServerSocketChannel和一個選擇器selector,設置爲非阻塞的(固定寫法,沒有爲何),將咱們的 selector綁定到咱們的ServerSocketChannel上,而後運行selector.select();進入阻塞狀態,別擔憂,這個阻塞沒影響,爲咱們提供了接收客戶端的請求,你沒有請求,我阻塞着,不會耽誤大傢什麼的。

  回到咱們的客戶端,仍是差很少的樣子,拿到咱們的NioClient開始鏈接咱們的服務端,這個時候,咱們的服務端接收到了咱們的客戶端請求,阻塞狀態的selector.select()繼續運行,而且給予了一個SelectionKey(Iterator<SelectionKey> it = selector.selectedKeys().iterator())也就是咱們剛纔的小例子中提到的鑰匙,key=鑰匙,還算是靠譜吧~!開始運行咱們的handle方法,有個if else,這個是說,你是第一次請求要創建通道,仍是要寫數據,仍是要讀取數據,記住啊,讀寫都是相對的,本身多琢磨幾回就能夠轉過圈來了,就是我上面畫圖說的read和write。拿咱們的創建通道來講,經過咱們的鑰匙key你就能夠獲得ServerSocketChannel,而後進行設置下次可能會發生的讀寫事件,而後看咱們的讀事件,咱們看到了int len = sc.read(buffer)這個讀在咱們的BIO中是阻塞的,而咱們的NIO這個方法不是阻塞的,這也就體現出來了咱們的BIO同步阻塞和NIO同步非阻塞,阻塞和非阻塞的區別也就說完了。畫個圖,咱們來看一下咱們的NIO模型。

  NIO 有三大核心組件: Channel(通道), Buffer(緩衝區),Selector(選擇器)

   這裏咱們的Buffer沒有去說,到netty會說的, Channel(通道), Buffer(緩衝區)都是雙向的,如今回過頭來想一想我舉的小例子,selector門衛大媽,SelectionKey鑰匙。對於NIO有了一些理解了吧,NIO看着很棒的,可是你有想過寫上述代碼的痛苦嗎?

AIO

  AIO(NIO 2.0) 異步非阻塞, 由操做系統完成後回調通知服務端程序啓動線程去處理, 通常適用於鏈接數較多且鏈接時間較長的應用。其實AIO就是對於NIO的二次封裝,要不怎麼叫作NIO2.0呢,咱們來簡單看一下代碼。

  服務端:

package com.xiaocai.aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class AIOServer {
    public static void main(String[] args) throws Exception {
        final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));

        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    // 再此接收客戶端鏈接,若是不寫這行代碼後面的客戶端鏈接連不上服務端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });

        Thread.sleep(Integer.MAX_VALUE);
    }
}

  客戶端:

package com.xiaocai.aio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;

public class AIOClient {

    public static void main(String... args) throws Exception {
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
        socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
        ByteBuffer buffer = ByteBuffer.allocate(512);
        Integer len = socketChannel.read(buffer).get();
        if (len != -1) {
            System.out.println("客戶端收到信息:" + new String(buffer.array(), 0, len));
        }
    }
}

  阻塞非阻塞都明白了,這裏來解釋一下同步,咱們看到咱們的AIO在serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {}直接開啓了線程,也就是說accept直接之後,我再也不須要考慮阻塞狀況,能夠繼續運行下面的代碼了,也就是咱們說到的異步執行,內部仍是咱們的NIO,不要以爲AIO多麼的6B,內部就是封裝了咱們的NIO,性能和NIO其實差很少的,可能有些時候還不如NIO(未實測)。

  遺漏一個知識點,NIO的多路複用器是如何工做的,在咱們的JDK1.5之前的,多路複用器是數組和鏈表的方式來遍歷的,到了咱們的JDK1.5採用hash來回調的。

 總結:

  咱們此次主要說了BIO、NIO、AIO三個網絡編程IO模式,最重要的就是咱們的NIO,一張圖來總結一下三個IO的差異吧。

 

 

最進弄了一個公衆號,小菜技術,歡迎你們的加入

相關文章
相關標籤/搜索