參考:
https://www.jianshu.com/p/b9f3f6a16911
https://www.sohu.com/a/272879207_463994
https://blog.csdn.net/tugangkai/article/details/80560495
認識Netty
什么是Netty?
Netty 是一個利用 Java 的高級網絡的能力,隱藏其背后的復雜性而提供一個易於使用的 API 的客戶端/服務器框架。
Netty 是一個廣泛使用的 Java 網絡編程框架(Netty 在 2011 年獲得了Duke's Choice Award,見https://www.java.net/dukeschoice/2011)。它活躍和成長於用戶社區,像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網絡抽象的核心代碼。
以上是摘自《Essential Netty In Action》這本書,本文的內容也是本人讀了這本書之后的一些整理心得,如有不當之處歡迎大蝦們指正
Netty和Tomcat有什么區別?
Netty和Tomcat最大的區別就在於通信協議,Tomcat是基於Http協議的,他的實質是一個基於http協議的web容器,但是Netty不一樣,他能通過編程自定義各種協議,因為netty能夠通過codec自己來編碼/解碼字節流,完成類似redis訪問的功能,這就是netty和tomcat最大的不同。
有人說netty的性能就一定比tomcat性能高,其實不然,tomcat從6.x開始就支持了nio模式,並且后續還有arp模式——一種通過jni調用apache網絡庫的模式,相比於舊的bio模式,並發性能得到了很大提高,特別是arp模式,而netty是否比tomcat性能更高,則要取決於netty程序作者的技術實力了。
為什么Netty受歡迎?
如第一部分所述,netty是一款收到大公司青睞的框架,在我看來,netty能夠受到青睞的原因有三:
- 並發高
- 傳輸快
- 封裝好
Netty為什么並發高
Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網絡通信框架,對比於BIO(Blocking I/O,阻塞IO),他的並發性能得到了很大提高,兩張圖讓你了解BIO和NIO的區別:


從這兩圖可以看出,NIO的單線程能處理連接的數量比BIO要高出很多,而為什么單線程能處理更多的連接呢?原因就是圖二中出現的
Selector
。
當一個連接建立之后,他有兩個步驟要做,第一步是接收完客戶端發過來的全部數據,第二步是服務端處理完請求業務之后返回response給客戶端。NIO和BIO的區別主要是在第一步。
在BIO中,等待客戶端發數據這個過程是阻塞的,這樣就造成了一個線程只能處理一個請求的情況,而機器能支持的最大線程數是有限的,這就是為什么BIO不能支持高並發的原因。
而NIO中,當一個Socket建立好之后,Thread並不會阻塞去接受這個Socket,而是將這個請求交給Selector,Selector會不斷的去遍歷所有的Socket,一旦有一個Socket建立完成,他會通知Thread,然后Thread處理完數據再返回給客戶端—— 這個過程是阻塞的,這樣就能讓一個Thread處理更多的請求了。
下面兩張圖是基於BIO的處理流程和netty的處理流程,輔助你理解兩種方式的差別:


除了BIO和NIO之外,還有一些其他的IO模型,下面這張圖就表示了五種IO模型的處理流程:

- BIO,同步阻塞IO,阻塞整個步驟,如果連接少,他的延遲是最低的,因為一個線程只處理一個連接,適用於少連接且延遲低的場景,比如說數據庫連接。
- NIO,同步非阻塞IO,阻塞業務處理但不阻塞數據接收,適用於高並發且處理簡單的場景,比如聊天軟件。
- 多路復用IO,他的兩個步驟處理是分開的,也就是說,一個連接可能他的數據接收是線程a完成的,數據處理是線程b完成的,他比BIO能處理更多請求,但是比不上NIO,但是他的處理性能又比BIO更差,因為一個連接他需要兩次system call,而BIO只需要一次,所以這種IO模型應用的不多。
- 信號驅動IO,這種IO模型主要用在嵌入式開發,不參與討論。
- 異步IO,他的數據請求和數據處理都是異步的,數據請求一次返回一次,適用於長連接的業務場景。
以上摘自Linux IO模式及 select、poll、epoll詳解
Netty為什么傳輸快
Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。我們知道,Java的內存有堆內存、棧內存和字符串常量池等等,其中堆內存是占用內存空間最大的一塊,也是Java對象存放的地方,一般我們的數據如果需要從IO讀取到堆內存,中間需要經過Socket緩沖區,也就是說一個數據會被拷貝兩次才能到達他的的終點,如果數據量大,就會造成不必要的資源浪費。
Netty針對這種情況,使用了NIO中的另一大特性——零拷貝,當他需要接收數據的時候,他會在堆內存之外開辟一塊內存,數據就直接從IO讀到了那塊內存中去,在netty里面通過ByteBuf可以直接對這些數據進行直接操作,從而加快了傳輸速度。
下兩圖就介紹了兩種拷貝方式的區別,摘自Linux 中的零拷貝技術,第 1 部分


上文介紹的ByteBuf是Netty的一個重要概念,他是netty數據處理的容器,也是Netty封裝好的一個重要體現,將在下一部分做詳細介紹。
為什么說Netty封裝好?
要說Netty為什么封裝好,這種用文字是說不清的,直接上代碼:
- 阻塞I/O
public class PlainOioServer { public void serve(int port) throws IOException { final ServerSocket socket = new ServerSocket(port); //1 try { for (;;) { final Socket clientSocket = socket.accept(); //2 System.out.println("Accepted connection from " + clientSocket); new Thread(new Runnable() { //3 @Override public void run() { OutputStream out; try { out = clientSocket.getOutputStream(); out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8"))); //4 out.flush(); clientSocket.close(); //5 } catch (IOException e) { e.printStackTrace(); try { clientSocket.close(); } catch (IOException ex) { // ignore on close } } } }).start(); //6 } } catch (IOException e) { e.printStackTrace(); } } }
- 非阻塞IO
public class PlainNioServer { public void serve(int port) throws IOException { ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); ServerSocket ss = serverChannel.socket(); InetSocketAddress address = new InetSocketAddress(port); ss.bind(address); //1 Selector selector = Selector.open(); //2 serverChannel.register(selector, SelectionKey.OP_ACCEPT); //3 final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes()); for (;;) { try { selector.select(); //4 } catch (IOException ex) { ex.printStackTrace(); // handle exception break; } Set<SelectionKey> readyKeys = selector.selectedKeys(); //5 Iterator<SelectionKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { if (key.isAcceptable()) { //6 ServerSocketChannel server = (ServerSocketChannel)key.channel(); SocketChannel client = server.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, msg.duplicate()); //7 System.out.println( "Accepted connection from " + client); } if (key.isWritable()) { //8 SocketChannel client = (SocketChannel)key.channel(); ByteBuffer buffer = (ByteBuffer)key.attachment(); while (buffer.hasRemaining()) { if (client.write(buffer) == 0) { //9 break; } } client.close(); //10 } } catch (IOException ex) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { // 在關閉時忽略 } } } } } }
- Netty
public class NettyOioServer { public void server(int port) throws Exception { final ByteBuf buf = Unpooled.unreleasableBuffer( Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8"))); EventLoopGroup group = new OioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); //1 b.group(group) //2 .channel(OioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() {//3 @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5 } }); } }); ChannelFuture f = b.bind().sync(); //6 f.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); //7 } } }
從代碼量上來看,Netty就已經秒殺傳統Socket編程了,但是這一部分博大精深,僅僅貼幾個代碼豈能說明問題,在這里給大家介紹一下Netty的一些重要概念,讓大家更理解Netty。
-
Channel
數據傳輸流,與channel相關的概念有以下四個,上一張圖讓你了解netty里面的Channel。
Channel一覽- Channel,表示一個連接,可以理解為每一個請求,就是一個Channel。
- ChannelHandler,核心處理業務就在這里,用於處理業務請求。
- ChannelHandlerContext,用於傳輸業務數據。
- ChannelPipeline,用於保存處理過程需要用到的ChannelHandler和ChannelHandlerContext。
- ByteBuf
ByteBuf是一個存儲字節的容器,最大特點就是使用方便,它既有自己的讀索引和寫索引,方便你對整段字節緩存進行讀寫,也支持get/set,方便你對其中每一個字節進行讀寫,他的數據結構如下圖所示:

他有三種使用模式:
- Heap Buffer 堆緩沖區
堆緩沖區是ByteBuf最常用的模式,他將數據存儲在堆空間。 - Direct Buffer 直接緩沖區
直接緩沖區是ByteBuf的另外一種常用模式,他的內存分配都不發生在堆,jdk1.4引入的nio的ByteBuffer類允許jvm通過本地方法調用分配內存,這樣做有兩個好處- 通過免去中間交換的內存拷貝, 提升IO處理速度; 直接緩沖區的內容可以駐留在垃圾回收掃描的堆區以外。
- DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的內存, GC對此”無能為力”,也就意味着規避了在高負載下頻繁的GC過程對應用線程的中斷影響.
- Composite Buffer 復合緩沖區
復合緩沖區相當於多個不同ByteBuf的視圖,這是netty提供的,jdk不提供這樣的功能。
除此之外,他還提供一大堆api方便你使用,在這里我就不一一列出了,具體參見ByteBuf字節緩存。
- Codec
Netty中的編碼/解碼器,通過他你能完成字節與pojo、pojo與pojo的相互轉換,從而達到自定義協議的目的。
在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。
Netty入門教程2——動手搭建HttpServer
Netty入門教程3——Decoder和Encoder
Netty入門教程4——如何實現長連接
以上就是我對《Netty實戰》這本書的一些心得和書外的一些相關知識整理,如果有不同的見解,歡迎討論!
本文基於 Netty 4.1 展開介紹相關理論模型,使用場景,基本組件、整體架構,知其然且知其所以然,希望給大家在實際開發實踐、學習開源項目方面提供參考。
Netty 是一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。
JDK 原生 NIO 程序的問題
JDK 原生也有一套網絡應用程序 API,但是存在一系列問題,主要如下:
- NIO 的類庫和 API 繁雜,使用麻煩。你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
- 需要具備其他的額外技能做鋪墊。例如熟悉 Java 多線程編程,因為 NIO 編程涉及到 Reactor 模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的 NIO 程序。
- 可靠性能力補齊,開發工作量和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等。 NIO 編程的特點是功能開發相對容易,但是可靠性能力補齊工作量和難度都非常大。
- JDK NIO 的 Bug。例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。 官方聲稱在 JDK 1.6 版本的 update 18 修復了該問題,但是直到 JDK 1.7 版本該問題仍舊存在,只不過該 Bug 發生概率降低了一些而已,它並沒有被根本解決。
Netty 的特點
Netty 對 JDK 自帶的 NIO 的 API 進行封裝,解決上述問題,主要特點有:
- 設計優雅,適用於各種傳輸類型的統一 API 阻塞和非阻塞 Socket;基於靈活且可擴展的事件模型,可以清晰地分離關注點;高度可定制的線程模型 - 單線程,一個或多個線程池;真正的無連接數據報套接字支持(自 3.1 起)。
- 使用方便,詳細記錄的 Javadoc,用戶指南和示例;沒有其他依賴項,JDK 5(Netty 3.x)或 6(Netty 4.x)就足夠了。
- 高性能,吞吐量更高,延遲更低;減少資源消耗;最小化不必要的內存復制。
- 安全,完整的 SSL/TLS 和 StartTLS 支持。
- 社區活躍,不斷更新,社區活躍,版本迭代周期短,發現的 Bug 可以被及時修復,同時,更多的新功能會被加入。
Netty 常見使用場景
Netty 常見的使用場景如下:
- 互聯網行業。在分布式系統中,各個節點之間需要遠程服務調用,高性能的 RPC 框架必不可少,Netty 作為異步高性能的通信框架,往往作為基礎通信組件被這些 RPC 框架使用。 典型的應用有:阿里分布式服務框架 Dubbo 的 RPC 框架使用 Dubbo 協議進行節點間通信,Dubbo 協議默認使用 Netty 作為基礎通信組件,用於實現各進程節點之間的內部通信。
- 游戲行業。無論是手游服務端還是大型的網絡游戲,Java 語言得到了越來越廣泛的應用。Netty 作為高性能的基礎通信組件,它本身提供了 TCP/UDP 和 HTTP 協議棧。 非常方便定制和開發私有協議棧,賬號登錄服務器,地圖服務器之間可以方便的通過 Netty 進行高性能的通信。
- 大數據領域。經典的 Hadoop 的高性能通信和序列化組件 Avro 的 RPC 框架,默認采用 Netty 進行跨界點通信,它的 Netty Service 基於 Netty 框架二次封裝實現。
有興趣的讀者可以了解一下目前有哪些開源項目使用了 Netty:Related Projects。
Netty 高性能設計
Netty 作為異步事件驅動的網絡,高性能之處主要來自於其 I/O 模型和線程處理模型,前者決定如何收發數據,后者決定如何處理數據。
I/O 模型
用什么樣的通道將數據發送給對方,BIO、NIO 或者 AIO,I/O 模型在很大程度上決定了框架的性能。
阻塞 I/O
傳統阻塞型 I/O(BIO)可以用下圖表示:
Blocking I/O
特點如下:
- 每個請求都需要獨立的線程完成數據 Read,業務處理,數據 Write 的完整操作問題。
- 當並發數較大時,需要創建大量線程來處理連接,系統資源占用較大。
- 連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。
I/O 復用模型
在 I/O 復用模型中,會用到 Select,這個函數也會使進程阻塞,但是和阻塞 I/O 所不同的是這兩個函數可以同時阻塞多個 I/O 操作。
而且可以同時對多個讀操作,多個寫操作的 I/O 函數進行檢測,直到有數據可讀或可寫時,才真正調用 I/O 操作函數。
Netty 的非阻塞 I/O 的實現關鍵是基於 I/O 復用模型,這里用 Selector 對象表示:
Nonblocking I/O
Netty 的 IO 線程 NioEventLoop 由於聚合了多路復用器 Selector,可以同時並發處理成百上千個客戶端連接。
當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。
線程通常將非阻塞 IO 的空閑時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道。
由於讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由於頻繁 I/O 阻塞導致的線程掛起。
一個 I/O 線程可以並發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。
基於 Buffer
傳統的 I/O 是面向字節流或字符流的,以流式的方式順序地從一個 Stream 中讀取一個或多個字節, 因此也就不能隨意改變讀取指針的位置。
在 NIO 中,拋棄了傳統的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能從 Channel 中讀取數據到 Buffer 中或將數據從 Buffer 中寫入到 Channel。
基於 Buffer 操作不像傳統 IO 的順序操作,NIO 中可以隨意地讀取任意位置的數據。
線程模型
數據報如何讀取?讀取之后的編解碼在哪個線程進行,編解碼后的消息如何派發,線程模型的不同,對性能的影響也非常大。
事件驅動模型
通常,我們設計一個事件處理模型的程序有兩種思路:
- 輪詢方式,線程不斷輪詢訪問相關事件發生源有沒有發生事件,有發生事件就調用事件處理邏輯。
- 事件驅動方式,發生事件,主線程把事件放入事件隊列,在另外線程不斷循環消費事件列表中的事件,調用事件對應的處理邏輯處理事件。事件驅動方式也被稱為消息通知方式,其實是設計模式中觀察者模式的思路。
以 GUI 的邏輯處理為例,說明兩種邏輯的不同:
- 輪詢方式,線程不斷輪詢是否發生按鈕點擊事件,如果發生,調用處理邏輯。
- 事件驅動方式,發生點擊事件把事件放入事件隊列,在另外線程消費的事件列表中的事件,根據事件類型調用相關事件處理邏輯。
這里借用 O'Reilly 大神關於事件驅動模型解釋圖:
事件驅動模型
主要包括 4 個基本組件:
- 事件隊列(event queue):接收事件的入口,存儲待處理事件。
- 分發器(event mediator):將不同的事件分發到不同的業務邏輯單元。
- 事件通道(event channel):分發器與處理器之間的聯系渠道。
- 事件處理器(event processor):實現業務邏輯,處理完成后會發出事件,觸發下一步操作。
可以看出,相對傳統輪詢模式,事件驅動有如下優點:
- 可擴展性好,分布式的異步架構,事件處理器之間高度解耦,可以方便擴展事件處理邏輯。
- 高性能,基於隊列暫存事件,能方便並行異步處理事件。
Reactor 線程模型
Reactor 是反應堆的意思,Reactor 模型是指通過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。
服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了復用統一監聽事件,收到事件后分發(Dispatch 給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor 模型中有 2 個關鍵組成:
- Reactor,Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對 IO 事件做出反應。它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯系人。
- Handlers,處理程序執行 I/O 事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor 通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作。
Reactor 模型
取決於 Reactor 的數量和 Hanndler 線程數量的不同,Reactor 模型有 3 個變種:
- 單 Reactor 單線程。
- 單 Reactor 多線程。
- 主從 Reactor 多線程。
可以這樣理解,Reactor 就是一個執行 while (true) { selector.select(); …} 循環的線程,會源源不斷的產生新的事件,稱作反應堆很貼切。
篇幅關系,這里不再具體展開 Reactor 特性、優缺點比較,有興趣的讀者可以參考我之前另外一篇文章:《理解高性能網絡模型》。
Netty 線程模型
Netty 主要基於主從 Reactors 多線程模型(如下圖)做了一定的修改,其中主從 Reactor 多線程模型有多個 Reactor:
- MainReactor 負責客戶端的連接請求,並將請求轉交給 SubReactor。
- SubReactor 負責相應通道的 IO 讀寫請求。
- 非 IO 請求(具體邏輯處理)的任務則會直接寫入隊列,等待 worker threads 進行處理。
這里引用 Doug Lee 大神的 Reactor 介紹:Scalable IO in Java 里面關於主從 Reactor 多線程模型的圖:
主從 Rreactor 多線程模型
特別說明的是:雖然 Netty 的線程模型基於主從 Reactor 多線程,借用了 MainReactor 和 SubReactor 的結構。但是實際實現上 SubReactor 和 Worker 線程在同一個線程池中:
EventLoopGroup bossGroup = newNioEventLoopGroup();
EventLoopGroup workerGroup = newNioEventLoopGroup();
ServerBootstrap server= newServerBootstrap();
server.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel. class)
上面代碼中的 bossGroup 和 workerGroup 是 Bootstrap 構造方法中傳入的兩個對象,這兩個 group 均是線程池:
- bossGroup 線程池則只是在 Bind 某個端口后,獲得其中一個線程作為 MainReactor,專門處理端口的 Accept 事件,每個端口對應一個 Boss 線程。
- workerGroup 線程池會被各個 SubReactor 和 Worker 線程充分利用。
異步處理
異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的部件在完成后,通過狀態、通知和回調來通知調用者。
Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture。
調用者並不能立刻獲得結果,而是通過 Future-Listener 機制,用戶可以方便的主動獲取或者通過通知機制獲得 IO 操作結果。
當 Future 對象剛剛創建時,處於非完成狀態,調用者可以通過返回的 ChannelFuture 來獲取操作執行的狀態,注冊監聽函數來執行完成后的操作。
常見有如下操作:
- 通過 isDone 方法來判斷當前操作是否完成。
- 通過 isSuccess 方法來判斷已完成的當前操作是否成功。
- 通過 getCause 方法來獲取已完成的當前操作失敗的原因。
- 通過 isCancelled 方法來判斷已完成的當前操作是否被取消。
- 通過 addListener 方法來注冊監聽器,當操作已完成(isDone 方法返回完成),將會通知指定的監聽器;如果 Future 對象已完成,則理解通知指定的監聽器。
例如下面的代碼中綁定端口是異步操作,當綁定操作處理完,將會調用相應的監聽器處理邏輯。
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System.out. println( newDate() + ": 端口["+ port + "]綁定成功!");
} else{
System.err. println( "端口["+ port + "]綁定失敗!");
}
});
相比傳統阻塞 I/O,執行 I/O 操作后線程會被阻塞住, 直到操作完成;異步處理的好處是不會造成線程阻塞,線程在 I/O 操作期間可以執行別的程序,在高並發情形下會更穩定和更高的吞吐量。
Netty 架構設計
前面介紹完 Netty 相關一些理論,下面從功能特性、模塊組件、運作過程來介紹 Netty 的架構設計。
功能特性
Netty 功能特性如下:
- 傳輸服務,支持 BIO 和 NIO。
- 容器集成,支持 OSGI、JBossMC、Spring、Guice 容器。
- 協議支持,HTTP、Protobuf、二進制、文本、WebSocket 等一系列常見協議都支持。還支持通過實行編碼解碼邏輯來實現自定義協議。
- Core 核心,可擴展事件模型、通用通信 API、支持零拷貝的 ByteBuf 緩沖對象。
模塊組件
Bootstrap、ServerBootstrap
Bootstrap 意思是引導,一個 Netty 應用通常由一個 Bootstrap 開始,主要作用是配置整個 Netty 程序,串聯各個組件,Netty 中 Bootstrap 類是客戶端程序的啟動引導類,ServerBootstrap 是服務端啟動引導類。
Future、ChannelFuture
正如前面介紹,在 Netty 中所有的 IO 操作都是異步的,不能立刻得知消息是否被正確處理。
但是可以過一會等它執行完成或者直接注冊一個監聽,具體的實現就是通過 Future 和 ChannelFutures,他們可以注冊一個監聽,當操作執行成功或失敗時監聽會自動觸發注冊的監聽事件。
Channel
Netty 網絡通信的組件,能夠用於執行網絡 I/O 操作。Channel 為用戶提供:
- 當前網絡連接的通道的狀態(例如是否打開?是否已連接?)
- 網絡連接的配置參數 (例如接收緩沖區大小)
- 提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味着任何 I/O 調用都將立即返回,並且不保證在調用結束時所請求的 I/O 操作已完成。 調用立即返回一個 ChannelFuture 實例,通過注冊監聽器到 ChannelFuture 上,可以 I/O 操作成功、失敗或取消時回調通知調用方。
- 支持關聯 I/O 操作與對應的處理程序。
不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應。下面是一些常用的 Channel 類型:
- NioSocketChannel,異步的客戶端 TCP Socket 連接。
- NioServerSocketChannel,異步的服務器端 TCP Socket 連接。
- NioDatagramChannel,異步的 UDP 連接。
- NioSctpChannel,異步的客戶端 Sctp 連接。
- NioSctpServerChannel,異步的 Sctp 服務器端連接,這些通道涵蓋了 UDP 和 TCP 網絡 IO 以及文件 IO。
Selector
Netty 基於 Selector 對象實現 I/O 多路復用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件。
當向一個 Selector 中注冊 Channel 后,Selector 內部的機制就可以自動不斷地查詢(Select) 這些注冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel 。
NioEventLoop
NioEventLoop 中維護了一個線程和任務隊列,支持異步提交執行任務,線程啟動時會調用 NioEventLoop 的 run 方法,執行 I/O 任務和非 I/O 任務:
- I/O 任務,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法觸發。
- 非 IO 任務,添加到 taskQueue 中的任務,如 register0、bind0 等任務,由 runAllTasks 方法觸發。
兩種任務的執行時間比由變量 ioRatio 控制,默認為 50,則表示允許非 IO 任務執行的時間與 IO 任務的執行時間相等。
NioEventLoopGroup
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解為一個線程池,內部維護了一組線程,每個線程(NioEventLoop)負責處理多個 Channel 上的事件,而一個 Channel 只對應於一個線程。
ChannelHandler
ChannelHandler 是一個接口,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。
ChannelHandler 本身並沒有提供很多方法,因為這個接口有許多的方法需要實現,方便使用期間,可以繼承它的子類:
- ChannelInboundHandler 用於處理入站 I/O 事件。
- ChannelOutboundHandler 用於處理出站 I/O 操作。
或者使用以下適配器類:
- ChannelInboundHandlerAdapter 用於處理入站 I/O 事件。
- ChannelOutboundHandlerAdapter 用於處理出站 I/O 操作。
- ChannelDuplexHandler 用於處理入站和出站事件。
ChannelHandlerContext
保存 Channel 相關的所有上下文信息,同時關聯一個 ChannelHandler 對象。
ChannelPipline
保存 ChannelHandler 的 List,用於處理或攔截 Channel 的入站事件和出站操作。
ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
下圖引用 Netty 的 Javadoc 4.1 中 ChannelPipeline 的說明,描述了 ChannelPipeline 中 ChannelHandler 通常如何處理 I/O 事件。
I/O 事件由 ChannelInboundHandler 或 ChannelOutboundHandler 處理,並通過調用 ChannelHandlerContext 中定義的事件傳播方法。
例如 ChannelHandlerContext.fireChannelRead(Object)和 ChannelOutboundInvoker.write(Object)轉發到其最近的處理程序。
入站事件由自下而上方向的入站處理程序處理,如圖左側所示。入站 Handler 處理程序通常處理由圖底部的 I/O 線程生成的入站數據。
通常通過實際輸入操作(例如 SocketChannel.read(ByteBuffer))從遠程讀取入站數據。
出站事件由上下方向處理,如圖右側所示。出站 Handler 處理程序通常會生成或轉換出站傳輸,例如 write 請求。
I/O 線程通常執行實際的輸出操作,例如 SocketChannel.write(ByteBuffer)。
在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關系如下:
一個 Channel 包含了一個 ChannelPipeline,而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表,並且每個 ChannelHandlerContext 中又關聯着一個 ChannelHandler。
入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表 head 往后傳遞到最后一個入站的 handler,出站事件會從鏈表 tail 往前傳遞到最前一個出站的 handler,兩種類型的 handler 互不干擾。
Netty 工作原理架構
初始化並啟動 Netty 服務端過程如下:
publicstaticvoidmain(String[] args) {
// 創建mainReactor
NioEventLoopGroup boosGroup = newNioEventLoopGroup();
// 創建工作線程組
NioEventLoopGroup workerGroup = newNioEventLoopGroup();
final ServerBootstrap serverBootstrap = newServerBootstrap();
serverBootstrap
// 組裝NioEventLoopGroup
. group(boosGroup, workerGroup)
// 設置channel類型為NIO類型
.channel(NioServerSocketChannel.class)
// 設置連接配置參數
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
// 配置入站、出站事件handler
.childHandler( newChannelInitializer<NioSocketChannel>() {
@ Override
protectedvoidinitChannel(NioSocketChannel ch) {
// 配置入站、出站事件channel
ch.pipeline().addLast(...);
ch.pipeline().addLast(...);
}
});
// 綁定端口
intport = 8080;
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System. out.println( newDate() + ": 端口["+ port + "]綁定成功!");
} else{
System.err.println( "端口["+ port + "]綁定失敗!");
}
});
}
基本過程如下:
- 初始化創建 2 個 NioEventLoopGroup,其中 boosGroup 用於 Accetpt 連接建立事件並分發請求,workerGroup 用於處理 I/O 讀寫事件和業務邏輯。
- 基於 ServerBootstrap(服務端啟動引導類),配置 EventLoopGroup、Channel 類型,連接參數、配置入站、出站事件 handler。
- 綁定端口,開始工作。
結合上面介紹的 Netty Reactor 模型,介紹服務端 Netty 的工作架構圖:
服務端 Netty Reactor 工作架構圖
Server 端包含 1 個 Boss NioEventLoopGroup 和 1 個 Worker NioEventLoopGroup。
NioEventLoopGroup 相當於 1 個事件循環組,這個組里包含多個事件循環 NioEventLoop,每個 NioEventLoop 包含 1 個 Selector 和 1 個事件循環線程。
每個 Boss NioEventLoop 循環執行的任務包含 3 步:
- 輪詢 Accept 事件。
- 處理 Accept I/O 事件,與 Client 建立連接,生成 NioSocketChannel,並將 NioSocketChannel 注冊到某個 Worker NioEventLoop 的 Selector 上。
- 處理任務隊列中的任務,runAllTasks。任務隊列中的任務包括用戶調用 eventloop.execute 或 schedule 執行的任務,或者其他線程提交到該 eventloop 的任務。
每個 Worker NioEventLoop 循環執行的任務包含 3 步:
- 輪詢 Read、Write 事件。
- 處理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可讀、可寫事件發生時進行處理。
- 處理任務隊列中的任務,runAllTasks。
其中任務隊列中的 Task 有 3 種典型使用場景。
①用戶程序自定義的普通任務
ctx.channel().eventLoop().execute( newRunnable() {
@Override
publicvoidrun(){
//...
}
});
②非當前 Reactor 線程調用 Channel 的各種方法
例如在推送系統的業務線程里面,根據用戶的標識,找到對應的 Channel 引用,然后調用 Write 類方法向該用戶推送消息,就會進入到這種場景。最終的 Write 會提交到任務隊列中后被異步消費。
③用戶自定義定時任務
ctx.channel().eventLoop().schedule( newRunnable() {
@Override
publicvoidrun(){
}
}, 60, TimeUnit.SECONDS);
總結
現在穩定推薦使用的主流版本還是 Netty4,Netty5 中使用了 ForkJoinPool,增加了代碼的復雜度,但是對性能的改善卻不明顯,所以這個版本不推薦使用,官網也沒有提供下載鏈接。
Netty 入門門檻相對較高,是因為這方面的資料較少,並不是因為它有多難,大家其實都可以像搞透 Spring 一樣搞透 Netty。
在學習之前,建議先理解透整個框架原理結構,運行過程,可以少走很多彎路。
參考資料:
- Netty入門與實戰:仿寫微信 IM 即時通訊系統
- Netty官網
- Netty 4.x學習筆記 - 線程模型
- Netty入門與實戰
- 理解高性能網絡模型
- Netty基本原理介紹
- software-architecture-patterns.pdf
- Netty高性能之道 —— 李林鋒
- 《Netty In Action》
- 《Netty權威指南》
作者:陳彩華
編輯:陶家龍、孫淑娟
出處:轉載自Hollis(ID:hollischuang)微信公眾號
netty原理分析
1. Netty簡介
Netty是一個高性能、異步事件驅動的NIO框架,基於JAVA NIO提供的API實現。它提供了對TCP、UDP和文件傳輸的支持,作為一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。 作為當前最流行的NIO框架,Netty在互聯網領域、大數據分布式計算領域、游戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。
2. Netty線程模型
在JAVA NIO方面Selector給Reactor模式提供了基礎,Netty結合Selector和Reactor模式設計了高效的線程模型。先來看下Reactor模式:
2.1 Reactor模式
Wikipedia這么解釋Reactor模型:“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。首先Reactor模式首先是事件驅動的,有一個或者多個並發輸入源,有一個Server Handler和多個Request Handlers,這個Service Handler會同步的將輸入的請求多路復用的分發給相應的Request Handler。可以如下圖所示:
從結構上有點類似生產者和消費者模型,即一個或多個生產者將事件放入一個Queue中,而一個或者多個消費者主動的從這個隊列中poll事件來處理;而Reactor模式則沒有Queue來做緩沖,每當一個事件輸入到Service Handler之后,該Service Handler會主動根據不同的Evnent類型將其分發給對應的Request Handler來處理。
2.2 Reator模式的實現
關於Java NIO 構造Reator模式,Doug lea在《Scalable IO in Java》中給了很好的闡述,這里截取PPT對Reator模式的實現進行說明
1.第一種實現模型如下:
這是最簡單的Reactor單線程模型,由於Reactor模式使用的是異步非阻塞IO,所有的IO操作都不會被阻塞,理論上一個線程可以獨立處理所有的IO操作。這時Reactor線程是個多面手,負責多路分離套接字,Accept新連接,並分發請求到處理鏈中。
對於一些小容量應用場景,可以使用到單線程模型。但對於高負載,大並發的應用卻不合適,主要原因如下:
- 當一個NIO線程同時處理成百上千的鏈路,性能上無法支撐,即使NIO線程的CPU負荷達到100%,也無法完全處理消息
- 當NIO線程負載過重后,處理速度會變慢,會導致大量客戶端連接超時,超時之后往往會重發,更加重了NIO線程的負載。
- 可靠性低,一個線程意外死循環,會導致整個通信系統不可用
為了解決這些問題,出現了Reactor多線程模型。
2.Reactor多線程模型:
相比上一種模式,該模型在處理鏈部分采用了多線程(線程池)。
在絕大多數場景下,該模型都能滿足性能需求。但是,在一些特殊的應用場景下,如服務器會對客戶端的握手消息進行安全認證。這類場景下,單獨的一個Acceptor線程可能會存在性能不足的問題。為了解決這些問題,產生了第三種Reactor線程模型
3.Reactor主從模型
該模型相比第二種模型,是將Reactor分成兩部分,mainReactor負責監聽server socket,accept新連接;並將建立的socket分派給subReactor。subReactor負責多路分離已連接的socket,讀寫網絡數據,對業務處理功能,其扔給worker線程池完成。通常,subReactor個數上可與CPU個數等同。
2.3 Netty模型
2.2中說完了Reactor的三種模型,那么Netty是哪一種呢?其實Netty的線程模型是Reactor模型的變種,那就是去掉線程池的第三種形式的變種,這也是Netty NIO的默認模式。Netty中Reactor模式的參與者主要有下面一些組件:
- Selector
- EventLoopGroup/EventLoop
- ChannelPipeline
Selector即為NIO中提供的SelectableChannel多路復用器,充當着demultiplexer的角色,這里不再贅述;下面對另外兩種功能和其在Netty之Reactor模式中扮演的角色進行介紹。
3.EventLoopGroup/EventLoop
當系統在運行過程中,如果頻繁的進行線程上下文切換,會帶來額外的性能損耗。多線程並發執行某個業務流程,業務開發者還需要時刻對線程安全保持警惕,哪些數據可能會被並發修改,如何保護?這不僅降低了開發效率,也會帶來額外的性能損耗。
為了解決上述問題,Netty采用了串行化設計理念,從消息的讀取、編碼以及后續Handler的執行,始終都由IO線程EventLoop負責,這就意外着整個流程不會進行線程上下文的切換,數據也不會面臨被並發修改的風險。這也解釋了為什么Netty線程模型去掉了Reactor主從模型中線程池。
EventLoopGroup是一組EventLoop的抽象,EventLoopGroup提供next接口,可以總一組EventLoop里面按照一定規則獲取其中一個EventLoop來處理任務,對於EventLoopGroup這里需要了解的是在Netty中,在Netty服務器編程中我們需要BossEventLoopGroup和WorkerEventLoopGroup兩個EventLoopGroup來進行工作。通常一個服務端口即一個ServerSocketChannel對應一個Selector和一個EventLoop線程,也就是說BossEventLoopGroup的線程數參數為1。BossEventLoop負責接收客戶端的連接並將SocketChannel交給WorkerEventLoopGroup來進行IO處理。
EventLoop的實現充當Reactor模式中的分發(Dispatcher)的角色。
4.ChannelPipeline
ChannelPipeline其實是擔任着Reactor模式中的請求處理器這個角色。
ChannelPipeline的默認實現是DefaultChannelPipeline,DefaultChannelPipeline本身維護着一個用戶不可見的tail和head的ChannelHandler,他們分別位於鏈表隊列的頭部和尾部。tail在更上層的部分,而head在靠近網絡層的方向。在Netty中關於ChannelHandler有兩個重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler。inbound可以理解為網絡數據從外部流向系統內部,而outbound可以理解為網絡數據從系統內部流向系統外部。用戶實現的ChannelHandler可以根據需要實現其中一個或多個接口,將其放入Pipeline中的鏈表隊列中,ChannelPipeline會根據不同的IO事件類型來找到相應的Handler來處理,同時鏈表隊列是責任鏈模式的一種變種,自上而下或自下而上所有滿足事件關聯的Handler都會對事件進行處理。
ChannelInBoundHandler對從客戶端發往服務器的報文進行處理,一般用來執行半包/粘包,解碼,讀取數據,業務處理等;ChannelOutBoundHandler對從服務器發往客戶端的報文進行處理,一般用來進行編碼,發送報文到客戶端。
下圖是對ChannelPipeline執行過程的說明:
關於Pipeline的更多知識可參考:淺談管道模型(Pipeline)
5.Buffer
Netty提供的經過擴展的Buffer相對NIO中的有個許多優勢,作為數據存取非常重要的一塊,我們來看看Netty中的Buffer有什么特點。
1.ByteBuf讀寫指針
- 在ByteBuffer中,讀寫指針都是position,而在ByteBuf中,讀寫指針分別為readerIndex和writerIndex,直觀看上去ByteBuffer僅用了一個指針就實現了兩個指針的功能,節省了變量,但是當對於ByteBuffer的讀寫狀態切換的時候必須要調用flip方法,而當下一次寫之前,必須要將Buffe中的內容讀完,再調用clear方法。每次讀之前調用flip,寫之前調用clear,這樣無疑給開發帶來了繁瑣的步驟,而且內容沒有讀完是不能寫的,這樣非常不靈活。相比之下我們看看ByteBuf,讀的時候僅僅依賴readerIndex指針,寫的時候僅僅依賴writerIndex指針,不需每次讀寫之前調用對應的方法,而且沒有必須一次讀完的限制。
2.零拷貝
- Netty的接收和發送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,然后才寫入Socket中。相比於堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。
- Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象,用戶可以像操作一個Buffer那樣方便的對組合Buffer進行操作,避免了傳統通過內存拷貝的方式將幾個小Buffer合並成一個大的Buffer。
- Netty的文件傳輸采用了transferTo方法,它可以直接將文件緩沖區的數據發送到目標Channel,避免了傳統通過循環write方式導致的內存拷貝問題。
3.引用計數與池化技術
- 在Netty中,每個被申請的Buffer對於Netty來說都可能是很寶貴的資源,因此為了獲得對於內存的申請與回收更多的控制權,Netty自己根據引用計數法去實現了內存的管理。Netty對於Buffer的使用都是基於直接內存(DirectBuffer)實現的,大大提高I/O操作的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外還有一個天生的缺點,即對於DirectBuffer的申請相比HeapBuffer效率更低,因此Netty結合引用計數實現了PolledBuffer,即池化的用法,當引用計數等於0的時候,Netty將Buffer回收致池中,在下一次申請Buffer的沒某個時刻會被復用。
總結
Netty其實本質上就是Reactor模式的實現,Selector作為多路復用器,EventLoop作為轉發器,Pipeline作為事件處理器。但是和一般的Reactor不同的是,Netty使用串行化實現,並在Pipeline中使用了責任鏈模式。
Netty中的buffer相對有NIO中的buffer又做了一些優化,大大提高了性能。