一、Netty簡介
1、Netty是異步的、基於事件驅動的網絡應用框架,它以高性能、高並發著稱。基於事件驅動,簡單點說就是 Netty 會根據客戶端的連接請求、讀、寫等事件 做出相應的響應。
2、Netty 主要用於開發基於 TCP 協議的網絡 IO 程序。例如構建高性能RPC,實現高性能服務器/客戶端程序等等。同時Netty也支持UDP、HTTP、WebSocket等多種主流協議。
3、Netty 是基於 Java NIO 構建出來的,NIO是指非阻塞式IO,利用它可以提升並發能力
圖1 是Netty的功能特性圖
1、在傳輸服務方面:它支持TCP UDP傳輸; 支持HTTP 隧道等
2、在協議支持方面: 它支持多種協議如HTTP WebSocket。 並且它提供了一些開箱即用的協議 例如可以用其提供的SSL 方便的進行認證與數據加密解密,利用其提供的zlib/gzip 可以方便的進行數據的壓縮和解壓縮,並且支持了google的protobuf序列化方式。並且支持大文件傳輸,實時的流傳輸
3、它的核心功能包括三方面:
3.1利用其提供的可拓展事件模型,我們可以方便的添加自己的業務邏輯
3.2利用其提供的通用通信API,我們可以告別java NIO 的繁瑣 復雜的代碼
3.3支持零拷貝,零拷貝可以減少數據在內存中的拷貝,可以大幅提高IO性能
1、首先Netty可以用於分布式應用開發中,Netty 作為異步高並發的網絡組件,常用於構建高性能 RPC 框架,以提升分布式服務群之間的服務調用或數據傳輸的並發度和速度。例如阿里 Dubbo 就可以使用 Netty 作為其網絡層
2、Netyy還可以用於大數據基礎設施的構建:比如 Hadoop在處理海量數據的時候,數據在多個計算節點之中傳輸,為了提高傳輸性能,也采用 Netty 構建性能高的網絡 IO 層
3、用Netyy還可以實現 應用層基於公有協議或私有協議的服務器
二、Netty原理
零拷貝技術
1) Netty 利用了零拷貝技術 提升了IO 性能
2) 零拷貝指的是 數據在內存中的拷貝次數為0次
3) 圖2 代表了 磁盤中的一個數據 發給網絡的過程,如果不利用零拷貝 磁盤的數據要先拷貝到內核緩沖區,再拷貝到應用程序內存,再拷貝到Socket緩沖區,最后再發向網絡。不利用零拷貝,數據在內存中拷貝了兩次,一次是內核緩沖區到用戶程序內存,另一次是應用程序內存到Socket緩沖區。
而零拷貝技術,將內核緩沖區 與 應用程序內存 和Socket緩沖區建立了地址映射,這樣數據在內存中的拷貝次數就是0次,減少了拷貝次數,可以大幅提升IO性能。
1) Netty 是基於NIO的,NIO的特點是可以利用一個線程,並發處理多個連接 也稱為IO多路復用
2) 圖3是 NIO 的示意圖,服務器中一個線程可以非阻塞地處理多個客戶端的IO請求。具體過程為服務器為每個客戶端 分配Channel和Buffer,數據是通過通道 Channel 傳輸的,往Channel中讀寫數據需要先經過緩沖區Buffer。接着將每個客戶端對應的Channel的IO事件注冊到多路復用器 Selector上,Selector通過輪詢,就可以找到有IO活動的channel並進行處理,這就是NIO的具體流程。以這種IO處理模式也稱為Reactor模式。
3) 這種模式非阻塞的原因是:若某通道無可用數據,線程不會阻塞在這個通道上等數據准備好,而是可以處理其他通道的讀寫。而傳統的阻塞式IO,采用一個線程對應一個客戶端的方式,若客戶端數據未准備好,則線程一直阻塞。傳統的阻塞式IO,線程利用率不高,且高並發是需要建立大量的線程。而NIO降低了線程數量,提高了線程的利用率 實現了IO 多路復用。Netty 正是利用這種非阻塞式的IO,實現了單個線程就可以並發處理多個連接。
Channel 通道:
1)數據是通過通道傳輸的,它為應用提供I/O操作接口,定義了與socket交互的操作集 比如讀、寫、連接、綁定等。
2)表1是一些常用的 Channel 類型,不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,,TCP連接中客戶端和服務器用不同的Channel,linux下可以使用EpollSocketChannel建立非阻塞的TCP連接,它是用linux的epoll命令實現的 效率更高。
1)ChannelHandler 通道處理接口:傳遞到通道的數據或者通道傳來的數據要利用ChannelHandler進行處理,例如可以進行編碼、解碼、加密、解密等
2) Netty 中流向Chnannel的有兩個方向的數據,入站數據指的是從網絡發至客戶端或者服務器的數據;出站數據指的是 客戶端或服務器 發到網絡中的數據。
3) 因此也有兩個方向的通道處理接口,ChannelInboundHanlder 繼承自ChanelHandler 專門用於處理入站數據
4) ChanneloutboundHandler 處理出站數據
5) 編碼器都繼承了ChanneloutboundHandler 因為發向網絡的數據一般要先經過編碼,比如說要將對象轉化成字節序列,再在網絡中傳輸。解碼器都繼承了ChannelintboundHandler,因為需要將字節序列轉化成對象。同理,加密繼承於ChanneloutboundHandler,解密繼承於ChannelintboundHandler。
1) 數據處理鏈是包含多個ChannelHandler的雙向鏈表。圖5 是ChannelPipline的示意圖,從網絡中接收的數據從左邊的Socket中傳入ChannelPipline,入站的時候從鏈表頭部,依次傳入所有的ChannelInboundHandler中進行處理。出站的從鏈表尾部依次傳入所有的CahnneloutboundHandler進行處理。
2、 ChannelPipeline其實就是一種高級形式的攔截過濾器。我們可以方便的增加刪除ChannelPipline中的ChannelHanlder,也可以自己實現ChannelHandler,這樣就能完全控制數據從入站到出戰的處理方式,以及各個ChannelHandler 之間的相互交互方式。
1) 一個事件循環對應一個線程,如圖6所示,一個事件循環內維護了一個多路復用器,selector,和一個任務隊列taskQueue。
2) 服務器給每個客戶端分配一個通道Channel,並將該通道的IO事件注冊到Selector上,Selector 用於輪詢各個Channel的IO事件
3) 任務隊列可以異步執行提交的IO任務與非IO任務任務,還可以執行定時任務,比如說我們可以利用任務隊列,向給建立連接的客戶端定時發消息。
如圖6所示 EventLoop 其實就循環執行三件事情
1、輪詢注冊在selector上的channel的IO事件
2、在對應的Channel處理IO事件
3、執行任務隊列中的任務
每個EventLoop可以負責處理多個Channel上的事件
一個Channel只對應於一個EventLoop (防止並發操作 出現Bug)
5) EvenLoopGroup 事件循環組
EvenLoopGroup中含有多個的EventLoop
可以簡單理解為一個線程池,內部維護了一組線程,
EvenLoopGroup 默認初始化 CPU核心數*2 個EventLoop
6) Bootstrap 引導類
一個Netty應用由一個Bootstrap開始,主要是用來配置整個 Netty 程序、設置業務處理類(Handler)、綁定端口、發起連接等
7) ChannelFuture 異步結果占位符
Netty的I/O操作是異步的,操作可能無法立即返回
ChannelFuture對象作為 異步操作結果的占位符 可確定異步執行的結果
通過addListener方法 可注冊了一個監聽ChannelFutureListener,當操作完成時,自動觸發注冊的監聽事件
圖7 是Netty 服務端的工作架構圖: 該圖中有兩個事件循環組:BossGroup 和 WorkerGroup,BossGroup 中的事件循環專門和客戶端建立連接,WorkerGroup 中的EventLoop專門負責處理連接上的讀寫。
在這里,我通過模擬一個客戶端給服務器發消息來解釋圖7:
1、首先初始化ServerSocketChannel 並將建立連接的事件Accept,注冊到BoosGroup的一個事件循環的Selector上
2、接着事件循環就會輪詢Channel上的建立連接事件
3、一個客戶端 發來建立連接請求后,Seletor通過輪詢可以發現此請求,並通過processSeleterKeys 處理處理連接請求
4、怎么處理連接請求呢?首先是為這個連接分配一個SocketChannel,並將這個Channel的讀寫事件注冊到一個WorkerGroup的事件循環的selector上,這時連接就建立好了,並且WorkerGroup會輪詢SocketChannel的讀寫事件。
5、當這個客戶端再發送消息時,事件循環會輪詢到寫事件,並通過processSeleterKeys處理消息
6、processSeleterKeys通過剛剛講的數據處理鏈過ChannelPipline來進行處理,可能包含先解碼、再進行業務處理,再編碼,再發送到SocketChannel中。
以上是服務端的具體流程,客戶端也會建立一個Channel ,也有一個Seletor輪詢IO事件,當消息到達時,也可以通過客戶端的ChannelPipline進行處理。
到現在,我們已經大概了解了Netty的工作原理,BoosGroup 用於專門創建連接,其中有多個事件循環線程,每個事件循環都監聽對應通道的建立連接請求並進行處理。WorkGroup 中也有多個事件循環線程,負責對應通道的IO事件。一個線程可以負責多個通道的IO,實現了IO多路復用。
建立連接、IO處理都由多個線程去做,提高了並發能力,也提高了系統的可靠性 (在之前的單線程處理IO的情況下 若意外終止 則服務不可用)。
三、ByteBuf和引用計數
Netty 利用ByteBuf作為緩沖區,利用Channel進行讀寫都要經過緩沖區,因此需要了解ByteBuf 的基本概念和操作才能更好的利用Netty編程。
ByteBuf 是存儲字節的容器 類似於NIO中的 ByteBuffer
ByteBuf 中存在
1、寫索引: writerIndex (當數據寫入ByteBuf時 writerIndex增加)
2、讀索引: readerIndex (當從ByteBuf讀數據時 readerIndex增加)
當writerIndex==readerIndex時 :代表無數據可以讀
capacity (ByteBuf的容量):默認為Integer.MAX_VALUE
因此可將ByteBuf 分為 三個部分
1. 可以被丟棄字節
2. 可讀字節
3. 可寫字節
ByteBuf 共有三種使用模式
模式1:Heap Buffer(堆緩沖區)
它是將數據存儲在JVM的堆空間(通過將數據存儲在數組中實現)
堆緩沖區可以通過JVM快速分配與釋放
模式2 :Direct Buffer 直接緩沖區
不在JVM的堆中分配內存,而是在JVM外通過本地方法調用分配虛擬機外內存
優點:免去中間交換的內存拷貝,提升IO處理速度:若在堆,則需要將數據先復制到直接緩沖區,再復制到堆 這體現了Netty的零拷貝特性
模式3:Composite Buffer 復合緩沖區
是一種視圖,不實際存數據,它可以由多個堆緩沖區和直接緩沖區 復合組成
優點:可將消息拆分為多個部分,若某部分不變,則不用每次都分配新的緩沖區存不變的部分(向多個客戶端發 相同的消息body不變 header變 可以復用body)
有兩種ByteBuf 分配方式,
上面我們講到,ByteBuf可以利用直接內存避免拷貝數據到用戶空間,並且Netty還使用池化技術降低內存使用率。因為用到了池化技術,Netty需要將用完的對象放回池中,java的垃圾回收器無法完成此功能,因此引入了引用計數,將用完的對象放回池中。
如圖所示 每個對象的初始引用計數為1
buf.retain() ,buf 引用計數加1
buf.release() ,buf引用計數減1
當引用計數為0時 釋放對象,並返回對象池。
ByteBuf引用計數的原則是:誰最后使用,誰負責釋放
Netty提供了檢查內存泄漏的方式,通過配置JVM 的leakDetectionLevel 可以開啟指定級別的泄漏檢測
默認是簡單級別,它會抽樣百分之1的樣本,並告訴我們是否發生內存泄漏。
高級級別可以告訴我們內存泄漏發生的地方。
偏執級別會檢測所有樣本。
參考資料: