文章核心内容:

为什么要学习Netty:
在java的网络编程方面,最开始的BIO 到现在被广泛使用的NIO,它们都能完成网络编程的工作,但是使用原生的java API去完成网络编程,往往非常困难。连接管理、消息分割、数据编解码,还有对应应用层协议的实现,这些对开发人员的要求非常高。所以使用和学习Netty这样的网络框架是非常有必要的。

Netty是什么
Netty是对 Java NIO 的进一步封装,实现了众多的协议,并且提供了更加易用的API来帮助我们进行网络编程。本篇博客对Netty的概念进行了详细说名,并且介绍了Netty常用的核心API和线程模型。后续还会有更多的案例来一步步学习Netty

一、使用Netty的理由

Java NIO 是一种非常高效的 I/O 技术,但它也存在一些缺陷:

  1. 复杂性:相对于传统的阻塞 IO,Java NIO 对程序员的要求更高,代码实现相对更复杂。
  2. 容易出错:由于 Java NIO 使用了多路复用器 Selector,需要正确处理各种状态和事件。如果处理不当,容易出现死锁、阻塞等问题。
  3. 开发效率低:使用 Java NIO 开发网络应用需要编写大量的重复代码,如网络连接管理、消息分割、数据编码解码等,增加了开发难度和耗时。
  4. JDK的NIO自1.4~1.7版本中,存在BUG- Epoll BUG,会导致Selector空轮询,导致CPU100%

Netty的优势

  1. 封装复杂性:Netty 对 Java NIO API 进行了封装和扩展,提供了简单易用的编程接口和功能,隐藏了底层复杂性,使得开发变得更加简便和高效。
  2. 高性能:Netty 采用了异步非阻塞 IO 模型,并支持零拷贝等技术,可以在保证高性能的同时,减少了 CPU 和内存资源的消耗。
  3. 易扩展性:Netty 支持插件式开发,可以很方便地扩展和定制自己的网络应用,同时也支持多种协议和编解码方式。

综上所述,使用 Netty 可以大大提高开发效率,减少错误和代码复杂度,同时还能获得高性能、可扩展的网络应用程序。



二、Netty概述

  • Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序。
  • Netty 是一个基于 NIO 的网络编程框架,使用Netty 可以帮助你快速、简单的开发出一 个网络应用,相当于简化和流程化了 NIO 的开发过程。
  • 作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、 通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。

Netty的协议和核心
image.png
Netty的优点

  1. 设计优雅,提供阻塞和非阻塞的Socket,提供灵活可拓展的事件模型
  2. 具备更大的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制减少资源消耗
  3. 提供安全传输的特性支持
  4. 支持多种主流协议,预制多种编解码功能,并且支持开发私有传输协议。



三、 常见的的线程模型介绍

reactor:反应器
目前主流的网络框架都采用了I/O多路复用方案(一个线程监听所有的网络连接IO事件是否就绪的模式,就是IO多路复用)。Reactor模式作为其中的事件分发器,负责将读写事件分发给对应的读写事件处理者。
Reactor 模式的组成角色:

  1. Reactor 监听IO事件,分配给对应的Handler
  2. Acceptor 处理客户端连接事件并分派请求道处理器链
  3. Handlers 执行非阻塞读/写任务

常见的的线程模型

  1. 传统BIO阻塞服务模型
  2. NIO的Reactor模型,根据reactor数量和处理资源池线程数量分成三种类型
    1. 单Reactor 单线程
    2. 单Reactor 多线程
    3. 多Reactor 多线程

3.1.1 传统BIO阻塞服务模型

image.png
问题:

  1. 当并发数很大,就会创建大量的线程,占用很大系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 read 操作,造成线程资源浪费

3.1.2 Reactor模式线程模型

Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式 , 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程, 因此 Reactor 模式也叫 Dispatcher模式. Reactor 模式使用IO 复用监听事件, 收到事件后,分发给某个线程(进程), 这点就是网络服务器高并发处理关键。

1.单Reactor 单线程

image.png
优点:模型简单,没有多线程就不用处理进程通信,资源竞争问题。
缺点:

  1. 单线程无法发挥全部的硬件性能,并且容易被某个任务阻塞导致整个进程无法工作。容易产生性能瓶颈。
  2. 一旦线程终止或者死循环,整个系统就无法使用。

2.单 Reactor多线程

