Netty核心技术十--Netty 核心源码剖析

news2024/10/6 16:42:30

1. 基本说明

  1. 只有看过Netty源码,才能说是真的掌握了Netty框架。

  2. 在 io.netty.example 包下,有很多Netty源码案例,可以用来分析

    image-20230708143830267

2. netty 启动过程源码分析

本次分析使用的是example包下的echo

2.1 源码剖析的目的

用源码分析的方式走一下 Netty(服务器〉的启动过程,更好的理解Netty的整体设计和运行机制。

2.2 源码剖析

  1. 源码需要剖析到Netty调用doBind方法,追踪到NioServerSocketChannel的doBind
  2. 并且要Debug程序到NioEventLoop类的run代码,无限循环,在服务器端运行。

2.2.1 EchoServer

/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package site.zhourui.nioAndNetty.netty.source.echo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;

/**
 * Echoes back any received data from a client.
 */
public final class EchoServer {

    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", "8888"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     p.addLast(new LoggingHandler(LogLevel.INFO));
                     //p.addLast(new EchoServerHandler());
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

2.2.1.1 先看启动类: main方法中,首先创建了关于SSL的配置类

2.2.1.2 重点分析下创建了两个EventLoopGroup对象

  1. 这两个对象是整个Netty的核心对象,可以说,整个Netty的运作都依赖于他们。bossGroup用于接受Tep 请求,他会将请求交给 workerGiroup , workerGroup 会获取到真正的连接,然后和连接进行通信,比如读写解码编码等操作。

  2. new NioEventLoopGiroup(1);这个1表示bossGroup事件组有1个线程你可以指定,如果 newNioEventLoopGroup()会含有默认个线程cpu核数*2,即可以充分的利用多核的优势

    debug分析在5.4.2章节

  3. EventLoopGroup是事件循环组(线程组)含有多个EventLoop,可以注册 channel ,用于在事件循环中去进行选择(和选择器相关) .。[debug看]

    1. 沿着**new NioEventLoopGroup()**一直下一步找到最终实现方法为MultithreadEventExecutorGroup

    2. 创建workerGroup时传入MultithreadEventExecutorGroup的参数

      • nThreads:使用的线程数,默认为core *2 [可以追踪源码]
      • executor执行器:如果传入null,则采用Netty默认的线程工厂和默认的执行器ThreadPerTaskExecutor
      • chooserFactory:单例new DefaultEventExecutorChooserFactory()
      • args在创建执行器的时候传入固定参数

      image-20230708155219893

    3. 创建并初始化NIOEventLoop

      NIOEventLoop实现了EventLoop接口和Executor接口

      image-20230708161043625

      children装的数据就是NIOEventLoop

      image-20230708161127292

    4. 为每一个NIOEventLoop添加一个监听器,并放入一个LinkedHashSet

      image-20230708161930141

    5. 服务器启动类源码总结

      1. 如果executor是 null,创建一个默认的ThreadPerTaskExecutor,使用Netty默认的线程工厂
      2. 根据传入的线程数(CPU*2)创建一个线程池〔单例线程池)数组。
      3. 循环填充数组中的元素。如果异常,则关闭所有的单例线程池
      4. 根据线程选择工厂创建一个线程选择器
      5. 为每一个单例线程池添加一个关闭监听器
      6. 将所有的单例线程池添加到一个HashSet中。

2.2.1.3 ServerBootstrap

  • 他是一个引导类,用于启动服务器和引导整个程序的初始化
  • 它和 ServerChannel 关联,而ServerChannel继承了Channel,有一些方法remotcAddress 等
  1. 我们进入空构造方法发现什么也没做

    image-20230708162604137

  2. group(EventLoopGroup parentGroup, EventLoopGroup childGroup)

    image-20230708162939531

  3. channel(NioServerSocketChannel.class)

    • 这里会对象反射创建一个ReflectiveChannelFactory(NioServerSocketChannel.class)
    • 注意这里并不会创建channel,只是创建了工厂

    image-20230708164820912

  4. option(ChannelOption.SO_BACKLOG, 100)

    放了一些参数

    image-20230708165133765

  5. handler(new LoggingHandler(LogLevel.INFO))

    添加了一个日志相关handler

  6. childHandler(ChannelInitializer)

    再添加一个SocketChannel(不是ServerSocketChannel)的handler。

  7. 引导类总结

    1. 链式调用: group方法,将 boss和 worker传入,boss 赋值给parentGroup 属性,worker 赋值给childGroup属性
    2. channel方法传入NioServerSocketChannel class 对象。会根据这个class创建channel对象。
    3. option方法传入TCP参数,放在一个LinkedHashMap中
    4. handler方法传入一个 handler中,这个hanlder 只专属于ServerSocketChannel而不是SocketChannel
    5. childHandler传入一个hanlder ,这个 handler将会在每个客户端连接的时候调用。供SocketChannel使用

2.2.1.4 端口绑定分析

  1. bind(PORT)

    1. 最终追到AbstractBootstrap的doBind方法

      image-20230708165550661

    2. initAndRegister()此处才创建channel

      • 说明channelFactory.newChannel()方法的作用通过ServerBootstrap 的通道工厂反射创建一个NioServerSocketChannel

      image-20230708170035069

      init的方法的核心作用在和ChannelPipeline相关。

      1. init方法。这是个抽象方法(.AbstractBootstrap类的)。由ServerBootstrap实现
      2. 设NioServerSocketChannel 的TCP属性.
      3. 由于LinkedHashMap是非线程安全的。使用同步进行处理.
      4. 对NioServerSocketChannel的ChanneIPipeline添加ChannelInitializer处理器.
      5. 可以看出,init的方法的桢心作用在和ChannelPipeline相关.
      6. 从NioServerSocketChannel的初始化过程中,我们知道,pipeline是一个双向链表,并且,他本身就初始化了head和 tail,这里调用了他的 addLast方法,也就是将整个handler插入到tail 的前面,因为tail永远会在后面,需要做一些系统的固定工作。

      image-20230708170228056

      init中的addLast方法通过debug最终会找到这里,然后最后调用addLast0

      1. addLast方法,在 DefaultChannelPipeline类中
      2. addLast方法这就是pipeline 方法的核心
      3. 检查该handler是否符合标准。
      4. 创建一个AbstractChannelHandlerContext对象,这里说一下,ChannelHandlerContext对象是ChannelHandler和ChannelPipeline 之间的关联,每当有ChannelHandler添加到 Pipeline 中时,都会创建Context。Context 的主要功能是管理他所关联的 Handler 和同一个Pipeline 中的其他Handler 之间的交互。
      5. 将Context添加到链表中。也就是追加到tail节点的前面。
      6. 最后,同步或者异步或者晚点异步的调用callHandlerAdded0方法

      image-20230708173033956

      addLast0本质上就是对双向链表的尾节点之前进行插入节点的操作

      image-20230708173007005

    3. 绑定regFuture, channel, localAddress, promise,(然后绑定端口并阻塞至连接成功。)

      找到doBind0方法了

      image-20230708171021117

      doBind0核心就是bind方法,这里就可以根据前面下的断点

      image-20230709104623128

      一直debug,将调用LoggingHandler(next)的invokeBind方法

      image-20230709105342878

      invokeBind方法

      image-20230709105610664

      反射调用LoggingHandler(next)的bind方法

      image-20230709105800704

      继续debug 第二圈再到这里这里的bind方法就会跳转到

      注意:unsafe.bind,要debug第二圈的时候,才能看到.

      image-20230709105610664

      这里的bind方法就会跳转到unsafe的bind方法

      image-20230709110610444

      unsafe的bind方法会调用dobind 方法,其实这个dobind0就是NioServerSocketChannel的doBind方法了

      image-20230709110742024

      最后找进来就是NioServerSocketChannel

      image-20230709110843797

    4. 绑定完成后

      最后一步: safeSetSuccess(promise),告诉 promise 任务成功了。其可以执行监听器的方法了。到此整个启动过程已经结束了,ok 了

      image-20230709111519716

    5. runAllTasks

      最后一直debug,会找到runAllTasks方法,这里就一直自旋直到所有异步任务执行完成

      image-20230709111713298

    6. NIOEventLoop的run方法

      最后终于执行到NIOEventLoop的run方法,

      然后一直自旋,processSelectedKeys然后runAllTasks

      image-20230709112225046

      image-20230709112246105

  2. closeFuture()

    最后main线程阻塞等待关闭。

  3. finally块中的代码将在服务器关闭时优雅关闭所有资源

  4. 端口绑定总结

    1. 基本说明:initAndRegister()初始化NioServerSocketChannel通道并注册各个 handler,返回一个future
    2. 通过ServerBootstrap的通道工厂反射创建一个NioServerSocketChannel.
    1. init初始化这个NioServerSocketChannel.
    2. config().group().register(channel)通过ServerBootstrap 的 bossGroup 注册NioServerSocketChannel.
    3. 最后,返回这个异步执行的占位符即 regFuture.

2.3 Netty启动过程梳理

  1. 创建2个 EventLoopGroup 线程池数组。数组默认大小CPU*2,方便chooser选择线程池时提高性能
  2. BootStrap 将 boss 设置为 group属性,将 worker 设置为childer 属性
  3. 通过 bind 方法启动,内部重要方法为 initAndRegister 和dobind 方法
  4. initAndRegister 方法会反射创建 NioServerSocketChannel 及其相关的NIO的对象,pipeline , unsafe,同时也为 pipeline 初始了 head 节点和tail 节点。
  5. 在register0 方法成功以后调用在 dobind 方法中调用doBind0 方法,该方法会调用 NioServerSocketChannel 的 doBind 方法对JDK 的channel 和端口进行绑定,完成 Netty 服务器的所有启动,并开始监听连接事件

3. Netty接受请求过程源码剖析

3.1 目的

服务器启动后肯定是要接受客户端请求并返回客户端想要的信息的,下面源码分析Netty 在启动之后是如何接受客户端请求的

3.2 源码剖析

3.2.1 说明

  1. 从之前服务器启动的源码中,我们得知,服务器最终注册了一个Accept事件等待客户端的连接。我们也知道,NioServerSocketChannel 将自己注册到了boss单例线程池(reactor 线程)上,也就是 EventLoop .

  2. 先简单说下EventLoop的逻辑(后面我们详细讲解 EventLoop)

    EventLoop的作用是一个死循环,而这个循环中做3件事情:

    1. 有条件的等待Nio事件。
    2. 处理Nio事件。
    3. 处理消息队列中的任务。
  3. 仍用前面的项目来分析:进入到NioEventLoop 源码中后,在private void processSelectedKey(SelectionKey key)

  4. AbstractNioChannel ch)方法开始调试最终我们要分析到AbstractNioChannel 的 doBeginRead 方法,当到这个方法时,针对于这个客户端的连接就完成了,接下来就可以监听读事件了

