本篇文章次要详细分析 Netty 中的外围组件。
启动器 Bootstrap 和 ServerBootstrap 作为 Netty 构建客户端和服务端的路口,是编写 Netty 网络程序的第一步。它能够让咱们把 Netty 的外围组件像搭积木一样组装在一起。在 Netty Server 端构建的过程中,咱们须要关注三个重要的步骤
- 配置线程池
- Channel 初始化
- Handler 处理器构建
调度器详解
后面咱们讲过 NIO 多路复用的设计模式之 Reactor 模型,Reactor 模型的次要思维就是把网络连接、事件散发、工作解决的职责进行拆散,并且通过引入多线程来进步 Reactor 模型中的吞吐量。其中包含三种 Reactor 模型
- 单线程单 Reactor 模型
- 多线程单 Reactor 模型
- 多线程多 Reactor 模型
在 Netty 中,能够十分轻松的实现上述三种线程模型,并且 Netty 举荐应用主从多线程模型,这样就能够轻松的实现成千上万的客户端连贯的解决。在海量的客户端并发申请中,主从多线程模型能够通过减少 SubReactor 线程数量,充分利用多核能力晋升零碎吞吐量。
Reactor 模型的运行机制分为四个步骤,如图 2 -10 所示。
- 连贯注册,Channel 建设后,注册到 Reactor 线程中的 Selector 选择器
- 事件轮询,轮询 Selector 选择器中曾经注册的所有 Channel 的 I / O 事件
- 事件散发,为准备就绪的 I / O 事件调配相应的解决线程
- 工作解决,Reactor 线程还负责工作队列中的非 I / O 工作,每个 Worker 线程从各自保护的工作队列中取出工作异步执行。
<center> 图 2 -10 Reactor 工作流程 </center>
EventLoop 事件循环
在 Netty 中,Reactor 模型的事件处理器是应用 EventLoop 来实现的,一个 EventLoop 对应一个线程,EventLoop 外部保护了一个 Selector 和 taskQueue,别离用来解决网络 IO 事件以及外部工作,它的工作原理如图 2 -11 所示。
<center> 图 2 -11 NioEventLoop 原理 </center>
EventLoop 根本利用
上面这段代码示意 EventLoop,别离实现 Selector 注册以及一般工作提交性能。
public class EventLoopExample {public static void main(String[] args) {EventLoopGroup group=new NioEventLoopGroup(2);
System.out.println(group.next()); // 输入第一个 NioEventLoop
System.out.println(group.next()); // 输入第二个 NioEventLoop
System.out.println(group.next()); // 因为只有两个,所以又会从第一个开始
// 获取一个事件循环对象 NioEventLoop
group.next().register(); // 注册到 selector 上
group.next().submit(()->{System.out.println(Thread.currentThread().getName()+"-----");
});
}
}
EventLoop 的外围流程
基于上述的解说,了解了 EventLoop 的工作机制后,咱们再通过一个整体的流程图来阐明,如图 2 -12 所示。
EventLoop 是一个 Reactor 模型 的事件处理器,一个 EventLoop 对应一个线程,其外部会保护一个 selector 和 taskQueue,负责解决 IO 事件和外部工作。IO 事件和外部工作执行工夫百分比通过 ioRatio 来调节,ioRatio 示意执行 IO 工夫所占百分比。工作包含一般工作和曾经到时的提早工作,提早工作寄存到一个优先级队列 PriorityQueue 中,执行工作前从 PriorityQueue 读取所有到时的 task,而后增加到 taskQueue 中,最初对立执行 task。
<center> 图 2 -12 EventLoop 工作机制 </center>
EventLoop 如何实现多种 Reactor 模型
-
单线程模式
EventLoopGroup group=new NioEventLoopGroup(1); ServerBootstrap b=new ServerBootstrap(); b.group(group);
-
多线程模式
EventLoopGroup group =new NioEventLoopGroup(); // 默认会设置 cpu 外围数的 2 倍 ServerBootstrap b=new ServerBootstrap(); b.group(group);
-
多线程主从模式
EventLoopGroup boss=new NioEventLoopGroup(1); EventLoopGroup work=new NioEventLoopGroup(); ServerBootstrap b=new ServerBootstrap(); b.group(boss,work);
EventLoop 实现原理
-
EventLoopGroup 初始化办法,在 MultithreadEventExecutorGroup.java 中,依据配置的 nThreads 数量,构建一个 EventExecutor 数组
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) {checkPositive(nThreads, "nThreads"); if (executor == null) {executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); } children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) { boolean success = false; try {children[i] = newChild(executor, args); } } }
-
注册 channel 到多路复用器的实现,MultithreadEventLoopGroup.register 办法()
SingleThreadEventLoop ->AbstractUnsafe.register ->AbstractChannel.register0->AbstractNioChannel.doRegister()
能够看到会把 channel 注册到某一个 eventLoop 中的 unwrappedSelector 复路器中。
protected void doRegister() throws Exception { boolean selected = false; for (;;) { try {selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); return; } } }
-
事件处理过程,通过 NioEventLoop 中的 run 办法一直遍历
protected void run() { int selectCnt = 0; for (;;) { try { int strategy; try { // 计算策略,依据阻塞队列中是否含有工作来决定以后的解决形式 strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); switch (strategy) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.BUSY_WAIT: // fall-through to SELECT since the busy-wait is not supported with NIO case SelectStrategy.SELECT: long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); if (curDeadlineNanos == -1L) {curDeadlineNanos = NONE; // nothing on the calendar} nextWakeupNanos.set(curDeadlineNanos); try {if (!hasTasks()) { // 如果队列中数据为空,则调用 select 查问就绪事件 strategy = select(curDeadlineNanos); } } finally {nextWakeupNanos.lazySet(AWAKE); } default: } } selectCnt++; cancelledKeys = 0; needsToSelectAgain = false; /* ioRatio 调节连贯事件和外部工作执行事件百分比 * ioRatio 越大,连贯事件处理占用百分比越大 */ final int ioRatio = this.ioRatio; boolean ranTasks; if (ioRatio == 100) { try {if (strategy > 0) { // 解决 IO 工夫 processSelectedKeys();} } finally { // 确保每次都要执行队列中的工作 ranTasks = runAllTasks();} } else if (strategy > 0) {final long ioStartTime = System.nanoTime(); try {processSelectedKeys(); } finally { // Ensure we always run tasks. final long ioTime = System.nanoTime() - ioStartTime; ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } } else {ranTasks = runAllTasks(0); // This will run the minimum number of tasks } if (ranTasks || strategy > 0) {if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector); } selectCnt = 0; } else if (unexpectedSelectorWakeup(selectCnt)) {// Unexpected wakeup (unusual case) selectCnt = 0; } } }
服务编排层 Pipeline 的协调解决
通过 EventLoop 能够实现工作的调度,负责监听 I / O 事件、信号事件等,当收到相干事件后,须要有人来响应这些事件和数据,而这些事件是通过 ChannelPipeline 中所定义的 ChannelHandler 实现的,他们是 Netty 中服务编排层的外围组件。
在上面这段代码中,咱们减少了 h1 和 h2 两个 InboundHandler,用来解决客户端数据的读取操作,代码如下。
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
// 配置 Server 的通道,相当于 NIO 中的 ServerSocketChannel
.channel(NioServerSocketChannel.class)
//childHandler 示意给 worker 那些线程配置了一个处理器,// 这个就是下面 NIO 中说的,把解决业务的具体逻辑形象进去,放到 Handler 外面
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {// socketChannel.pipeline().addLast(new NormalMessageHandler());
socketChannel.pipeline().addLast("h1",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("handler-01");
super.channelRead(ctx, msg);
}
}).addLast("h2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("handler-02");
super.channelRead(ctx, msg);
}
});
}
});
上述代码构建了一个 ChannelPipeline,失去如图 2 -13 所示的构造,每个 Channel 都会绑定一个 ChannelPipeline,一个 ChannelPipeline 蕴含多个 ChannelHandler,这些 Handler 会被包装成 ChannelHandlerContext 退出到 Pipeline 构建的双向链表中。
ChannelHandlerContext 用来保留 ChannelHandler 的上下文,它蕴含了 ChannelHandler 生命周期中的所有事件,比方 connect/bind/read/write 等,这样设计的益处是,各个 ChannelHandler 进行数据传递时,前置和后置的通用逻辑就能够间接保留到 ChannelHandlerContext 中进行传递。
<center> 图 2 -13</center>
出站和入站操作
依据网络数据的流向,ChannelPipeline 分为入站 ChannelInBoundHandler 和出站 ChannelOutboundHandler 两个处理器,如图 2 -14 所示,客户端与服务端通信过程中,数据从客户端发向服务端的过程叫出站,对于服务端来说,数据从客户端流入到服务端,这个时候是入站。
<center> 图 2 -14 InBound 和 OutBound 的关系 </center>
ChannelHandler 事件触发机制
当某个 Channel 触发了 IO 事件后,会通过 Handler 进行解决,而 ChannelHandler 是围绕 I / O 事件的生命周期来设计的,比方建设连贯、读数据、写数据、连贯销毁等。
ChannelHandler 有两个重要的子接口实现,别离拦挡数据流入和数据流出的 I / O 事件
- ChannelInboundHandler
- ChannelOutboundHandler
图 2 -15 中显示的 Adapter 类,提供很多默认操作,比方 ChannelHandler 中有很多很多办法,咱们用户自定义的办法有时候不须要重载全副,只须要重载一两个办法,那么能够应用 Adapter 类,它外面有很多默认的办法。其它框架中结尾是 Adapter 的类的作用也大都是如此。所以咱们在应用 netty 的时候,往往很少间接实现 ChannelHandler 的接口,常常是继承 Adapter 类。
<img src=”https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/202111090025881.png” alt=”image-20210816200206761″ style=”zoom:67%;” />
<center> 图 2 -15 ChannelHandler 类关系图 </center>
ChannelInboundHandler 事件回调和触发机会如下
事件回调办法 | 触发机会 |
---|---|
channelRegistered | Channel 被注册到 EventLoop |
channelUnregistered | Channel 从 EventLoop 中勾销注册 |
channelActive | Channel 处于就绪状态,能够被读写 |
channelInactive | Channel 处于非就绪状态 |
channelRead | Channel 能够从远端读取到数据 |
channelReadComplete | Channel 读取数据实现 |
userEventTriggered | 用户事件触发时 |
channelWritabilityChanged | Channel 的写状态发生变化 |
ChannelOutboundHandler 工夫回调触发机会
事件回调办法 | 触发机会 |
---|---|
bind | 当申请将 channel 绑定到本地地址时被调用 |
connect | 当申请将 channel 连贯到近程节点时被调用 |
disconnect | 当申请将 channel 从近程节点断开时被调用 |
close | 当申请敞开 channel 时被调用 |
deregister | 当申请将 channel 从它的 EventLoop 登记时被调用 |
read | 当申请通过 channel 读取数据时被调用 |
flush | 当申请通过 channel 将入队数据刷新到近程节点时调用 |
write | 当申请通过 channel 将数据写到近程节点时被调用 |
事件流传机制演示
public class NormalOutBoundHandler extends ChannelOutboundHandlerAdapter {
private final String name;
public NormalOutBoundHandler(String name) {this.name = name;}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {System.out.println("OutBoundHandler:"+name);
super.write(ctx, msg, promise);
}
}
public class NormalInBoundHandler extends ChannelInboundHandlerAdapter {
private final String name;
private final boolean flush;
public NormalInBoundHandler(String name, boolean flush) {
this.name = name;
this.flush = flush;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("InboundHandler:"+name);
if(flush){ctx.channel().writeAndFlush(msg);
}else {super.channelRead(ctx, msg);
}
}
}
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
// 配置 Server 的通道,相当于 NIO 中的 ServerSocketChannel
.channel(NioServerSocketChannel.class)
//childHandler 示意给 worker 那些线程配置了一个处理器,// 这个就是下面 NIO 中说的,把解决业务的具体逻辑形象进去,放到 Handler 外面
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline()
.addLast(new NormalInBoundHandler("NormalInBoundA",false))
.addLast(new NormalInBoundHandler("NormalInBoundB",false))
.addLast(new NormalInBoundHandler("NormalInBoundC",true));
socketChannel.pipeline()
.addLast(new NormalOutBoundHandler("NormalOutBoundA"))
.addLast(new NormalOutBoundHandler("NormalOutBoundB"))
.addLast(new NormalOutBoundHandler("NormalOutBoundC"));
}
});
上述代码运行后会失去如下执行后果
InboundHandler:NormalInBoundA
InboundHandler:NormalInBoundB
InboundHandler:NormalInBoundC
OutBoundHandler:NormalOutBoundC
OutBoundHandler:NormalOutBoundB
OutBoundHandler:NormalOutBoundA
当客户端向服务端发送申请时,会触发服务端的 NormalInBound 调用链,依照排列程序一一调用 Handler,当 InBound 解决实现后调用 WriteAndFlush 办法向客户端写回数据,此时会触发 NormalOutBoundHandler 调用链的 write 事件。
从执行后果来看,Inbound 和 Outbound 的事件流传方向是不同的,Inbound 流传方向是 head->tail,Outbound 流传方向是 Tail-Head。
异样流传机制
ChannelPipeline 工夫流传机制是典型的责任链模式,那么有同学必定会有疑难,如果这条链路中某个 handler 出现异常,那会导致什么问题呢?咱们对后面的例子批改 NormalInBoundHandler
public class NormalInBoundHandler extends ChannelInboundHandlerAdapter {
private final String name;
private final boolean flush;
public NormalInBoundHandler(String name, boolean flush) {
this.name = name;
this.flush = flush;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("InboundHandler:"+name);
if(flush){ctx.channel().writeAndFlush(msg);
}else {
// 减少异样解决
throw new RuntimeException("InBoundHandler:"+name);
}
}
}
这个时候一旦抛出异样,会导致整个申请链被中断,在 ChannelHandler 中提供了一个异样捕捉办法,这个办法能够防止 ChannelHandler 链中某个 Handler 异样导致申请链路中断。它会把异样依照 Handler 链路的程序从 head 节点流传到 Tail 节点。如果用户最终没有对异样进行解决,则最初由 Tail 节点进行对立解决
批改 NormalInboundHandler,重写上面这个办法。
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("InboundHandlerException:"+name);
super.exceptionCaught(ctx, cause);
}
在 Netty 利用开发中,好的异样解决十分重要可能让问题排查变得很轻松,所以咱们能够通过一种对立拦挡的形式来解决异样解决问题。
增加一个复合处理器实现类
public class ExceptionHandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {if(cause instanceof RuntimeException){System.out.println("解决业务异样");
}
super.exceptionCaught(ctx, cause);
}
}
把新增的 ExceptionHandler 增加到 ChannelPipeline 中
bootstrap.group(bossGroup, workerGroup)
// 配置 Server 的通道,相当于 NIO 中的 ServerSocketChannel
.channel(NioServerSocketChannel.class)
//childHandler 示意给 worker 那些线程配置了一个处理器,// 这个就是下面 NIO 中说的,把解决业务的具体逻辑形象进去,放到 Handler 外面
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline()
.addLast(new NormalInBoundHandler("NormalInBoundA",false))
.addLast(new NormalInBoundHandler("NormalInBoundB",false))
.addLast(new NormalInBoundHandler("NormalInBoundC",true));
socketChannel.pipeline()
.addLast(new NormalOutBoundHandler("NormalOutBoundA"))
.addLast(new NormalOutBoundHandler("NormalOutBoundB"))
.addLast(new NormalOutBoundHandler("NormalOutBoundC"))
.addLast(new ExceptionHandler());
}
});
最终,咱们就可能实现异样的对立解决。
版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自
Mic 带你学架构
!
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!