NETTY框架的使用

1、Netty 簡介

Netty 是基於 Java NIO 的異步事件驅動的網絡應用框架,使用 Netty 能夠快速開發網絡應用,Netty 提供了高層次的抽象來簡化 TCP 和 UDP 服務器的編程,可是你仍然可使用底層的 API。react

Netty 的內部實現是很複雜的,可是 Netty 提供了簡單易用的API從網絡處理代碼中解耦業務邏輯。Netty 是徹底基於 NIO 實現的,因此整個 Netty 都是異步的。編程

Netty 是最流行的 NIO 框架,它已經獲得成百上千的商業、商用項目驗證,許多框架和開源組件的底層 rpc 都是使用的 Netty,如 Dubbo、Elasticsearch 等等。下面是官網給出的一些 Netty 的特性:json

設計方面bootstrap

  • 對各類傳輸協議提供統一的 API(使用阻塞和非阻塞套接字時候使用的是同一個 API,只是須要設置的參數不同)。
  • 基於一個靈活、可擴展的事件模型來實現關注點清晰分離。
  • 高度可定製的線程模型——單線程、一個或多個線程池。
  • 真正的無數據報套接字(UDP)的支持(since 3.1)。

易用性後端

  • 完善的 Javadoc 文檔和示例代碼。
  • 不須要額外的依賴,JDK 5 (Netty 3.x) 或者 JDK 6 (Netty 4.x) 已經足夠。

性能瀏覽器

  • 更好的吞吐量,更低的等待延遲。
  • 更少的資源消耗。
  • 最小化沒必要要的內存拷貝。

安全性安全

  • 完整的 SSL/TLS 和 StartTLS 支持

對於初學者,上面的特性咱們在腦中有個簡單瞭解和印象便可, 下面開始咱們的實戰部分。性能優化

2、一個簡單 Http 服務器

開始前說明下我這裏使用的開發環境是 IDEA+Gradle+Netty4,固然你使用 Eclipse 和 Maven 都是能夠的,而後在 Gradle 的 build 文件中添加依賴 compile 'io.netty:netty-all:4.1.26.Final',這樣就能夠編寫咱們的 Netty 程序了,正如在前面介紹 Netty 特性中提到的,Netty 不須要額外的依賴。服務器

第一個示例咱們使用 Netty 編寫一個 Http 服務器的程序,啓動服務咱們在瀏覽器輸入網址來訪問咱們的服務,便會獲得服務端的響應。功能很簡單,下面咱們看看具體怎麼作?網絡

首先編寫服務啓動類

