EventExecutorChooser,Channel注册,NioEventLoop工作流程、Channel重要结论

238 阅读5分钟

Channel的注册

为Channel选择Executor

  • 负载均衡用简单的循环选择算法,列表中按照顺序选择下一个,直到选择到最后一个后,再从头开始选择,如此循环往复。 image.png
    • 这两个Chooser的区别就是,用不用位运算的区别,位运算比取模快。
    • 2的次幂转换为二进制后,只有最高位是1,其余位都是0,就是2的次幂的二进制表示只有一个1。例如,4的二进制表示为100,3的二进制表示为011。当我们对它们进行与运算时,相当于将4的二进制表示中的最高位1变成了0,也就是消掉了最高位的1。例如,4 & 3的结果为0。这样在位运算中可以非常方便地进行与、或、异或等操作。

ThreadPerTaskExecutor和SingleThreadEventExecutor的关系

  • 在new NioEventLoopGroup()时会调用到如下代码: image.png
  • children会变成SingleThreadEventExecutor,追踪newChild(executor, args): image.png
    • 简单说就是SingleThreadEventExecutor里面封装了ThreadPerTaskExecutor。 image.png
    • 也就说,SingleThreadEventExecutor执行任务就靠ThreadPerTaskExecutor。
  • 那么SingleThreadEventExecutor每执行一个任务就会导致ThreadPerTaskExecutor创建一个新线程吗?毕竟ThreadPerTaskExecutor是一任务一线程。
    • 在 SingleThreadEventExecutor 内部的任务队列中,任务的执行是通过 ThreadPerTaskExecutor.execute() 方法来完成的,而 ThreadPerTaskExecutor.execute() 方法是将任务提交给当前的线程去执行。也就是说,如果任务队列中已经有任务在等待执行,那么执行这些任务的线程就是当前的线程,不需要再创建新的线程。
    • 换句话说,整一个死循环给ThreadPerTaskExecutor,对它来说这个死循环就是一个任务,所以是一个线程,但是SingleThreadEventExecutor可以执行很多任务,即,SingleThreadEventExecutor中的任务队列,对ThreadPerTaskExecutor来说其实是一个任务。
  • 最终 image.png

Channel的注册流程

  • 调用serverBootstrap.bind()方法就会有Channel被注册: image.png
  • 追踪到如下代码: image.png
  • 最终会调用jdk提供的Nio提供的内容 image.png
  • ServerSocketChannel应该要对OP_ACCEPT感兴趣的,而不是ops 0 image.png
    • 避免了在注册和绑定之间产生不必要的事件通知,例如如果在注册时就对ACCEPT事件感兴趣,那么在绑定(bind)端口之前就可能收到ACCEPT事件的通知,但此时还没有绑定地址,无法处理新连接。
  • Channel的注册结果就是被放入Selector中,准确的说是包装成SelectionKey,再放入Selector中。

那么NioEventLoop就好理解了

  • 就是一个实现多路复用的类。 image.png
  • 依旧是调用select(),等待对应的Channel感兴趣的事件就绪 image.png
  • NioEventLoop是不断从Selector中获取就绪事件,然后根据事件的类型和对应的Channel,去执行相应的处理器(Handler)或者任务(Task)。但是NioEventLoop不仅仅是执行Selector中的事件,它还会执行一些其他的任务,比如定时任务(ScheduledTask),尾部任务(TailTask),或者用户提交的任务(UserTask)。这些任务都会被放入一个任务队列(TaskQueue)中,等待NioEventLoop执行。所以,NioEventLoop的执行流程大致如下:
    1. 调用Selector的select方法,阻塞等待就绪事件。
    2. 处理就绪事件,根据事件的类型和对应的Channel,调用相应的处理器或者任务。
    3. 执行所有的定时任务和尾部任务。
    4. 执行任务队列中的任务,直到达到一定的时间或者数量限制。
    5. 回到第一步,重复循环。

