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

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的调度

6. Netty 心跳(heartbeat)服务源码剖析

6.1 源码剖析目的

Netty 作为一个网络框架,提供了诸多功能,比如编码解码等,Netty 还提供了非常重要的一个服务-----心跳机制heartbeat。通过心跳检查对方是否有效,这是RPC框架中是必不可少的功能。下面我们分析一下Netty内部心跳服务源码实现。

6.2 源码剖析

本次剖析的源码是之前写的

Netty核心技术六–Netty核心模块组件的第12章节Netty心跳检测机制及实现

6.2.1 说明

Netty 提供了 IdleStateHandlerReadTimeoutHandlerWriteTimeoutHandler三个Handler 检测连接的有效性,重点分析 IdleStateHandler .

image-20230713174447316

ReadTimeout事件和WriteTimeout事件都会白动关闭连接,而且,属于异常处理,所以,这里只是介绍一下,我们重点看IdleStateHandler。

6.2.2 IdleStateHandler源码剖析

6.2.2.1 四个重要属性

解释observeOutput:

假设:当你的客户端应用每次接收数据是10秒,而你的写空闲时间是 5 秒,那么,当你数据还没有写出的时候,写空闲时间触发了。实际上是不合乎逻辑的。因为你的应用根本不空闲。

    private final boolean observeOutput;//是否考虑出站时较慢的情况。默认值是false
    private final long readerIdleTimeNanos;//读事件空闲时间,0则禁用事件(纳秒)

    private final long writerIdleTimeNanos;//写事件空闲时间,0则禁用事件(纳秒)

    private final long allIdleTimeNanos;//读或写空闲时间,0则禁用事件(纳秒)
6.2.2.2 handlerAdded方法

当该handler被添加到pipeline中时,则调用initialize方法


6.2.2.2.1 前置知识-System.nanoTime()

发现是调用的本地方法

public static native long nanoTime();
6.2.2.2.2 System.nanoTime()使用
package site.zhourui.nioAndNetty.netty.source.echo;
public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(System.nanoTime());
        Thread.sleep(1000);
        System.out.println(System.nanoTime());
    }
}

执行结果

结论1000毫秒=1秒=10亿纳秒

image-20230713181355566


6.2.2.2.3 initialize方法

image-20230714103222119

6.2.2.3 该类内部的3个定时任务类
  • ReaderIdleTimeoutTask:读超时任务
  • WriterIdleTimeoutTask:写超时任务
  • AllIdleTimeoutTask:读或写超时任务

image-20230714103353280

这3个定时任务分别对应读,写,读或者写事件。共有一个父类(AbstractldleTask)。这个父类提供了一个模板方法

