一文詳解 Netty 元件

京東雲開發者發表於2023-03-02

作者:京東物流 張弓言

一、背景

Netty 是一款優秀的高效能網路框架,內部透過 NIO 的方式來處理網路請求,在高負載下也能可靠和高效地處理 I/O 操作

作為較底層的網路通訊框架,其被廣泛應用在各種中介軟體的開發中,比如 RPC框架、MQ、Elasticsearch等,這些中介軟體框架的底層網路通訊模組大都利用到了 Netty 強大的網路抽象

下面這篇文章將主要對 Netty 中的各個元件進行分析,並在介紹完了各個元件之後,透過 JSF 這個 RPC 框架為例來分析 Netty 的使用,希望讓大家對 Netty 能有一個清晰的瞭解

二、Netty Server

透過 Netty 來構建一個簡易服務端是比較簡單的,程式碼如下:

public class NettyServer {
    public static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class);

    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ChannelFuture channelFuture = serverBootstrap
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelHandlerAdapter() {
                    @Override
                    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
                        LOGGER.info("Handler Added");
                    }
                })
                .childHandler(new ServerChannelInitializer())
                .bind(8100);

        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    LOGGER.info("Netty Server Start !");
                }
            }
        });

        try {
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

上面程式碼的主要邏輯如下:

  1. 新建服務端引導啟動類 ServerBootstrap,內部封裝了各個元件,用來進行服務端的啟動
  2. 新建了兩個 EventLoopGroup 用來進行連線處理,此時可以簡單的將 EventLoopGroup 理解為多個執行緒的集合。bossGroup 中的執行緒用來處理新連線的建立,當新連線建立後,workerGroup 中的每個執行緒則都會和唯一的客戶端 Channel 連線進行繫結,用來處理該 Channel 上的讀、寫事件
  3. 指定服務端建立的 Channel 型別為 NioServerSocketChannel
  4. childOption 用來配置客戶端連線的 NioSocketChannel 底層網路引數
  5. handler 用來指定針對服務端 Channel 的處理器,內部定義了一系列的回撥方法,會在服務端 Channel 發生指定事件時進行回撥
  6. childHandler 用來指定客戶端 Channel 的處理器,當客戶端 Channel 中發生指定事件時,會進行回撥
  7. bind 指定服務端監聽埠號

三、Netty Client

public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            // 1. 啟動類
            ChannelFuture channelFuture = new Bootstrap()
                    // 2. 新增 EventLoop
                    .group(workGroup)
                    // 3. 選擇客戶端 channel 實現
                    .channel(NioSocketChannel.class)
                    // 4. 新增處理器
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override // 在連線建立後被呼叫
                        protected void initChannel(NioSocketChannel ch) throws Exception {
     ZAS                       ch.pipeline().addLast(new LoggingHandler());
                            ch.pipeline().addLast(new StringEncoder());
                        }
                    })
                    // 5. 連線到伺服器
                    .connect(new InetSocketAddress("localhost", 8100));
            channelFuture.addListener(future -> {
                if (future.isSuccess()) {
                    ((ChannelFuture) future).channel().writeAndFlush("hello");
                }
            });
            channelFuture.channel().closeFuture().sync();
        } finally {
            workGroup.shutdownGracefully();
        }
    }
}

上面程式碼的主要邏輯如下:

  1. 新建 Bootstrap 用來進行客戶端啟動
  2. group() 指定一個 NioEventLoopGroup 例項,用來處理客戶端連線的建立和後續事件處理
  3. handler() 指定 Channel 處理器,
  4. 當將客戶端啟動類中的各個屬性都設定完畢後,呼叫 connect() 方法進行服務端連線