3.2.2 源码剖析

接着启动过程源码分析的最后的位置即run方法,这次我们要分析的是processSelectedKeys

  1. 断点打在processSelectedKeys处,然后启动服务端,然后通过浏览器或者客户端访问服务端

    image-20230709114307500

  2. 当客户端启动成功后processSelectedKeys的size大于0就代表监听到事件了,监听到事件就执行processSelectedKeysOptimized方法

    image-20230709114506093

  3. processSelectedKeysOptimized方法判断如果是NioChannel就执行processSelectedKey方法

    image-20230709114533132

  4. processSelectedKey

    1. isValid:判断selectedKey是否合法
    2. 如果合法就判断类型,我这里是启动客户端发送了数据,那么服务端接收到的readyOps是16 ,也就是Accept事件。
    3. 那么就执行unsafe.read();

    image-20230709114718089

  5. unsafe.read()

    1. 断言检查该eventloop线程是否是当前线程。assert eventLoop().inEventLoop()
    2. 执行doReadMessages方法,并传入一个readBuf变量,这个变量是一个List,也就是容器。
    3. 循环容器,执行pipeline.fireChannelRead(readBuf.get(i));
    4. doReadMessages是读取 boss 线程中的NioServerSocketChannel接受到的请求。并把这些请求放进容器,
    5. 循环遍历容器中的所有请求,调用 pipeline 的 fireChannelRead方法,用于处理这些接受的请求或者其他事件,在read 方法中,循环调用ServerSocket 的 pipeline 的 fireChannelRead 方法,开始执行管道中的handler 的ChannelRead方法(debug进入)
    1. debug到doReadMessages时size为0

      image-20230709115030563

    2. doReadMessages方法

      获取到一个JDK 的SocketChannel,然后,使用NioSocketChannel进行封装。最后添加到容器中并返回

      image-20230709121941816

      doReadMessages执行完成后

      image-20230709125140957

    3. 循环容器,执行pipeline.fireChannelRead(readBuf.get(i));

      我们传入其实就是服务端与客户的连接的channel

      image-20230709125734926

    4. fireChannelRead 方法

      在read方法中,循环调用 ServerSocket 的 pipeline的fireChannelRead 方法,开始执行管道中的 handler的ChannelRead方法

      image-20230709130452509

      这里就开始执行handler调用链了

      经过dubug(多次),可以看到会反复执行多个handler 的ChannelRead ,我们知道,pipeline 里面有4个handler ,分别是 Head,LoggingHandler,ServerBootstrapAcceptor,Tail。

      image-20230709130554759

    5. 我们需要在next是ServerBootstrapAcceptor的时候进入((ChannelInboundHandler) handler()).channelRead(this, msg);才会进入客户端连接注册到worker线程池的源码

      image-20230709133540164

    6. 将客户端连接注册到worker 线程池 childGroup就是我们workerGroup

      注册规则默认从第一个顺序注册,之前讲过

      image-20230709133825795

    7. 以上总结

      1. msg强转成Channel ,实际上就是NioSocketChannel .
      2. 添加NioSocketChannel 的 pipeline的 handler ,就是我们 main方法里面设置的childHandler 方法里的。
      3. 设置 NioSocketChannel的各种属性。
      4. 将该 NioSocketChannel注册到 childGroup 中的一个EventLoop 上,并添加一个监听器。
      5. 这个childGroup就是我们main方法创建的数组workerGroup。
    8. register方法

      image-20230709134521677

      1. next()方法

        会调用super的next方法

        image-20230709134657852

        super的next方法

        image-20230709134844566

      2. register方法

        1. 进入register方法又是一个register,继续进入

          image-20230709134937437

        2. 找到unsafe().register

          image-20230709135041818

        3. unsafe().register才是我们最终处理注册的方法,然后进入register0(promise)

          image-20230709135534212

        4. 最终会调用doBeginRead方法,也就是 AbstractNioChannel类的方法

          什么时候调用?

          在执行doReadMessages及fireChannelRead时执行fireChannelReadComplete时调用

          image-20230709144850188

          这个地方调试时,请把前面的断点都去掉,然后启动服务器就会停止在 doBeginRead(需要先放过该断点,然后浏览器请求,才能看到效果)

          执行到这里时,针对于这个客户端的连接就完成了,接下来就可以监听读事件了,即workerGroup的NioEventLoop 可以执行processSelectedKeys然后runAllTasks

          image-20230709143733279

        5. doBeginRead()

          监听workerGroup的事件了

          image-20230709143816257

    9. Netty 接受请求过程梳理

      总体流程:接受连接……>创建一个新的NioSocketChanne–…>注册到一个 worker EventLoop 上.-…>注册selecot Read 事件。

      1. 服务器轮询Accept事件,获取事件后调用unsafe的 read 方法,这个unsafe是ServerSocket 的内部类,该方法内部由2部分组成
    10. doReadMessages用于创建NioSocketChannel对象,该对象包装JDK的 Nio Channel 客户端。该方法会像创建ServerSocketChanel 类似创建相关的 pipeline , unsafe,config

      1. 随后执行执行 pipeline.fireChannelRead 方法,并将自己绑定到一个chooser选择器选择的 workerGroup 中的一个 EventLoop。并且注册一个0,表示注册成功,但并没有注册读(1)事件

