Netty源码之请求处理流程
Netty源码之启动服务
我们了解到Netty本身是基于JDK的NIO进行优化和改进一个框架,其实Netty本身还是基于JDK的NIO实现,所以我们再次把JDK的NIO的流程拉出来看一看。
从上图可以看到,要启动NIO,那么就需要创建Selector,同样Netty要启动,同样也需要创建Selector。
那么我们可以采用调试跟踪的方式来分析源码。
主线:
主线程
- 创建Selector
- 创建ServerSocketChannel
- 初始化ServerSocketChannel
- 给ServerSocketChannel 从 boss group 中选择一个 NioEventLoop
BossThread
- 将ServerSocketChannel 注册到选择的 NioEventLoop的Selector
- 绑定地址启动
- 注册接受连接事件(OP_ACCEPT)到Selector 上
主线程
创建Selector
调试案例中的启动Netty服务端的代码
最后可以得出:NioEventLoopGroup的构造方法中调用JDK的SelectorProvider中provider()创建 selector
创建ServerSocketChannel
那么我们需要在这里加上断点:
初始化ServerSocketChannel
给ServerSocketChannel 从boss group中选择一个NioEventLoop
从上图可以看到,这里都是main方法线程。那么main方法线程其实已经完成了从boss group中选择一个NioEventLoop,不要还要做的事情就是衔接NioEventLoop的子线程。
BossThread
将ServerSocketChannel 注册到选择的 NioEventLoop的Selector
绑定地址启动
所以这里就进行了绑定地址启动。(这里因为涉及到责任链模式pipeline,我们先快速过,后面再来讲)
注册接受连接事件(OP_ACCEPT)到Selector上
在断点处,使用alt+鼠标左键点击pipeline,
这里发现处理都是DefaultChannelPipeline这个类
加个断点继续调试
Netty之责任链模式
适用场景:
- 对于一个请求来说,如果有个对象都有机会处理它,而且不明确到底是哪个对象会处理请求时,我们可以考虑使用责任链模式实现它,让请求从链的头部往后移动,直到链上的一个节点成功处理了它为止
优点:
- 发送者不需要知道自己发送的这个请求到底会被哪个对象处理掉,实现了发送者和接受者的解耦
- 简化了发送者对象的设计
- 可以动态的添加节点和删除节点
缺点:
- 所有的请求都从链的头部开始遍历,对性能有损耗
- 极差的情况,不保证请求一定会被处理
Netty的pipeline设计,就采用了责任链设计模式,底层采用双向链表的数据结构,将链上的各个处理器串联起来
客户端每一个请求的到来,netty都认为pipeline中的所有的处理器都有机会处理它。
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
ChannelInboundHandler的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel 状态发生改变时被调用。正如我们前面所提到的,这些方法和Channel 的生命周期密切相关。
ChannelOutboundHandler出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、Channel-Pipeline 以及ChannelHandlerContext 调用。
添加删除责任处理器的接口
netty中所有的处理器最终都在添加在pipeline上,所以,添加删除责任处理器的接口的行为 netty在channelPipeline中的进行了规定
上下文
pipeline中的handler被封装进了上下文中,如下, 通过上下文,可以轻松拿到当前节点所属的channel, 以及它的线程执行器
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对象,把数据写会客户端也意味着事件的终止
事件的传播
底层事件的传播使用的就是针对链表的操作
Netty的责任链源码解读
Netty源码之构建连接
从前面我们知道一个技术点,就是绑定IP也好,处理连接也好都是NioEventLoop来做,于是我们来看对应的核心run方法
断点在这里调试一下,同时我们看下ops的标志代表着什么?
在这里我们需要debug一下看下连接事件的发生
然后启动一个客户端连接
再回到服务端的断点,可以看到ops=16,这个是accpct
跟完之后整体流程图就出来了
Netty源码之接收数据
接收数据,也就是读数据(对于服务端来说),也是在NioEventLoop中操作的,不过具体的实现却不同。
具体再回到构建连接之后代码
这里很明显使用到了责任链模式
然后我们回到NioEventLoop中在以下位置打上断点,再次调试运行server,同时发起client调用
第一次进入断点,发现这里是NioServerSocketChannel说明走的是创建连接
第二次进入断点,发现这里是NioSocketChannel,这里走的应该是读事件了
另外也可以观察到两次进入的方法除了对象不同,线程也不同,因为这里采用了主从的方式。
然后我们接着进入来看读数据如何处理的
读数据技巧
每次该读多大
自适应数据大小的分配器(AdaptiveRecvByteBufAllocator)
第一次靠猜,默认是1024
然后会根据情况进行buffer大小的调整,默认 都是这个AdaptiveRecvByteBufAllocator的record方法来调整。
不过这里有两种情况,第一种就是读的数据超过1024,那么就要扩容,如果是小于1024,那么就要缩容
这段业务逻辑比较复杂,不过看这个类的话可以得出结论
这里要注意的一个点就是。
AdaptiveRecvByteBufAllocator 对 bytebuf 的猜测:放大果断,缩小谨慎(需要连续 2 次判断)
连续尝试读取16次
"什么是16次最优,不是其它的2的幂次方倍?"这个本身不是问题,因为写32的话,别人也会问为什么不是16,这里的思想是“雨露均沾”,就是说给别人读的机会,所以要控制中的次数(不要无限循环)
如果是超过16次数据还没有读完也没关系,还会触发读事件的,所以还是能处理。是说把这次机会(否则一直读下去,别的连接就没有什么机会读到了)让出去了。
Netty源码之业务处理
跳转几次后,就可以进入自己写的业务的handler
总结一下就是:
**处理业务本质:**数据在 pipeline 中所有的 handler 的 channelRead() 执行过程
Handler 要实现 io.netty.channel.ChannelInboundHandler#channelRead (ChannelHandlerContext ctx ,
Object msg),且不能加注解 @Skip 才能被执行到
Netty源码之写数据
write :写到一个 buffer
调试一下,走到执行这里
这个地方为什么要做这个处理?
Netty 待写数据太多,超过一定的水位线( writeBufferWaterMark.high) () ),会将可写的标志位改成
false ,让应用端自己做决定要不要发送数据了
flush : 把 buffer 里的数据发送出去
这里可以分析出两个亮点:
1、只要有数据要写,且能写,则一直尝试,直到 16 次( writeSpinCount ),写 16 次还没有写完,就直接 schedule 一个 task 来继续写,而不是用注册写事件来触发,更简洁有力。
2、批量写数据时,如果尝试写的都写进去了,接下来会尝试写更多( maxBytesPerGatheringWrite )