Netty框架學習之(一):Netty框架簡介

1. 簡介

官方定義爲:”Netty 是一款異步的事件驅動的網絡應用程序框架,支持快速地開發可維護的高性能的面向協議的服務器
和客戶端”,按照慣例貼上一張High Level的架構圖:

這裏寫圖片描述

縱觀Java系的多種服務器/大數據框架,都離不開Netty做出的貢獻,本文對Netty做一個簡單的概述

2. 主要特性

Netty有很多重要的特性,主要特性如下:
- 優雅的設計
- 統一的API接口,支持多種傳輸類型,例如OIO,NIO
- 簡單而強大的線程模型
- 豐富的文檔
- 卓越的性能
- 擁有比原生Java API 更高的性能與更低的延遲
- 基於池化和複用技術,使資源消耗更低
- 安全性
- 完整的SSL/TLS以及StartTLS支持
- 可用於受限環境,如Applet以及OSGI

Netty的以上特性,比較適合客戶端數據較大的請求/處理場景,例如web服務器等,要想知道有哪些系統使用了Netty,可以參考:http://netty.io/wiki/adopters.html

3. 主要術語

在正式開始之前,先對Netty涉及到的一些術語做個簡單的說明

3.1 IO模型:BIO/NIO/Netty

3.1.1 BIO(Blocking IO):阻塞IO

早期的Java API(java.net)提供了由本地系統套接字庫提供的所謂的阻塞函數,樣例代碼如下:

ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out =new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
while ((request = in.readLine()) != null) {
    if ("Done".equals(request)) {
        break;
}
response = processRequest(request);
out.println(response);
}

這段代碼片段將只能同時處理一個連接,要管理多個併發客戶端,需要爲每個新的客戶端
Socket 創建一個新的 Thread,線程模型如下圖所示:

這裏寫圖片描述

該種模型存在以下兩個問題:
1. 在任何時候都可能有大量的線程處於休眠狀態,只是等待輸入或者輸出數據就緒,這可能算是一種資源浪費
2. 需要爲每個線程的調用棧都分配內存
3. 即使 Java 虛擬機(JVM) 在物理上可以支持非常大數量的線程, 但是遠在到達該極限之前, 上下文切換所帶來的開銷就會帶來麻煩

3.1.2 NIO(Non Blocking IO):非阻塞IO

Java的NIO特性在JDK 1.4中引入,其結構如下:

這裏寫圖片描述

從該圖可以看出Selector 是Java 的非阻塞 I/O 實現的關鍵。它使用了事件通知 API
以確定在一組非阻塞套接字中有哪些已經就緒能夠進行 I/O 相關的操作。因爲可以在任何的時間檢查任意的讀操作或者寫操作的完成狀態。該種模型下,一個單一的線程便可以處理多個併發的連接。
與BIO相比,該模型有以下特點:
1. 使用較少的線程便可以處理許多連接,因此也減少了內存管理和上下文切換所帶來開銷
2. 當沒有 I/O 操作需要處理的時候,線程也可以被用於其他任務

雖然Java 的NIO在性能上比BIO已經相當的優秀,但是要做到如此正確和安全並
不容易。特別是,在高負載下可靠和高效地處理和調度 I/O 操作是一項繁瑣而且容易出錯的任務,此時就時Netty上場的時間了。

3.1.3 Netty

Netty對NIO的API進行了封裝,通過以下手段讓性能又得到了一定程度的提升
1. 使用多路複用技術,提高處理連接的併發性
2. 零拷貝:
1. Netty的接收和發送數據採用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩衝區的二次拷貝
2. Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象進行一次操作
3. Netty的文件傳輸採用了transferTo方法,它可以直接將文件緩衝區的數據發送到目標Channel,避免了傳統通過循環write方式導致的內存拷貝問題
3. 內存池:爲了減少堆外直接內存的分配和回收產生的資源損耗問題,Netty提供了基於內存池的緩衝區重用機制
4. 使用主從Reactor多線程模型,提高併發性
5. 採用了串行無鎖化設計,在IO線程內部進行串行操作,避免多線程競爭導致的性能下降
6. 默認使用Protobuf的序列化框架
7. 靈活的TCP參數配置