4. Pipeline Handler HandlerContext创建源码剖析

4.1 源码剖析目的

Netty 中的 ChannelPipeline 、 ChannelHandler 和ChannelHandlerContext是非常核心的组件, 我们从源码来分析Netty 是如何设计这三个核心组件的,并分析是如何创建和协调工作的.

4.2 源码剖析

4.2.1 ChannelPipeline | ChannelHandler | ChannelHandlerContext介绍

4.2.1.1 三者关系

  1. 每当ServerSocket创建一个新的连接,就会创建一个Socket,对应的就是目标客户端。

  2. 每一个新创建的Socket 都将会分配一个全新的 ChanneIPipeline(以下简称 pipeline)

  3. 每一个ChannelPipeline内部都含有多个ChannelHandlerContext(以下简称 Context)

  4. 他们一起组成了双向链表,这些Context 用于包装我们调用addLast 方法时添加的ChannelHandler (以下简称handler)

  5. 关系图

    image-20230710153030789

    1. 上图中:ChannelSocket 和 ChannelPipeline是一对一的关联关系,而 pipeline 内部的多个Context 形成了链表,Context只是对Handler 的封装。
    2. 当一个请求进来的时候,会进入Socket对应的 pipeline,并经过 pipeline 所有的 handler,对,就是设计模式中的过滤器模式

