Netty中的ChannelInitializer、ChannelHandlerContext和addLast()方法

889 阅读8分钟

ChannelInitializer

  • ChannelInitializer 是一个特殊的 ChannelInboundHandler。
  • 它提供了一种简单的方式来初始化一个 Channel,一旦它被注册到其 EventLoop 中。
  • 通常,在
    Bootstrap.handler(ChannelHandler);
    ServerBootstrap.handler(ChannelHandler);
    ServerBootstrap.childHandler(ChannelHandler);
    
    的上下文中使用它来设置 Channel 的 ChannelPipeline。
  • 用例:
    public class MyChannelInitializer extends ChannelInitializer {
      public void initChannel(Channel channel) {
          channel.pipeline().addLast("myHandler", new MyHandler());
      }
    }
    
    ServerBootstrap bootstrap = ...;
    ...
    bootstrap.childHandler(new MyChannelInitializer());
    ...
    
    
  • 请注意,这个类被标记为 ChannelHandler.Sharable,因此实现必须安全地重用。
    • 这个类被标记为 ChannelHandler.Sharable,这意味着多个Channel可以共享一个MyChannelInitializer实例。因此,实现这个类的时候需要保证线程安全和无状态性,以便可以在多个Channel上重用。

源码阅读

  • 省内存 image.png
    • 这段代码定义了一个 Set 集合,用于保存 ChannelHandlerContext 对象。在 Netty 的 BootstrapServerBootstrap 中,ChannelInitializer 经常被多个 Channel 共享。为了避免为每个 Channel 分别保存 ChannelHandlerContext 对象导致内存开销过大,这里使用了一个共享的 Set 集合来保存这些对象。
    • 相比于使用 Attribute,使用共享的 Set 集合可以减少内存开销,因为 Attribute 会将值保存到每个 Channel 的属性中,而 Set 集合只需要保存对 ChannelHandlerContext 的引用,而不是保存具体的值。这意味着,共享的 Set 集合不需要为每个 Channel 分别保存一份值,因此可以减少内存开销。
  • initChannel()方法 image.png
    • 这个方法会在Channel注册后被调用。在方法返回后,这个实例将从Channel的ChannelPipeline中移除image.png
    • 换句话说,这个方法提供了一种在Channel注册之后进行一些初始化操作的机制,初始化完成后,这个实例就不再被ChannelPipeline所使用了
    • 可以用如下代码验证:
      class MyServerInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast("httpServerCodec", new HttpServerCodec())
                    .addLast("testHttpServerHandler", new MyHttpServerHandler())
                    .addLast("myException",new ExceptionHandler());
        }
      
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            System.out.println("ChannelInitializer 被移除");
        }
      }
      
    • 毕竟这个类只是提供了一个简便的初始化Channel的方法,用完就不需要了
      class CustomServerInitializer extends ChannelInitializer<SocketChannel> {  
         @Override  
         protected void initChannel(SocketChannel ch) throws Exception {  
            ChannelPipeline pipeline = ch.pipeline();  
            pipeline  
                    .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))  
                    .addLast(new LengthFieldPrepender(4))  
                    .addLast(new StringDecoder(CharsetUtil.UTF_8))  
                    .addLast(new StringEncoder(CharsetUtil.UTF_8))  
                    .addLast(new CustomServerHandler());  
         }  
      }
      

ChannelHandlerContext

