三、Netty源码之请求处理流程

Netty源码之请求处理流程

Netty源码之启动服务

我们了解到Netty本身是基于JDK的NIO进行优化和改进一个框架,其实Netty本身还是基于JDK的NIO实现,所以我们再次把JDK的NIO的流程拉出来看一看。

image.png

从上图可以看到,要启动NIO,那么就需要创建Selector,同样Netty要启动,同样也需要创建Selector。

那么我们可以采用调试跟踪的方式来分析源码。

主线:

主线程

  • 创建Selector
  • 创建ServerSocketChannel
  • 初始化ServerSocketChannel
  • 给ServerSocketChannel 从 boss group 中选择一个 NioEventLoop

BossThread

  • 将ServerSocketChannel 注册到选择的 NioEventLoop的Selector
  • 绑定地址启动
  • 注册接受连接事件(OP_ACCEPT)到Selector 上

主线程

创建Selector

调试案例中的启动Netty服务端的代码

image.png

image.png

image.png

最后可以得出:NioEventLoopGroup的构造方法中调用JDK的SelectorProvider中provider()创建 selector

创建ServerSocketChannel

那么我们需要在这里加上断点:

image.png

image.png

image.png

image.png

image.png

初始化ServerSocketChannel

image.png

image.png

给ServerSocketChannel 从boss group中选择一个NioEventLoop

image.png

image.png

image.png

image.png

image.png

image.png

从上图可以看到,这里都是main方法线程。那么main方法线程其实已经完成了从boss group中选择一个NioEventLoop,不要还要做的事情就是衔接NioEventLoop的子线程。

image.png

image.png

BossThread

将ServerSocketChannel 注册到选择的 NioEventLoop的Selector

image.png

image.png

image.png

绑定地址启动

image.png

image.png

image.png

image.png

所以这里就进行了绑定地址启动。(这里因为涉及到责任链模式pipeline,我们先快速过,后面再来讲)

image.png

image.png

image.png

注册接受连接事件(OP_ACCEPT)到Selector上

image.png

在断点处,使用alt+鼠标左键点击pipeline,

image.png

image.png

这里发现处理都是DefaultChannelPipeline这个类

image.png

加个断点继续调试

image.png

image.png

image.png

image.png

Netty之责任链模式

适用场景:

  • 对于一个请求来说,如果有个对象都有机会处理它,而且不明确到底是哪个对象会处理请求时,我们可以考虑使用责任链模式实现它,让请求从链的头部往后移动,直到链上的一个节点成功处理了它为止

优点:

  • 发送者不需要知道自己发送的这个请求到底会被哪个对象处理掉,实现了发送者和接受者的解耦
  • 简化了发送者对象的设计
  • 可以动态的添加节点和删除节点

缺点:

  • 所有的请求都从链的头部开始遍历,对性能有损耗
  • 极差的情况,不保证请求一定会被处理

Netty的pipeline设计,就采用了责任链设计模式,底层采用双向链表的数据结构,将链上的各个处理器串联起来

客户端每一个请求的到来,netty都认为pipeline中的所有的处理器都有机会处理它。

image.png

package com.msb.netty.pipeline;

import java.text.DecimalFormat;

/**
 * Pipeline实现类
 */
public class Pipeline {
    //handler上下文,维护链表和负责链表的执行
    class HandlerChainContext {
        HandlerChainContext next;// 持有下一个节点
        AbstractHandler handler;
        public HandlerChainContext(AbstractHandler handler) {
            this.handler = handler;
        }
        // 将节点持有下去
        void handler(Object arg0) {
            this.handler.doHandler(this, arg0);
        }
        // 继续执行下一个
        void runNext(Object arg0) {
            if (this.next != null) {
                this.next.handler(arg0);
            }
        }
    }
    // 持有上下文(可以获得需要的数据,属性)
    public HandlerChainContext context = new HandlerChainContext(new AbstractHandler() {
        @Override
        void doHandler(HandlerChainContext context, Object arg0) {
            System.out.println("折扣前"+arg0);
            context.runNext(arg0);
        }
    });

    // 添加责任链
    public void addLast(AbstractHandler handler) {
        HandlerChainContext next = context;
        while (next.next != null) {
            next = next.next;
        }
        next.next = new HandlerChainContext(handler);
    }