4.1.2.2 ChannelPipeline作用及设计

  1. pipeline 的接口设计

    image-20230710154434738

    可以看到该接口继承了inBound,outBound,lterable接口,表示他可以调用敷据出站的方法和入站的方法,同时也能遍历内部的链表

  2. 部分方法

    image-20230710160241012

    看看他的几个代表性的方法,基本上都是针对handler链表的插入,追加,删除,替换操作,类似是一个LinkedList同时,也能返回channel(也就是 socket)

  3. 在pipeline的接口文档上,提供了一幅图

    出站和入站的理解:

    • 入站:数据进入Pipeline
    • 出站:数据出Pipeline

    image-20230710154820727

    • 这是一个handler 的 list,handler 用于处理或拦截入站事件和出站事件,pipeline 实现了过滤器的高级形式,以便用户控制事件如何处理以及handler在 pipeline中如何交互。

    • 上图描述了一个典型的 handler 在 pipeline 中处理I/О事件的方式,IO事件由inboundHandler或者outBounidHlandler 处理,并通过调用ChannelHandlerContext.fireChannelRead方法转发给其最近的处理程序。

      image-20230710163821398

      • 入站调用findContextInbound–入站从头节点往尾节点执行
        • 会调用findContextInbound(int mask)方法,从头至尾遍历InboundHandler,注意,只遍历Inbound操作;
      • 出站调用findContextOutbound–出站从尾节点往头节点执行
        • 会调用findContextOutbound(int mask),从尾到头遍历OutboundHandler,这时只有OutBound操作被执行

      image-20230710164132029

    • 入站事件由入站处理程序以自下而上的方向处理,如图所示。入站处理程序通常处理由图底部的Ⅰ/ O线程生成入站数据。入站数据通常从如SocketChannel.read(ByteBuffer)获取。

    • 通常一个pipeline 有多个handler,例如,一个典型的服务器在每个通道的管道中都会有以下处理程序

      • 协议解码器–将二进制数据转换为.Java对象。
      • 协议编码器–将.Java.对象转换为二进制数据。
      • 业务逻辑处理程序–执行实际业务逻辑〔例如数据库访问)
    • 你的业务程序不能将线程阻塞,会影响IO 的速度,进而影响整个Netty程序的性能。如果你的业务程序很快,就可以放在IO线程中,反之,你需要异步执行(使用taskQueen或者scheduleTaskQueen执行)。或者在添加 handler的时候添加一个线程池,例如:

      //下面这个任务执行的时候,将不会阻塞IO线程,执行的线程来自group 线程池
      pipeline.addLast(group,“handler”, new MyBusinessLogicHandler());
      