小结

  • 一个EventLoopGroup当中会包含一个或多个EventLoop。
  • 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定。
    • 一般是SingleThreadEventExecutor;
  • 所有由EventLoop所处理的各种I/O事件都将在它所关联的那个Thread上进行处理。
    • SingleThreadEventExecutor(EventLoop的父类)有一个成员变量Thread,其实是来自于ThreadPerTaskExecutor;
  • 一个Channel在它的整个生命周期中只会注册在一个EventLoop上。
    • chooser会选一个Executor给Channel。
    • 也就是说,对Channel的所有操作都是线程安全的,单线程没有竞争。
  • 一个EventLoop在运行过程当中,会被分配给一个或者多个Channel。

重要结论

  • 在Netty中,Channel的实现一定是线程安全的;基于此、我们可以存储一个Channel的引用,并且在需要向远程端点发送数据时,通过这个引用来调用channel相应的方法;即便当时有很多线程都在使用它也不会出现多线程问题;而且,消息一定会按照顺序发送出去。
    Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
    System.out.println("Connected to server: " + channel.remoteAddress());
    // Create multiple threads that send messages to the same channel 
    for (int i = 0; i < 10; i++) { 
      new Thread(() -> { 
         for (int j = 0; j < 5; j++) { 
            channel.writeAndFlush("Thread " + Thread.currentThread().getId() + ", message "  + "\n"); 
         } 
      }).start(); 
    }
    
  • 我们在业务开发中,不要将长时间执行的耗时任务放入到EventLoop的执行队列中,因为它将会一直阻塞该线程所对应的所有Channel上的其他执行任务,如果我们需要进行阻塞调用或是耗时的操作(实际开发中很常见),那么我们就需要使用一个专门的EventBxecutor(业务线程池)。
    • 通常会有两种实现方式:
      1. 在ChannelHandler的回调方法中,使用自己定义的业务线程池,这样就可以实现异步调用。
        public class EchoServerHandler extends ChannelInboundHandlerAdapter {
          private ExecutorService executorService;
        
          public EchoServerHandler(ExecutorService executorService) {
              this.executorService = executorService;
          }
          
          @Override
          public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
              executorService.execute(() -> {
               // 异步处理数据
               // ...
        
               // 发送数据回客户端
               ctx.writeAndFlush(Unpooled.copiedBuffer("Echo: " + msg, CharsetUtil.UTF_8));
            });
         }
        }
        
      2. 借助于Netty提供的向ChannelPipeline添加ChannelHandler时调用的addLast方法来传递EventExecutor.
        • 说明:默认情况下(调用addLast(handler) ,ChannelHandler中的回调方法都是由I/O线程所执行,如果调用了ChannelPipeline addLast(EventExecutorGroup group,ChannelBandler... handlers);方法,那么ChannelHandler中的回调方法就是由参数中的group线程组来执行的。
          // 创建业务线程池 
          EventExecutorGroup businessGroup = new DefaultEventExecutorGroup(10); 
          
          // 添加业务处理器,指定使用业务线程池执行回调方法 
          pipeline.addLast(businessGroup, new MyBusinessHandler());
          
  • 现在有一个场景: 一个客户端c1与一个服务端s1,但是s1需要将客户端c1的数据转发到另外一个服务器s2,那么,s1就是既是服务器,又是客户端 image.png
    • 最佳实践就是,c1与s1的channel和s1与s2的channel应该共用同一个EventLoop
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
          Channel c1Channel = ctx.channel();//与客户端c1的连接
          Bootstrap bootstrap = new Bootstrap();//服务端充当客户端
          bootstrap
                  .group(c1Channel.eventLoop())//与客户端用同一个eventLoop
                  .channel(NioSocketChannel.class)//客户端Channel
                  .handler(new SimpleChannelInboundHandler<String>() {
                      @Override
                      protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                          //.....
                      }
                  });
          Channel channel = bootstrap.connect("remoteHost", 8899).sync().channel();
          channel.writeAndFlush("Hello World");
      }
    

    不一定要在channelActive方法里搞,只是说明一下用意。