Java IO流(四)Netty理论[模型|核心组件]

概述

  • 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支持如下协议
    • TCP/UDP: Netty提供了基于NIO的TCP和UDP编程框架,可以用来构建高性能、高可用性的网络应用
    • HTTP/HTTPS: Netty提供了HTTP/HTTPS编程框架,可以用来开发Web服务器和客户端
    • WebSocket: Netty提供了WebSocket编程框架,可以用来实现双向通信应用程序,如聊天室等
    • SPDY/HTTP2: Netty提供了SPDY和HTTP2编程框架,可以用来实现高效的Web应用程序
    • MQTT/CoAP: Netty提供了MQTT和CoAP编程框架,可以用来构建IoT应用程序

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("监听端口失败!");
        		}
        	}
        });

核心组件

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

就相当于线程池中的一个线程

服务编排层核心组件

Channel&&ChannelPipeline&&ChannelHandler关系

ChannelPipeline

  • ChannelPipeline是Handler集合,它负责处理和拦截inbound或者outbound的事件和操作,即用于处理Channel的入站和出站操作
  • Netty中每个Channel都有一个ChannelPipeline与之对应,两者可以互相获取;而且Pipeline维护了一个channelHandlerContext组成的双向链表,并且channelHandlerContext关联一个ChannelHandler
    • ​​​​​如果事件是从客户端到服务端的,那么我们称这些事件为出站的,出站消息会被通过编码器编码转换成字节
    • 反之,服务端发送响应消息给到客户端称为入站,入站消息会被解码器从字节转换为另一种格式(比如java对象);
    • 出站和入站的数据都会通过pipeline中的一系列Handler进行处理
    • 注意:编码解码器handler在pipleline中的链表位置必须在业务handler之前,否则无法正确调用
  • 当Channel中发生指定事件时,该事件就会在ChannelPipeline中沿着双向链表进行传播并调用各个ChannelHandler中的指定方法完成相应的业务处理
  • ChannelPipeline常用方法如下
    • addFirst:把业务处理类Handler添加到链表中第一个位置
    • addLast:把业务处理类Handler添加到链表中最后一个位置

提示:Netty正是通过ChannelPipeline这种数据结构为用户提供了自定义业务逻辑的扩展点,用户只需要向ChannelPipeline中添加处理对应业务逻辑的ChannelHandler即可,当指定事件发生时,该ChannelHandler中的对应方法就会进行回调进而实现业务的处理

ChannelHandler

ChannelInboundHandler&&ChannelOutboundHandler分别用来处理读&&写事件
  • ChannelHandler是Netty 中业务处理的核心类,当有 IO 事件发生时,该事件会在ChannelPipeline中进行传播,并依次调用具体的ChannelHandler进行业务处理
ChannInboundHandler
  • 在ChannelInboundHandler中定义了一系列的回调方法,用户可以实现该接口并重写相应的方法来自定义的业务逻辑​​​​​​​
    • channelActive: 当 Channel调用bind()完成端口绑定之后,channelActive() 方法会进行回调

    • channelRegistered: 当 Channel 注册到了对应 Selector 上之后就会进行回调

    • channelInactive(ChannelHandlerContext ctx)通道处于非激活(关闭),调用了close方法时,会触发该事件,然后触发channelUnregistered 事件

    •  channelUnregistered(ChannelHandlerContext ctx)通道取消注册到Selector时被调用,通常在通道关闭时触发,首先触发channelInactive事件,然后再触发channelUnregistered 事件

    • channelRead0

      • 服务端channelRead0
        • 服务端Channel绑定到Selector上时监听的是Accept事件,当客户端有新连接接入时回调channelRead()方法并完成新连接的接入
      • 客户端channelRead0
        • 当服务端处理完Accept事件后会生成一个和客户端通信的Channel,该Channel也会注册到对应的Selector上并监听read事件,当客户端向该Channel中发送数据时就会触发read事件,调用channelRead()方法
    • 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支持
  • 社区活跃、版本迭代周期短

猜你喜欢

转载自blog.csdn.net/qq_34020761/article/details/132376980
今日推荐