Netty:一个进阶的门槛

零、放在前头
【重要提示】本文档所有Netty理论基于Netty4,Netty5会大有变化。因为Netty4是NIO,网络通信模型采用的是Reactor。而Netty5是AIO的,其网络通信模型采用的是Proactor;但是为什么不介绍Netty5呢,因为Netty5被官方直接废弃了!!!虽然现在又发布了Netty5的alpha版本,但是大多数公司都在使用Netty4,所以对Netty5暂不做讨论。其主要原因有几下几点:

  1. Netty5 中使用了 ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显

  2. 多个分支的代码同步工作量很大

  3. 官方还在逐渐完善Netty5
    一、概述
    Netty到底有多重要?
    有大佬曾说:
    作为一个学Java的,如果没有研究过Netty,那么你对Java语言的使用和理解仅仅停留在表面水平,会点SSH,写几个MVC,访问数据库和缓存,这些只是初、中等Java程序员干的事。如果你要进阶,想了解Java服务器的深层高阶知识,Netty绝对是一个必须要过的门槛。
    在这里插入图片描述

  4. 互联网行业,在分布式系统架构中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty通常作为其底层通信组件被广泛使用

  5. Dubbo协议默认使用Netty作为基础通信组件

  6. 游戏领域,要求高时效性的玩家通信,底层通常是Netty

  7. 淘宝的消息中间件RocketMQ;

  8. Hadoop 的高性能通信和序列化组件Avro的 RPC 框架;


  9. 总而言之,Netty是最流行的NIO框架,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。它已经得到成百上千的商业/商用项目验证,如Hadoop的RPC框架Avro、RocketMQ以及主流的分布式通信框架Dubbox等等。
    最重要的是,Netty内部的设计非常复杂,但是其暴露的api却十分简单,这对开发人员来说十分友好,使用Netty不必编写复杂的逻辑代码去实现通信,再也不需要去考虑性能问题,不需要考虑编码问题,半包读写等问题。强大的Netty已经帮我们实现好了,我们只需要使用即可。
    往通俗了讲,可以将Netty理解为:一个将Java NIO进行了大量封装,并大大降低Java NIO使用难度和上手门槛的超牛逼框架。


想要学好Netty,最重要的不是代码的编写和会使用。而是去理解它的工作模式。
官方介绍:
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
上述文字对于不了解网络编程的童鞋来说还是有些抽象,所以我们先不着急去深入Netty学习,第一步,先把认知提上来。
让我们回顾一下Java的三大IO模型,分别是BIO【同步阻塞IO】、NIO【同步非阻塞IO】、AIO【异步非阻塞IO】。这里所有IO调用的双端有两个线程,一个是客户端,一个是服务端。并且现在客户端要向服务端发送数据。那么借助这个例子,同步,异步,阻塞,非阻塞的概念可以解释如下
1、同步:客户端发起IO后,直到数据返回才能继续
2、异步:客户端发起IO后,不需要等待数据返回就可以继续
3、阻塞:线程被挂起
4、非阻塞:线程不被挂起
那么同步阻塞IO、同步非阻塞IO、异步非阻塞IO到底是个什么东东…