4.1.2.3 ChannelHandler作用及设计

image-20230710170453487

  • ChannelHandler的两个重要方法:

    • handlerAdded: 当把 ChannelHandler添加到pipeline时被调用
    • handlerRemoved:当从pipeline中移除时调用
    • exceptionCaught(已过时):当处理过程中在 pipeline发生异常时调用
  • ChannelHandler 的作用就是处理IO事件或拦截IO 事件,并将其转发给下一个处理程序ChannelHandler。Handler 处理事件时分入站和出站的,两个方向的操作都是不同的,因此,Netty定义了两个子接口继承ChannelHandler

    • ChannelInboundHandler:处理入站的Handler

      image-20230710170839013

      • channelActive 用于当Channel处于活动状态时被调用:
      • channelRead当从Channel读取数据时被调用等等方法。
      • 程序员需要重写一些方法,当发生关注的事件,需要在方法中实现我们的业务逻辑,因为当事件发生时,Netty 会回调对应的方法。
    • ChannelOutboundHandler:处理出站的Handler

      image-20230710170939611

      • bind方法,当请求将Channel绑定到本地地址时调用
      • close方法,当请求关闭Channel时调用等等
      • 出站操作都是一些连接和写出数据类似的方法
    • ChannelDuplexHandler:既能处理出站又能处理入站事件

      image-20230710171433771

      • 间接实现了入站接口并直接实现了出站接口。
      • 是一个通用的能够同时处理入站事件和出站事件的类
      • 尽量不要使用:容易出现出站和入站调度的混淆

4.1.2.4 ChannelHandlerContext作用及设计

  1. ChannelHandlerContext UML图

    image-20230710171731185

    ChannelHandlerContext继承了出站方法调用接口和入站方法调用接口

    • 这两个invoker就是针对入站或出站方法来的,就是在入站或出站 handler 的外层再包装一层,达到在方法前后拦戴并做一些特定操作的目的
    • ChannelInboundInvoker

      image-20230710171941893

    • ChannelOutboundInvoker

      image-20230710172005937

  2. ChannelHandlerContext 方法

    image-20230710172240996

    • ChannelHIandlerContext不仅仅时继承了他们两个的方法,同时也定义了一些自己的方法
    • 这些方法能够获取Context 上下文环境中对应的比如 channel,executor,handler , pipeline,内存分配器,关联的handler是否被删除。
    • Context就是包装了handler 相关的一切,以方便Context可以在 pipeline方便的操作 handler

4.2.2 ChannclPipeline | ChannelHandler | ChannelHandlerContext创建过程

分为3个步骤来看创建的过程:

  • 任何一个ChannelSocket创建的同时都会创建一个pipeline.
  • 当用户或系统内部调用pipeline的 ad***方法添加 handler 时,都会创建一个包装这handler 的 Context.
  • 这些Context在pipeline中组成了双向链表。

4.2.2.1 Socket创建的时候创建pipeline

