来看上面这个图,当客户端发动一次 Http 申请时,服务端的解决流程时怎么样的?
简略来说能够分为以下几个步骤:
- 基于 TCP 协定建设网络通信。
- 开始向服务端端传输数据。
- 服务端承受到数据进行解析,开始解决本次申请逻辑。
- 服务端解决实现后返回后果给客户端。
在这个过程中,会波及到网络 IO 通信,在传统的 BIO 模式下,客户端向服务端发动一个数据读取申请,客户端在收到服务端返回数据之前,始终处于阻塞状态,直到服务端返回数据后实现本次会话。这个过程就叫同步阻塞 IO,在 BIO 模型中如果想实现异步操作,就只能应用多线程模型,也就是一个申请对应一个线程,这样就可能防止服务端的链接被一个客户端占用导致连接数无奈进步。
同步阻塞 IO 次要体现在两个阻塞点
- 服务端接管客户端连贯时的阻塞。
- 客户端和服务端的 IO 通信时,数据未就绪的状况下的阻塞。
在这种传统 BIO 模式下,会造成一个十分重大的问题,如下图所示,如果同一时刻有 N 个客户端发动申请,依照 BIO 模型的特点,服务端在同一时刻只能解决一个申请。将导致客户端申请须要排队解决,带来的影响是,用户在期待一次申请解决返回的工夫十分长。意味着服务端没有并发解决能力,这显然不适合。
那么,服务端应该如何优化呢?
非阻塞 IO
从后面的剖析发现,服务端在解决一次申请时,会处于阻塞状态无奈解决后续申请,那是否可能让被阻塞的中央优化成不阻塞呢?于是就有了非阻塞 IO(NIO)
非阻塞 IO,就是客户端向服务端发动申请时,如果服务端的数据未就绪的状况下,客户端申请不会被阻塞,而是间接返回。然而有可能服务端的数据还未筹备好的时候,客户端收到的返回是一个空的,那客户端怎么拿到最终的数据呢?
如图所示,客户端只能通过轮询的形式来取得申请后果。NIO 相比 BIO 来说,少了阻塞的过程在性能和连接数上都会有明显提高。
NIO 依然有一个弊病,就是轮询过程中会有很多空轮询,而这个轮询会存在大量的零碎调用(发动内核指令从网卡缓冲区中加载数据,用户空间到内核空间的切换),随着连贯数量的减少,会导致性能问题。
多路复用机制
I/ O 多路复用的实质是通过一种机制(零碎内核缓冲 I / O 数据),让单个过程能够监督多个文件描述符,一旦某个描述符就绪(个别是读就绪或写就绪),可能告诉程序进行相应的读写操作
什么是 fd:在 linux 中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个 fd(文件描述符)。而对于一个 socket 的读写也会有相应的文件描述符,成为 socketfd。
常见的 IO 多路复用形式有【select、poll、epoll】,都是 Linux API 提供的 IO 复用形式,那么接下来重点讲一下 select、和 epoll 这两个模型
-
select:过程能够通过把一个或者多个 fd 传递给 select 零碎调用,过程会阻塞在 select 操作上,这样 select 能够帮咱们检测多个 fd 是否处于就绪状态,这个模式有两个毛病
- 因为他可能同时监听多个文件描述符,如果说有 1000 个,这个时候如果其中一个 fd 处于就绪状态了,那么以后过程须要线性轮询所有的 fd,也就是监听的 fd 越多,性能开销越大。
- 同时,select 在单个过程中能关上的 fd 是有限度的,默认是 1024,对于那些须要反对单机上万的 TCP 连贯来说的确有点少
- epoll:linux 还提供了 epoll 的零碎调用,epoll 是基于事件驱动形式来代替程序扫描,因而性能相对来说更高,次要原理是,当被监听的 fd 中,有 fd 就绪时,会告知以后过程具体哪一个 fd 就绪,那么以后过程只须要去从指定的 fd 上读取数据即可,另外,epoll 所能反对的 fd 上线是操作系统的最大文件句柄,这个数字要远远大于 1024
【因为 epoll 可能通过事件告知利用过程哪个 fd 是可读的,所以咱们也称这种 IO 为异步非阻塞 IO,当然它是伪异步的,因为它还须要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据曾经齐全筹备好了,我只须要从用户空间读就行】
I/ O 多路复用的益处是能够通过把多个 I / O 的阻塞复用到同一个 select 的阻塞上,从而使得零碎在单线程的状况下能够同时解决多个客户端申请。它的最大劣势是零碎开销小,并且不须要创立新的过程或者线程,升高了零碎的资源开销,它的整体实现思维如图 2 - 3 所示。
客户端申请到服务端后,此时客户端在传输数据过程中,为了防止 Server 端在 read 客户端数据过程中阻塞,服务端会把该申请注册到 Selector 复路器上,服务端此时不须要期待,只须要启动一个线程,通过 selector.select()阻塞轮询复路器上就绪的 channel 即可,也就是说,如果某个客户端连贯数据传输实现,那么 select()办法会返回就绪的 channel,而后执行相干的解决即可。
异步 IO
异步 IO 和多路复用机制,最大的区别在于:当数据就绪后,客户端不须要发送内核指令从内核空间读取数据,而是零碎会异步把这个数据间接拷贝到用户空间,应用程序只须要间接应用该数据即可。
<center> 图 2 -4 异步 IO</center>
在 Java 中,咱们能够应用 NIO 的 api 来实现多路复用机制,实现伪异步 IO。在网络通信演进模型剖析这篇文章中演示了 Java API 实现多路复用机制的代码,发现代码不仅仅繁琐,而且应用起来很麻烦。
所以 Netty 呈现了,Netty 的 I / O 模型是基于非阻塞 IO 实现的,底层依赖的是 JDK NIO 框架的多路复用器 Selector 来实现。
一个多路复用器 Selector 能够同时轮询多个 Channel,采纳 epoll 模式后,只须要一个线程负责 Selector 的轮询,就能够接入成千上万个客户端连贯。
Reactor 模型
http://gee.cs.oswego.edu/dl/c…
理解了 NIO 多路复用后,就有必要再和大家说一下 Reactor 多路复用高性能 I / O 设计模式,Reactor 实质上就是基于 NIO 多路复用机制提出的一个高性能 IO 设计模式,它的核心思想是把响应 IO 事件和业务解决进行拆散,通过一个或者多个线程来解决 IO 事件,而后将就绪失去事件散发到业务解决 handlers 线程去异步非阻塞解决,如图 2 - 5 所示。
Reactor 模型有三个重要的组件:
- Reactor:将 I / O 事件发派给对应的 Handler
- Acceptor:解决客户端连贯申请
- Handlers:执行非阻塞读 / 写
<center> 图 2 -5 Reactor 模型 </center>
这是最根本的单 Reactor 单线程模型(整体的 I / O 操作是由同一个线程实现的)。
其中 Reactor 线程,负责多路拆散套接字,有新连贯到来触发 connect 事件之后,交由 Acceptor 进行解决,有 IO 读写事件之后交给 hanlder 解决。
Acceptor 次要工作就是构建 handler,在获取到和 client 相干的 SocketChannel 之后,绑定到相应的 hanlder 上,对应的 SocketChannel 有读写事件之后,基于 racotor 散发,hanlder 就能够解决了(所有的 IO 事件都绑定到 selector 上,有 Reactor 散发)
Reactor 模式实质上指的是应用
I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O)
的模式。
多线程单 Reactor 模型
单线程 Reactor 这种实现形式有存在着毛病,从实例代码中能够看出,handler 的执行是串行的,如果其中一个 handler 解决线程阻塞将导致其余的业务解决阻塞。因为 handler 和 reactor 在同一个线程中的执行,这也将导致新的无奈接管新的申请,咱们做一个小试验:
- 在上述 Reactor 代码的 DispatchHandler 的 run 办法中,减少一个 Thread.sleep()。
- 关上多个客户端窗口连贯到 Reactor Server 端,其中一个窗口发送一个信息后被阻塞,另外一个窗口再发信息时因为后面的申请阻塞导致后续申请无奈被解决。
为了解决这种问题,有人提出应用多线程的形式来解决业务,也就是在业务解决的中央退出线程池异步解决,将 reactor 和 handler 在不同的线程来执行,如图 4 - 7 所示。
<center> 图 2 -6</center>
多线程多 Reactor 模型
在多线程单 Reactor 模型中,咱们发现所有的 I / O 操作是由一个 Reactor 来实现,而 Reactor 运行在单个线程中,它须要解决包含 Accept()
/read()
/write
/connect
操作,对于小容量的场景,影响不大。然而对于高负载、大并发或大数据量的利用场景时,容易成为瓶颈,次要起因如下:
- 一个 NIO 线程同时解决成千盈百的链路,性能上无奈撑持,即使 NIO 线程的 CPU 负荷达到 100%,也无奈满足海量音讯的读取和发送;
- 当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连贯超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量音讯积压和解决超时,成为零碎的性能瓶颈;
所以,咱们还能够更进一步优化,引入多 Reactor 多线程模式,如图 2 - 7 所示,Main Reactor 负责接管客户端的连贯申请,而后把接管到的申请传递给 SubReactor(其中 subReactor 能够有多个),具体的业务 IO 解决由 SubReactor 实现。
Multiple Reactors 模式通常也能够等同于 Master-Workers 模式,比方 Nginx 和 Memcached 等就是采纳这种多线程模型,尽管不同的我的项目实现细节略有区别,但总体来说模式是统一的。
<center> 图 2 -7</center>
- Acceptor,申请接收者,在实际时其职责相似服务器,并不真正负责连贯申请的建设,而只将其申请委托 Main Reactor 线程池来实现,起到一个转发的作用。
- Main Reactor,主 Reactor 线程组,次要 负责连贯事件,并将IO 读写申请转发到 SubReactor 线程池。
- Sub Reactor,Main Reactor 通常监听客户端连贯后会将通道的读写转发到 Sub Reactor 线程池中一个线程(负载平衡),负责数据的读写。在 NIO 中 通常注册通道的读(OP_READ)、写事件(OP_WRITE)。
高性能通信框架之 Netty
在 Java 中,网络编程框架有很多,比方 Java NIO、Mina、Netty、Grizzy 等。然而在大家接触到的所有中间件中,绝大部分都是采纳 Netty。
起因是 Netty 是目前最风行的一款高性能 Java 网络编程框架,它被宽泛援用在中间件、直播、社交、游戏等畛域。谈及到开源中间件,大家熟知的 Dubbo、RocketMQ、Elasticsearch、Hbase、RocketMQ 等都是采纳 Netty 实现。
在理论开发中,明天来听课的同学,99% 的人都不会波及到应用 Netty 做网络编程开发,然而为什么还要花精力给大家讲呢?起因有几个
-
在很多大厂面试的时候,会波及到相干的知识点
- Netty 高性能体现在哪些方面
- Netty 中有哪些重要组件
- Netty 的内存池、对象池的设计
- 很多中间件都是用 netty 来做网络通信,那么咱们在剖析这些中间件的源码时,升高网络通信的了解难度
- 晋升 Java 常识体系,尽可能的实现对技术体系了解的全面性。
为什么抉择 Netty
Netty 其实就是一个高性能 NIO 框架,所以它是基于 NIO 根底上的封装,实质上是提供高性能网络 IO 通信的性能。因为后面的课程中咱们曾经具体的对网络通信做了剖析,因而在学习 Netty 时,学习起来应该是更轻松的。
Netty 提供了上述三种 Reactor 模型的反对,咱们能够通过 Netty 封装好的 API 来疾速实现不同 Reactor 模型的开发,这也是为什么大家都抉择 Netty 的起因之一,除此之外,Netty 相比于 NIO 原生 API,它有以下特点:
- 提供了高效的 I / O 模型、线程模型和工夫解决机制
- 提供了非常简单易用的 API,相比 NIO 来说,针对根底的 Channel、Selector、Sockets、Buffers 等 api 提供了更高层次的封装,屏蔽了 NIO 的复杂性
- 对数据协定和序列化提供了很好的反对
- 稳定性,Netty 修复了 JDK NIO 较多的问题,比方 select 空转导致的 cpu 耗费 100%、TCP 断线重连、keep-alive 检测等问题。
- 可扩展性在同类型的框架中都是做的十分好的,比方一个是可定制化的线程模型,用户能够在启动参数中抉择 Reactor 模型、可扩大的事件驱动模型,将业务和框架的关注点拆散。
-
性能层面的优化,作为网络通信框架,须要解决大量的网络申请,必然就面临网络对象须要创立和销毁的问题,这种对 JVM 的 GC 来说不是很敌对,为了升高 JVM 垃圾回收的压力,引入了两种优化机制
- 对象池复用,
- 零拷贝技术
Netty 的生态介绍
首先,咱们须要去理解 Netty 到底提供了哪些性能,如图 2 - 1 所示,示意 Netty 生态中提供的性能阐明。后续内容中会逐渐的剖析这些性能。
<center> 图 2 -1 Netty 性能生态 </center>
Netty 的根本应用
须要阐明一下,咱们解说的 Netty 版本是 4.x 版本,之前有一段时间 netty 公布了一个 5.x 版本,然而被官网舍弃了,起因是:应用 ForkJoinPool 减少了复杂性,并且没有显示出显著的性能劣势。同时放弃所有的分支同步是相当多的工作,没有必要。
增加 jar 包依赖
应用 4.1.66 版本
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
创立 Netty Server 服务
大部分场景中,咱们应用的主从多线程 Reactor 模型,Boss 线程是住 Reactor,Worker 是从 Reactor。他们别离应用不同的 NioEventLoopGroup
主 Reactor 负责解决 Accept,而后把 Channel 注册到从 Reactor,从 Reactor 次要负责 Channel 生命周期内的所有 I / O 事件。
public class NettyBasicServerExample {public void bind(int port){
// 咱们要创立两个 EventLoopGroup,// 一个是 boss 专门用来接管连贯,能够了解为解决 accept 事件,// 另一个是 worker,能够关注除了 accept 之外的其它事件,解决子工作。// 下面留神,boss 线程个别设置一个线程,设置多个也只会用到一个,而且多个目前没有利用场景,// worker 线程通常要依据服务器调优,如果不写默认就是 cpu 的两倍。EventLoopGroup bossGroup=new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
// 服务端要启动,须要创立 ServerBootStrap,// 在这外面 netty 把 nio 的模板式的代码都给封装好了
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup) // 配置 boss 和 worker 线程
// 配置 Server 的通道,相当于 NIO 中的 ServerSocketChannel
.channel(NioServerSocketChannel.class)
//childHandler 示意给 worker 那些线程配置了一个处理器,// 配置初始化 channel,也就是给 worker 线程配置对应的 handler,当收到客户端的申请时,调配给指定的 handler 解决
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new NormalMessageHandler()); // 增加 handler,也就是具体的 IO 事件处理器
}
});
// 因为默认状况下是 NIO 异步非阻塞,所以绑定端口后,通过 sync()办法阻塞直到连贯建设
// 绑定端口并同步期待客户端连贯(sync 办法会阻塞,直到整个启动过程实现)ChannelFuture channelFuture=bootstrap.bind(port).sync();
System.out.println("Netty Server Started,Listening on :"+port);
// 期待服务端监听端口敞开
channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();
} finally {
// 开释线程资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();}
}
public static void main(String[] args) {new NettyBasicServerExample().bind(8080);
}
}
上述代码阐明如下:
- EventLoopGroup,定义线程组,相当于咱们之前在写 NIO 代码时定义的线程。这里定义了两个线程组别离是 boss 线程和 worker 线程,boss 线程负责接管连贯,worker 线程负责解决 IO 事件。boss 线程个别设置一个线程,设置多个也只会用到一个,而且多个目前没有利用场景。而 worker 线程通常要依据服务器调优,如果不写默认就是 cpu 的两倍。
- ServerBootstrap,服务端要启动,须要创立 ServerBootStrap,在这外面 netty 把 nio 的模板式的代码都给封装好了。
- ChannelOption.SO_BACKLOG
设置 Channel 类型
NIO 模型是 Netty 中最成熟也是被宽泛援用的模型,因而在应用 Netty 的时候,咱们会采纳 NioServerSocketChannel 作为 Channel 类型。
bootstrap.channel(NioServerSocketChannel.class);
除了 NioServerSocketChannel 以外,还提供了
- EpollServerSocketChannel,epoll 模型只有在 linux kernel 2.6 以上能力反对,在 windows 和 mac 都是不反对的,如果设置 Epoll 在 window 环境下运行会报错。
- OioServerSocketChannel,用于服务端阻塞地接管 TCP 连贯
- KQueueServerSocketChannel,kqueue 模型,是 Unix 中比拟高效的 IO 复用技术,常见的 IO 复用技术有 select, poll, epoll 以及 kqueue 等等。其中 epoll 为 Linux 独占,而 kqueue 则在许多 UNIX 零碎上存在。
注册 ChannelHandler
在 Netty 中能够通过 ChannelPipeline 注册多个 ChannelHandler,该 handler 就是给到 worker 线程执行的处理器,当 IO 事件就绪时,会依据这里配置的 Handler 进行调用。
这里能够注册多个 ChannelHandler,每个 ChannelHandler 各司其职,比方做编码和解码的 handler,心跳机制的 handler,音讯解决的 handler 等。这样能够实现代码的最大化复用。
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new NormalMessageHandler());
}
});
ServerBootstrap 中的 childHandler 办法须要注册一个 ChannelHandler,这里配置了一个 ChannelInitializer 的实现类,通过实例化 ChannelInitializer 来配置初始化 Channel。
当收到 IO 事件后,这个数据会在这多个 handler 中进行流传。上述代码中配置了一个 NormalMessageHandler,用来接管客户端音讯并输入。
绑定端口
实现 Netty 的根本配置后,通过 bind()办法真正触发启动,而 sync()办法会阻塞,直到整个启动过程实现。
ChannelFuture channelFuture=bootstrap.bind(port).sync();
NormalMessageHandler
ServerHandler 继承了 ChannelInboundHandlerAdapter,这是 netty 中的一个事件处理器,netty 中的处理器分为 Inbound(进站)和 Outbound(出站)处理器,前面会具体介绍。
public class NormalMessageHandler extends ChannelInboundHandlerAdapter {
//channelReadComplete 办法示意音讯读完了的解决,writeAndFlush 办法示意写入并发送音讯
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {// 这里的逻辑就是所有的音讯读取结束了,在对立写回到客户端。Unpooled.EMPTY_BUFFER 示意空音讯,addListener(ChannelFutureListener.CLOSE)示意写完后,就敞开连贯
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
//exceptionCaught 办法就是产生异样的解决
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();
ctx.close();}
//channelRead 办法示意读到音讯当前如何解决,这里咱们把音讯打印进去
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf in=(ByteBuf) msg;
byte[] req=new byte[in.readableBytes()];
in.readBytes(req); // 把数据读到 byte 数组中
String body=new String(req,"UTF-8");
System.out.println("服务器端收到音讯:"+body);
// 写回数据
ByteBuf resp=Unpooled.copiedBuffer(("receive message:"+body+"").getBytes());
ctx.write(resp);
//ctx.write 示意把音讯再发送回客户端,然而仅仅是写到缓冲区,没有发送,flush 才会真正写到网络下来
}
}
通过上述代码发现,咱们只须要通过极少的代码就实现了 NIO 服务端的开发,相比传统的 NIO 原生类库的服务端,代码量大大减少,开发难度也大幅度降低。
Netty 和 NIO 的 api 对应
TransportChannel —- 对应 NIO 中的 channel
EventLoop—- 对应于 NIO 中的 while 循环
EventLoopGroup: 多个 EventLoop,就是事件循环
ChannelHandler 和 ChannelPipeline— 对应于 NIO 中的客户逻辑实现 handleRead/handleWrite(interceptor pattern)
ByteBuf—- 对应于 NIO 中的 ByteBuffer
Bootstrap 和 ServerBootstrap — 对应 NIO 中的 Selector、ServerSocketChannel 等的创立、配置、启动等
Netty 的整体工作机制
Netty 的整体工作机制如下,整体设计就是后面咱们讲过的多线程 Reactor 模型,拆散申请监听和申请解决,通过多线程别离执行具体的 handler。
<center> 图 2 -2</center>
网络通信层
网络通信层次要的职责是执行网络的 IO 操作,它反对多种网络通信协定和 I / O 模型的链接操作。当网络数据读取到内核缓冲区后,会触发读写事件,这些事件在分发给工夫调度器来进行解决。
在 Netty 中,网络通信的外围组件以下三个组件
- Bootstrap,客户端启动 api,用来链接近程 netty server,只绑定一个 EventLoopGroup
- ServerBootStrap,服务端监听 api,用来监听指定端口,会绑定两个 EventLoopGroup,bootstrap 组件能够十分方便快捷的启动 Netty 应用程序
- Channel,Channel 是网络通信的载体,Netty 本人实现的 Channel 是以 JDK NIO channel 为根底,提供了更高层次的形象,同时也屏蔽了底层 Socket 的复杂性,为 Channel 提供了更加弱小的性能。
如图 2 - 3 所示,示意的是 Channel 的罕用实现实现类关系图,AbstractChannel 是整个 Channel 实现的基类,派生出了 AbstractNioChannel(非阻塞 io)、AbstractOioChannel(阻塞 io),每个子类代表了不同的 I / O 模型和协定类型。
<center> 图 2 -3 Channel 的类关系图 </center>
随着连贯和数据的变动,Channel 也会存在多种状态,比方连贯建设、连贯注册、连贯读写、连贯销毁。随着状态的变动,Channel 也会处于不同的生命周期,每种状态会绑定一个相应的事件回调。以下是常见的工夫回调办法。
- channelRegistered,channel 创立后被注册到 EventLoop 上
- channelUnregistered,channel 创立后未注册或者从 EventLoop 勾销注册
- channelActive,channel 处于就绪状态,能够被读写
- channelInactive,Channel 处于非就绪状态
- channelRead,Channel 能够从源端读取数据
- channelReadComplete,Channel 读取数据实现
简略总结一下,Bootstrap 和 ServerBootStrap 别离负责客户端和服务端的启动,Channel 是网络通信的载体,它提供了与底层 Socket 交互的能力。
而当 Channel 生命周期中的事件变动,就须要触发进一步解决,这个解决是由 Netty 的事件调度器来实现。
事件调度器
事件调度器是通过 Reactor 线程模型对各类事件进行聚合解决,通过 Selector 主循环线程集成多种事件(I/ O 工夫、信号工夫),当这些事件被触发后,具体针对该事件的解决须要给到服务编排层中相干的 Handler 来解决。
事件调度器外围组件:
- EventLoopGroup。相当于线程池
- EventLoop。相当于线程池中的线程
EventLoopGroup 实质上是一个线程池,次要负责接管 I / O 申请,并调配线程执行解决申请。为了更好的了解 EventLoopGroup、EventLoop、Channel 之间的关系,咱们来看图 2 - 4 所示的流程。
<center> 图 2 -4,EventLoop 的工作机制 </center>
从图中可知
- 一个 EventLoopGroup 能够蕴含多个 EventLoop,EventLoop 用来解决 Channel 生命周期内所有的 I / O 事件,比方 accept、connect、read、write 等
- EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责解决多个 Channel
- 每新建一个 Channel,EventLoopGroup 会抉择一个 EventLoop 进行绑定,该 Channel 在生命周期内能够对 EventLoop 进行屡次绑定和解绑。
图 2 - 5 示意的是 EventLoopGroup 的类关系图,能够看出 Netty 提供了 EventLoopGroup 的多种实现,如 NioEventLoop、EpollEventLoop、NioEventLoopGroup 等。
从图中能够看到,EventLoop 是 EventLoopGroup 的子接口,咱们能够把 EventLoop 等价于 EventLoopGroup,前提是 EventLoopGroup 中只蕴含一个 EventLoop。
<img src=”https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/202111090024225.png” alt=”image-20210812221329760″ style=”zoom:80%;” />
<center> 图 2 -5 EventLoopGroup 类关系图 </center>
EventLoopGroup 是 Netty 的外围解决引擎,它和后面咱们解说的 Reactor 线程模型有什么关系呢?其实,咱们能够简略的把 EventLoopGroup 当成是 Netty 中 Reactor 线程模型的具体实现,咱们能够通过配置不同的 EventLoopGroup 使得 Netty 反对多种不同的 Reactor 模型。
- 单线程模型,EventLoopGroup 只蕴含一个 EventLoop,Boss 和 Worker 应用同一个 EventLoopGroup。
- 多线程模型:EventLoopGroup 蕴含多个 EventLoop,Boss 和 Worker 应用同一个 EventLoopGroup。
- 主从多线程模型:EventLoopGroup 蕴含多个 EventLoop,Boss 是主 Reactor,Worker 是从 Reactor 模型。他们别离应用不同的 EventLoopGroup,主 Reactor 负责新的网络连接 Channel 的创立(也就是连贯的事件),主 Reactor 收到客户端的连贯后,交给从 Reactor 来解决。
服务编排层
服务编排层的职责是负责组装各类的服务,简略来说,就是 I / O 事件触发后,须要有一个 Handler 来解决,所以服务编排层能够通过一个 Handler 解决链来实现网络事件的动静编排和有序的流传。
它蕴含三个组件
-
ChannelPipeline,它采纳了双向链表将多个 Channelhandler 链接在一起,当 I / O 事件触发时,ChannelPipeline 会顺次调用组装好的多个 ChannelHandler,实现对 Channel 的数据处理。ChannelPipeline 是线程平安的,因为每个新的 Channel 都会绑定一个新的 ChannelPipeline。一个 ChannelPipeline 关联一个 EventLoop,而一个 EventLoop 只会绑定一个线程,如图 2 - 6 所示,示意 ChannelPIpeline 结构图。
<img src=”https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/202111090024172.png” alt=”image-20210812223234507″ style=”zoom: 50%;” />
<center> 图 2 -6 ChannelPipeline</center>
从图中能够看出,ChannelPipeline 中蕴含入站 ChannelInBoundHandler 和出站 ChannelOutboundHandler,前者是接收数据,后者是写出数据,其实就是 InputStream 和 OutputStream,为了更好的了解,咱们来看图 2 -7。
<center> 图 2 -7 InBound 和 OutBound 的关系 </center>
- ChannelHandler,针对 IO 数据的处理器,数据接管后,通过指定的 Handler 进行解决。
-
ChannelHandlerContext,ChannelHandlerContext 用来保留 ChannelHandler 的上下文信息,也就是说,当事件被触发后,多个 handler 之间的数据,是通过 ChannelHandlerContext 来进行传递的。ChannelHandler 和 ChannelHandlerContext 之间的关系,如图 2 - 8 所示。
每个 ChannelHandler 都对应一个本人的 ChannelHandlerContext,它保留了 ChannelHandler 所须要的上下文信息,多个 ChannelHandler 之间的数据传递,是通过 ChannelHandlerContext 来实现的。
<center> 图 2 -8 ChannelHandler 和 ChannelHandlerContext 关系 </center>
以上就是 Netty 中外围的组件的个性和工作机制的介绍,后续的内容中还会具体的剖析这几个组件。能够看出,Netty 的架构分层设计是十分正当的,它屏蔽了底层 NIO 以及框架层的实现细节,对于业务开发者来说,只须要关怀业务逻辑的编排和实现即可。
组件关系及原理总结
如图 2 - 9 所示,示意 Netty 中要害的组件协调原理,具体的工作机制形容如下。
- 服务单启动初始化 Boss 和 Worker 线程组,Boss 线程组负责监听网络连接事件,当有新的连贯建设时,Boss 线程会把该连贯 Channel 注册绑定到 Worker 线程
- Worker 线程组会调配一个 EventLoop 负责解决该 Channel 的读写事件,每个 EventLoop 相当于一个线程。通过 Selector 进行事件循环监听。
- 当客户端发动 I / O 事件时,服务端的 EventLoop 讲就绪的 Channel 分发给 Pipeline,进行数据的解决
- 数据传输到 ChannelPipeline 后,从第一个 ChannelInBoundHandler 进行解决,依照 pipeline 链一一进行传递
- 服务端解决实现后要把数据写回到客户端,这个写回的数据会在 ChannelOutboundHandler 组成的链中流传,最初达到客户端。
<center> 图 2 -9 Netty 各个组件的工作原理 </center>
Netty 中外围组件的具体介绍
在 2.5 节中对 Netty 有了一个全局意识后,咱们再针对这几个组件做一个十分具体的阐明,加深大家的了解。
启动器 Bootstrap 和 ServerBootstrap 作为 Netty 构建客户端和服务端的路口,是编写 Netty 网络程序的第一步。它能够让咱们把 Netty 的外围组件像搭积木一样组装在一起。在 Netty Server 端构建的过程中,咱们须要关注三个重要的步骤
- 配置线程池
- Channel 初始化
- Handler 处理器构建
版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自
Mic 带你学架构
!
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!