image.png

  1. Reactor 对象通过 selector 监控客户端请求事件, 收到事件后,通过 dispatch 进行分发
  2. 如果建立连接请求, 则右 Acceptor 通过accept 处理连接请求
  3. 如果不是连接请求,则由 reactor 分发调用连接对应的 handler 来处理
  4. handler 只负责响应事件,不做具体的业务处理, 通过 read 读取数据后,会分发给后面的
  5. worker 线程池的某个线程处理业务
  6. worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler
  7. handler 收到响应后,通过 send 将结果返回给 client

优点:可以充分利用多个CPU能力
缺点:多线程数据共享和访问容易出问题,一个reactor处理所有事件的监听和响应单线程运行时在高并发场景容易出现性能瓶颈。

3.主从 Reactor 多线程

image.png

  1. Reactor 主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过Acceptor 处理客户端连接事件
  2. 当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给 SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由 SubReactor 监听后面的 IO 事件。)
  3. SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理
  4. Handler 通过 read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理
  5. Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据
  6. 一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个SubReactor 线程

优点:

  1. MainReactor 线程与 SubReactor 线程的数据交互简单职责明确,MainReactor 线程只需要接收新连接,SubReactor 线程完成后续的业务处理.
  2. MainReactor 线程与 SubReactor 线程的数据交互简单, MainReactor 线程只需要把新连接传给 SubReactor 线程,SubReactor 线程无需返回数据
  3. 多个 SubReactor 线程能够应对更高的并发请求

缺点:
这种模式的缺点是编程复杂度较高。但是由于其优点明显,在许多项目中被广泛使用,包括Nginx、Memcached、Netty 等。这种模式也被叫做服务器的 1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个,1 只是表示相对较少)连接建立线程+M 个 IO 线程+N 个业务处理线程。这是业界成熟的服务器程序设计模式



四、 Netty的线程模型

Netty线程模型采用了 多Reactor多线程模式设计,并且做了一定的改进。

4.1 简易版理解Netty线程模型

image.png

  1. BossGroup 线程维护 Selector,ServerSocketChannel 注册到这个 Selector 上,只关注连接建立请求事件(主 Reactor)
  2. 当接收到来自客户端的连接建立请求事件的时候,通过 ServerSocketChannel.accept 方法获得对应的 SocketChannel,并封装成 NioSocketChannel 注册到 WorkerGroup 线程中的Selector,每个 Selector 运行在一个线程中(从 Reactor)
  3. 当 WorkerGroup 线程中的 Selector 监听到自己感兴趣的 IO 事件后,就调用 Handler 进行处理

4.2 进阶版Netty模型

image.png

  1. 有两组线程池:BossGroup 和 WorkerGroup,BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写
  2. BossGroup 和 WorkerGroup 含有多个不断循环的执行事件处理的线程,每个线程都包含一个 Selector,用于监听注册在其上的 Channel
  3. 每个 BossGroup 中的线程循环执行以下三个步骤
    1. 轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)
    2. 处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到WorkerGroup 中某个线程上的 Selector 上
    3. 再去以此循环处理任务队列中的下一个事件
  4. 每个 WorkerGroup 中的线程循环执行以下三个步骤
    1. 轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)
    2. 在对应的 NioSocketChannel 上处理 read/write 事件
    3. 再去以此循环处理任务队列中的下一个事件

4.3 详细版的Netty线程模型

image.png

  1. Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,也可以叫做BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是NioEventLoopGroup
  2. NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。
  3. NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个Selector,用于监听注册在其上的 Socket 网络连接(Channel)
  4. NioEventLoopGroup 可以含有多个线程,即可以含有多个 NioEventLoop
  5. 每个 BossNioEventLoop 中循环执行以下三个步骤
    1. select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)
    2. processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上
    3. runAllTasks:再去以此循环处理任务队列中的其他任务
  6. 每个 WorkerNioEventLoop 中循环执行以下三个步骤
    1. select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)
    2. processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件
    3. runAllTasks:再去以此循环处理任务队列中的其他任务

在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了Channel,即通过Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。



五、Netty的核心API

Netty的核心API:ChannelHandler(通道处理器)、ChannelPipeLine(通道流水线)、

5.1 ChannelHandler 接口及其实现类

Netty开发需要自定义一个Handler类来实现 ChannelHandler接口或者其子类,然后重写相应的业务方法来实现具体的功能。