6.2.2.3.1 共有父类AbstractldleTask
  • 实现了Runnable接口
  • 在执行run方法时首先判断ctx.channel().isOpen(),
    • 当通道关闭了,就不执行任务了。
    • 反之,执行子类的run方法
    private abstract static class AbstractIdleTask implements Runnable {

        private final ChannelHandlerContext ctx;

        AbstractIdleTask(ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        public void run() {
            if (!ctx.channel().isOpen()) {
                return;
            }

            run(ctx);
        }

        protected abstract void run(ChannelHandlerContext ctx);
    }
6.2.2.3.2 读事件的run方法(即ReaderldleTimeoutTask 的run方法)分析
  1. 得到用户设置的超时时间。
  2. 如果读取操作结束了(执行了channelReadComplete方法设置)就用当前时间(ticksInNanos())减去给定时间(readerIdleTimeNanos)和最后一次读时间(lastReadTime)(执操作的时间行了 channelReadComplete方法设置)
    • 如果小于0,就触发事件。
    • 反之,继续放入队列。间隔时间是新的计算时间。
  3. 触发的逻辑是:首先将任务再次放到队列,时间是刚开始设置的时间,返回一个 promise对象,用于做取消操作。然后,设置 first 属性为 false ,表示,下一次读取不再是第一次了,这个属性在channelRead 方法会被改成true.
  4. 创建一个IdleStateEvent类型的写事件对象,将此对象传递给用户的 UserEventTriggered 方法。完成触发事件的操作。
  5. 总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发UserEventTriggered 方法。//前面介绍ldleStateHandler说过,可以看一下
    private final class ReaderIdleTimeoutTask extends AbstractIdleTask {

        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
            super(ctx);
        }

        @Override
        protected void run(ChannelHandlerContext ctx) {
            long nextDelay = readerIdleTimeNanos;
            if (!reading) {
                nextDelay -= ticksInNanos() - lastReadTime;
            }

            if (nextDelay <= 0) {
                // Reader is idle - set a new timeout and notify the callback.
                //用于取消任务promise
                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstReaderIdleEvent;
                firstReaderIdleEvent = false;

                try {
                    //再次提交任务
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                    //触发用户Handler
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Read occurred before the timeout - set a new timeout with shorter delay.
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }
6.2.2.3.2.1 debug分析
  1. 将心跳服务端的读间隔时间设置得小一点,方便观察读事件

    image-20230714111904299

  2. 服务端启动后使用浏览器连接服务端,打上断点

    image-20230714112933138

  3. 一直debug进入方法来到我们自定义Handler的userEventTriggered方法,IdleStateEvent拿到为READER_IDLE读事件,然后做我们自定义的处理

    image-20230714113238144


6.2.2.3.3 写事件的run方法(即WriterldleTimeoutTask 的run方法)分析

写任务的run 代码逻辑基本和读任务的逻辑一样,唯一不同的就是有一个针对出站较慢数据的判断hasOutputChanged

image-20230714113558189

6.2.2.3.3.1 debug分析
  1. 将心跳服务端的写间隔时间设置得小一点,方便观察写事件

    image-20230714114155505

  2. 服务端启动后使用浏览器连接服务端,打上断点

    image-20230714114506021

  3. 一直debug进入方法来到我们自定义Handler的userEventTriggered方法,IdleStateEvent拿到为WRITER_IDLE写事件,然后做我们自定义的处理

    image-20230714114611791

6.2.2.3.4 读写事件的run方法(即AllldleTimeoutTask 的run方法分析
  1. 表示这个监控着所有的事件。当读写事件发生时,都会记录。代码逻辑和写事件的的基本一致

  2. 需要大家注意的地方是 :

    当前时间(ticksInNanos())减去最后一次写或读的时间(谁最大的时间)、若大于0,说明超时了

    image-20230714115406052

  3. **这里的时间计算是取读写事件中的最大值来的。**然后像写事件一样,判断是否发生了写的慢的情况。

6.2.2.3.5.1 debug分析
  1. 将心跳服务端的所有间隔时间设置得小一点,方便观察所以事件

    image-20230714115738490

  2. 服务端启动后使用浏览器连接服务端,打上断点

    image-20230714115848406

  3. 一直debug进入方法来到我们自定义Handler的userEventTriggered方法,IdleStateEvent拿到为ALL_IDLE读写事件,然后做我们自定义的处理

    image-20230714120030282

6.2.3 小结Netty的心跳机制

  1. ldleStatcHandler可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会触发用户 handler的uscrEventTriggered方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关闭连接。
  2. ldleStatcHandler的实现基于EventLoop 的定时任务,每次读写都会记录一个值,在定时任务运行的时候,通过计算当前时间和设置时间和上次事件发生时间的结果,来判断是香空闲。
  3. 内部有3个定时任务,分别对应读事件,写事件,读写事件。通常用户监听读写事件就足够了。
  4. 同时,IdleStatcHandler内部也考虑了一些极端情况:客户端接收缓慢,一次接收数据的速度超过了设置的空闲时间。Netty通过构造方法中的observeOutput 属性来决定是否对出站缓冲区的情况进行判断。
  5. 如果出站缓慢,Netty不认为这是空闲,也就不触发空闲事件。==但第一次无论如何也是要触发的。因为第一次无法判断是出站缓慢还是空闲。==当然,出站缓慢的话,可能造成OOM , OOM比空闲的问题更大。
  6. 所以,当你的应用出现了内存溢出,OOM之类,并且写空闲极少发生(使用了observeOutput为 true) ,那么就需要注意是不是致据出站速度过慢。
  7. 还有一个注意的地方:就是ReadTimeoutHandler,它继承自IdleStateHandler,当触发读空闲事件的时候,就触发ctx.fireExceptionCaught 方法,并传入一个 ReadTimeoutException。然后关闭Socket。
  8. 而 **WriteTimeoutHandler 的实现不是基于IdleStateHandler的,他的原理是,当调用 write方法的时候,会创建一个定时任务,任务内容是根据传入的promise的完成情况来判断是否超出了写的时间。**当定时任务根据指定时间开始运行,发现 promise的 isDone方法返回false,表明还没有写完,说明超时了,则抛出异常。当write方法完成后,会打断定时任务。

7. Netty 核心组件 EventLoop 源码剖析

7.1 源码剖析目的

Echo第一行代码就是 :EventLoopGroup bossGroup = new NioEventLoopGroup(1);下面分析其最核心的组件 EventLoop。

7.2 源码剖析

7.2.1 EventLoop 介绍

image-20230714155959067

说明重点:

  1. ScheduledExecutorService接口表示是一个定时任务接口,EventLoop可以接受定时任务。

    image-20230714162141771

  2. EventLoop 接口:Netty接口文档说明该接口作用:一旦Channel 注册了,就处理该Channel对应的所有I/O操作。

  3. SingleThreadEventExecutor:表示这是一个单个线程的线程池

  4. EventLoop是一个单例的线程池,里面含有一个死循环的线程不断的做着3件事情:

    每个EventLoop都可以绑定多个Channel,而每个Channel 始终只能由一个 EventLoop来处理

    • 监听端口。
    • 处理端口事件。
    • 处理队列事件。

7.2.2 NioEventLoop的使用- execute方法源码剖析

  • 在EventLoop的使用一般就是eventloop.execute(task);

  • execute方法的实现(在SingleThreadEventExecutor类中)

在下图位置打上断点,启动一个服务端

说明:

  1. 首先判断该EventLoop的线程是否是当前线程

    if (!inEventLoop)
    
    • 如果是,直接添加到任务队列中去
    • 如果不是,则尝试启动线程(但由于线程是单个的,因此只能启动一次),随后再将任务添加到队列中去。
  2. 如果线程已经停止,并且删除任务失败,则执行拒绝策略,默认是抛出异常UnsupportedOperationException

    if (isShutdown())
    
  3. 如果addTaskWakesUp是false,并且任务不是 NonWakeupRunnable类型的,就尝试唤醒 selector。这个时候,阻塞在selecor 的线程就会立即返回

    if (!addTaskWakesUp && wakesUpForTask(task))
    
  4. 可以下断点来追踪

image-20230714163718563

7.2.2.1 debug addTask和offerTask方法源码
  1. execute执行到addTask(task)

    image-20230714171245561

  2. addTask方法

    准备将task向taskQueue中添加,添加不成功就采用拒接策略

    image-20230714171339807

  3. offerTask方法

    如果isShutdown是false就添加到taskQueue中,为true就采用拒接策略

    image-20230714171553709

7.2.2.2 NioEventLoop的父类SingleThreadEventExecutor 的startThread方法
  1. 执行到startThread()

    image-20230714171923361

  2. startThread()方法

    说明:

    1. 该方法首先判断是否启动过了,保证 EventLoop只有一个线程
    2. 如果没有启动过,则尝试使用Cas 将 state状态改为 ST_STARTED,也就是已启动。
    3. 然后调用doStartThread 方法。如果失败,则进行回滚

    image-20230714172023556

  3. doStartThread方法会调用run()方法

    image-20230714172131019

  4. 来到我们熟悉的自旋run方法了

    image-20230714174031813

    image-20230714174146054

  5. 上面的三个方法我们就追一下select 方法(体现非阻塞)核心select 方法解析(其他两个方法都追过了,看前面)

    说明:

    • 调用selector 的 select方法,默认阻塞一秒钟
    • 如果有定时任务,则在定时任务剩余时间的基础上在加上0.5秒进行阻塞
    • 当执行execute方法的时候,也就是添加任务的时候,唤醒 selector
      • 防止: selector阻塞时间过长

    image-20230714182735177

7.3 EventLoop作为Netty的核心的运行机制小结

  1. 每次执行 execute方法都是向队列中添加任务。当第一次添加时就启动线程,执行 run方法,而 run方法是整个 EventLoop 的核心,就像EventLoop的名字一样,Loop Loop ,不停的 Loop , Loop做什么呢?做3件事情。
    • 调用selector的 select 方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上再加上0.5秒进行阻塞。当执行 execute方法的时候,也就是添加任务的时候,唤醒 selector,防止 selector阻塞时间过长。
    • 当selector返回的时候,回调用processSelectedKeys方法对selectKey进行处理。
    • 当processSclectedKeys方法执行结束后,则按照 ioRatio的比例执行runAllTasks方法,默认是IO任务时间和非IO任务时间是相同的,你也可以根据你的应用特点进行调优。
      • 比如非IO 任务比较多,那么你就将ioRatio调小一点,这样非IO任务就能执行的长一点。防止队列积攒过多的任务。

8. handler中加入线程池和Context中添加线程池的源码剖析

8.1 源码剖析目的

  1. 在Netty中做耗时的,不可预料的操作,比如数据库,网络请求,会严重影响Netty 对Socket 的处理速度。
  2. 而解决方法就是将耗时任务添加到异步线程池中。但就添加线程池这步操作来讲,可以有2种方式,而且这2种方式实现的区别也蛮大的。
  3. 处理耗时业务的第一种方式—handler中加入线程池
  4. 处理耗时业务的第二种方式—Contcxt中添加线程池
  5. 我们就来分析下两种方式

8.2 源码剖析

8.2.1 解决方案1 用户程序自定义的普通任务

8.2.1.1 EchoClient

说明:

  • 一个简单的客户端,加入了我们自定义的EchoClientHandler
/*
 * 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.echo2;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

/**
 * Sends one message when a connection is open and echoes back any received
 * data to the server.  Simply put, the echo client initiates the ping-pong
 * traffic between the echo client and server by sending the first message to
 * the server.
 */
public final class EchoClient {

    static final boolean SSL = System.getProperty("ssl") != null;
    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
    static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.git
        final SslContext sslCtx;
        if (SSL) {
            sslCtx = SslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        } else {
            sslCtx = null;
        }

        // Configure the client.
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoClientHandler());
                 }
             });

            // Start the client.
            ChannelFuture f = b.connect(HOST, PORT).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }
    }
}
8.2.1.2 EchoClientHandler
  • 通道连接时给服务器发消息
  • 接收服务器消息时打印消息