詳細說明,可參考: http://www.infoq.com/cn/articles/netty-high-performance#anch111813

3.1.4 簡單的性能測試

通過在本地分別使用BIO,NIO,Netty NIO實現了一個簡單的服務端程序(該程序接收到請求後,sleep 1毫秒,並返回簡單的一句話)分別對三種方式使用Jemeter進行性能測試(一百個併發,每個併發發送一百個相同消息),結果如下:

單線程的java net:

這裏寫圖片描述

NIO:
這裏寫圖片描述

Netty NIO:
這裏寫圖片描述

以上結果或是受到其他條件的影響,結果僅供供參考

3.2 Callback:

回調在廣泛的編程場景中都有應用,一般是在完成某個特定的操作後對相關方法進行調用。

Netty 在內部使用回調來處理事件;當一個回調被觸發時,相關的事件可以被一個 interfaceChannelHandler 的實現處理,例如Channel激活時會調用ChannelActive方法,樣例代碼如下:

public class ConnectHandler extends ChannelInboundHandlerAdapter {
@Override
    public void channelActive(ChannelHandlerContext ctx)throws Exception {
        System.out.println("Client " + ctx.channel().remoteAddress() + connected");

    }
}

3.3 Future:

Future一般用在當執行異步操作時需要獲取未來的某個時候才能獲取到的結果。

JDK 預置了 interface java.util.concurrent.Future,但是其所提供的實現,只
允許手動檢查對應的操作是否已經完成,或者一直阻塞直到它完成。這是非常繁瑣的,所以 Netty提供了它自己的實現——ChannelFuture,用於在執行異步操作的時候使用。

ChannelFuture提供了幾種額外的方法,這些方法使得我們能夠註冊一個或者多個
ChannelFutureListener實例。監聽器的回調方法operationComplete(),將會在對應的
操作完成時被調用。然後監聽器可以判斷該操作是成功地完成了還是出錯了。如果是後者,我們可以檢索產生的Throwable。 通過使用ChannelFutureListener機制可以避免對
操作結果進行手動檢查。

每個 Netty 的出站 I/O 操作都將返回一個ChannelFuture,即不會阻塞後續的操作。

下面的例子中的connect()方法會直接返回,後續的成功或失敗將由其註冊的FutureListener來處理。

        try {
            // 使用異步的方式連接Server,不管成功失敗,都是執行下面System.out的語句,最後的連接結果由FutureListener進行處理
            ChannelFuture future = bootstrap.connect();
            System.out.println("Finished connect operation");
            future.addListener((ChannelFutureListener) future1 -> {
                if (future1.isSuccess()){
                    ByteBuf buffer = Unpooled.copiedBuffer(
                            "Hello", Charset.defaultCharset());
                    ChannelFuture wf = future1.channel()
                            .writeAndFlush(buffer);
                    System.out.println("Connect successful!");
                }else{
                    System.out.println("Connect failed!");
                    Throwable cause = future1.cause();
                    cause.printStackTrace();
                }
            });
            System.out.println("Finished connect operation2");
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

最後的打印結果如下:

Finished connect operation
Finished connect operation2
Connect failed!
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: localhost/127.0.0.1:8888
    at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
    at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)
    at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:325)
    at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect
    ...............................................
    Caused by: java.net.ConnectException: Connection refused: no further information
    ... 11 more

3.4 Event

Netty 使用不同的事件來通知狀態的改變或者是操作的狀態。事件可能包括:
- 連接已被激活或者連接失活
- 數據讀取;
- 用戶事件;
- 錯誤事件。
- 打開或者關閉到遠程節點的連接;
- 將數據寫到或者沖刷到套接字。

每個事件都可以被分發給 ChannelHandler 類中的某個用戶實現的方法。這是將事件驅動範式直接轉換爲應用程序邏輯處理比較理想的位置。
下圖展示了事件是怎麼被處理的:

這裏寫圖片描述

對每個事件可以進行,記錄日誌,數據轉換,應用程序邏輯處理等操作,

Netty 提供了大量預定義的可以開箱即用的 ChannelHandler 實現,包括用於各種協議
(如 HTTP 和 SSL/TLS)的 ChannelHandler。後續博文會對一些Handler進行簡單的介紹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章