image.png
ChannelOutboundhandler 服务端写出数据处理
ChannelInboundHandler 服务端接收数据处理
ChannerlHandlerAdapter 接收&写出数据处理
一般需要重写的方法

  • public void channelActive(ChannelHandlerContext ctx),通道就绪事件
  • public void channelRead(ChannelHandlerContext ctx, Object msg),通道读取数据事件
  • public void channelReadComplete(ChannelHandlerContext ctx) ,数据读取完毕事件
  • public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause),通道发生异常事件

5.2 ChannelPipeLine 通道流水线

ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的责任链.

  1. 如果客户端和服务器的Handler是一样的,消息从客户端到服务端或者反过来,每个Inbound类型或Outbound类型的Handler只会经过一次,混合类型的Handler(实现了Inbound和Outbound的Handler)会经过两次。
  2. 准确的说ChannelPipeline中是一个ChannelHandlerContext,每个上下文对象中有ChannelHandler。InboundHandler是按照Pipleline的加载顺序的顺序执行, OutboundHandler是按照Pipeline的加载顺序,逆序执行。

5.3 ChannelHandlerContext 通道处理上下文

这是事件处理器上下文对象 Pipeline链中的实际处理节点 。每个处理节点ChannelHandlerContext中包含一个 具体的事件处理器 ChannelHandler ,同时ChannelHandlerContext 中也绑定了对应的 ChannelPipeline和Channel 的信息,方便对ChannelHandler 进行调用。常用方法如下所示:

  1. ChannelFuture close(),关闭通道
  2. ChannelOutboundInvoker flush(),刷新
  3. ChannelFuture writeAndFlush(Object msg) ,将数据写到ChannelPipeline中当前 ChannelHandler 的下一个 ChannelHandler 开始处理(出站)。

5.4 ChannelOption 通道配置选项

Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。ChannelOption 是 Socket 的标
准参数,而非 Netty 独创的。常用的参数配置有:

  • ChannelOption.SO_BACKLOG 对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户 端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定 了队列的大小。
  • ChannelOption.SO_KEEPALIVE ,一直保持连接活动状态。该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

5.5 ChannelFuture

表示 Channel 中异步 I/O 操作的结果,在 Netty 中所有的 I/O 操作都是异步的,I/O 的调用会直接返回,调用者并不能立刻获得结果,但是可以通过 ChannelFuture 来获取 I/O 操作 的处理状态。常用方法如下所示:

  • Channel channel(),返回当前正在进行 IO 操作的通道
  • ChannelFuture sync(),等待异步操作执行完毕,将异步改为同步

5.6 EventLoopGroup和实现类NioEventLoopGroup

EventLoopGroup 是一组 EventLoop 的抽象,Netty为了更好的利用多核 CPU 资源,一般会有多个EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例。

EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop 来处理任
务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup。
例如:BossEventLoopGroup 和 WorkerEventLoopGroup。 通常一个服务端口即一个 ServerSocketChannel
对应一个Selector 和一个EventLoop线程。 BossEventLoop 负责接收客户端的连接并将SocketChannel 交给 WorkerEventLoopGroup 来进 行 IO 处理,如下图所示:

image.png
BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一个注册了ServerSocketChannel 的 Selector 实例,BossEventLoop 不断轮询 Selector 将连接事件分离出来, 通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup,WorkerEventLoopGroup 会由 next 选择其中一个 EventLoopGroup 来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理。
常用方法如下:

  • public NioEventLoopGroup(),构造方法,创建线程组
  • public Future<?> shutdownGracefully(),断开连接,关闭线程

5.7 ServerBootstrap和Bootstrap

ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置;Bootstrap 是 Netty 中的客户端启动助手,通过它可以完成客户端的各种配置。
常用方法:

  • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroupchildGroup), 该方法用于服务器端,用来设置两个 EventLoop
  • public B group(EventLoopGroup group) ,该方法用于客户端,用来设置一个 EventLoop
  • public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道 实现
  • public B option(ChannelOption option, T value),用来给 ServerChannel 添加配置
  • public ServerBootstrap childOption(ChannelOption childOption, T value),用来给接收到的通道添加配置
  • public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的 handler)
  • public ChannelFuture bind(int inetPort) ,该方法用于服务器端,用来设置占用的端口号
  • public ChannelFuture connect(String inetHost, int inetPort) ,该方法用于客户端,用来连接服务器端

5.8 Unpooled类

是 Netty 提供的一个专门用来操作缓冲区的工具类
常见用法:public static ByteBuf copiedBuffer(CharSequence string, Charset charset),通过给定的数据 和
字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 对象)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