複製代碼
public class HttpServer { public static void main(String[] args) { //構造兩個線程組 EventLoopGroup bossrGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //服務端啓動輔助類 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new HttpServerInitializer()); ChannelFuture future = bootstrap.bind(8080).sync(); //等待服務端口關閉  future.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 優雅退出,釋放線程池資源  bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
複製代碼

 

在編寫 Netty 程序時,一開始都會生成 NioEventLoopGroup 的兩個實例,分別是 bossGroup 和 workerGroup,也能夠稱爲 parentGroup 和 childGroup,爲何建立這兩個實例,做用是什麼?能夠這麼理解,bossGroup 和 workerGroup 是兩個線程池, 它們默認線程數爲 CPU 核心數乘以 2,bossGroup 用於接收客戶端傳過來的請求,接收到請求後將後續操做交由 workerGroup 處理。

在這裏我向你們推薦一個架構學習交流羣。交流學習羣號:747981058 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。

接下來咱們生成了一個服務啓動輔助類的實例 bootstrap,boostrap 用來爲 Netty 程序的啓動組裝配置一些必需要組件,例如上面的建立的兩個線程組。channel 方法用於指定服務器端監聽套接字通道 NioServerSocketChannel,其內部管理了一個 Java NIO 中的ServerSocketChannel實例。

channelHandler 方法用於設置業務職責鏈,責任鏈是咱們下面要編寫的,責任鏈具體是什麼,它其實就是由一個個的 ChannelHandler 串聯而成,造成的鏈式結構。正是這一個個的 ChannelHandler 幫咱們完成了要處理的事情。

接着咱們調用了 bootstrap 的 bind 方法將服務綁定到 8080 端口上,bind 方法內部會執行端口綁定等一系列操,使得前面的配置都各就各位各司其職,sync 方法用於阻塞當前 Thread,一直到端口綁定操做完成。接下來一句是應用程序將會阻塞等待直到服務器的 Channel 關閉。

啓動類的編寫大致就是這樣了,下面要編寫的就是上面提到的責任鏈了。如何構建一個鏈,在 Netty 中很簡單,不須要咱們作太多,代碼以下:

複製代碼
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> { protected void initChannel(SocketChannel sc) throws Exception { ChannelPipeline pipeline = sc.pipeline(); //處理http消息的編解碼 pipeline.addLast("httpServerCodec", new HttpServerCodec()); //添加自定義的ChannelHandler pipeline.addLast("httpServerHandler", new HttpServerHandler()); } }
複製代碼

 

咱們自定義一個類 HttpServerInitializer 繼承 ChannelInitializer 並實現其中的 initChannel方法。

ChannelInitializer 繼承 ChannelInboundHandlerAdapter,用於初始化 Channel 的 ChannelPipeline。經過 initChannel 方法參數 sc 獲得 ChannelPipeline 的一個實例。

當一個新的鏈接被接受時, 一個新的 Channel 將被建立,同時它會被自動地分配到它專屬的 ChannelPipeline。

ChannelPipeline 提供了 ChannelHandler 鏈的容器,推薦讀者仔細本身看看 ChannelPipeline 的 Javadoc,文章後面也會繼續說明 ChannelPipeline 的內容。

Netty 是一個高性能網絡通訊框架,同時它也是比較底層的框架,想要 Netty 支持 Http(超文本傳輸協議),必需要給它提供相應的編解碼器。

因此咱們這裏使用 Netty 自帶的 Http 編解碼組件 HttpServerCodec 對通訊數據進行編解碼,HttpServerCodec 是 HttpRequestDecoder 和 HttpResponseEncoder 的組合,由於在處理 Http 請求時這兩個類是常用的,因此 Netty 直接將他們合併在一塊兒更加方便使用。因此對於上面的代碼:

pipeline.addLast("httpServerCodec", new HttpServerCodec())

 

咱們替換成以下兩行也是能夠的。

pipeline.addLast("httpResponseEndcoder", new HttpResponseEncoder()); pipeline.addLast("HttpRequestDecoder", new HttpRequestDecoder());

 

經過 addLast 方法將一個一個的 ChannelHandler 添加到責任鏈上並給它們取個名稱(不取也能夠,Netty 會給它個默認名稱),這樣就造成了鏈式結構。在請求進來或者響應出去時都會通過鏈上這些 ChannelHandler 的處理。

最後再向鏈上加入咱們自定義的 ChannelHandler 組件,處理自定義的業務邏輯。下面就是咱們自定義的 ChannelHandler。

複製代碼
public class HttpServerChannelHandler0 extends SimpleChannelInboundHandler<HttpObject> { private HttpRequest request; @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { if (msg instanceof HttpRequest) { request = (HttpRequest) msg; request.method(); String uri = request.uri(); System.out.println("Uri:" + uri); } if (msg instanceof HttpContent) { HttpContent content = (HttpContent) msg; ByteBuf buf = content.content(); System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8)); ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf); response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain"); response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes()); ctx.writeAndFlush(response); } } }
複製代碼

 

至此一個簡單的 Http 服務器就完成了。首先咱們來看看效果怎樣,咱們運行 HttpServer 中的 main 方法。讓後使用 Postman 這個工具來測試下,使用 post 請求方式(也能夠 get,但沒有請求體),並一個 json 格式數據做爲請求體發送給服務端,服務端返回給咱們一個hello world字符串。

 

服務端控制檯打印以下:

 

對於自定義的 ChannelHandler, 通常會繼承 Netty 提供的SimpleChannelInboundHandler類,而且對於 Http 請求咱們能夠給它設置泛型參數爲 HttpOjbect 類,而後覆寫 channelRead0 方法,在 channelRead0 方法中編寫咱們的業務邏輯代碼,此方法會在接收到服務器數據後被系統調用。

Netty 的設計中把 Http 請求分爲了 HttpRequest 和 HttpContent 兩個部分,HttpRequest 主要包含請求頭、請求方法等信息,HttpContent 主要包含請求體的信息。

因此上面的代碼咱們分兩塊來處理。在 HttpContent 部分,首先輸出客戶端傳過來的字符,而後經過 Unpooled 提供的靜態輔助方法來建立未池化的 ByteBuf 實例, Java NIO 提供了 ByteBuffer 做爲它的字節容器,Netty 的 ByteBuffer 替代品是 ByteBuf。

接着構建一個 FullHttpResponse 的實例,併爲它設置一些響應參數,最後經過 writeAndFlush 方法將它寫回給客戶端。

上面這樣獲取請求和消息體則至關不方便,Netty 又提供了另外一個類 FullHttpRequest,FullHttpRequest 包含請求的全部信息,它是一個接口,直接或者間接繼承了 HttpRequest 和 HttpContent,它的實現類是 DefalutFullHttpRequest。

所以咱們能夠修改自定義的 ChannelHandler 以下:

複製代碼
public class HttpServerChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> { protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { ctx.channel().remoteAddress(); FullHttpRequest request = msg; System.out.println("請求方法名稱:" + request.method().name()); System.out.println("uri:" + request.uri()); ByteBuf buf = request.content(); System.out.print(buf.toString(CharsetUtil.UTF_8)); ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf); response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain"); response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes()); ctx.writeAndFlush(response); } }
複製代碼

 

這樣修改就能夠了嗎,若是你去啓動程序運行看看,是會拋異常的。前面說過 Netty 是一個很底層的框架,對於將請求合併爲一個 FullRequest 是須要代碼實現的,然而這裏咱們並不須要咱們本身動手去實現,Netty 爲咱們提供了一個 HttpObjectAggregator 類,這個 ChannelHandler做用就是將請求轉換爲單一的 FullHttpReques。

因此在咱們的 ChannelPipeline 中添加一個 HttpObjectAggregator 的實例便可。

複製代碼
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> { protected void initChannel(SocketChannel sc) { ChannelPipeline pipeline = sc.pipeline(); //處理http消息的編解碼 pipeline.addLast("httpServerCodec", new HttpServerCodec()); pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); //添加自定義的ChannelHandler pipeline.addLast("httpServerHandler", new HttpServerChannelHandler0()); } }
複製代碼

 

啓動程序運行,一切都順暢了,好了,這個簡單 Http 的例子就 OK 了。

在這裏我向你們推薦一個架構學習交流羣。交流學習羣號:747981058 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。

3、編寫 Netty 客戶端

上面的兩個示例中咱們都是以 Netty 作爲服務端,接下來看看如何編寫 Netty 客戶端,以第一個 Http 服務的例子爲基礎,編寫一個訪問 Http 服務的客戶端。

複製代碼
public class HttpClient { public static void main(String[] args) throws Exception { String host = "127.0.0.1"; int port = 8080; EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpClientCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new HttpClientHandler()); } }); // 啓動客戶端. ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } }
複製代碼

 

客戶端啓動類編寫基本和服務端相似,在客戶端咱們只用到了一個線程池,服務端使用了兩個,由於服務端要處理 n 條鏈接,而客戶端相對來講只處理一條,所以一個線程池足以。

而後服務端啓動輔助類使用的是 ServerBootstrap,而客戶端換成了 Bootstrap。經過 Bootstrap 組織一些必要的組件,爲了方便,在 handler 方法中咱們使用匿名內部類的方式來構建 ChannelPipeline 鏈容器。最後經過 connect 方法鏈接服務端。

接着編寫 HttpClientHandler 類。

複製代碼
public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { URI uri = new URI("http://127.0.0.1:8080"); String msg = "Are you ok?"; FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toASCIIString(), Unpooled.wrappedBuffer(msg.getBytes("UTF-8"))); // 構建http請求 // request.headers().set(HttpHeaderNames.HOST, "127.0.0.1"); // request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);  request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes()); // 發送http請求  ctx.channel().writeAndFlush(request); } @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { FullHttpResponse response = msg; response.headers().get(HttpHeaderNames.CONTENT_TYPE); ByteBuf buf = response.content(); System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8)); } }
複製代碼

 

在 HttpClientHandler 類中,咱們覆寫了 channelActive 方法,當鏈接創建時,此方法會被調用,咱們在方法中構建了一個 FullHttpRequest 對象,而且經過 writeAndFlush 方法將請求發送出去。

channelRead0 方法用於處理服務端返回給咱們的響應,打印服務端返回給客戶端的信息。至此,Netty 客戶端的編寫就完成了,咱們先開啓服務端,而後開啓客戶端就能夠看到效果了。

但願經過前面介紹的幾個例子能讓你們基本知道如何編寫 Netty 客戶端和服務端,下面咱們來講說 Netty 程序爲何是這樣編寫的,這也是 Netty 中最爲重要的一部分知識,可讓你在編寫 netty 程序時作到心中有數。

在這裏我向你們推薦一個架構學習交流羣。交流學習羣號:747981058 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。

4、Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContext 之間的關係

在編寫 Netty 程序時,常常跟咱們打交道的是上面這幾個對象,這也是 Netty 中幾個重要的對象,下面咱們來看看它們之間有什麼樣的關係。

Netty 中的 Channel 是框架本身定義的一個通道接口,Netty 實現的客戶端 NIO 套接字通道是 NioSocketChannel,提供的服務器端 NIO 套接字通道是 NioServerSocketChannel。

當服務端和客戶端創建一個新的鏈接時, 一個新的 Channel 將被建立,同時它會被自動地分配到它專屬的 ChannelPipeline。

ChannelPipeline 是一個攔截流經 Channel 的入站和出站事件的 ChannelHandler 實例鏈,並定義了用於在該鏈上傳播入站和出站事件流的 API。那麼就很容易看出這些 ChannelHandler 之間的交互是組成一個應用程序數據和事件處理邏輯的核心。

 

上圖描述了 IO 事件如何被一個 ChannelPipeline 的 ChannelHandler 處理的。

ChannelHandler分爲 ChannelInBoundHandler 和 ChannelOutboundHandler 兩種,若是一個入站 IO 事件被觸發,這個事件會從第一個開始依次經過 ChannelPipeline中的 ChannelInBoundHandler,先添加的先執行。

如果一個出站 I/O 事件,則會從最後一個開始依次經過 ChannelPipeline 中的 ChannelOutboundHandler,後添加的先執行,而後經過調用在 ChannelHandlerContext 中定義的事件傳播方法傳遞給最近的 ChannelHandler。

在 ChannelPipeline 傳播事件時,它會測試 ChannelPipeline 中的下一個 ChannelHandler 的類型是否和事件的運動方向相匹配。

若是某個ChannelHandler不能處理則會跳過,並將事件傳遞到下一個ChannelHandler,直到它找到和該事件所指望的方向相匹配的爲止。

假設咱們建立下面這樣一個 pipeline:

複製代碼
ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA()); p.addLast("2", new InboundHandlerB()); p.addLast("3", new OutboundHandlerA()); p.addLast("4", new OutboundHandlerB()); p.addLast("5", new InboundOutboundHandlerX()); 
複製代碼

 

在上面示例代碼中,inbound 開頭的 handler 意味着它是一個ChannelInBoundHandler。outbound 開頭的 handler 意味着它是一個 ChannelOutboundHandler。

當一個事件進入 inbound 時 handler 的順序是 1,2,3,4,5;當一個事件進入 outbound 時,handler 的順序是 5,4,3,2,1。在這個最高準則下,ChannelPipeline 跳過特定 ChannelHandler 的處理:

  • 3,4 沒有實現 ChannelInboundHandler,於是一個 inbound 事件的處理順序是 1,2,5。
  • 1,2 沒有實現 ChannelOutBoundhandler,於是一個 outbound 事件的處理順序是 5,4,3。
  • 5 同時實現了 ChannelInboundHandler 和 channelOutBoundHandler,因此它同時能夠處理 inbound 和 outbound 事件。

ChannelHandler 能夠經過添加、刪除或者替換其餘的 ChannelHandler 來實時地修改 ChannelPipeline 的佈局。

(它也能夠將它本身從 ChannelPipeline 中移除。)這是 ChannelHandler 最重要的能力之一。

ChannelHandlerContext 表明了 ChannelHandler 和 ChannelPipeline 之間的關聯,每當有 ChannelHandler 添加到 ChannelPipeline 中時,都會建立 ChannelHandlerContext。

ChannelHandlerContext 的主要功能是管理它所關聯的 ChannelHandler 和在同一個 ChannelPipeline 中的其餘 ChannelHandler 之間的交互。事件從一個 ChannelHandler 到下一個 ChannelHandler 的移動是由 ChannelHandlerContext 上的調用完成的。

 

可是有些時候不但願老是從 ChannelPipeline 的第一個 ChannelHandler 開始事件,咱們但願從一個特定的 ChannelHandler 開始處理。

你必須引用於此 ChannelHandler 的前一個 ChannelHandler 關聯的 ChannelHandlerContext,利用它調用與自身關聯的 ChannelHandler 的下一個 ChannelHandler。

以下:

ChannelHandlerContext ctx = ...;   // 得到 ChannelHandlerContext引用 // write()將會把緩衝區發送到下一個ChannelHandler ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8)); //流經整個pipeline ctx.channel().write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

 

若是咱們想有一些事件流所有經過 ChannelPipeline,有兩個不一樣的方法能夠作到:

  • 調用 Channel 的方法
  • 調用 ChannelPipeline 的方法
    這兩個方法均可以讓事件流所有經過 ChannelPipeline,不管從頭部仍是尾部開始,由於它主要依賴於事件的性質。若是是一個 「 入站 」 事件,它開始於頭部;如果一個 「 出站 」 事件,則開始於尾部。

那爲何你可能會須要在 ChannelPipeline 某個特定的位置開始傳遞事件呢?

  • 減小由於讓事件穿過那些對它不感興趣的 ChannelHandler 而帶來的開銷
  • 避免事件被那些可能對它感興趣的 ChannlHandler 處理

5、Netty 線程模型

在這裏我向你們推薦一個架構學習交流羣。交流學習羣號:747981058 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。

在前面的示例中咱們程序一開始都會生成兩個 NioEventLoopGroup 的實例,爲何須要這兩個實例呢?這兩個實例能夠說是 Netty 程序的源頭,其背後是由 Netty 線程模型決定的。

Netty 線程模型是典型的 Reactor 模型結構,其中經常使用的 Reactor 線程模型有三種,分別爲:Reactor 單線程模型、Reactor 多線程模型和主從 Reactor 多線程模型。

而在 Netty 的線程模型並不是固定不變,經過在啓動輔助類中建立不一樣的 EventLoopGroup 實例並經過適當的參數配置,就能夠支持上述三種 Reactor 線程模型。

Reactor 線程模型

Reactor 單線程模型

Reactor 單線程模型指的是全部的 IO 操做都在同一個 NIO 線程上面完成。做爲 NIO 服務端接收客戶端的 TCP 鏈接,做爲 NIO 客戶端向服務端發起 TCP 鏈接,讀取通訊對端的請求或向通訊對端發送消息請求或者應答消息。

因爲 Reactor 模式使用的是異步非阻塞 IO,全部的 IO 操做都不會致使阻塞,理論上一個線程能夠獨立處理全部 IO 相關的操做。

 

Netty 使用單線程模型的的方式以下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup) .channel(NioServerSocketChannel.class) ...

 

在實例化 NioEventLoopGroup 時,構造器參數是 1,表示 NioEventLoopGroup 的線程池大小是 1。而後接着咱們調用 b.group(bossGroup) 設置了服務器端的 EventLoopGroup,所以 bossGroup和 workerGroup 就是同一個 NioEventLoopGroup 了。

Reactor 多線程模型

對於一些小容量應用場景,可使用單線程模型,可是對於高負載、大併發的應用卻不合適,須要對該模型進行改進,演進爲 Reactor 多線程模型。

Rector 多線程模型與單線程模型最大的區別就是有一組 NIO 線程處理 IO 操做。

在該模型中有專門一個 NIO 線程 -Acceptor 線程用於監聽服務端,接收客戶端的 TCP 鏈接請求;而 1 個 NIO 線程能夠同時處理N條鏈路,可是 1 個鏈路只對應 1 個 NIO 線程,防止發生併發操做問題。

網絡 IO 操做-讀、寫等由一個 NIO 線程池負責,線程池能夠採用標準的 JDK 線程池實現,它包含一個任務隊列和 N 個可用的線程,由這些 NIO 線程負責消息的讀取、解碼、編碼和發送。

 

 

Netty 中實現多線程模型的方式以下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) ...

 

bossGroup 中只有一個線程,而 workerGroup 中的線程是 CPU 核心數乘以 2,那麼就對應 Recator 的多線程模型。

主從 Reactor 多線程模型

在併發極高的狀況單獨一個 Acceptor 線程可能會存在性能不足問題,爲了解決性能問題,產生主從 Reactor 多線程模型。

主從 Reactor 線程模型的特色是:服務端用於接收客戶端鏈接的再也不是 1 個單獨的 NIO 線程,而是一個獨立的 NIO 線程池。

Acceptor 接收到客戶端 TCP 鏈接請求處理完成後,將新建立的 SocketChannel 註冊到 IO 線程池(sub reactor 線程池)的某個 IO 線程上,由它負責 SocketChannel 的讀寫和編解碼工做。

Acceptor 線程池僅僅只用於客戶端的登錄、握手和安全認證,一旦鏈路創建成功,就將鏈路註冊到後端 subReactor 線程池的 IO 線程上,由 IO 線程負責後續的 IO 操做。

 

根據前面所講的兩個線程模型,很容想到 Netty 實現多線程的方式以下:

EventLoopGroup bossGroup = new NioEventLoopGroup(4); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) ...

 

可是,在 Netty 的服務器端的 acceptor 階段,沒有使用到多線程, 所以上面的主從多線程模型在 Netty 的實現是有誤的。

服務器端的 ServerSocketChannel 只綁定到了 bossGroup 中的一個線程,所以在調用 Java NIO 的 Selector.select 處理客戶端的鏈接請求時,其實是在一個線程中的,因此對只有一個服務的應用來講,bossGroup 設置多個線程是沒有什麼做用的,反而還會形成資源浪費。

至於 Netty 中的 bossGroup 爲何使用線程池,我在 stackoverflow 找到一個對於此問題的討論 。

the creator of Netty says multiple boss threads are useful if we share NioEventLoopGroup between different server bootstraps

EventLoopGroup 和 EventLoop

當系統在運行過程當中,若是頻繁的進行線程上下文切換,會帶來額外的性能損耗。多線程併發執行某個業務流程,業務開發者還須要時刻對線程安全保持警戒,哪些數據可能會被併發修改,如何保護?這不只下降了開發效率,也會帶來額外的性能損耗。

爲了解決上述問題,Netty採用了串行化設計理念,從消息的讀取、編碼以及後續 ChannelHandler 的執行,始終都由 IO 線程 EventLoop 負責,這就意外着整個流程不會進行線程上下文的切換,數據也不會面臨被併發修改的風險。

EventLoopGroup 是一組 EventLoop 的抽象,一個 EventLoopGroup 當中會包含一個或多個 EventLoop,EventLoopGroup 提供 next 接口,能夠從一組 EventLoop 裏面按照必定規則獲取其中一個 EventLoop 來處理任務。

在 Netty 服務器端編程中咱們須要 BossEventLoopGroup 和 WorkerEventLoopGroup 兩個 EventLoopGroup 來進行工做。

BossEventLoopGroup 一般是一個單線程的 EventLoop,EventLoop 維護着一個註冊了 ServerSocketChannel 的 Selector 實例,EventLoop 的實現涵蓋 IO 事件的分離,和分發(Dispatcher),EventLoop 的實現充當 Reactor 模式中的分發(Dispatcher)的角色。

因此一般能夠將 BossEventLoopGroup 的線程數參數爲 1。

BossEventLoop 只負責處理鏈接,故開銷很是小,鏈接到來,立刻按照策略將 SocketChannel 轉發給 WorkerEventLoopGroup,WorkerEventLoopGroup 會由 next 選擇其中一個 EventLoop 來將這 個SocketChannel 註冊到其維護的 Selector 並對其後續的 IO 事件進行處理。

ChannelPipeline 中的每個 ChannelHandler 都是經過它的 EventLoop(I/O 線程)來處理傳遞給它的事件的。因此相當重要的是不要阻塞這個線程,由於這會對總體的 I/O 處理產生嚴重的負面影響。但有時可能須要與那些使用阻塞 API 的遺留代碼進行交互。

對於這種狀況, ChannelPipeline 有一些接受一個 EventExecutorGroup 的 add() 方法。若是一個事件被傳遞給一個自定義的 EventExecutorGroup, DefaultEventExecutorGroup 的默認實現。

就是在把 ChannelHanders 添加到 ChannelPipeline 的時候,指定一個 EventExecutorGroup,ChannelHandler 中全部的方法都將會在這個指定的 EventExecutorGroup 中運行。

static final EventExecutor group = new DefaultEventExecutorGroup(16); ... ChannelPipeline p = ch.pipeline(); pipeline.addLast(group, "handler", new MyChannelHandler()); 

 

最後小結一下:(若是你還沒明白,能夠看一下羣裏面的視頻解析)
  • NioEventLoopGroup 實際上就是個線程池,一個 EventLoopGroup 包含一個或者多個 EventLoop;
  • 一個 EventLoop 在它的生命週期內只和一個 Thread 綁定;
  • 全部有 EnventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理;
  • 一個 Channel 在它的生命週期內只註冊於一個 EventLoop;
  • 每個 EventLoop 負責處理一個或多個 Channel;
相關文章
相關標籤/搜索