/*
 * 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.echo2;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.nio.charset.Charset;

/**
 * Handler implementation for the echo client.  It initiates the ping-pong
 * traffic between the echo client and server by sending the first message to
 * the server.
 */
public class EchoClientHandler extends ChannelInboundHandlerAdapter {

    private final ByteBuf firstMessage;

    /**
     * Creates a client-side handler.
     */
    public EchoClientHandler() {
        firstMessage = Unpooled.buffer(EchoClient.SIZE);
        for (int i = 0; i < firstMessage.capacity(); i ++) {
            firstMessage.writeByte((byte) i);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {

        //给服务器发消息
        ctx.writeAndFlush(Unpooled.copiedBuffer(("hello i am client").getBytes()));


    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {

        //接收服务器消息
        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String s = new String(bytes, Charset.forName("UTF-8"));
        System.out.println("s=" + s);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
       ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        //cause.printStackTrace();
        ctx.close();
    }
}
8.2.1.3 EchoServer

一个简单的服务端,加入我们自定义的EchoServerHandler

/*
 * 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.echo2;

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;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;

/**
 * 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", "8007"));

    //创建业务线程池
    //这里我们就创建2个子线程
    static final EventExecutorGroup group = new DefaultEventExecutorGroup(2);

    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();
        }
    }
}
8.2.1.4 EchoServerHandler
  • 读取客户发送的消息时模拟耗时任务-用户程序自定义的普通任务来处理
  • 打印出Handler的线程和普通任务中的线程
/*
 * 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.echo2;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;

/**
 * Handler implementation for the echo server.
 */
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        System.out.println("EchoServer Handler 的线程是=" + Thread.currentThread().getName());

        //按照原来的方法处理耗时任务

        //解决方案1 用户程序自定义的普通任务

        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    //输出线程名
                    System.out.println("EchoServerHandler execute 线程是=" + Thread.currentThread().getName());
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));

                } catch (Exception ex) {
                    System.out.println("发生异常" + ex.getMessage());
                }
            }
        });

        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    //输出线程名
                    System.out.println("EchoServerHandler execute 线程2是=" + Thread.currentThread().getName());
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));

                } catch (Exception ex) {
                    System.out.println("发生异常" + ex.getMessage());
                }
            }
        });

        System.out.println("go on ");


    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        //cause.printStackTrace();
        ctx.close();
    }
}
8.2.1.5 测试
  1. 启动服务端

    image-20230715110941242

  2. 启动客户端

    image-20230715111028587

    image-20230715111035785

  3. 启动5秒后

    image-20230715111106006

    我们发现EchoServerHandler execute使用的线程和EchoServerHandler一样

    image-20230715111116897

  4. 再过5秒后

    image-20230715111639239

    我们发现第二个EchoServerHandler execute使用的线程和EchoServerHandler也是一样

    image-20230715111655611