image.png

  • 该类提供了让 ChannelHandler 与其 ChannelPipeline 和其他处理器(Handler)进行交互的功能。其中,处理器可以通知 ChannelPipeline 中的下一个 ChannelHandler,并动态地修改其所属的 ChannelPipeline,以及从其他线程中访问 ChannelHandlerContext 等功能。

  • 通知

  • 修改

    • Pipeline 您可以通过调用 pipeline() 方法来获取处理器所属的 ChannelPipeline。在运行时,一个复杂的应用程序可以动态地插入、删除或替换处理器管道中的处理器。
      public class MyHandler extends ChannelInboundHandlerAdapter {
      
        private boolean isOn;
      
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            isOn = true;
            ctx.fireChannelActive();
        }
      
         @Override
         public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
           if (isOn) {
               System.out.println("Handler is ON: " + msg);
               // 增加一个处理器到 pipeline 中
               ctx.pipeline().addLast("myHandler", new MyHandler2());
               isOn = false;
          } else {
               System.out.println("Handler is OFF: " + msg);
               // 移除一个处理器
               ctx.pipeline().remove(this);
          }
           super.channelRead(ctx, msg);
         }
      }
      
      public class MyHandler2 extends ChannelInboundHandlerAdapter {
      
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("MyHandler2: " + msg);
            super.channelRead(ctx, msg);
        }
      }
      /**
      在这个例子中,当处理器 MyHandler 第一次接收到消息时,它会在 pipeline 中添加一个新的处理器
      MyHandler2,并将 isOn 标志设置为 false。当处理器再次接收到消息时,它会从 pipeline 中移除自身。
      可以看到,通过在处理器中使用 pipeline() 方法,我们可以在运行时动态地修改 ChannelPipeline。
      */
      
  • 检索以供以后使用

    • 您可以保留 ChannelHandlerContext 以供以后使用,例如从处理程序方法外触发事件,甚至从不同的线程中触发。
    public class MyHandler extends ChannelDuplexHandler {
    
       private ChannelHandlerContext ctx;//保留 ChannelHandlerContext
    
       public void beforeAdd(ChannelHandlerContext ctx) {
           this.ctx = ctx;
       }
    
       public void login(String username, password) {
           ctx.write(new LoginMessage(username, password));
       }
       ...
    }
    
  • 存储有状态的信息

    • attr(AttributeKey) 允许您存储和访问与 ChannelHandler/Channel 及其上下文相关的有状态信息。请参阅 ChannelHandler 以了解管理有状态信息的各种推荐方式。
      public class MyHandler extends SimpleChannelInboundHandler<String> {
      
        private static final AttributeKey<String> CLIENT_ID = AttributeKey.valueOf("clientId");
      
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            // 为客户端生成一个唯一的 ID,并将其与 ChannelHandlerContext 关联起来
            String clientId = UUID.randomUUID().toString();
            ctx.channel().attr(CLIENT_ID).set(clientId);
            System.out.println("Client " + clientId + " connected.");
        }
      
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            // 处理客户端发送的消息
            String clientId = ctx.channel().attr(CLIENT_ID).get();
            System.out.println("Received message '" + msg + "' from client " + clientId + ".");
        }
      }
      /**
      在这个例子中,当一个新的客户端连接时,我们为它生成一个唯一的 ID,
      并将它与客户端的 `ChannelHandlerContext` 关联起来。
      当客户端发送消息时,我们可以通过这个 ID 来识别它,从而对消息进行处理。
      */
      
  • 处理器(Handler)可以拥有多个 ChannelHandlerContext 请注意,ChannelHandler 实例可以添加到多个 ChannelPipeline 中。这意味着单个 ChannelHandler 实例可以拥有多个 ChannelHandlerContext,并且因此如果将其添加到一个或多个 ChannelPipeline 中,则可以使用不同的 ChannelHandlerContexts 调用单个实例。还请注意,应将要添加到多个 ChannelPipeline 中的 ChannelHandler 标记为 ChannelHandler.Sharable。

    public class MyHandler extends ChannelInboundHandlerAdapter {
    
      private ChannelHandlerContext ctx1;
      private ChannelHandlerContext ctx2;
    
      @Override
      public void channelRegistered(ChannelHandlerContext ctx) {
          if (ctx.name().equals("pipeline1")) {
              this.ctx1 = ctx;
          } else if (ctx.name().equals("pipeline2")) {
              this.ctx2 = ctx;
          }
      }
    
      @Override
      public void channelRead(ChannelHandlerContext ctx, Object msg) {
          if (ctx == ctx1) {
              // 处理 pipeline1 中的消息
          } else if (ctx == ctx2) {
              // 处理 pipeline2 中的消息
          }
      }
    
      // 其他处理器方法省略
    }
    ChannelPipeline pipeline1 = ...
    ChannelPipeline pipeline2 = ...
    
    MyHandler myHandler = new MyHandler();
    
    pipeline1.addLast("handler1", myHandler);
    pipeline2.addLast("handler2", myHandler);
    /**
    由于 MyHandler 实例可以添加到多个 ChannelPipeline 中,因此我们可以在两个不同的
    ChannelPipeline 中使用同一个 MyHandler 实例,并且每个 ChannelPipeline 都会创建一个新的
    ChannelHandlerContext 对象来处理事件。
    */
    