首先,我们需要知道,执行一个IO操作分为三步,分别是IO执行前(read或write方法),IO执行中(业务处理),IO执行后(send,关闭连接等)。在这三个阶段,IO执行中这个过程是必定要阻塞的,而IO执行前(read)这个过程可以阻塞也可以非阻塞。所以NIO相较于BIO来说称之为非阻塞的原因就在于在IO执行前是非阻塞的。【这个一定要理解对!!】

  • BIO:客户端线程一旦发起IO请求,无论服务端是否有数据,在等待结果返回的过程中,将一直处于【阻塞】状态,此时线程将被挂起,不能继续往下执行,只有当服务端准备好了数据,客户端将数据读取完毕,才能继续往下执行【同步】
  • NIO:客户端线程发起IO请求后,服务端线程将立马给予一个响应,这个响应主要是告诉客户端是否有数据的,倘若没有数据,客户端线程将不等待,继续往下执行【非阻塞】。同时,按一定的周期进行轮询,直到服务端线程有了数据,处于【同步】态,数据读取完毕后结束继续往下执行
  • AIO:客户端线程发起IO请求后,可以继续往下执行【异步】,无需等待服务端返回数据,当服务端有了数据后,将通过回调机制通知客户端,客户端知道有了数据后,将执行IO操作。【非阻塞】
    同样,上述的客户端可以和服务端互换位置也是一样的。
    那么Netty的底层是什么呢?
    Netty的底层是NIO【为什么不是AIO,因为当时Netty的开发团队JBoss对Netty5采用了AIO后,发现并没有在NIO的基础上有多少提升,甚至还有落后,所以就不用了】,设计思想是Reactor设计模式。
    二、Reactor设计模式
    中文名字不重要,我们需要做到的是看到这个Reactor就能联想到其含义。
    NIO的思想就来源于Reactor设计模式
    Reactor模式也叫Dispatcher模式,他的功能主要是 将客户端的请求分发到一个或多个服务器上去【有点像Nginx,其实Nginx、redis底层思想就是这个】。工作原理是
    由一个线程来接收所有的请求,然后派发这些请求到相关的工作线程中。
    为什么会出现这样一个东西呢?
    在BIO中,一个请求对应一个线程,在高并发的情况下,会创建大量的线程,线程之前的切换也会消耗大量的资源,所以提出了Reactor模式。
    Reactor模型有三个角色:
    Reactor:负责响应事件,将事件分发到绑定了对应事件的Handler,如果是连接事件,则分发到Acceptor。Handler:事件处理器。负责执行对应事件对应的业务逻辑。
    Acceptor:绑定了 connect 事件,当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。
    其原理如下图
    在这里插入图片描述

ps:看到这张图,我立马想到了SpringMVC
Reactor模型经历了三个过程,原理如图

  1. 单Reactor单线程
    在这里插入图片描述

  2. 单Reactor多线程
    在这里插入图片描述

  3. 多Reactor多线程【Netty的设计思想】
    在这里插入图片描述

三个模型不一一介绍,只介绍下这三个模型的通用工作流程

  1. Reactor对象通过select监听客户端请求事件,收到事件后,通过dispatch进行分发
  2. 如果成功建立连接请求,则Acceptor通过accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件。
  3. 不是connect请求,则由reactor直接分发到handler处理。
  4. handler只负责相应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池的某个线程处理业务。
  5. worker线程池会分配独立线程完成真正的业务,并将结果返回给handler。
  6. handler收到响应后,通过send分发将结果返回给client。
    Netty主要基于第三个模型,所以我们要好好理解下第三个模型的工作流程
    1、Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件。
    2、当Acceptor处理连接事件后,MainReactor将连接分配给SubAcceptor。
    3、SubAcceptor将连接加入到连接队列进行监听,并创建handler进行各种事件处理。
    4、当有新事件发生时,SubAcceptor就会调用对应的handler进行各种事件处理。
    5、handler通过read读取数据,分发给后面的work线程处理。
    6、work线程池分配独立的work线程进行业务处理,并返回结果。
    7、handler收到响应的结果后,再通过send返回给client。
    通俗来讲,在多Reactor多线程的模型中,Reactor主线程只负责处理连接请求,而将业务处理请求下发到Reactor子线程。子线程通过分发到对应的Handler,Handler在利用线程池获取线程进行业务处理,处理结果重新交给Handler,Handler再将结果分发出去
    三、真正的Netty
    Netty是完全基于NIO实现的,所以整个Netty都是异步的
    直接上图