從上面的的兩個例子可以看出,如果想透過 Netty 實現一個簡易的伺服器其實是非常簡單的,只需要在啟動引導類中設定好對應屬性,然後完成埠繫結就可以實現。但也正是因為這種簡易的實現方式,導致很多人在學習 Netty 的過程中,發現程式碼是寫的出來,但是對內部的元件有什麼作用以及為什麼這麼寫可能就不是很清楚了,因此希望透過這一系列文章來加深大家對 Netty 的理解

四、Netty 基本元件

Channel

Netty 中的 Channel 可以看成網路程式設計中的 Socket,其提供了一系列 IO 操作的 API,比如 read、write、bind、connect 等,大大降低了直接使用 Socket 類的複雜性

整體類繼承關係如下:

從上面的繼承關係可以看出,NioSocketChannel 和 NioServerSocketChannel 分別對應客戶端和服務端的 Channel,兩者的直接父類不一致,因此對外提供的功能也是不相同的。比如當發生 read 事件時,NioServerSocketChannel 的主要邏輯就是建立新的連線,而 NioSocketChannel 則是讀取傳輸的位元組進行業務處理

下面就以 NioServerSocketChannel 為例,帶大家瞭解下該類的初始化過程,整體流程如下:

  1. 啟動引導類中透過 channel() 指定底層建立的 Channel 型別
  2. 根據指定的 Channel 型別建立出 ChannelFactory,後續透過該工廠類進行 Channel 的例項化
  3. 例項化 Channel

channel() 指定 ChannelFactory 型別

在上面的服務端啟動過程中,ServerBootstrap 呼叫 channel() 方法並傳入 NioServerSocketChannel,其底層程式碼邏輯為:

public B channel(Class<? extends C> channelClass) {
        return channelFactory(new ReflectiveChannelFactory<C>(
                ObjectUtil.checkNotNull(channelClass, "channelClass")
        ));
}

// ReflectiveChannelFactory 構造方法
public ReflectiveChannelFactory(Class<? extends T> clazz) {
  ObjectUtil.checkNotNull(clazz, "clazz");
  try {
    this.constructor = clazz.getConstructor();
  } catch (NoSuchMethodException e) {
    throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +
                                       " does not have a public non-arg constructor", e);
  }
}

整體邏輯很簡單,透過傳入的 Class 物件指定一個 Channel 反射工廠,後續呼叫工廠方法獲取指定型別的 Channel 物件

channel 例項化

當服務端啟動引導類 ServerBootstrap 呼叫 bind() 方法之後,內部會走到 Channel 的例項化過程,程式碼精簡如下:

// channel 初始化流程,內部透過 channelFactory 構造
final ChannelFuture initAndRegister() {
    channel = channelFactory.newChannel();
}

// channelFactory 的 newChannel 方法邏輯
public T newChannel() {
  try {
    return constructor.newInstance();
  } catch (Throwable t) {
    throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
  }
}

ChannelFactory 的整體邏輯就是透過反射的方式新建 Channel 物件,而 Channel 物件的型別則是在啟動引導類中透過 channel() 方法進行指定的

在例項化 Channel 的過程中,會對其內部的一些屬性進行初始化,而對這些屬性的瞭解,可以使我們對 Netty 中各個元件的作用範圍有一個更加清晰的理解,下面看下 NioServerSocketChannel 的建構函式原始碼

public NioServerSocketChannel() {
  this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

public NioServerSocketChannel(ServerSocketChannel channel) {
  super(null, channel, SelectionKey.OP_ACCEPT);
  config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
  super(parent, ch, readInterestOp);
}

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
  super(parent);
  this.ch = ch;
  this.readInterestOp = readInterestOp;
  try {
    ch.configureBlocking(false);
  } catch (IOException e) {
    try {
      ch.close();
    } catch (IOException e2) {
      if (logger.isWarnEnabled()) {
        logger.warn(
          "Failed to close a partially initialized socket.", e2);
      }
    }

    throw new ChannelException("Failed to enter non-blocking mode.", e);
  }
}

