1. 程式人生 > >Netty 入門(一):基本元件與執行緒模型

Netty 入門(一):基本元件與執行緒模型

  Netty 的學習內容主要是圍繞 TCP 和 Java NIO 這兩個點展開的,後文中所有的內容如果沒有特殊說明,那麼所指的內容都是與這兩點相關的。由於 Netty 是基於 Java NIO 的 API 之上構建的網路通訊框架,Java NIO 中的幾個元件,都能在 Netty 中找到對應的封裝。下面我們就來一一熟悉 Netty 中的基本元件。

一、基本元件

Netty 的元件主要有以下 8 個:

  1. Channel
  2. ByteBuf
  3. ChannelHandler
  4. ChannelHandlerContext
  5. Pipeline
  6. EventLoop
  7. EventLoopGroup
  8. ServerBootstrap/Bootstrap

1.1 Channel

  Netty 中的 Channel 封裝了 JDK 中原生的 Channel,所有對 Netty 中 Channel 的操作,最後都會轉化成對原生 Channel 的操作。那麼為什麼要封裝呢,主要有兩點:1. 原生的 Channel 與 Netty 框架的結構不夠相容,所有 Netty 進行了一層包裝,使其更符合 Netty 使用邏輯;2. 避免了對 SocketChannel 的直接操作,提供更直觀和友好的 API 給開發人員。

  Netty 常用的是 NioSocketChannel 和 NioServerSocketChannel,對應了 JDK 中的 SocketChannel 和 ServerSocketChannel。Netty 中的 Channel 都有與之對應的 EventLoop 和 Pipeline,後面也會介紹這兩個元件。 

1.2 ByteBuf

  ByteBuf 與 JDK 中的 ByteBuffer 類似。Netty 中的 ByteBuf 有基於 ByteBuffer 構建的,也有自身設計的其他實現。從不同的層級可以有多種劃分方式,使用時主要關注的可能這 3 個方面:1. 池化與非池化;2. 堆記憶體與直接記憶體;3. 是否使用了 Unsafe 類來操作記憶體。關於這幾點暫時有個大致的瞭解即可。

  ByteBuffer 只使用一個指標來儲存讀寫的索引,使用起來比較麻煩,容易出錯。而 ByteBuf 則使用了兩個指標分別儲存當前讀和寫的索引,使用起來就很方便,從 ByteBuf 讀取資料時,只需要關注 readerIndex 即可。將資料寫入 ByteBuf 時,只需要關注 writerIndex。ByteBuff 的容量 capacity 與兩個指標之間的大小關係:0 <= readerIndex <= writerIndex <= capacity。

  Netty 原始碼中 ByteBuf 類註釋中很好的展示了它的結構:

1 /*
2  *      +-------------------+------------------+------------------+
3  *      | discardable bytes |  readable bytes  |  writable bytes  |
4  *      |                   |     (CONTENT)    |                  |
5  *      +-------------------+------------------+------------------+
6  *      |                   |                  |                  |
7  *      0      <=      readerIndex   <=   writerIndex    <=    capacity
8  */

 

1.3  ChannelHandler

  ChannelHandler 簡言之就是一個處理器, 它的功能就是處理訊息。它就像流水線上的工人,對每一個從他面前經過的部件進行加工。與工人稍有不同的地方是,它可以什麼也不做,將訊息直接交給下一個處理器,也可以直接將訊息丟掉,不再傳遞。而且 ChannelHandler 是有方向的,這一點我們後面再詳細學習。對於開發人員來說,就是在 ChannelHandler 中編寫業務邏輯的操作程式碼。

1.4 ChannelHandlerContext

  從名稱即可知道它是 ChannelHandler 的容器,每個 ChannelHandler 都有與之一一對應的 ChannelHandlerContext 對應。實際上,每個 ChannelHandler 並不直接互動,都是通過 ChannelHandlerContext 將彼此聯絡起來。ChannelHandlerContext 則是一個 Node,它有前驅和後繼。對於一個 Channel 來說,它看到的是一個雙向連結串列。

1.5 Pipeline

  Pipeline 中儲存了由 ChannelHandlerContext 組成的雙向連結串列。Netty 中的每個 Channel 都有一條自己的 Pipeline,每當該通道有需要處理的訊息時,就會遍歷 Pipeline 中的連結串列,通過每一個處理器來處理訊息。Pipeline 的連結串列中預設就儲存了一個 Head  和一個 Tail,所有使用者新增的處理器都在這兩個節點之間。下圖就是包含一個使用者處理器的 Pipeline:

1.6 EventLoop

  EventLoop 可以簡單的看作是一個執行緒,用來處理分配給它的 Channel 上的事件,也就是說一個 EventLoop 下面可能掛了多個 Channel。