在SocketChannel 的抽象父类AbstractChannel 的构造方法中被创建

    /**
     * Creates a new instance.
     *
     * @param parent
     *        the parent of this channel. {@code null} if there's no parent.
     */
    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }
  1. 打上断点开始debug

    image-20230710173037433

  2. newChannelPipeline()

    创建了一个DefaultChannelPipeline

    image-20230710173156924

  3. DefaultChannelPipeline

    image-20230710173459117

    1. 将channel赋值给channel字段,用于pipeline操作channel。
    2. 创建一个future和 promise,用于异步回调使用。
    3. 创建一个inbound 的 tailContext,创建一个既是 inbound类型又是 outbound类型的 headContext.
    4. 最后,将两个Context互相连接。形成双向链表。
    5. tailContext和HeadContext非常的重要,所有 pipeline中的事件都会流经他们,
    6. 这里构建了只有头尾两个节点的双向链表

4.2.2.2 在add**添加处理器的时候创建Context**

看下DefaultChannelPipeline 的 addLast方法如何创建的Context,代码如下

  1. 打上断点开始debug

    image-20230710174128317

  2. addLast(executor, null, h)

    image-20230710174357620

    1. pipeline添加 handler,参数是线程池,name是null,handler 是我们或者系统传入的 handler。Netty为了防止多个线程导致安全问题,同步了这段代码,步骤如下:
    2. 检查这个 handler实例是否是共享的,如果不是,并且已经被别的 pipeline使用了,则抛出异常。
    3. 调用newContext(group, filterName(name, handler), handler)方法,创建一个Context。从这里可以看出来了,每次添加一个handler都会创建一个关联Context.
    4. 调用addLast方法,将Context追加到链表中。
    5. 如果这个通道还没有注册到 selecor 上,就将这个Context添加到这个pipeline 的待办任务中。当注册好了以后,就会调用callHandlerAdded0方法(默认是什么都不做,用户可以实现这个方法)。
    6. 到这里,针对三对象创建过程,了解的差不多了,和最初说的一样,每当创建ChannelSocket 的时候都会创建一个绑定的 pipeline,一对一的关系,创建 pipeline 的时候也会创建tail节点和 head 节点,形成最初的链表。 tail是入站inbound 类型的 handlerhead 既是 inbound 也是 outbound 类型的 handler在调用 pipeline的 addLast方法的时候,会根据给定的 handler创建一个Context,然后,将这个Context 插入到链表的尾端(tail前面)。到此就OK 了

5. ChannelPipeline 调度 handler 的源码剖析

    @Override
    public final ChannelPipeline fireChannelActive() {
        AbstractChannelHandlerContext.invokeChannelActive(head);
        return this;
    }

5.1 分析目的

  1. 当一个请求进来的时候,ChannelPipeline是如何调用内部的这些handler的呢?我们一起来分析下。
  2. 首先,当一个请求进来的时候,会第一个调用pipeline 的相关方法,如果是入站事件,这些方法由fire 开头,表示开始管道的流动。让后面的handler继续处理

5.2 源码剖析

说明:

  1. 当浏览器输入 http://localhost:8888。可以看到会执行handler

  2. 在Debug时,可以将断点下在 DefaultChannelPipeline 类的fireChannelRead方法为例

  3. 同理其他fireChannelxxx方法也是这个原理

        @Override
        public final ChannelPipeline fireChannelRead(Object msg) {
            AbstractChannelHandlerContext.invokeChannelRead(head, msg);
            return this;
        }
    
  1. 先启动服务端,再启动一个客户端,执行invokeChannelActive(head)

    注意:这里就是传入的头结点,因为是入站

    image-20230710181502389

  2. invokeChannelActive(head)

    • 因为有4个handler ,分别是 Head,LoggingHandler,EchoServerHandler,Tail。
    • 我们自定义的在第三个,所以我们放行之前的handler,直到next为我们想要的为止,这里就是EchoServerHandler

    image-20230710183324299

  3. channelActive(this)

    我们进入channelActive()方法,就直接到我们自定义handler的channelRead方法了

    image-20230710183458376

  4. 说明

    • 可以看出来,这些方法都是inbound 的方法(因为我们示例的是fireChannelRead所以是入站,出站可以调用其他方法),也就是入站事件,调用静态方法传入的也是inbound 的类型headhandler。这些静态方法则会调用head 的ChannelInboundInvoker接口的方法fireChannelxxx方法,再然后调用handler的真正方法

    • 如果这些都是出站的实现,但是调用的是 outbound类型的 tail handler来进行处理,因为这些都是outbound事件。

      如果自定义Handler继承了ChannelOutboundHandlerAdapter并重写了以下方法

      image-20230711102053884

    • 出站是 tail开始,入站从 head 开始。

      • 因为出站是从内部向外面写,从tail 开始,能够让前面的 handler进行处理,防止 handler被遗漏,比如编码。
    • 反之,入站当然是从head 往内部输入,让后面的 handler 能够处理这些输入的数据。

      • 比如解码。因此虽然head 也实现了outbound 接口,但不是从head 开始执行出站任务