protected AbstractChannel(Channel parent) {
  this.parent = parent;
  id = newId();
  unsafe = newUnsafe();
  pipeline = newChannelPipeline();
}
上述原始碼就是一層一層的父類構造,可以對照前面的類關係圖進行閱讀

NioServerSocketChannel 例項化過程中主要完成了以下內部屬性的初始化:

  1. unsafe 屬性進行賦值為 NioMessageUnsafe,後續 Channel 上事件處理的主要邏輯都是由該類完成
  2. pipeline 屬性進行初始化賦值,pipeline 是 Channel 中特別重要的一個屬性,後續的所有業務處理器都是透過該 pipeline 組織的
  3. 指定當前 Channel 的 readInterestOp 屬性為 SelectionKey.OP_ACCEPT,用於後續繫結到 Selector 時指定當前 Channel 監聽的事件型別
  4. 指定當前 Channel 非阻塞,ch.configureBlocking(false)

總結

對於 Channel 的例項化流程可以總結如下:

  1. 啟動引導類中透過 channel() 方法指定生成的 ChannelFactory 型別
  2. 透過 ChannelFactory 來構造對應 Channel,並在例項化的過程中初始化了一些重要屬性,比如 pipeline

ChannelPipeline

ChannelPipeline 也是 Netty 中的一個比較重要的元件,從上面的 Channel 例項化過程可以看出,每一個 Channel 例項中都會包含一個對應的 ChannelPipeline 屬性

ChannelPipeline 初始化

ChannelPipeline 底層初始化原始碼:

protected DefaultChannelPipeline(Channel channel) {
  this.channel = ObjectUtil.checkNotNull(channel, "channel");
  succeededFuture = new SucceededChannelFuture(channel, null);
  voidPromise =  new VoidChannelPromise(channel, true);

  tail = new TailContext(this);
  head = new HeadContext(this);

  head.next = tail;
  tail.prev = head;
}

從 ChannelPipeline 的建構函式可以看出,每一個 ChannelPipeline 底層都是一個雙向連結串列結構,預設會包含 head 和 tail 頭尾節點,用來進行一些預設的邏輯處理,處理細節會在後續文章中展現

addLast()
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
  final AbstractChannelHandlerContext newCtx;
  synchronized (this) {
    checkMultiplicity(handler);

    newCtx = newContext(group, filterName(name, handler), handler);

    addLast0(newCtx);

    // If the registered is false it means that the channel was not registered on an eventLoop yet.
    // In this case we add the context to the pipeline and add a task that will call
    // ChannelHandler.handlerAdded(...) once the channel is registered.
    if (!registered) {
      newCtx.setAddPending();
      callHandlerCallbackLater(newCtx, true);
      return this;
    }

    EventExecutor executor = newCtx.executor();
    if (!executor.inEventLoop()) {
      callHandlerAddedInEventLoop(newCtx, executor);
      return this;
    }
  }
  // 回撥 ChannelHandler 中的 handlerAdded() 方法
  callHandlerAdded0(newCtx);
  return this;
}

private void addLast0(AbstractChannelHandlerContext newCtx) {
  AbstractChannelHandlerContext prev = tail.prev;
  newCtx.prev = prev;
  newCtx.next = tail;
  prev.next = newCtx;
  tail.prev = newCtx;
}

addLast() 方法是向 ChannelPipeline 中新增 ChannelHandler 用來進行業務處理

整個方法的邏輯為:

  • 判斷當前 ChannelHandler 是否已經新增
  • 將當前 ChannelHandler 包裝成 ChannelHandlerContext,並將其新增到 ChannelPipeline 的雙向連結串列中
  • 回撥新增的 ChannelHandler 中的 handlerAdded() 方法
Channel、ChannelPipeline、ChannelHandler 關係