小结

  • ChannelHandlerContext 是 Netty 框架中用于实现 ChannelHandler 与 ChannelPipeline 之间交互的重要组件,其主要功能包括:

    1. 提供与 ChannelPipeline 中其他 ChannelHandler 的交互,通过调用 ChannelHandlerContext 的方法向前或向后传递事件。
    2. 允许动态修改 ChannelPipeline 中的处理器,可以在运行时插入、删除或替换处理器。
    3. 允许存储和访问与 ChannelHandler/Channel 及其上下文相关的有状态信息。
    4. 允许在 ChannelHandler 中保留 ChannelHandlerContext,以便稍后触发事件。
    5. 允许在一个 ChannelHandler 实例被添加到多个 ChannelPipeline 中时拥有多个 ChannelHandlerContext 实例,从而实现对不同 ChannelPipeline 的管理。
  • 总的来说,ChannelHandlerContext 提供了 ChannelHandler 和 ChannelPipeline 之间高效交互的方式,并允许开发者实现更为灵活和高效的网络编程。

  • 它代表了一个 ChannelHandler 和 ChannelPipeline 中的上下文信息。通过 ChannelHandlerContext,我们可以访问到所属的 Channel、ChannelPipeline,以及相关的一些状态信息。

addLast()方法

image.png

  • 创建上下文 image.png

  • 添加进双向链表 image.png

    • 具体来说,它的实现分为以下几个步骤:
      1. 获取当前链表尾部的前一个节点 prev
      2. 将新的节点 newCtxprev 设置为 prev
      3. 将新的节点 newCtxnext 设置为 tail,即链表尾部的哨兵节点。
      4. prevnext 设置为 newCtx,即将新的节点插入到链表中。
      5. tailprev 设置为 newCtx,即更新链表尾部的哨兵节点的前驱节点为新的节点。
    • 如此看来,ChannelPipeline其实是ChannelHandlerContext的容器image.png
    • pipeline是一个双向链表,每个节点包含一个handler和一个context。
    • pipeline有一个head和一个tail节点,分别表示入站和出站的起始点。
      • pipeline内部有一个head和一个tail属性,分别指向链表的头节点和尾节点,它们都是DefaultChannelHandlerContext的实例。
        - header
        - InboundHandler1
        - InboundHandler2
        - OutboundHandler1
        - OutboundHandler2
        - OutboundHandler3
        - InboundHandler3
        - tail
        
        • 当一个inbound事件从header开始流动时,它会按照顺序经过所有的inbound handler,直到到达tail。
        • 当一个outbound事件从tail开始流动时,它会按照逆序经过所有的outbound handler,直到到达header。
        • 这样可以保证事件总是从最近的handler开始处理。
        • head是outbound事件的终点,tail是inbound事件的终点。
          • header负责将outbound事件转换为底层的I/O操作,例如Socket.write()。
          • tail负责将inbound事件转发给最近的inbound handler,或者丢弃或记录它们,如果没有inbound handler可以处理它们。
    • pipeline可以动态地添加或删除handler,每个handler可以有一个name或者一个默认的id来标识。
    • pipeline可以根据handler的类型(入站或出站)和顺序(先添加或后添加)来决定handler的执行顺序。
    • pipeline可以通过context来传递事件和数据给下一个或上一个handler,也可以通过channel来传递给整个pipeline。
  • 调用Handler的added()方法 image.png

    • 注意它的调用逻辑: image.png
      1. 如果当前 Channel 还未注册到 EventLoop 上,那么设置新的 AbstractChannelHandlerContext 实例为 "addPending" 状态,并且添加一个任务到 EventLoop 中,等待 Channel 注册完成后回调 ChannelHandler.handlerAdded() 方法。
      2. 如果当前线程(就是调用addLast方法的线程)不是 EventLoop 线程,那么添加一个任务到 EventLoop 中,在 EventLoop 中回调 ChannelHandler.handlerAdded() 方法。
      3. 如果当前线程就是 EventLoop 线程,直接调用 callHandlerAdded0() 方法回调 ChannelHandler.handlerAdded() 方法。