1.7 EventLoopGroup

   從它的名稱就可以知道它維護了一組 EventLoop,可以看作是一個 Netty 實現的執行緒池,負責給每一個新建裡的 Channel 分配執行緒。

學習 Netty 的過程:對基本元件先有一個大致的瞭解,然後逐步熟悉各個元件的細節。

1.8 ServerBootstrap/Bootstrap

  這兩個元件分別是用來啟動服務端和客戶端 ,在啟動之前,可以通過這兩個元件設定各種引數,新增 Handler,指定通道型別等。(Bootstrap 是鞋帶的意思,為啥跟啟動掛上勾了,可以參考知乎上的解答:Boot一詞是為什麼被用作計算機並作為引導解釋的?或者說他的由來? - 知乎

二、Netty 執行緒模型

2.1 圖解

  Netty 採用的是 Reactor 執行緒模型,先從一個最簡單的 HelloWorldServer 級別的執行緒模型來入手,如下圖:

  先了解一個 Channel 的建立過程:

  1. 服務端啟動時,會啟用一個執行緒並建立一個 NioServerSocketChannel 來監聽指定的埠。這個執行緒上有一個 Selector,它關注的是 Accpet 事件;
  2. 當有客戶端連線過來時,上圖中的 EventLoop-0會建立一個 NioSocketChannel ,將該通道註冊到 EventLoop-1 的 Selector 上,然後 EventLoop-1 就負責此後該 Channel 生命週期上所有的讀寫事件的處理;
  3. EventLoop-1 對其所屬通道資料讀寫及其他處理,就通過 Pipeline 中的處理器鏈來實現。

  Netty 還包含了普通任務和定時任務的執行,暫且不管。

2.2 演示程式碼

  下面就貼一下簡單的服務端演示程式碼: 

 1 package netty;
 2 
 3 import io.netty.bootstrap.ServerBootstrap;
 4 import io.netty.channel.ChannelFuture;
 5 import io.netty.channel.ChannelInitializer;
 6 import io.netty.channel.ChannelOption;
 7 import io.netty.channel.EventLoopGroup;
 8 import io.netty.channel.nio.NioEventLoopGroup;
 9 import io.netty.channel.socket.SocketChannel;
10 import io.netty.channel.socket.nio.NioServerSocketChannel;
11 
12 import java.net.InetSocketAddress;
13 
14 /**
15  * Protocol:
16  * Desc:
17  *
18  * @author xi
19  * @date 2018/7/24 20:25
20  */
21 public class HelloWorldServer {
22 
23     private int port;
24 
25     public HelloWorldServer(int port) {
26         this.port = port;
27     }
28 
29     public void start() {
30         EventLoopGroup bossGroup = new NioEventLoopGroup(1);
31         EventLoopGroup workerGroup = new NioEventLoopGroup();
32         try {
33             ServerBootstrap sbs = new ServerBootstrap().group(bossGroup, workerGroup)//設定兩個 Group
34                     .channel(NioServerSocketChannel.class)//指定使用的通道型別
35                     .localAddress(new InetSocketAddress(port))
36                     .handler(new HelloWorldServerHandler())//新增 NioServerSocket 的處理器
37                     .childHandler(new ChannelInitializer<NioSocketChannel>() {//新增 NioSocketChannel 的處理器
38                         @Override
39                         protected void initChannel(NioSocketChannel ch) throws Exception {
40                             ch.pipeline().addLast(new InBoundHandlerA());                          
41                         }
42                     }).option(ChannelOption.SO_BACKLOG, 128)
43                     .childOption(ChannelOption.SO_KEEPALIVE, true);
44 
45             ChannelFuture future = sbs.bind(port).sync();//監聽指定埠,同步操作,阻塞到建立監聽成功
46             System.out.println("Server start listen at " + port);
47             future.channel().closeFuture().sync();//等待上一步建立監聽的通道關閉
48         } catch (Exception e) {
49             bossGroup.shutdownGracefully();
50             workerGroup.shutdownGracefully();
51         }
52     }
53 
54     public static void main(String[] args) throws Exception {
55         int port = 8080;
56         new HelloWorldServer(port).start();
57     }
58 }

  演示程式碼很簡單,唯二沒有貼出來的地方就是兩個處理器的程式碼,暫且不管,對於下一篇熟悉服務端啟動的原始碼沒有影響。

 

  參考資料:

  1. 『Netty 實戰』- 中文版
  2. 『Netty 權威指南』- 第二版,這本書是基於 Netty 5 寫的,雖然 Netty 5 專案已經關閉了,但是本書還是值得參考的
  3. Java讀原始碼之Netty深入剖析-慕課網實戰 - 基本上對原始碼的分析和了解,都是基於這個視訊課程的內容,講的挺好的