    // 开始调用
    public void requestProcess(Object arg0) {
        context.handler(arg0);
    }
    //处理器抽象类
    static abstract class AbstractHandler {
        abstract void doHandler(HandlerChainContext context, Object arg0);
    }
    //具体的处理器实现类(购物折扣1)
    static class Handler1 extends AbstractHandler {
        @Override
        void doHandler(HandlerChainContext handlerChainContext, Object arg0) {
            System.out.println("--首次购买打9折!");
            arg0 = new DecimalFormat("0.00").format(Double.valueOf(arg0.toString())*0.9);
            System.out.println("折扣后金额:"+arg0);
            // 继续执行下一个
            handlerChainContext.runNext(arg0);
        }
    }
    //具体的处理器实现类(购物折扣2)
    static class Handler2 extends AbstractHandler {
        @Override
        void doHandler(HandlerChainContext handlerChainContext, Object arg0) {
            System.out.println("--满200减20!");
            if(Double.valueOf(arg0.toString()) >= 200){
                arg0 = Double.valueOf(arg0.toString())-20;
                // 继续执行下一个
                System.out.println("折扣后金额:"+arg0);
                handlerChainContext.runNext(arg0);

            }else{
                System.out.println("不满足条件,折扣结束:"+arg0);
            }
        }
    }
    //具体的处理器实现类(购物折扣3)
    static class Handler3 extends AbstractHandler {
        @Override
        void doHandler(HandlerChainContext handlerChainContext, Object arg0) {
            System.out.println("--第二件减10元!");
            arg0 = Double.valueOf(arg0.toString())-20;
            System.out.println("折扣后金额:"+arg0);
            // 继续执行下一个
            handlerChainContext.runNext(arg0);
        }
    }

    public static void main(String[] args) {
        Pipeline p = new Pipeline();
        p.addLast(new Handler1());
        p.addLast(new Handler2());
        p.addLast(new Handler3());

        p.requestProcess("500");
    }

}

Netty的责任链设计

根据我们在前面对Netty的使用,我们知道,我们不需要自己创建pipeline,因为使用ServerBootstrap或者Bootstrap启动服务端或者客户端时,Netty会为每个Channel连接创建一个独立的pipeline。对于使用者而言,只需要将自定义的拦截器加入到 pipeline 中即可。

ChannelPipeline支持运行态动态的添加或者删除 ChannelHandler,在某些场景下这个特性非常实用。例如当业务高峰期需要对系统做拥塞保护时,就可以根据当前的系统时间进行判断,如果处于业务高峰期,则动态地将系统拥塞保护ChannelHandler添加到当前的ChannelPipeline中,当高峰期过去之后,就可以动态删除拥塞保护 ChannelHandler了。

ChannelPipeline是线程安全的,这意味着N个业务线程可以并发地操作ChannelPipeline而不存在多线程并发问题。但是,ChannelHandler却不是线程安全的,这意味着尽管ChannelPipeline是线程安全的,但是用户仍然需要自己保证ChannelHandler的线程安全。

netty的责任处理器接口

责任处理器接口, pipeline中的所有的handler的顶级抽象接口,它规定了所有的handler统一要有添加,移除,异常捕获的行为。netty对责任处理接口,做了更细粒度的划分, 处理器被分成了两种, 一种是入站处理器 ChannelInboundHandler,另一种是出站处理器 ChannelOutboundHandler,这两个接口都继承自ChannelHandler

image.png

ChannelInboundHandler的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel 状态发生改变时被调用。正如我们前面所提到的,这些方法和Channel 的生命周期密切相关。

image.png

ChannelOutboundHandler出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、Channel-Pipeline 以及ChannelHandlerContext 调用。

image.png

添加删除责任处理器的接口

netty中所有的处理器最终都在添加在pipeline上,所以,添加删除责任处理器的接口的行为 netty在channelPipeline中的进行了规定

image.png

上下文

pipeline中的handler被封装进了上下文中,如下, 通过上下文,可以轻松拿到当前节点所属的channel, 以及它的线程执行器

image.png

alloc 返回和这个实例相关联的Channel 所配置的ByteBufAllocator
bind 绑定到给定的SocketAddress,并返回ChannelFuture
channel 返回绑定到这个实例的Channel
close 关闭Channel,并返回ChannelFuture
connect 连接给定的SocketAddress,并返回ChannelFuture
deregister 从之前分配的EventExecutor 注销,并返回ChannelFuture
disconnect 从远程节点断开,并返回ChannelFuture
executor 返回调度事件的EventExecutor
fireChannelActive 触发对下一个ChannelInboundHandler 上的channelActive()方法(已连接)的调用
fireChannelInactive 触发对下一个ChannelInboundHandler 上的channelInactive()方法(已关闭)的调用
fireChannelRead 触发对下一个ChannelInboundHandler 上的channelRead()方法(已接收的消息)的调用
fireChannelReadComplete 触发对下一个ChannelInboundHandler 上的channelReadComplete()方法的调用
fireChannelRegistered 触发对下一个ChannelInboundHandler 上的fireChannelRegistered()方法的调用
fireChannelUnregistered 触发对下一个ChannelInboundHandler 上的fireChannelUnregistered()方法的调用
fireChannelWritabilityChanged 触发对下一个ChannelInboundHandler 上的fireChannelWritabilityChanged()方法的调用
fireExceptionCaught 触发对下一个ChannelInboundHandler 上的fireExceptionCaught(Throwable)方法的调用
fireUserEventTriggered 触发对下一个ChannelInboundHandler 上的fireUserEventTriggered(Object evt)方法的调用
handler 返回绑定到这个实例的ChannelHandler
isRemoved 如果所关联的ChannelHandler 已经被从ChannelPipeline中移除则返回true
name 返回这个实例的唯一名称
pipeline 返回这个实例所关联的ChannelPipeline
read 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发一个channelRead事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler 的channelReadComplete(ctx)方法
write 通过这个实例写入消息并经过ChannelPipeline
writeAndFlush 通过这个实例写入并冲刷消息并经过ChannelPipeline

