IO模型之BIO,NIO和AIO

瞭解IO模型

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

BIO(Blocking IO)

同步阻塞模型,一個客戶端連接對應一個處理線程

缺點:

1、IO代碼裏read操作是阻塞操作,如果連接不做數據讀寫操作會導致線程阻塞,浪費資源
2、如果線程很多,會導致服務器線程太多,壓力太大。

應用場景:

BIO 方式適用於連接數目比較小且固定的架構, 這種方式對服務器資源要求比較高, 但程序簡單易理解。
在這裏插入圖片描述
BIO代碼示例:

//服務端示例
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();
            //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();
    }
}
//客戶端示例
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();
    }
}

NIO(Non Blocking IO)

同步非阻塞,服務器實現模式爲一個線程可以處理多個請求(連接),客戶端發送的連接請求都會註冊到多路複用器selector上,多路複用器輪詢到連接有IO請求就進行處理。
I/O多路複用底層一般用的Linux API(select,poll,epoll)來實現,他們的區別如下表:
在這裏插入圖片描述

應用場景:

NIO方式適用於連接數目多且連接比較短(輕操作) 的架構, 比如聊天服務器, 彈幕系統, 服務器間通訊,編程比較複雜, JDK1.4 開始支持
在這裏插入圖片描述
NIO 有三大核心組件: Channel(通道), Buffer(緩衝區),Selector(選擇器)
在這裏插入圖片描述
1、channel 類似於流,每個 channel 對應一個 buffer緩衝區,buffer 底層就是個數組
2、channel 會註冊到 selector 上,由 selector 根據 channel 讀寫事件的發生將其交由某個空閒的線程處理
3、selector 可以對應一個或多個線程
4、NIO 的 Buffer 和 channel 都是既可以讀也可以寫

NIO代碼示例:

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();
        }
    }
}
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服務端程序詳細分析:
1、創建一個 ServerSocketChannel 和 Selector ,並將 ServerSocketChannel 註冊到 Selector 上
2、 selector 通過 select() 方法監聽 channel 事件,當客戶端連接時,selector 監聽到連接事件, 獲取到 ServerSocketChannel 註冊時
綁定的 selectionKey
3、selectionKey 通過 channel() 方法可以獲取綁定的 ServerSocketChannel
4、ServerSocketChannel 通過 accept() 方法得到 SocketChannel
5、將 SocketChannel 註冊到 Selector 上,關心 read 事件
6、註冊後返回一個 SelectionKey, 會和該 SocketChannel 關聯
7、selector 繼續通過 select() 方法監聽事件,當客戶端發送數據給服務端,selector 監聽到read事件,獲取到 SocketChannel 註冊時
綁定的 selectionKey
8、selectionKey 通過 channel() 方法可以獲取綁定的 socketChannel
9、將 socketChannel 裏的數據讀取出來
10、用 socketChannel 將服務端數據寫回客戶端

總結:NIO模型的selector 就像一個大總管,負責監聽各種IO事件,然後轉交給後端線程去處理
NIO相對於BIO非阻塞的體現就在,BIO的後端線程需要阻塞等待客戶端寫數據(比如read方法),如果客戶端不寫數據線程就要阻塞,
NIO把等待客戶端操作的事情交給了大總管 selector,selector 負責輪詢所有已註冊的客戶端,發現有事件發生了才轉交給後端線程處
理,後端線程不需要做任何阻塞等待,直接處理客戶端事件的數據即可,處理完馬上結束,或返回線程池供其他客戶端事件繼續使用。還
有就是 channel 的讀寫是非阻塞的。
Redis就是典型的NIO線程模型,selector收集所有連接的事件並且轉交給後端線程,線程連續執行所有事件命令並將結果寫回客戶端
在這裏插入圖片描述

AIO(NIO 2.0)

異步非阻塞, 由操作系統完成後回調通知服務端程序啓動線程去處理, 一般適用於連接數較多且連接時間較長的應用

應用場景:

AIO方式適用於連接數目多且連接比較長(重操作) 的架構,JDK7 開始支持
AIO代碼示例:

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);
    }
}
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));
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章