5.3 图解如何调度

image-20230711102948380

  1. pipeline 首先会调用Context 的静态方法 fireXXx,并传入Context
  2. 然后,静态方法调用Context 的 invoker方法,而 invoker方法内部会调用该Context所包含的Handler的真正的XXX方法,调用结束后,如果还需要继续向后传递,就调用Context的 fireXXX2方法,循环往复。

5.4 ChannelPipeline 调度 handler 梳理

  1. Context 包装 handler,多个 Context 在 pipeline 中形成了双向链表,入站方向叫inbound,由 head 节点开始,出站方法叫 outbound ,由tail 节点开始。
  2. 而节点中间的传递通过 AbstractChannelHandlerContext 类内部的fire系列方法,找到当前节点的下一个节点不断的循环传播。是一个过滤器形式完成对handler的调度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/740483.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 动态规划 Leetcode 63. 不同路径 II

该题大部分思路可以根据Leetcode 62. 不同路径这篇博客了解 这里进行基于上面那篇博客后来对该题进行补充 代码展示&#xff1a; class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int mobstacleGrid.length;int nobstacleGrid[0].length;//创建…

HDFS块详解

HDFS块详解 传统型分布式文件系统的缺点 现在想象一下这种情况&#xff1a;有四个文件 0.5TB的file1&#xff0c;1.2TB的file2&#xff0c;50GB的file3&#xff0c;100GB的file4&#xff1b;有7个服务器&#xff0c;每个服务器上有10个1TB的硬盘。 在存储方式上&#xff0c;我…

Docker安装ElasticSearch8.X docker安装elasticsearch8.X完整详细教程

Docker安装ElasticSearch8.X docker安装elasticsearch8.X完整详细教程 Docker 上安装 ElasticSearch 8.8.1 的步骤&#xff1a;选择要安装的ElasticSearch 版本1、拉取 ElasticSearch 镜像2、创建并运行容器关闭容器启动容器重启容器 3、elasticsearch常用端口以及作用4、测试&…

基于spring cloud alibaba的低代码核心工具,jvs-logic逻辑引擎

在现代企业管理中&#xff0c;决策扮演着至关重要的角色。然而&#xff0c;随着业务规模的扩大和数据量的增加&#xff0c;人工决策变得越来越困难和耗时&#xff0c;而且容易受到主观因素的影响。逻辑引擎的出现为企业提供了一种高效、准确的决策推理工具&#xff0c;能够以逻…

[工业互联-23]:EtherCat从站 - EtherCAT协议栈与工作原理, 软硬件解决方案

目录 第1章 EtherCAT通信原理 1.1 网络架构 1.2 分层模型 2.1 物理层 1.2 数据链路层 1.2.1 EtherCAT数据帧结构 1.2.2 EtherCAT报文寻址 第2章 EtherCAT从站 2.1 概述 2.2 EtherCAT从站的组成包括&#xff1a; 2.3 EtherCAT从站的硬件 2.4 从站控制信息芯片&#…

LeetCode[394]字符串解码

难度&#xff1a;Medium 题目&#xff1a; 给定一个经过编码的字符串&#xff0c;返回它解码后的字符串。 编码规则为: k[encoded_string]&#xff0c;表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。 你可以认为输入字符串总是有效的&#xff1b;…

【LeetCode热题100】打卡第34天:排序链表乘积最大的子数组

文章目录 【LeetCode热题100】打卡第34天&#xff1a;排序链表&乘积最大的子数组⛅前言 排序链表&#x1f512;题目&#x1f511;题解 乘积最大的子数组&#x1f512;题目&#x1f511;题解 【LeetCode热题100】打卡第34天&#xff1a;排序链表&乘积最大的子数组 ⛅前…

如何将文字转化为语音?三个方法帮你轻松实现!

如何将文字转化为语音&#xff1f;在工作或学习中&#xff0c;我们可能会遇到需要将文字转化为语音的情况&#xff0c;这可能会让一些人感到困惑&#xff0c;不知道如何实现这个转换。其实&#xff0c;只需要利用一些第三方工具&#xff0c;就可以轻松地将文字转化为语音。下面…

十四、flex弹性容器属性样式2

