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

  • 2019 年 11 月 25 日
  • 筆記

先說兩個事

(1)上周五的那篇文章發重複了,是定時任務設置錯誤導致,給大家帶來干擾,這裡說聲抱歉。

(2)之前的問卷調查結果出來了,認為先講案例的票數較多,所以後面的文章都是先講案例,再以案例展開講解組件。

簡介

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

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

Java BIO

BIO概念解析

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

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

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來測試:

[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我是人才quitConnection closed by foreign host.

BIO的使用方式非常簡單,服務端接收到一個連接就啟動一個線程來處理這個連接的所有請求。

所以,BIO最大的缺點就是浪費資源,只能處理少量的連接,線程數隨着連接數線性增加,連接越多線程越多,直到抗不住。

Java NIO

NIO概念解析

NIO,New IO,JDK1.4開始支持,內部是基於多路復用的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 > 0) {                        buffer.flip();                        byte[] bytes = new byte[buffer.remaining()];                        // 將數據讀入到byte數組中                        buffer.get(bytes);                          // 換行符會跟着消息一起傳過來                        String content = new String(bytes, "UTF-8").replace("rn", "");                        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我是人才quitConnection closed by foreign host.

NIO的使用方式就有點複雜了,但是一個線程就可以處理很多連接。

首先,需要註冊一個ServerSocketChannel並把它註冊到selector上並監聽accept事件,然後accept到連接後會獲取到SocketChannel,同樣把SocketChannel也註冊到selector上,但是監聽的是read事件。

NIO最大的優點,就是一個線程就可以處理大量的連接,缺點是不適合處理阻塞性任務,因為阻塞性任務會把這個線程佔有着,其它連接的請求將得不到及時處理。

Java AIO

AIO概念介紹

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

使用異步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() > 0) {                            buffer.flip();                            byte[] bytes = new byte[buffer.remaining()];                            // 將數據讀入到byte數組中                            buffer.get(bytes);                              String content = new String(bytes, "UTF-8");                            // 換行符會當成另一條消息傳過來                            if (content.equals("rn")) {                                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我是人才quitConnection 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呢?