概述
- Netty是由JBOSS提供的一个Java开源框架,可从Github获取独立项目
- Netty是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端(摘录官网)
- Netty所谓的异步是针对用户使用Channel进行IO操作,会立即返回ChannelFuture。但IO操作的任务是提交给了Netty的NIO底层去进行处理,所以我们说Netty的异步事件驱动与Netty底层基于NIO(同步非阻塞)是不矛盾的
- Netty主要针对在TCP传输协议下,面对Clients端高并发应用;本质属于NIO框架,适用于服务器通讯相关多种应用场景
- 阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议,而Dubbo协议默认使用Netty作为基础的通信组件,实现节点间内部通信
- 大数据领域中Hadoop的RPC框架默认采用的就是Netty进行跨节点通信
- Netty官网版本推荐:目前稳定版为Netty4.x
Netty模型
Netty基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型中存在多个Reactor
工作原理执行流程
- Netty中抽象了两组线程池BossGroup和WorkerGroup,都属于NioEventLoopGroup,其中BossGroup负责接收客户端连接,WorkerGroup负责网络读写
- NioEventLoopGroup属于事件循环组,这个组中包含多个事件循环,每个事件循环是NioEventLoop
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上socket的网络通讯
- BossGroup下每个NioEventLoop执行流程
- 轮询accept事件
- 处理accept事件,与client建立连接生成NIOSocketChannel,并将其注册到对应worker下NIOEventLoop上的selector
- 处理任务队列任务,即runAllTasks
- WorkerGroup下每个NioEventLoop执行流程
- 轮询read,write事件
- 在对应NIOSocketChannel中处理read,write I/O事件
- 处理任务队列任务,即runAllTasks
- 每个Worker NioEventLoop处理业务时都会交由pipeLine管道处理,pipeline中包含了channel,即通过pipeline可以获取对应channel通道,管道中维护了很多处理业务的Handler处理器
TaskQueue任务队列
BossGroup和WorkerGroup下每个NioEventLoop中都维护了一个TaskQueue,可以用来异步处理比较耗时的任务,TaskQueue中的Task有三种典型使用场景
- 用户程序自定义普通任务,该任务会提交到TaskQueue
public class NettyServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //读取客户端发送的数据 //ctx:上下文对象,包含管道pipeline,通道等信息 //用户程序自定义普通任务 ctx.channel().eventLoop().execute(new Runnable() { @Override public void run() { try { Thread.sleep(10 * 1000); //模拟耗时长的任务 ctx.writeAndFlush(Unpooled.copiedBuffer("Hello, Client!", CharsetUtil.UTF_8)); //将数据写到缓存并刷新 } catch (Exception e) { System.out.println("Exception Info: " + e.getMessage()); } } }); } }
- 用户自定义定时任务,该任务提交到scheduleTaskQueue
ctx.channel().eventLoop().schedule(() -> { try { Thread.sleep(5 * 1000); ctx.writeAndFlush(Unpooled.copiedBuffer("Hello, Client!", CharsetUtil.UTF_8)); } catch (Exception e) { System.out.println("Exception Info: " + e.getMessage()); } },5, TimeUnit.SECONDS);
- 非当前Reactor线程调用Channel的各种方法(常用于推送任务场景)
异步模型
- 异步和同步相对,指的是当一个异步过程调用发出后,调用者不能立刻得到结果,实际处理这个调用的组件在完成后,会通过状态、通知和回调来通知调用者
- Netty中的I/O操作是异步的,包括Bind、Write、Connect等会返回一个ChannelFuture
- Netty的异步模型是建立在future和callback之上的,callback即回调,Future核心思想在于对于处理某些耗时的业务时会立刻返回一个Future(表示异步执行结果,其中通过监听方法检测任务执行情况),通过Future监控该业务处理的过程最终获得I/O操作结果(即Future-Listener机制)
- Future-Listener机制
- 当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture来获取操作执行状态,注册监听函数来执行完成后的操作
- 常见操作方法
- isDone: 判断当前操作是否完成
- isSuccess: 判断已完成的当前操作是否成功
- getCause: 判断当前操作失败的原因
- isCancelled: 判断已完成的当前操作是否被取消
- addListener: 用来注册监听器,当前操作已完成,将会通知指定的监听器(如果Future对象已完成,则通知指定的监听器)
-
Future-Listener示例
ChannelFuture cf = bootstrap.bind(6668).sync(); //绑定指定端口并同步处理 cf.addListener(new ChannelFutureListener() { //为端口添加监听 @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { if (cf.isSuccess()){ System.out.println("监听端口成功!"); } else{ System.out.println("监听端口失败!"); } } });
- Future-Listener机制
核心组件
Netty中根据各组件职责不同可以分为网络通信层、事件调度层、服务编排层
网络通信层核心组件
BootStrap|ServerBootStrap
BootStrap是Netty中客户端启动类,负责客户端启动并连接Netty服务端;ServerBootStrap是服务端启动引导类,负责服务端启动并监听端口;常用方法包括如下
- group:服务端用来设置BossGroup和WorkerGroup,而对于客户端则是设置一个EventGroup
- channel:用来设置服务器端的通道类型,如服务器端设置为NioServerSocketChannel类型
- option:用来给ServerChannel添加配置,如设置线程队列连接个数
- childOption:用来给接收通道添加配置,如设置活动连接状态
- handler:该handler作用于BossGroup
- childHandler:该handler作用于WorkerGroup,设置业务处理类,包括自定义的handler
- bind:用于服务器端,设置对外暴露的端口号
- connect:用于客户端,设置连接服务器的ip和端口等信息
Channel
提示:NioSocketChannel和NioServerSocketChannel分别对应客户端和服务端的Channel,两者的直接父类不一致,因此对外提供的功能也是不相同的。比如当发生read事时,NioServerSocketChannel 的主要逻辑就是建立新的连接,而NioSocketChannel则是读取传输的字节进行业务处理
- Netty中的Channel是完成一系列网络通信的载体
- Channel可以看成网络编程中的Socket,提供了执行网络异步IO操作API,包括read|write|bind|connect,通过ChannelFuture中已注册的监听器在IO操作成功、失败等回调通知调用结果,大大降低了直接使用Socket类的复杂性
-
Future|ChannelFuture:Netty中所有IO操作都是异步的,不能立刻得知消息是否正确处理,但可以通过Future和ChannelFuture注册监听事件,当操作成功或者失败自动触发注册的监听事件,常用方法如下
- channel:返回当前正在进行的IO操作的通道
- sync:等待异步操作执行完毕
-
- 通过Channel可获取当前网络连接的通道的状态以及配置参数等
- 不同协议、不同阻塞类型的连接都有不同的Channel类型与之对应,常用Channel类型如下
- NioSocketChannel:异步客户端TCP Socket连接
- NioServerSocketChannel:异步服务器端TCP Socket连接
- NioDatagramChannel:异步的UDP连接
事件调度层核心组件
EventLoopGroup
就是一个线程池,负责接收I/O请求并分配线程执行处理请求,在Netty服务端中的bossGroup中的线程用来处理新连接的建立,当连接建立后分发给workerGroup,workerGroup中每个线程则都会和唯一的客户端Channel连接进行绑定并处理该Channel上的读、写事件
EventLoop
就相当于线程池中的一个线程
服务编排层核心组件
ChannelPipeline
- ChannelPipeline是Handler集合,它负责处理和拦截inbound或者outbound的事件和操作,即用于处理Channel的入站和出站操作
- Netty中每个Channel都有一个ChannelPipeline与之对应,两者可以互相获取;而且Pipeline维护了一个channelHandlerContext组成的双向链表,并且channelHandlerContext关联一个ChannelHandler
- 当Channel中发生指定事件时,该事件就会在ChannelPipeline中沿着双向链表进行传播并调用各个ChannelHandler中的指定方法完成相应的业务处理
- ChannelPipeline常用方法如下
- addFirst:把业务处理类Handler添加到链表中第一个位置
- addLast:把业务处理类Handler添加到链表中最后一个位置
提示:Netty正是通过ChannelPipeline这种数据结构为用户提供了自定义业务逻辑的扩展点,用户只需要向ChannelPipeline中添加处理对应业务逻辑的ChannelHandler即可,当指定事件发生时,该ChannelHandler中的对应方法就会进行回调进而实现业务的处理
ChannelHandler
- ChannelHandler是Netty 中业务处理的核心类,当有 IO 事件发生时,该事件会在ChannelPipeline中进行传播,并依次调用具体的ChannelHandler进行业务处理
ChannInboundHandler
- 在ChannelInboundHandler中定义了一系列的回调方法,用户可以实现该接口并重写相应的方法来自定义的业务逻辑
-
channelRegistered: 当 Channel 绑定到了对应 Selector 上之后就会进行回调
-
channelActive: 当 Channel调用bind()完成端口绑定之后,channelActive() 方法会进行回调
-
channelRead0
- 服务端channelRead0
- 服务端Channel绑定到Selector上时监听的是Accept事件,当客户端有新连接接入时回调channelRead()方法并完成新连接的接入
- 客户端channelRead0
- 当服务端处理完Accept事件后会生成一个和客户端通信的Channel,该Channel也会注册到对应的Selector上并监听read事件,当客户端向该Channel中发送数据时就会触发read事件,调用channelRead()方法
- 服务端channelRead0
-
exceptionCaught:当前ChannelHandler中各回调方法处理过程中若发生异常则回调该方法
-
ChannelHandlerContext
- ChannelHandlerContext主要保存了Channel通道、ChannelHandler通道处理者相关信息,它们之间对应关系如下
- ChannelPipeline与ChannelHandlerContext对应关系: 1:n
- ChannelHandlerContext与ChannelHandler 对应关系: 1:1
- ChannelHandlerContext 常用方法如下
- channel(): 获取当前通道
- pipeline(): 获取当前管道
- handler(): 获取当前handler处理器
- close(): 关闭当前通道
- writeAndFlush(): 将数据写出到ChannelPipeline管道中
其他
Unpooled类
- Unpooled是Netty提供的专门操作缓冲区的工具类
- copiedBuffer:用来设置数据以及字符编码并返回一个ByteBuf对象
- ByteBuf:Netty中提供的缓冲区,底层就是一个数组
- ByteBuf不需要调用flip方法进行模式切换,底层通过readerIndex|writerIndex|capactiy将byteBuf分成三个区域
- 0-readerIndex:表示已经读取的区域
- readerIndex-writerIndex:可读的区域
- writerIndex-capactiy:可写的区域
Netty的编码解码机制
Netty提供的编码和解码器包括如下
- StringEncoder:对字符串数据进行编码
- ObjectEncoder:对Java对象进行编码
- StringDecoder:对字符串数据进行解码
- ObjectDecoder:对Java对象进行解码
存在问题
Netty的对java对象进行解码编码的底层使用的是Java序列化技术,但其效率不高,且无法跨语言,序列化体积是二进制编码5倍多,性能太低
解决方案
可以使用Google的Protobuf,它是一种轻便高效的结构化数据存储格式,用于结构化数据串行化,很适合数据存储或RPC数据交换格式,而且它支持跨语言
常见问题
已经存在NIO,为何还要开发出Netty框架?
- NIO类库和API繁杂,使用麻烦
- NIO属于单Ractor单线程模式,需要熟悉掌握线程和网络编程,才可以编写高质量NIO程序
- 开发工作量和难度大,如面临断连重连、失败缓存、网络拥塞等问题
- NIO本身存在的Epoll bug,产生空轮询导致CPU爆满
而Netty则解决了如上问题,它基于NIO进行了封装优化,具有如下优点
- 设计优雅:适用于各种传输类型的统一
- 高性能、吞吐量更高,延迟更低,减少资源消耗
- 安全:完整的SSL/TLS支持
- 社区活跃、版本迭代周期短