左图是Netty的原理图,右图是Reactor的设计模型图
上面的图先别着急去对比着理解,我们先把Netty的组件了解完毕后再来看就会比较清晰了
Netty模块组件
Bootstrap、ServerBootstrap
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
Future、ChannelFuture
在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。
但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
Channel
Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:

  • 当前网络连接的通道的状态(例如是否打开?是否已连接?)
  • 网络连接的配置参数 (例如接收缓冲区大小)
  • 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
  • 支持关联 I/O 操作与对应的处理程序。
    不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。下面是一些常用的 Channel 类型:
  • NioSocketChannel,异步的客户端 TCP Socket 连接。
  • NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
  • NioDatagramChannel,异步的 UDP 连接。
  • NioSctpChannel,异步的客户端 Sctp 连接。
  • NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
    Selector
    Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
    当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
    NioEventLoop
    NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
  • I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
  • 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
    两种任务的执行时间必有变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。
    NioEventLoopGroup
    NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
    ChannelHandler
    ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
    ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
  • ChannelInboundHandler 用于处理入站 I/O 事件。
  • ChannelOutboundHandler 用于处理出站 I/O 操作。
    或者使用以下适配器类:
  • ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
  • ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
  • ChannelDuplexHandler 用于处理入站和出站事件。
    ChannelHandlerContext
    保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
    ChannelPipline
    保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。
    ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
    看到没,其实很多组件只是Netty将Reactor的模型组件换了个名字而已,其本质还是一样的
    现在再来看这张图

Server 端包含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup。
NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
每个 Boss NioEventLoop 循环执行的任务包含 3 步:

  • 轮询 Accept 事件。
  • 处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上。
  • 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。
    每个 Worker NioEventLoop 循环执行的任务包含 3 步:
  • 轮询 Read、Write 事件。
  • 处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理。
  • 处理任务队列中的任务,runAllTasks。
    其中任务队列中的Task有 3 种典型使用场景
    ①用户程序自定义的普通任务
    ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run(){
    //…
    }
    });
    ②非当前 Reactor 线程调用 Channel 的各种方法
    例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费。
    ③用户自定义定时任务
    ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run(){
    //…
    }
    },60,TimeUnit.SECONDS);
    看完上面的一大堆,相信你心里应该对Netty的底层逻辑稍微有点感觉了。下面是定义一个NettyServer的代码样例
    public static void main(String[] args) throws Exception{
    // boss线程组
    NioEventLoopGroup bossGroup = new NioEventLoopGroup();
    // worker线程组
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();
    // 创建服务端的启动对象,设置参数
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    serverBootstrap.group(bossGroup,workerGroup)
    .channel(NioServerSocketChannel.class)
    .option(ChannelOption.SO_BACKLOG,128)
    .childOption(ChannelOption.SO_KEEPALIVE,true)
    .childHandler(new ChannelInitializer() {
    // 初始化通道Channel,可以在此加入多个Handler
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
    socketChannel.pipeline().addLast(new MyServerHandler());
    }
    });
    //绑定服务,确定端口号,该实例将提供有关IO操作的结果或状态的信息
    ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();
    channelFuture.channel().closeFuture().sync();
    //关闭EventLoopGroup并释放所有资源,包括所有创建的线程
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
    }
    首先,定义了两个线程组BossGroup和WorkerGroup,一个负责接收客户端的连接,另一个负责负责处理消息IO。利用Boostrap引导对象设置Netty初始化参数,然后给child【Child就是workerGroup,看Netty的源码可以发现,线程组只有父线程组和子线程组的概念】添加Handler处理器,其中pipeline中定义了一系列的handler方法。Future的目的是注册一个监听事件,在上述代码中的作用是监听通道,当关闭时触发监听事件。
    下面是NettyClient的代码样例:
    public static void main(String[] args) throws Exception {
    NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
    try {
    //创建bootstrap对象,配置参数
    Bootstrap bootstrap = new Bootstrap();
    //设置线程组
    bootstrap.group(eventExecutors)
    //设置客户端的通道实现类型
    .channel(NioSocketChannel.class)
    //使用匿名内部类初始化通道
    .handler(new ChannelInitializer() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
    //添加客户端通道的处理器
    ch.pipeline().addLast(new MyClientHandler());
    }
    });
    //连接服务端,和服务端主要的区别在此
    ChannelFuture channelFuture = bootstrap.connect(“127.0.0.1”, 6666).sync();
    //对通道关闭进行监听
    channelFuture.channel().closeFuture().sync();
    } finally {
    //关闭线程组
    eventExecutors.shutdownGracefully();
    }
    其中,每一个Netty组件的各个方法,都是基于上述理论和NIO及多线程写出来的,明白了以上,还只是处于会使用的阶段,要想有更深入的理解,还是要深入去阅读源码。
    好了,今天就到这~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值