小结: 用户程序自定义的普通任务来执行任务只有一个线程执行,还是会阻塞队列,生产时不会使用

8.2.2 处理耗时业务的第一种方式—handler中加入线程池

8.2.2.1 EchoServerHandler源码修改
  1. 将8.2.1.4中channelRead方法中使用ctx.channel().eventLoop().execute()的方式注释起来

  2. 新建一个自定义线程池

    image-20230715112644749

  3. 想业务线程池group中提交三次任务,三次都等待10秒,模拟业业务处理10秒

    image-20230715112807657

8.2.2.2 测试
  1. 启动服务端

    image-20230715112951242

  2. 启动客户端

    image-20230715113057109

    image-20230715113107364

  3. 启动10秒后

    image-20230715113126231

    • 发现10秒后三个提交的异步任务都返回了,并没有阻塞
    • 每个异步任务使用的都是业务线程池group中的线程,而不是EchoServerHandler中的线程
    • 并且每个任务使用group线程池中的线程都不一样

    image-20230715113135029

8.2.2.3 使用这种方式后的逻辑

image-20230715113818771

  1. 解释一下上图,当IO 线程轮询到一个 socket 事件,然后,IO 线程开始处理,当走到耗时 handler 的时候,将耗时任务交给业务线程池。

  2. 当耗时任务执行完毕再执行 pipeline write方法的时候(代码中使用的是 context的 write方法,上图画的是执行pipeline方法,是一个意思)会将任务这个任务交给IO线程

    image-20230715114103972

  3. write方法的源码(在AbstractChannelHandlerContext类)

    说明:

    1. 当判定下个outbound 的 executor线程不是当前线程的时候,会将当前的工作封装成task ,然后放入mpsc队列中,等待IO任务执行完毕后执行队列中的任务。
    2. 这里可以Debug 来验证(提醒:Debug时,服务器端Debug ,客户端Run的方式),
      • 当我们使用了group.submit(new Callable<Object>()在 handler 中加入线程池,就会进入到safeExecute(executor,task,promise,m);
      • 如果使用普通方式来执行耗时的业务,那么就不会进入到 safeExecute(executor,task, promise, m); 而是进入next.invokeWriteAndFlush(m, promise);就不会使用业务线程池中的线程

    image-20230715114514692

8.2.3 处理耗时业务的第二种方式-Context中添加线程池

8.2.3.1 EchoServerHandler源码修改
  • 将8.2.2.1中使用group提交任务的代码也注释掉

  • 使用普通方式

            //普通方式
            //接收客户端信息
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            String body = new String(bytes, "UTF-8");
            //休眠10秒
            Thread.sleep(10 * 1000);
            System.out.println("普通调用方式的 线程是=" + Thread.currentThread().getName());
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
    
image-20230715115859965
8.2.3.2 EchoServer源码修改
  • 这次我们在EchoServer创建自定义线程池group
  • 然后在addLast的时候就加入线程池

image-20230715120101598

8.2.3.3 测试
  1. 启动服务端

    image-20230715120746084

  2. 启动一个客户端

    image-20230715120818666

    image-20230715120827719

  3. 10秒后

    image-20230715120850758

    image-20230715120901286

  4. 再启动一个客户端

    image-20230715120948563

    image-20230715120955809

  5. 10秒后

    image-20230715121018536

    image-20230715121026480

小结:虽然IO和业务使用的同一个线程,但是不同的客户端连接的时候也会使用不同的线程,所以还是异步的

8.2.3.4 源码剖析

说明:

  1. handler中的代码就使用普通的方式来处理耗时业务。

  2. 当我们在调用addLast 方法添加线程池后,handler 将优先使用这个线程池,如果不添加,将使用IO 线程

  3. 当走到AbstractChannelHandlerContext 的invokeChannelRead 方法的时候,executor.inEventLoop()是不会通过的,因为当前线程是IO线程Context(也就是Handler)的 executor 是业务线程,所以会异步执行

  4. 验证时,我们如果去掉 p.addLast(group,new EchoServerHandler() );改成p.addL.astnewEchoServerHandler() );你会发现代码不会进行异步执行

    image-20230715121837151

  5. 后面的整个流程就变成和第一个方式一样了