Channel、ChannelPipeline和 ChannelHandler 三者的關係如圖所示:

  • 每一個 Channel 中都會包含一個 ChannelPipeline 屬性
  • ChannelPipeline 是一個雙向連結串列結構,預設會包含 HeadContext 和 TailContext 兩個節點
  • 當向 ChannelPipeline 中新增 ChannelHandler 時,會包裝成 ChannelContext 插入到 ChannelPipeline 連結串列中
  • 當 Channel 中發生指定事件時,該事件就會在 ChannelPipeline 中沿著雙向連結串列進行傳播,呼叫各個 ChannelHandler 中的指定方法,完成相應的業務處理

Netty 正是透過 ChannelPipeline 這一結構為使用者提供了自定義業務邏輯的擴充套件點,使用者只需要向 ChannelPipeline 中新增處理對應業務邏輯的 ChannelHandler,之後當指定事件發生時,該 ChannelHandler 中的對應方法就會進行回撥,實現業務的處理

ChannelHandler

ChannelHandler 是 Netty 中業務處理的核心類,當有 IO 事件發生時,該事件會在 ChannelPipeline 中進行傳播,並依次呼叫到 ChannelHandler 中的指定方法

ChannelHandler 的類繼承關係如下:

從上面的類繼承關係可以看出,ChannelHandler 大致可以分為 ChannelInboundHandler 和 ChannelOutboundHandler,分別用來處理讀、寫事件

ChannInboundHandler

public interface ChannelInboundHandler extends ChannelHandler {

    void channelRegistered(ChannelHandlerContext ctx) throws Exception;

    void channelUnregistered(ChannelHandlerContext ctx) throws Exception;

    void channelActive(ChannelHandlerContext ctx) throws Exception;

    void channelInactive(ChannelHandlerContext ctx) throws Exception;

    void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;

    void channelReadComplete(ChannelHandlerContext ctx) throws Exception;

    void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;

    void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;

    @Override
    @SuppressWarnings("deprecation")
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

在 ChannelInboundHandler 中定義了一系列的回撥方法,使用者可以實現該介面並重寫相應的方法來自定義的業務邏輯。

重寫方法邏輯是簡單的,但很多人其實不清楚的是這些回撥方法到底在什麼場景下會被呼叫,如何呼叫,只有瞭解了這些回撥方法的呼叫時機,才能在更適宜的地方完成相應功能

channelRegistered

channelRegistered() 從方法名理解是當 Channel 完成註冊之後會被呼叫,那麼何為 Channel 註冊?

下面就以 Netty 服務端啟動過程中的部分原始碼為例(詳細原始碼分析會在後續文章中),看下 channelRegistered() 的呼叫時機

在 Netty 服務端啟動時,會呼叫到 io.netty.channel.AbstractChannel.AbstractUnsafe#register 方法,精簡程式碼如下:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
  
  AbstractChannel.this.eventLoop = eventLoop;
  if (eventLoop.inEventLoop()) {
    register0(promise);
  } else {
    try {
      eventLoop.execute(new Runnable() {
        @Override
        public void run() {
          register0(promise);
        }
      });
    } catch (Throwable t) {
      logger.warn(
        "Force-closing a channel whose registration task was not accepted by an event loop: {}",
        AbstractChannel.this, t);
      closeForcibly();
      closeFuture.setClosed();
      safeSetFailure(promise, t);
    }
  }
}


private void register0(ChannelPromise promise) {
  try {
    // neverRegistered 初始值為 true
    boolean firstRegistration = neverRegistered;
    // 將 Channel 繫結到對應 eventLoop 中的 Selector 上
    doRegister();
    neverRegistered = false;
    registered = true;

    pipeline.invokeHandlerAddedIfNeeded();

    safeSetSuccess(promise);
    // 呼叫 ChannelHandler 中的 ChannelRegistered() 
    pipeline.fireChannelRegistered();

  }
}

protected void doRegister() throws Exception {
  boolean selected = false;
  for (;;) {
    try {
      selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
      return;
    } catch (CancelledKeyException e) {
      if (!selected) {
        eventLoop().selectNow();
        selected = true;
      } else {
        throw e;
      }
    }
  }
}