目录&#xff1a; 1.准备工作 2.属性解析&#xff1a; align-items 3.属性解析&#xff1a; align-content 4.弹性元素的属性 一、准备工作 我们在前面的基础上&#xff0c;修改代码&#xff0c;把ul的高度定下来&#xff0c;设置800px, li的高度不定。 然后&#xff0c;body里…

音频采样器 Native Instruments Kontakt7 forMac/Windows图文安装教程

Native Instruments Kontakt是一款功能强大、灵活易用的音乐采样软件&#xff0c;适用于各种音乐创作和制作需求。无论是专业音乐制作人还是初学者&#xff0c;都能通过它来实现创意的音乐作品。 Kontakt具有直观的用户界面&#xff0c;可通过拖放方式导入和管理采样库。它支持…

Vmware虚拟机网络配置回顾

如何配置Vmware里的虚拟机网络&#xff1f;这个东西不常用&#xff0c;都是自己练手用的。能用就行&#xff0c;千万不要花时间记&#xff0c;没意义。 很简单&#xff0c;照着敲 首先登陆自己的虚拟机 vim /etc/sysconfig/network-scripts/ifcfg-ens32 TYPE"Ethernet&q…

数据结构--并查集

数据结构–并查集 逻辑结构―—“集合” 所有元素的全集s 将各个元素划分为若干个互不相交的子集 用互不相交的树&#xff0c;表示多个“集合” “并查集”的存储结构 用一个数组S[ ]即可表示“集合”关系 ‘并查集”的基本操作 集合的两个基本操作―— “并” \color{red}“…

ios14~14.3越狱/root(Taurine牛磺酸1.1.6)

Taurine牛磺酸 一键完美越狱 windows安装时建议关闭本地安全中心&#xff08;若报毒的话&#xff0c;没有则忽略&#xff09; 1.安装windows端AltInstaller&#xff1a;安装成功后&#xff0c;电脑右下角控制中心有一个&#xff08;灰色的 小方块&#xff09; 2.安装手机端A…

layui入门

layui入门 一.ayui简介1.简单易用2.组件丰富3.高度定制化4.响应式布局5.轻量灵活 2.layui的入门基础操作3.登录实例4.注册实例 一.ayui简介 Layui&#xff08;流行音 “layui”&#xff0c;来自“领域的模块化”&#xff09;是一款前端UI框架&#xff0c;专注于提升 Web 开发效…

Jmeter接口关联(三)【使用正则表达式提取值】以及正则表达式提取器中模板的含义及用法

文章目录 前言一、Jmeter中使用正则表达式匹配 1、选择 RegExp Tester2、在线程组------》添加------》后置处理器-------里面添加一个“正则表达式提取器”二、关于正则表达式提取器里面字段的解释 参数说明三、进一步解释Jmeter正则表达式提取器中的模板 1、当模板设置为$0$ …

每个开发人员都应该知道的VS Code入门技巧

这里有一些每个开发人员都应该知道的关于Visual Studio Code (VS Code)的技巧: 1、自定义键盘快捷键:VS Code允许您根据自己的喜好自定义键盘快捷键。点击“文件”->“首选项”->“键盘快捷键”或使用快捷键Ctrl K和Ctrl S打开键盘快捷键编辑器。可以修改现有快捷方式或…

抖音seo源码打包分享

抖音seo源码搭建----分享给各位开发者 获取视频列表 $Video_model new App_Model_Douyin_MysqlVideoStorage(); $video_list $Video_model->getList($where,$this->index,$this->count,$sort); $temp_video_model new App_Model_Douyin_…

微信小程序input的placeholder脱离文档流

今天进行真机调试时input的提示词 placeholder脱离了文档流&#xff0c;但是奇怪的是input框没有脱离文档流 如下图所示&#xff1a; 微信开发工具正常&#xff1a; 真机&#xff1a;不正常 脱离文档流 解决方法&#xff1a; <view clas…

给一个体积水,用不同体积的容器去装

这个有两个方案&#xff1a; 1.每个都装得最满&#xff0c;减少瓶子容积损失 //xzlist 瓶子容积排序 tj水总体积 xzzc各个体积瓶子数 public static void Boxjs(int tj, List<Map<String,Object>> xzlist, List<Map<String,Object>> xzzc){boolean f…

Linux信号机制-2

转自&#xff1a;Linux信号处理_linux 信号处理函数_努力啃C语言的小李的博客-CSDN博客 什么是信号 信号本质上是在软件层次上对中断机制的一种模拟&#xff0c;其主要有以下几种来源&#xff1a; 程序错误&#xff1a;除零&#xff0c;非法内存访问等。 外部信号&#xff1a…