责任终止机制
  • 在pipeline中的任意一个节点,只要我们不手动的往下传播下去,这个事件就会终止传播在当前节点
  • 对于入站数据,默认会传递到尾节点,进行回收,如果我们不进行下一步传播,事件就会终止在当前节点,别忘记回收msg
  • 对于出站数据,用header节点的使用unsafe对象,把数据写会客户端也意味着事件的终止
事件的传播

底层事件的传播使用的就是针对链表的操作

image.png

Netty的责任链源码解读

image.png

image.png

image.png

Netty源码之构建连接

从前面我们知道一个技术点,就是绑定IP也好,处理连接也好都是NioEventLoop来做,于是我们来看对应的核心run方法

image.png

image.png

image.png

image.png

image.png

断点在这里调试一下,同时我们看下ops的标志代表着什么?

image.png

在这里我们需要debug一下看下连接事件的发生

image.png

然后启动一个客户端连接

image.png

再回到服务端的断点,可以看到ops=16,这个是accpct

image.png

image.png

image.png

image.png

image.png

image.png跟完之后整体流程图就出来了

image.png

Netty源码之接收数据

接收数据,也就是读数据(对于服务端来说),也是在NioEventLoop中操作的,不过具体的实现却不同。

image.png

具体再回到构建连接之后代码

image.png

这里很明显使用到了责任链模式

然后我们回到NioEventLoop中在以下位置打上断点,再次调试运行server,同时发起client调用

第一次进入断点,发现这里是NioServerSocketChannel说明走的是创建连接

image.png

第二次进入断点,发现这里是NioSocketChannel,这里走的应该是读事件了

image.png

另外也可以观察到两次进入的方法除了对象不同,线程也不同,因为这里采用了主从的方式。

然后我们接着进入来看读数据如何处理的

image.png

image.png

读数据技巧

每次该读多大

自适应数据大小的分配器(AdaptiveRecvByteBufAllocator)

第一次靠猜,默认是1024

image.png

image.png

image.png

然后会根据情况进行buffer大小的调整,默认 都是这个AdaptiveRecvByteBufAllocator的record方法来调整。

image.png

不过这里有两种情况,第一种就是读的数据超过1024,那么就要扩容,如果是小于1024,那么就要缩容

image.png

image.png

image.png

这段业务逻辑比较复杂,不过看这个类的话可以得出结论

image.png

image.png

这里要注意的一个点就是。

AdaptiveRecvByteBufAllocator 对 bytebuf 的猜测:放大果断,缩小谨慎(需要连续 2 次判断)

image.png

连续尝试读取16次

image.png

image.png

image.png

image.png

"什么是16次最优,不是其它的2的幂次方倍?"这个本身不是问题,因为写32的话,别人也会问为什么不是16,这里的思想是“雨露均沾”,就是说给别人读的机会,所以要控制中的次数(不要无限循环)

如果是超过16次数据还没有读完也没关系,还会触发读事件的,所以还是能处理。是说把这次机会(否则一直读下去,别的连接就没有什么机会读到了)让出去了。

Netty源码之业务处理

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png跳转几次后,就可以进入自己写的业务的handler

image.png

image.png

image.png

image.png

总结一下就是:

**处理业务本质:**数据在 pipeline 中所有的 handler 的 channelRead() 执行过程
Handler 要实现 io.netty.channel.ChannelInboundHandler#channelRead (ChannelHandlerContext ctx ,
Object msg),且不能加注解 @Skip 才能被执行到

Netty源码之写数据

write :写到一个 buffer

image.png

image.png

image.png

调试一下,走到执行这里

image.png

image.png

image.png

image.png

image.png

image.png

这个地方为什么要做这个处理?

Netty 待写数据太多,超过一定的水位线( writeBufferWaterMark.high) () ),会将可写的标志位改成
false ,让应用端自己做决定要不要发送数据了

image.png

flush : 把 buffer 里的数据发送出去

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

这里可以分析出两个亮点:

1、只要有数据要写,且能写,则一直尝试,直到 16 次( writeSpinCount ),写 16 次还没有写完,就直接 schedule 一个 task 来继续写,而不是用注册写事件来触发,更简洁有力。
2、批量写数据时,如果尝试写的都写进去了,接下来会尝试写更多( maxBytesPerGatheringWrite )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JavaHxg

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值