Netty的知识点
前言
为了让大家对Netty有个整体认知,本文首先会对Netty的整个运作过程捋一遍,细节的东西后面再说,直接上图:
结合图示进行全过程讲解:
-
ServerBootStrap作为Netty的服务端入口,会对BossGroup和WorkGroup进行相关初始化操作,在BossGroup中,主要是对客户端的新连接请求进行处理(即OP_ACCEPT事件,但其实OP_ACCEPT事件的具体处理也会涉及到读写事件,因为数据不是读就是写),在WorkGroup中,则负责处理IO读写、编解码、业务逻辑等(即OP_READ事件、OP_WRITE事件)。服务端启动的时候会绑定一个端口,作为后续客户端连接入口,绑定端口的时候会在BossGroup(由NioEventLoopGroup类创建的对象)的其中一个NioEventLoop的Selector(多路复用器)上注册一条NioServerSocketChannel通道,后面的连接处理就是在通道中进行的。
-
BootStrap则作为Netty的客户端入口,会对ClientGroup进行相关初始化操作,在ClientGroup中,第一就是创建与服务端的连接(即OP_CONNECT事件),第二就是进行IO读写、编解码、业务逻辑等操作(即OP_READ事件、OP_WRITE事件)。
-
服务端和客户端启动之后,当服务端收到客户端发来的连接请求,由于属于OP_ACCEPT事件,在BossGroup中处理。BossGroup(由NioEventLoopGroup类创建的对象)管理着若干个NioEventLoop,每个NioEventLoop持有一个线程(就好比线程池中的一组线程并发处理若干个连接请求),每个NioEventLoop上会创建一个Selector,一个Selector上可以注册多个通道(所以叫多路复用器),且它会以不断轮询的方式同时监听每个通道上是否有IO事件发生,每个通道里都会有个ChannelPipeline管道,管道里全是Handler,包括管道头Head和管道尾Tail,以及进行IO读写、编解码,业务处理的若干个Handler,Handler也可以自定义,把需要的Handler注册进管道就可以执行了。当请求到达Head时,代表“请求数据”已准备好,OP_ACCEPT事件已就绪,Selector监听到事件已就绪,就会让持有的线程对事件进行处理,处理过程是在Handler中进行。首先会创建一个NioSocketChannel实例,然后交给ServerBootStrapAcceptor这个Handler,它是Netty底层代码注册的,Acceptor具体操作就是向WorkGroup中的某个Selector注册刚才创建好的NioSocketChannel,自此客户端连接请求处理结束。
-
客户端发出连接请求的同时会自己创建一条NioSocketChannel通道与服务端NioSocketChannel进行互通,连接完之后就是WorkGroup的事了,不需要BossGroup管了,一个客户端连接对应一条服务端NioSocketChannel。比如现在客户端要进行一个远程方法的调用,将方法参数传给服务端后,服务端处理完将结果返回给客户端。首先请求从客户端通道传输到WorkGroup中的对应通道,然后Head会申请一块堆外内存来缓冲请求内容,缓冲完之后,代表数据已准备好,OP_READ事件已就绪,selector监听到就绪事件之后,让持有的线程对事件进行处理,这里我定义了Decode解码,Compute方法调用处理和Encode编码三个Handler进行操作,其中Inbound入站Handler包括Decode和Compute(从Head到Tail就是入站),Outbound出站Handler包括Encode(从Tail到Head就是出站),每一个Handler被注册到Pipeline中的时候都会创建一个与之对应的ChannelHandlerContext,它包含着Handler的上下文信息,主要负责管理和其他在同一管道里的Handler之间的交互,它有一个前指针和后指针,可以与其他ChannelHandlerContext关联,这样Handler处理就变得更加灵活,比如这次请求需要三个Handler,而下次请求只涉及到Decode和Encode,那下次就可以执行完Decode然后指针直接指向Encode,next指针具体指向谁是依靠ChannelHandlerContext中的数据类型与其他Handler类型进行匹配得出的。在处理完读事件之后,接着处理Handler中涉及到的写事件,将处理结果写到ByteBuf中,回到Head,执行flush操作将ByteBuf内容写到SocketBuffer中,然后再到网卡buffer,通过互联网把结果传回给客户端,客户端拿到结果之后同样要进行解码,反序列化等操作,那么回过头发现客户端在发送调用请求之前在Pipeline中也进行了Encode处理的。(Head的主要作用:从SocketBuffer读请求内容到ByteBuf,从ByteBuf写返回结果到SocketBuffer)
-
假设又有另外一个客户端连接了服务端,且和之前那个NioSocketChannel注册到了同一个Selector上,当线程正在处理另一个通道上的事件的时候,这时该客户端也发起了一个处理请求,请求到达服务端通道之后会被Head读到堆外内存中缓冲着,此时OP_READ事件已就绪,Selector监听到了就绪事件,但由于线程正在处理另外一个通道上的事件,所以就要等当前通道的事件处理完,下一轮循环监听再处理了(这也是堆外内存的作用体现之一,数据可以先在缓冲区放着)。当两个通道被注册在不同的Selector上的时候就互不影响了,因为是在不同的线程中并行处理的。另外补充两点,第一个TaskQueue任务队列中的任务都是非IO任务,从性能上来考虑,千万不要将一个需要长时间来运行的任务放入到任务队列中,因为事件任务在一个线程中是串行执行的,这样会阻塞其他任务。解决方案是使用一个专门的EventExecutor来执行它(ChannelPipeline提供了带有EventExecutorGroup参数的addXXX()方法,该方法可以将传入的ChannelHandler绑定到你传入的EventExecutor之中),这样它就会在另一条线程中执行,与其他任务隔离。第二个Channel注册到Selector后返回的是一个SelectionKey,这个SelectionKey有以下几个重要属性:
- interest set,通道感兴趣的事件集,就是会把该通道可能执行的事件类型都告诉Selector
- ready set,感兴趣的事件集中的“就绪事件集”
- 保存着的Channel
- 保存着的Selector
IO事件类型:
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_CONNECT
因此Selector每次循环监听的其实就是SelectionKey中的就绪事件集,看是否存在已就绪的事件,存在就进行处理。SelectionKey相当于是Selector和Channel之间的桥梁。
一、NIO 基本概念
1. 阻塞(Block)与非阻塞(Non-Block)
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。
阻塞: 往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。
非阻塞: 当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。
阻塞 IO :
非阻塞 IO :
2. 同步(Synchronous)与异步(Asynchronous)
同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。比如
同步: 是应用程序要直接参与 IO 读写的操作。
异步: 所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。
同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。
所以异步相比较于同步带来的直接好处就是在我们处理IO数据的时候,异步的方式我们可以把这部分等待所消耗的资源用于处理其他事务,提升我们服务自身的性能。
同步 IO :
异步 IO :
二、Java BIO与NIO对比
1. BIO(传统IO)
BIO是一个同步并阻塞的IO模式,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
2. NIO(Non-blocking/New I/O)
NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel
, Selector
,Buffer
等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
3. BIO与NIO的对比
IO模型 | BIO | NIO |
---|---|---|
通信 | 面向流 | 面向缓冲 |
处理 | 阻塞IO | 非阻塞IO |
触发 | 无 | 选择器 |
4. NIO 的 Server 通信的简单模型
5. BIO 的 Server 通信的简单模型
6. NIO的特点
- 一个线程可以处理多个通道,减少线程创建数量;
- 读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费
三、Reactor 模型
Reactor对应的叫法:1.反应器模式 2.分发者模式 3.通知者模式
- 基于I/O复用模型: 多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
- 基于线程池复用线程资源: 不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。
Reactor模式中核心组成:
- Reactor: Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
- Handlers: 处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
1. 单线程的 Reactor 模型
- Select是前面I/O复用模型介绍额标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求
- Reactor对象通过Select监控客户端请求事件,受到事件后通过Dispatch进行分发
- 如果是建立连接请求事件,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理。
- 如果不是简介事件,则Reactor会分发调用连接对应的Handler来响应
- Handler会完成Read-》业务处理-》Send的完整业务流程。
优缺点:
- 优点:模型简单,没有多线程、进程通信、竞争问题,全部都在一个线程中完成。
- 缺点:性能问题,只有一个线程,无法发挥多核CPU性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。
- 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接受和处理外部消息,造成节点故障。
2. 多线程的 Reactor 模型
handler只负责响应事件,不做具体的业务处理,会分发给后面的worker线程池的某个线程处理。
优缺点:
- 优点:可以充分利用多核CPU性能
- 缺点:多线程数据共享和访问比较复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现瓶颈。
3. 多线程主从 Reactor 模型
- Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
- subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
- 当有新事件发生时,subreactor 就会调用对应的 handler 处理
- handler 通过 read 读取数据,分发给后面的 worker 线程处理
- worker 线程池分配独立的 worker 线程进行业务处理,并返回结果
- handler 收到响应的结果后,再通过 send 将结果返回给 client
- Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor
四、Netty 基础概念
- Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯
- NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
- 每个 BossNioEventLoop 循环执行的步骤有 3 步
轮询 accept 事件
处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个worker NIOEventLoop
上的 Selector
处理任务队列的任务,即 runAllTasks - 每个
Worker NIOEventLoop
循环执行的步骤
轮询 read,write 事件
处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理
处理任务队列的任务,即 runAllTasks - 每个
Worker NIOEventLoop
处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。
1. Netty 简介
Netty 是一个 NIO 客户端服务器框架,可快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。
“快速简便”并不意味着最终的应用程序将遭受可维护性或性能问题的困扰。Netty 经过精心设计,结合了许多协议(例如FTP,SMTP,HTTP 以及各种基于二进制和文本的旧式协议)的实施经验。结果,Netty 成功地找到了一种无需妥协即可轻松实现开发,性能,稳定性和灵活性的方法。
2. Netty 执行流程
3. Netty 核心组件
Channel
Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。
EventLoop 与 EventLoopGroup
EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。
EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。
一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。
ServerBootstrap 与 Bootstrap
Bootstarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。
Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。
ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。
ChannelHandler 与 ChannelPipeline
ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
ChannelFuture
Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个ChannelFuture
对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册回调:当结果出来后马上调用执行。
Netty 的异步编程模型都是建立在 Future 与回调概念之上的。
五、Netty 源码阅读
源码阅读,最好可以再 Debug 的情况下进行,这样更容易帮助理解,因此在分析 Netty 前的我准备一个客户端和服务端的代码。
1. Netty - Server 代码
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new SomeSocketServerHandler());
}
});
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("服务器已启动。。。");
future.channel().closeFuture().sync();
} finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
2. Server 端 Handler
public class DemoSocketServerHandler
extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("Client Address ====== " + ctx.channel().remoteAddress());
ctx.channel().writeAndFlush("from server:" + UUID.randomUUID());
ctx.fireChannelActive();
TimeUnit.MILLISECONDS.sleep(500);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
3. Netty - Client 代码
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new DemoSocketClientHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
future.channel().closeFuture().sync();
} finally {
if(eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
}
}
}
4. Client 端 Handler
public class DemoSocketClientHandler
extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println(msg);
ctx.channel().writeAndFlush("from client: " + System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(5000);
}
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
ctx.channel().writeAndFlush("from client:begin talking");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
5. NioEventLoopGroup 初始化分析
首先根据 Server 服务端代码,分析 NioEventLoopGroup 的初始化过程。而在分析 NioEventLoopGroup 之前,有必要简单的说一说 NioEventLoopGroup 与 NioEventLoop ,方便后续源码的理解。
NioEventLoop 的继承体系
从 NioEventLoop 的继承体系中可以看到,NioEventLoop 本身就是一个 Executor,并且还是一个 单线程的 Executor。Executor 必然拥有一个 execute(Runnable command)
的实现方法,而 NioEventLoop 的 execute() 实现方法在其父类 SingleThreadEventExecutor
中,找到具体代码:
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
addTask(task);
if (!inEventLoop) {
startThread();
if (isShutdown()) {
boolean reject = false;
try {
if (removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException e) {
// The task queue does not support removal so the best thing we can do is to just move on and
// hope we will be able to pick-up the task before its completely terminated.
// In worst case we will log on termination.
}
if (reject) {
reject();
}
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
这里不细说,但是贴出这段代码主要为了引出 startThread()
; 这句代码,在跟这句代码会发现,它最终调用了 NioEventLoop 的一个成员 Executor 执行了当前成员的 execute() 方法。对应的成员 io.netty.util.concurrent.SingleThreadEventExecutor#executor
而 executor 成员的初始化也是在当前代码执行时创建的匿名 Executor ,也就是执行到即新建并且执行当前 匿名 executor() 方法。
总结
- NioEventLoop 本身就是一个 Executor。
- NioEventLoop 内部封装这一个新的线程 Executor 成员。
- NioEventLoop 有两个 execute 方法,除了本身的 execute() 方法对应的还有成员属性 Executor 对应的execute() 方法。
备注: 因为这里出现了四个 Executor,为了区分,我们给其新的名称:
NioEventLoop 本身 Executor:NioEventLoop
NioEventLoop 的成员 Executor:子 Executor
NioEventLoopGroup 本身 Executor :NioEventLoopGroup
NioEventLoopGroup 的构造参数 Executor :总Executor
6. NioEventLoopGroup 的继承体系
看到继承体系可以直接知道 NioEventLoopGroup 也是一个 Executor,并且是一个线程池的 Executor,所以他也有 execute() 方法。对应的实现再其父类之中:io.netty.util.concurrent.AbstractEventExecutorGroup#execute
而这里还需要说到的一点是:在 NioEventLoopGroup 的构造中,再其父类 MultithreadEventExecutorGroup
的构造再次引入了一个新的 Executor,
之所以这里提到这个 Executor,是因为这个 Executor 是对应的 execute() 就是在 NioEventLoop 中的成员 Executor 的 execute() 执行时调用的。也就是下面对应的代码调用。io.netty.util.internal.ThreadExecutorMap#apply(java.util.concurrent.Executor, io.netty.util.concurrent.EventExecutor)
到这如果不明白,没关系,因为只是为了引入 NioEventLoopGroup 和 NioEventLoop 的对应的两个 Executor,和两个 Executor 对应的两个 execute() 方法。这个后面还会有详细分析。
总结
- NioEventLoopGroup 是一个线程池线程 Executor。
- NioEventLoopGroup 也封装了一个线程 Executor。
- NioEventLoopGroup 也有两个 execute()方法。
参考文章:
https://blog.csdn.net/qq_35190492/article/details/113174359