3. 彤哥說netty系列之Java BIO NIO AIO進化史

nio

你好,我是彤哥,本篇是netty系列的第三篇。java

歡迎來個人公從號彤哥讀源碼系統地學習源碼&架構的知識。shell

簡介

上一章咱們介紹了IO的五種模型,實際上Java只支持其中的三種,即BIO/NIO/AIO。數組

本文將介紹Java中這三種IO的進化史,並從使用的角度剖析它們背後的故事。多線程

Java BIO

BIO概念解析

BIO,Blocking IO,阻塞IO,它是Java的上古產品,自出生就有的東西(JDK 1.0)。架構

使用BIO則數據準備和數據從內核空間拷貝到用戶空間兩個階段都是阻塞的。異步

blocking-io

BIO使用案例

public class EchoServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true) {
            System.out.println("start accept");
            Socket socket = serverSocket.accept();
            System.out.println("new conn: " + socket.getRemoteSocketAddress());

            new Thread(()->{
                try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String msg;
										// 讀取消息,本文來源公從號彤哥讀源碼
                    while ((msg = reader.readLine()) != null) {
                        if (msg.equalsIgnoreCase("quit")) {
                            reader.close();
                            socket.close();
                            break;
                        } else {
                            System.out.println("receive msg: " + msg);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

客戶端可使用telnet來測試,並且你可使用多個telnet來測試:socket

[c:\~]$ telnet 127.0.0.1 8080


Connecting to 127.0.0.1:8080...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.
hello world
我是人才
quit
Connection closed by foreign host.

BIO的使用方式很是簡單,服務端接收到一個鏈接就啓動一個線程來處理這個鏈接的全部請求。async

bio_nio_aio

因此,BIO最大的缺點就是浪費資源,只能處理少許的鏈接,線程數隨着鏈接數線性增長,鏈接越多線程越多,直到抗不住。ide

Java NIO

NIO概念解析

NIO,New IO,JDK1.4開始支持,內部是基於多路複用的IO模型。學習

multiplexing-io

這裏有個歧義,不少人認爲Java的NIO是Non-Blocking IO的縮寫,其實並非。

使用NIO則多條鏈接的數據準備階段會阻塞在select上,數據從內核空間拷貝到用戶空間依然是阻塞的。

由於第一階段並非鏈接自己處於阻塞階段,因此一般來講NIO也能夠看做是同步非阻塞IO。

NIO使用案例

public class EchoServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 將accept事件綁定到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 阻塞在select上
            selector.select();
            Set<selectionkey> selectionKeys = selector.selectedKeys();
            // 遍歷selectKeys
            Iterator<selectionkey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 若是是accept事件
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = ssc.accept();
                    System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    // 若是是讀取事件,本文來源公從號彤哥讀源碼
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 將數據讀入到buffer中
                    int length = socketChannel.read(buffer);
                    if (length &gt; 0) {
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        // 將數據讀入到byte數組中
                        buffer.get(bytes);

                        // 換行符會跟着消息一塊兒傳過來
                        String content = new String(bytes, "UTF-8").replace("\r\n", "");
                        if (content.equalsIgnoreCase("quit")) {
                            selectionKey.cancel();
                            socketChannel.close();
                        } else {
                            System.out.println("receive msg: " + content);
                        }
                    }
                }
                iterator.remove();
            }
        }
    }
}

這裏一樣使用telnet測試,並且你可使用多個telnet來測試:

[c:\~]$ telnet 127.0.0.1 8080


Connecting to 127.0.0.1:8080...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.
hello world
我是人才
quit
Connection closed by foreign host.

NIO的使用方式就有點複雜了,可是一個線程就能夠處理不少鏈接。

首先,須要註冊一個ServerSocketChannel並把它註冊到selector上並監聽accept事件,而後accept到鏈接後會獲取到SocketChannel,一樣把SocketChannel也註冊到selector上,可是監聽的是read事件。

bio_nio_aio

NIO最大的優勢,就是一個線程就能夠處理大量的鏈接,缺點是不適合處理阻塞性任務,由於阻塞性任務會把這個線程佔有着,其它鏈接的請求將得不到及時處理。

Java AIO

AIO概念介紹

AIO,Asynchronous IO,異步IO,JDK1.7開始支持,算是一種比較完美的IO,Windows下比較成熟,但Linux下還不太成熟。

asynchronous-io

使用異步IO則會在請求時當即返回,並在數據已準備且已拷貝到用戶空間後進行回調處理,兩個階段都不會阻塞。

AIO使用案例

public class EchoServer {
    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 監聽accept事件,本文來源公從號彤哥讀源碼
        serverSocketChannel.accept(null, new CompletionHandler<asynchronoussocketchannel, object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
                    // 再次監聽accept事件
                    serverSocketChannel.accept(null, this);

                    // 消息的處理
                    while (true) {
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        // 將數據讀入到buffer中
                        Future<integer> future = socketChannel.read(buffer);
                        if (future.get() &gt; 0) {
                            buffer.flip();
                            byte[] bytes = new byte[buffer.remaining()];
                            // 將數據讀入到byte數組中
                            buffer.get(bytes);

                            String content = new String(bytes, "UTF-8");
                            // 換行符會當成另外一條消息傳過來
                            if (content.equals("\r\n")) {
                                continue;
                            }
                            if (content.equalsIgnoreCase("quit")) {
                                socketChannel.close();
                                break;
                            } else {
                                System.out.println("receive msg: " + content);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.out.println("failed");
            }
        });

        // 阻塞住主線程
        System.in.read();
    }
}

這裏一樣使用telnet測試,並且你可使用多個telnet來測試:

[c:\~]$ telnet 127.0.0.1 8080


Connecting to 127.0.0.1:8080...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.
hello world
我是人才
quit
Connection closed by foreign host.

AIO的使用方式不算太複雜,默認會啓一組線程來處理用戶的請求,並且若是在處理阻塞性任務,還會自動增長新的線程來處理其它鏈接的任務。

首先,建立一個AsynchronousServerSocketChannel並調用其accept方法,這一步至關於監聽了accept事件,在收到accept事件後會獲取到AsynchronousSocketChannel,而後就能夠在回調方法completed()裏面讀取數據了,固然也要繼續監聽accept事件。

AIO最大的優勢,就是少許的線程就能夠處理大量的鏈接,並且能夠處理阻塞性任務,但不能大量阻塞,不然線程數量會膨脹。

槽點

(1)三種IO的實現方式中對於換行符的處理居然都不同,BIO中不會把換行符帶過來(實際上是帶過來了,由於用了readLine()方法,因此換行符沒了),NIO中會把換行符加在消息末尾,AIO中會把換行符當成一條新的消息傳過來,很神奇,爲啥不統一處理呢,也很疑惑。

(2)JDK自帶的ByteBuffer是一個難用的東西。

總結

本文咱們從概念和使用兩個角度分別介紹了BIO/NIO/AIO三種IO模型。

問題

看起來JDK的實現彷佛很完美啊,爲何還會有Netty呢?

最後,也歡迎來個人公從號彤哥讀源碼系統地學習源碼&架構的知識。

code </integer></asynchronoussocketchannel,></selectionkey></selectionkey>

相關文章
相關標籤/搜索