從 Netty 底層的 register() 方法可以看出,ChannelHandler 中的 ChannelRegistered() 呼叫時機是在呼叫 pipeline.fireChannelRegistered() 時觸發的,此時已經完成的邏輯為:

  • 透過傳入的 EventLoopGroup 得到了該 Channel 對應的 EventLoop,並與Channel 中的對應屬性完成了繫結;AbstractChannel.this.eventLoop = eventLoop 邏輯
  • 當前 Channel 已經繫結到了對應 EventLoop 中的 Selector 上;doRegister() 邏輯
  • ChannelHandler 中的 handlerAdded() 方法已經完成了回撥;pipeline.invokeHandlerAddedIfNeeded() 邏輯

因此當 Channel 和對應的 Selector 完成了繫結,Channel 中 pipeline 上繫結的 ChannelHandler 的channelRegisted() 方法就會進行回撥

channelActive

上面已經分析了channelRegistered() 方法的呼叫時機,也就是當 Channel 繫結到了對應 Selector 上之後就會進行回撥,下面開始分析 channelActive() 方法的呼叫時機

對於服務端 Channel,前面還只是將 Channel 註冊到了 Selector 上,還沒有呼叫到 bind() 方法完成真正的底層埠繫結,那麼有沒有可能當服務端 Channel 完成埠監聽之後,就會呼叫到 channelActive() 方法呢?

下面繼續分析,在上面完成了 Channel 和 Selector 的註冊之後,Netty 服務端啟動過程中會繼續呼叫到 io.netty.channel.AbstractChannel.AbstractUnsafe#bind 邏輯:

public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
  assertEventLoop();

  if (!promise.setUncancellable() || !ensureOpen(promise)) {
    return;
  }

  boolean wasActive = isActive();
  try {
    doBind(localAddress);
  } catch (Throwable t) {
    safeSetFailure(promise, t);
    closeIfClosed();
    return;
  }

  if (!wasActive && isActive()) {
    invokeLater(new Runnable() {
      @Override
      public void run() {
        pipeline.fireChannelActive();
      }
    });
  }

  safeSetSuccess(promise);
}


protected void doBind(SocketAddress localAddress) throws Exception {
  if (PlatformDependent.javaVersion() >= 7) {
    javaChannel().bind(localAddress, config.getBacklog());
  } else {
    javaChannel().socket().bind(localAddress, config.getBacklog());
  }
}

在該方法中完成了以下邏輯:

  • 完成了 Channel 和本地埠的繫結
  • 繫結成功後,isActive() 方法返回 true,此時釋出 ChannelActive 事件,進行方法回撥
  • safeSetSuccess() 中會回撥到服務端啟動過程中新增的 listener 方法,表明當前 Channel 完成了埠繫結

總結:

當 Channel 呼叫了 bind() 方法完成埠繫結之後,channelActive() 方法會進行回撥

channelRead

該方法的呼叫時機,服務端和客戶端是不一致的

服務端 channelRead

服務端 Channel 繫結到 Selector 上時監聽的是 Accept 事件,當客戶端有新連線接入時,會回撥 channelRead() 方法,完成新連線的接入

Netty 在服務端啟動過程中,會預設新增一個 ChannelHandler io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor 來處理新連線的接入

客戶端 channelRead

當服務端處理完 Accept 事件後,會生成一個和客戶端通訊的 Channel,該 Channel 也會註冊到對應的 Selector 上,並監聽 read 事件

當客戶端向該 Channel 中傳送資料時就會觸發 read 事件,呼叫到 channelRead() 方法(Netty 內部的原始碼處理會在後續的文章中進行分析)

exceptionCaught

當前 ChannelHandler 中各回撥方法處理過程中如果發生了異常就會回撥該方法

相關文章