8.2.4 两种方式的比较

  1. 第一种方式在 handler 中添加异步,可能更加的自由
    • 优点:比如如果需要访问数据库,那我就异步,如果不需要,就不异步,异步会拖长接口响应时间。因为需要将任务放进 mpscTask 中。
    • 缺点:如果IO 时间很短,task 很多,可能一个循环下来,都没时间执行整个task,导致响应时间达不到指标。
  2. 第二种方式是 Netty标准方式(即加入到队列),但是,这么做会将整个 handler 都交给业务线程池。不论耗时不耗时,都加入到队列里,不够灵活。
  3. 各有优劣,从灵活性考虑,第一种较好
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty5.0 架构剖析源码解读 作者:李林锋 版权所有 email neu_lilinfeng@ © Netty5.0 架构剖析源码解读1 1. 概述2 1.1. JAVA 的IO演进2 1.1.1. 传统BIO通信的弊端2 1.1.2. Linux 的网络IO模型简介4 1.1.3. IO复用技术介绍7 1.1.4. JAVA的异步IO8 1.1.5. 业界主流的NIO框架介绍10 2.NIO入门10 2.1. NIO服务端10 2.2. NIO客户端13 3.Netty源码分析16 3.1. 服务端创建16 3.1.1. 服务端启动辅助类ServerBootstrap16 3.1.2. NioServerSocketChannel 的注册21 3.1.3. 新的客户端接入25 3.2. 客户端创建28 3.2.1. 客户端连接辅助类Bootstrap28 3.2.2. 服务端返回ACK应答,客户端连接成功32 3.3. 读操作33 3.3.1. 异步读取消息33 3.4. 写操作39 3.4.1. 异步消息发送39 3.4.2. Flush操作42 4.Netty架构50 4.1. 逻辑架构50 5. 附录51 5.1. 作者简介51 5.2. 使用声明51 1. 概述 1.1.JAVA 的IO演进 1.1.1. 传统BIO通信的弊端 在JDK 1.4推出JAVANIO1.0之前,基于JAVA 的所有Socket通信都采用 BIO 了同步阻塞模式( ),这种一请求一应答的通信模型简化了上层的应用开发, 但是在可靠性和性能方面存在巨大的弊端。所以,在很长一段时间,大型的应 C C++ 用服务器都采用 或者 开发。当并发访问量增大、响应时间延迟变大后, 采用JAVABIO作为服务端的软件只有通过硬件不断的扩容来满足访问量的激 增,它大大增加了企业的成本,随着集群的膨胀,系统的可维护性也面临巨大 的挑战,解决这个问题已经刻不容缓。 首先,我们通过下面这幅图来看下采用BIO 的服务端通信模型:采用BIO 通信模型的 1connect NewThread1 WebBrowse 2connect 2handle(Req) WebBrowse 3connect Acceptor NewThread2 WebBrowse WebBrowse 4connect NewThread3 3sendResponsetopeer NewThread4 图1.1.1-1 BIO通信模型图 服务端,通常由一个独立的Accepto 线程负责监听客户端的连接,接收到客户 端连接之后为客户端连接创建一个新的线程处理请求消息
FastThreadLocal 是 Netty 中的一个优化版 ThreadLocal 实现。与 JDK 自带的 ThreadLocal 相比,FastThreadLocal 在性能上有所提升。 FastThreadLocal 的性能优势主要体现在以下几个方面: 1. 线程安全性:FastThreadLocal 使用了一种高效的方式来保证线程安全,避免了使用锁的开销,使得在高并发场景下性能更好。 2. 内存占用:FastThreadLocal 的内部数据结构更加紧凑,占用的内存更少,减少了对堆内存的占用,提高了内存的利用效率。 3. 访问速度:FastThreadLocal 在访问时,使用了直接索引的方式,避免了哈希表查找的开销,使得访问速度更快。 在 Netty 源码中,FastThreadLocal 主要被用于优化线程的局部变量存储,提高线程之间的数据隔离性和访问效率。通过使用 FastThreadLocal,Netty 在高性能的网络通信中能够更好地管理线程的局部变量,提供更高的性能和并发能力。 引用中提到的代码片段展示了 Netty 中的 InternalThreadLocalMap 的获取方式。如果当前线程是 FastThreadLocalThread 类型的线程,那么就直接调用 fastGet 方法来获取 InternalThreadLocalMap 实例;否则,调用 slowGet 方法来获取。 fastGet 方法中,会先尝试获取线程的 threadLocalMap 属性,如果不存在则创建一个新的 InternalThreadLocalMap,并设置为线程的 threadLocalMap 属性。最后返回获取到的 threadLocalMap。 slowGet 方法中,通过调用 UnpaddedInternalThreadLocalMap.slowThreadLocalMap 的 get 方法来获取 InternalThreadLocalMap 实例。如果获取到的实例为 null,则创建一个新的 InternalThreadLocalMap,并将其设置到 slowThreadLocalMap 中。最后返回获取到的 InternalThreadLocalMap。 综上所述,FastThreadLocal 是 Netty 中为了优化线程局部变量存储而设计的一种高性能的 ThreadLocal 实现。它通过减少锁的开销、优化内存占用和加快访问速度来提升性能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [FastThreadLocal源码分析](https://blog.csdn.net/lvlei19911108/article/details/118021402)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Netty 高性能之道 FastThreadLocal 源码分析(快且安全)](https://blog.csdn.net/weixin_33871366/article/details/94653953)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值