架构师必知必会之Netty原理

内容分享1周前发布
2 2 0

1、Netty是什么?

1.1、Netty简介

Netty是⼀个⾼性能的、异步的、基于事件驱动的⽹络应⽤框架。官⽹:https://netty.io/ 官⽅对它有这样的描述:

架构师必知必会之Netty原理

1.1.1、异步、事件驱动

同步、异步是相对的,在请求或执⾏过程中,如果会阻塞等待,就是同步操作,反之就是异步操作。

架构师必知必会之Netty原理

在我们熟悉的Ajax请求,就是异步并且是基于事件驱动的:

架构师必知必会之Netty原理

1.1.2、核⼼架构

架构师必知必会之Netty原理

核⼼

  • 可扩展的事件模型
  • 统⼀的通信api
    • ⽆论是http还是socket都使⽤统⼀的api,简化了操作
    • 零拷⻉机制与字节缓冲区
  • 传输服务
    • ⽀持socket以及datagram(数据报)
    • ⽀持http协议
    • In-VM Pipe (管道协议)
  • 协议⽀持
    • http 以及 websocket
    • SSL 安全套接字协议⽀持
    • Google Protobuf (序列化框架)
    • ⽀持zlib、gzip压缩
    • ⽀持⼤⽂件的传输
    • RTSP(实时流传输协议,是TCP/IP协议体系中的⼀个应⽤层协议)
    • ⽀持⼆进制协议并且提供了完整的单元测试

1.1.3、Netty优势

  • Netty是基于Java的NIO实现的,Netty将各种传输类型、协议的实现API进⾏了统⼀封装,实现了阻塞和⾮阻塞Socket。
  • 基于事件模型实现,可以清晰的分离关注点,让开发者可以聚焦业务,提升开发效率。
  • ⾼度可定制的线程模型-单线程、⼀个或多个线程池,如SEDA(Staged Event-DrivenArchitecture)
    • SEDA:把⼀个请求处理过程分成⼏个Stage,不同资源消耗的Stage使⽤不同数量的线程来处理,Stage间使⽤事件驱动的异步通信模式。
  • Netty只依赖了JDK底层api,没有其他的依赖,如:Netty 3.X依赖JDK5以上,Netty4.x依赖JDK6以上。
  • Netty在⽹络通信⽅⾯更加的⾼性能、低延迟,尽可能的减少不必要的内存拷⻉,提⾼性能。
  • 在安全⽅⾯,完整的SSL/TLS和StartTLS⽀持。
  • 社区⽐较活跃,版本迭代周期短,发现bug可以快速修复,新版本也会不断的加⼊。

1.1.4、版本说明

Netty的版本分为,3.x、4.x和5.x,其中5.x版本已经被官⽅废弃,详情查看github的issue:
https://github.com/netty/netty/issues/4466

架构师必知必会之Netty原理

废弃5.x的主要缘由是,使⽤ForkJoinPool后复杂度提升了,但是性能⽅⾯并没有明显的优势,反⽽给项⽬的维护带来了很⼤的⼯作量,因此还有到发布新版本的时机,所以将5.x废弃。

Netty的下载:

架构师必知必会之Netty原理

⽬前Netty的最新版本为4.1.50.Final,本套课程基于此版本学习的。

1.2、为什么选择Netty,⽽不选择原⽣的NIO?

在⽹络编程⽅⾯,⼀般都不会选择原⽣的NIO,⽽是会选择Netty、Mina等封装后的框架,主要缘由是:

  • NIO的类库和API繁杂,使⽤麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是由于NIO编程涉及到Reactor模式,你必须对多线程和⽹路编程⾮常熟悉,才能编写出⾼质量的NIO程序。
  • 可靠性能⼒补⻬,⼯作量和难度都⾮常⼤。例如客户端⾯临断连重连、⽹络闪断、半包读写、失败缓存、⽹络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能⼒补⻬的⼯作量和难度都⾮常⼤。
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官⽅声称在JDK 1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发⽣概率降低了⼀些⽽已,它并没有得到根本性解决。具体问题查看:https://www.jianshu.com/p/3ec120ca46b2

1.3、Netty应⽤场景

Netty的应⽤场景是⾮常⼴泛的,⽐如:互联⽹⾏业的、游戏⾏业、⼤数据⾏业、医疗⾏业、⾦融等⾏业

互联⽹⾏业

  • 在互联⽹⾏业项⽬中,最具代表性的就是分布式系统架构的远程服务调⽤,通过RPC的⽅式进⾏⾼性能的服务调⽤,⽬前主流的RPC框架底层均采⽤了Netty作为⽹络通信组件。
  • ⽐如:阿⾥巴巴的分布式服务治理框架Dubbo,底层就是使⽤Netty作为通信组件。
  • gRPC,是Google提供的⾼性能RPC框架,底层也使⽤了Netty。

⼤数据⾏业

  • ⼤数据⾏业中的许多技术也采⽤了Netty作为通信组件,如:Flink、Spark、Elasticsearch等。

官⽅列出了使⽤Netty的⼀些项⽬:
https://netty.io/wiki/related-projects.html

架构师必知必会之Netty原理

1.4、电商系统⾃研RPC

市⾯上有许多的RPC框架,⽐如:dubbo、gRPC、thrift等产品,在开发项⽬时,我们可以选择使⽤已有的RPC产品,也可以⾃研RPC,⼀线⼤⼚⼀般会选择⾃研RPC,会根据⾃身的业务特点进⾏研发,以追求更⾼的性能。

本套课程,我们也从⾃研RPC需求出发,尝试着使⽤Netty来开发属于⾃⼰的RPC框架。

RPC基本的调⽤示意图:

架构师必知必会之Netty原理

在实现⾃研RPC后,我们将基于此来实现电商系统中的订单模块的业务,当然了,这⾥所实现的业务⽐较简单,主要是学习⾃研RPC为主。

架构师必知必会之Netty原理

2、Netty的⾼性能设计

在本⼩节中,我们来探索Netty的⾼性能设计,来了解Netty的性能⾼在哪⾥?

要想了解Netty的性能为什么⾼,就需要从Java的IO模型聊起,然后对⽹络编程中的Reactor线程模型理解,Netty就是使⽤Java的NIO实现了Reactor线程模型,理解这么多内容需要理解许多的概念,下⾯我们将⼀点点的进⾏学习了解。

2.1、Java中的IO模型

在JDK1.4之前,基于Java所有的socket通信都采⽤了同步阻塞模型(BIO),这种模型性能低下,当时⼤型的服务均采⽤C或C++开发,由于它们可以直接使⽤操作系统提供的异步IO或者AIO,使得性能得到⼤幅提升。

2002年,JDK1.4发布,新增了java.nio包,提供了许多异步IO开发的API和类库。新增的NIO,极⼤的促进了基于Java的异步⾮阻塞的发展和应⽤。

2011年,JDK7发布,将原有的NIO进⾏了升级,称为NIO2.0,其中也对AIO进⾏了⽀持。

2.1.1、BIO模型

java中的BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接⼝在java.io下。

BIO模型简单来讲,就是服务端为每⼀个请求都分配⼀个线程进⾏处理,如下:

架构师必知必会之Netty原理

示例代码:

架构师必知必会之Netty原理

这种模式存在的问题:

  • 客户端的并发数与后端的线程数成1:1的⽐例,线程的创建、销毁是⾮常消耗系统资源的,随着并发量增⼤,服务端性能将显著下降,甚⾄会发⽣线程堆栈溢出等错误。
  • 当连接创建后,如果该线程没有操作时,会进⾏阻塞操作,这样极⼤的浪费了服务器资源。

2.1.2、NIO模型

NIO,称之为New IO 或是 non-block IO (⾮阻塞IO),这两种说法都可以,实则称之为⾮阻塞IO更恰当⼀些。

NIO相关的代码都放在了java.nio包下,其三⼤核⼼组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复⽤器)

Buffer

  • 在NIO中,所有的读写操作都是基于缓冲区完成的,底层是通过数组实现的,常⽤的缓冲区是ByteBuffer,每⼀种java基本类型都有对应的缓冲区对象(除了Boolean类型),如:CharBuffer、IntBuffer、LongBuffer等。

Channel

  • 在BIO中是基于Stream实现,⽽在NIO中是基于通道实现,与流不同的是,通道是双向的,既可以读也可以写。

Selector

  • Selector是多路复⽤器,它会不断的轮询注册在其上的Channel,如果某个Channel上发⽣读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey获取就绪Channel的集合,进⾏IO的读写操作。

基本示意图如下:

架构师必知必会之Netty原理

可以看出,NIO模型要优于BIO模型,主要是:

  • 通过多路复⽤器就可以实现⼀个线程处理多个通道,避免了多线程之间的上下⽂切换导致系统开销过⼤。
  • NIO⽆需为每⼀个连接开⼀个线程处理,并且只有通道真正有有事件时,才进⾏读写操作,这样⼤⼤的减少了系统开销。

示例代码:

架构师必知必会之Netty原理

架构师必知必会之Netty原理

2.1.3、AIO模型

在NIO中,Selector多路复⽤器在做轮询时,如果没有事件发⽣,也会进⾏阻塞,如何能把这个阻塞也优化掉呢?那么AIO就在这样的背景下诞⽣了。

AIO是asynchronous I/O的简称,是异步IO,该异步IO是需要依赖于操作系统底层的异步IO实现。

AIO的基本流程是:⽤户线程通过系统调⽤,告知kernel内核启动某个IO操作,⽤户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知⽤户程序,⽤户执⾏后续的业务操作。

架构师必知必会之Netty原理

⽬前AIO模型存在的不⾜:

  • 需要完成事件的注册与传递,这⾥边需要底层操作系统提供⼤量的⽀持,去做⼤量的⼯作。
  • Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就⽬前的业界形式来说,Windows 系统,很少作为百万级以上或者说⾼并发应⽤的服务器操作系统来使⽤。
  • ⽽在 Linux 系统下,异步IO模型在2.6版本才引⼊,⽬前并不完善。所以,这也是在 Linux 下,实现⾼并发⽹络编程时都是以 NIO 多路复⽤模型模式为主

2.2、Reactor线程模型

Reactor线程模型不是Java专属,也不是Netty专属,它实则是⼀种并发编程模型,是⼀种思想,具有指导意义。⽐如,Netty就是结合了NIO的特点,应⽤了Reactor线程模型所实现的。

Reactor模型中定义的三种⻆⾊:

  • Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建⽴就绪、读就绪、写就绪等。
  • Acceptor:处理客户端新连接,并分派请求到处理器链中。
  • Handler:将⾃身与事件绑定,执⾏⾮阻塞读/写任务,完成channel的读⼊,完成处理业务逻辑后,负责将结果写出channel。

常⻅的Reactor线程模型有三种,如下:

  • Reactor单线程模型
  • Reactor多线程模型
  • 主从Reactor多线程模型

2.2.1、单Reactor单线程模型

架构师必知必会之Netty原理

说明:

  • Reactor充当多路复⽤器⻆⾊,监听多路连接的请求,由单线程完成
  • Reactor收到客户端发来的请求时,如果是新建连接通过Acceptor完成,其他的请求由Handler完成。
  • Handler完成业务逻辑的处理,基本的流程是:Read –> 业务处理 –> Send 。

优点

  • 结构简单,由单线程完成,没有多线程、进程通信等问题。
  • 适合⽤在⼀些业务逻辑⽐较简单、对于性能要求不⾼的应⽤场景。

缺点

  • 由于是单线程操作,不能充分发挥多核CPU的性能。
  • 当Reactor线程负载过重之后,处理速度将变慢,这会导致⼤量客户端连接超时,超时之后往往会进⾏重发,这更加重Reactor线程的负载,最终会导致⼤量消息积压和处理超时,成为系统的性能瓶颈。
  • 可靠性差,如果该线程进⼊死循环或意外终⽌,就会导致整个通信系统不可⽤,容易造成单点故障。

2.2.2、单Reactor多线程模型

架构师必知必会之Netty原理

说明:

  • 在Reactor多线程模型相⽐较单线程模型⽽⾔,不同点在于,Handler不会处理业务逻辑,只是负责响应⽤户请求,真正的业务逻辑,在另外的线程中完成。
  • 这样可以降低Reactor的性能开销,充分利⽤CPU资源,从⽽更专注的做事件分发⼯作了,提升整个应⽤的吞吐。

但是这个模型存在的问题:

  • 多线程数据共享和访问⽐较复杂。如果⼦线程完成业务处理后,把结果传递给主线程Reactor进⾏发送,就会涉及共享数据的互斥和保护机制。
  • Reactor承担所有事件的监听和响应,只在主线程中运⾏,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握⼿进⾏安全认证,但是认证本身⾮常损耗性能。

为了解决性能问题,产⽣了第三种主从Reactor多线程模型。

2.2.3、主从Reactor多线程模型

架构师必知必会之Netty原理

在主从模型中,将Reactor分成2部分:

  • MainReactor负责监听server socket,⽤来处理⽹络IO连接建⽴操作,将建⽴的socketChannel指定注册给SubReactor。
  • SubReactor主要完成和建⽴起来的socket的数据交互和事件业务处理操作。

该模型的优点:

  • 响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的。
  • 可扩展性强,可以⽅便地通过增加SubReactor实例个数来充分利⽤CPU资源。
  • 可复⽤性⾼,Reactor模型本身与具体事件处理逻辑⽆关,具有很⾼的复⽤性。

2.3、Netty模型

Netty模型是基于Reactor模型实现的,对于以上三种模型都有⾮常好的⽀持,也⾮常的灵活,⼀般情况,在服务端会采⽤主从架构模型,基本示意图如下:

架构师必知必会之Netty原理

说明:

  • 在Netty模型中,负责处理新连接事件的是BossGroup,负责处理其他事件的是WorkGroup。Group就是线程池的概念。
  • NioEventLoop表明⼀个不断循环的执⾏处理任务的线程,⽤于监听绑定在其上的读/写事件。
  • 通过Pipeline(管道)执⾏业务逻辑的处理,Pipeline中会有多个ChannelHandler,真正的业务逻辑是在ChannelHandler中完成的。

3、Netty快速⼊⻔

开发环境:JDK8 + Idea

3.1、创建itcast-MyRPC项⽬

pom.xml⽂件:

架构师必知必会之Netty原理

3.2、服务端

3.2.1、MyRPCServer

架构师必知必会之Netty原理

3.2.2、MyChannelInitializer

架构师必知必会之Netty原理

3.2.3、MyChannelHandler

架构师必知必会之Netty原理

架构师必知必会之Netty原理

3.2.4、测试⽤例

架构师必知必会之Netty原理

3.2.5、测试

架构师必知必会之Netty原理

可以看到,客户端发送数据到服务端。

3.3、客户端

3.3.1、MyRPCClient

架构师必知必会之Netty原理

架构师必知必会之Netty原理

3.3.2、MyClientHandler

架构师必知必会之Netty原理

3.3.3、测试⽤例

架构师必知必会之Netty原理

3.3.4、测试

架构师必知必会之Netty原理

4、Netty核⼼组件

通过前⾯的学习,我们对Netty的整体开发有了初步的了解,在Netty中有⼀些核⼼组件,我们必须对其要有深刻的理解,下⾯我们⼀⼀来了解下。

4.1、Channel

Channel可以理解为是socket连接,在客户端与服务端连接的时候就会建⽴⼀个Channel,它负责基本的IO操作,⽐如:bind()、connect(),read(),write() 等。

Netty 的 Channel 接⼝所提供的 API,⼤⼤地降低了直接使⽤ Socket 类的复杂性。

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常⽤的 Channel 类型:

  • NioSocketChannel,NIO的客户端 TCP Socket 连接。
  • NioServerSocketChannel,NIO的服务器端 TCP Socket 连接。
  • NioDatagramChannel, UDP 连接。
  • NioSctpChannel,客户端 Sctp 连接。
  • NioSctpServerChannel,Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP ⽹络 IO 以及⽂件

4.2、EventLoop、EventLoopGroup

有了 Channel 连接服务,连接之间可以消息流动。如果服务器发出的消息称作“出站”消息,服务器接受的消息称作“⼊站”消息。那么消息的“出站”/“⼊站”就会产⽣事件(Event)。

例如:连接已激活;数据读取;⽤户事件;异常事件;打开链接;关闭链接等等。

有了事件,就需要⼀个机制去监控和协调事件,这个机制(组件)就是EventLoop。

在 Netty 中每个 Channel 都会被分配到⼀个 EventLoop。⼀个 EventLoop 可以服务于多个 Channel。

每个 EventLoop 会占⽤⼀个 Thread,同时这个 Thread 会处理 EventLoop 上⾯发⽣的所有 IO 操作和事件。

架构师必知必会之Netty原理

EventLoopGroup 是⽤来⽣成 EventLoop 的,在前⾯的例⼦中,第⼀⾏代码就是 newNioEventLoopGroup();

架构师必知必会之Netty原理

如果没有指定线程数⼤⼩,默认线程数为:cpu核数*2,源码如下:

架构师必知必会之Netty原理

上图关系为:

  • ⼀个 EventLoopGroup 包含⼀个或者多个 EventLoop;
  • ⼀个 EventLoop 在它的⽣命周期内只和⼀个 Thread 绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • ⼀个 Channel 在它的⽣命周期内只注册于⼀个 EventLoop;
  • ⼀个 EventLoop 可能会被分配给⼀个或多个 Channel。

4.3、ChannelHandler

ChannelHandler对使⽤者⽽⾔,可以说是最重大的组件了,由于对于数据的⼊站和出站的业务逻辑的编写都是在ChannelHandler中完成的。

在前⾯的例⼦中,MyChannelHandler就是实现了channelRead⽅法,获取到客户端传来的数据。

对于数据的出站和⼊站,有着不同的ChannelHandler类型与之对应:

  • ChannelInboundHandler ⼊站事件处理器
  • ChannelOutBoundHandler 出站事件处理器

接⼝继承关系如下:

架构师必知必会之Netty原理

ChannelHandlerAdapter提供了⼀些⽅法的默认实现,可减少⽤户对于ChannelHandler的编写。

4.4、ChannelPipeline

在Channel的数据传递过程中,对应着有许多的业务逻辑需要处理,⽐如:编码解码处理、读写操作等,那么对于每种业务逻辑实现都需要有个ChannelHandler完成,也就意味着,⼀个Channel对应着多个ChannelHandler,多个ChannelHandler如何去管理它们,它们的执⾏顺序⼜该是怎么样的,这就需要ChannelPipeline进⾏管理了。

⼀个Channel包含了⼀个ChannelPipeline,⽽ChannelPipeline中维护了⼀个ChannelHandler的列表。

ChannelHandler与Channel和ChannelPipeline之间的映射关系,由ChannelHandlerContext进⾏维护。

它们关系如下:

架构师必知必会之Netty原理

ChannelHandler按照加⼊的顺序会组成⼀个双向链表,⼊站事件从链表的head往后传递到最后⼀个ChannelHandler,出站事件从链表的tail向前传递,直到最后⼀个ChannelHandler,两种类型的ChannelHandler相互不会影响。

4.5、Bootstrap

Bootstrap是引导的意思,它的作⽤是配置整个Netty程序,将各个组件都串起来,最后绑定端⼝、启动Netty服务。

Netty中提供了2种类型的引导类,⼀种⽤于客户端(Bootstrap),⽽另⼀种(ServerBootstrap)⽤于服务器。

它们的区别在于:

  • ServerBootstrap 将绑定到⼀个端⼝,由于服务器必须要监听连接,⽽ Bootstrap 则是由想要连接到远程节点的客户端应⽤程序所使⽤的。
  • 引导⼀个客户端只需要⼀个EventLoopGroup,但是⼀个ServerBootstrap则需要两个。
    • 由于服务器需要两组不同的 Channel
    • 第⼀组将只包含⼀个 ServerChannel,代表服务器⾃身的已绑定到某个本地端⼝的正在监听的套接字。
    • 第⼆组将包含所有已创建的⽤来处理传⼊客户端连接。

架构师必知必会之Netty原理

与ServerChannel相关联的EventLoopGroup 将分配⼀个负责为传⼊连接请求创建 Channel 的EventLoop。⼀旦连接被接受,第⼆个 EventLoopGroup 就会给它的 Channel 分配⼀个 EventLoop。

4.6、Future

Future提供了⼀种在操作完成时通知应⽤程序的⽅式。这个对象可以看作是⼀个异步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问。

JDK 预置了 interface
java.util.concurrent.Future,但是其所提供的实现,只允许⼿动检查对应的操作是否已经完成,或者⼀直阻塞直到它完成。这是⾮常繁琐的,所以 Netty 提供了它⾃⼰的实现——ChannelFuture,⽤于在执⾏异步操作的时候使⽤。

  • ChannelFuture提供了⼏种额外的⽅法,这些⽅法使得我们能够注册⼀个或者多个ChannelFutureListener实例。
  • 监听器的回调⽅法operationComplete(),将会在对应的 操作完成时被调⽤ 。然后监听器可以判断该操作是成功地完成了还是出错了。
  • 每个 Netty 的出站 I/O 操作都将返回⼀个 ChannelFuture,也就是说,它们都不会阻塞。 所以说,Netty完全是异步和事件驱动的。

架构师必知必会之Netty原理

上图是 serverBootstrap.bind(port) ⽅法底层的逻辑实现。

4.7、⼩结

架构师必知必会之Netty原理

通过以上图将Netty中的核⼼组件串起来

5、详解ByteBuf

5.1、⼯作原理

Java NIO 提供了ByteBuffer 作为它 的字节容器,但是这个类使⽤起来过于复杂,⽽且也有些繁琐。Netty 的 ByteBuffer 替代品是 ByteBuf,⼀个强⼤的实现,既解决了JDK API 的局限性, ⼜为⽹络应⽤程序的开发者提供了更好的API。

从结构上来说,ByteBuf 由⼀串字节数组构成。数组中每个字节⽤来存放信息。

ByteBuf 提供了两个索引,⼀个⽤于读取数据,⼀个⽤于写⼊数据。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。

当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。

同样,当写 ByteBuf 时,它的 writerIndex(写索引) 也会根据写⼊的字节数进⾏递增。

架构师必知必会之Netty原理

如果 readerIndex 超过了 writerIndex 的时候,Netty 会抛出
IndexOutOf-BoundsException 异常。

5.2、基本使⽤

5.2.1、读取操作

架构师必知必会之Netty原理

架构师必知必会之Netty原理

5.2.2、写⼊操作

架构师必知必会之Netty原理

5.2.3、丢弃已读字节

架构师必知必会之Netty原理

架构师必知必会之Netty原理

架构师必知必会之Netty原理

5.2.4、clear()

架构师必知必会之Netty原理

架构师必知必会之Netty原理

5.3、ByteBuf 使⽤模式

根据存放缓冲区的不同分为三类:

  • 堆缓冲区(HeapByteBuf),内存的分配和回收速度⽐较快,可以被JVM⾃动回收,缺点是,如果进⾏socket的IO读写,需要额外做⼀次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有⼀定程度的下降。由于在堆上被 JVM 管理,在不被使⽤时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
  • 直接缓冲区(DirectByteBuf),⾮堆内存,它在对外进⾏内存分配,相⽐堆内存,它的分配和回收速度会慢⼀些,但是将它写⼊或从Socket Channel中读取时,由于减少了⼀次内存拷⻉,速度⽐堆内存块。
  • 复合缓冲区,顾名思义就是将上述两类缓冲区聚合在⼀起。Netty 提供了⼀个 CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在⼀起,让使⽤更加⽅便。

架构师必知必会之Netty原理

5.4、ByteBuf 的分配

Netty 提供了两种 ByteBufAllocator 的实现,分别是:

  • PooledByteBufAllocator,实现了 ByteBuf 的对象的池化,提⾼性能减少并最⼤限度地减少内存碎⽚。
  • UnpooledByteBufAllocator,没有实现对象的池化,每次会⽣成新的对象实例。

架构师必知必会之Netty原理

架构师必知必会之Netty原理

5.5、ByteBuf的释放

ByteBuf如果采⽤的是堆缓冲区模式的话,可以由GC回收,但是如果采⽤的是直接缓冲区,就不受GC的管理,就得⼿动释放,否则会发⽣内存泄露。

关于ByteBuf的释放,分为⼿动释放与⾃动释放。

5.5.1、⼿动释放

⼿动释放,就是在使⽤完成后,调⽤
ReferenceCountUtil.release(byteBuf); 进⾏释放。

通过release⽅法减去 byteBuf 的使⽤计数,Netty 会⾃动回收 byteBuf 。

架构师必知必会之Netty原理

⼿动释放可以达到⽬的,但是这种⽅式会⽐较繁琐,如果⼀旦忘记释放就可能会造成内存泄露。

5.5.2、⾃动释放

⾃动释放有三种⽅式,分别是:⼊站的TailHandler、继承
SimpleChannelInboundHandler、HeadHandler的出站释放。

5.5.2.1、TailHandler

Netty的ChannelPipleline的流⽔线的末端是TailHandler,默认情况下如果每个⼊站处理器Handler都把消息往下传,TailHandler会释放掉ReferenceCounted类型的消息。

架构师必知必会之Netty原理

在DefaultChannelPipeline中的TailContext内部类会在最后执⾏:

架构师必知必会之Netty原理

需要注意的是,如果没有进⾏向下传递,那么在TailHandler中是不会进⾏释放操作的。

5.5.2.2、SimpleChannelInboundHandler

当ChannelHandler继承了
SimpleChannelInboundHandler后,在
SimpleChannelInboundHandler的channelRead()⽅法中,将会进⾏资源的释放,我们的业务代码也需要写⼊到channelRead0()中。

使⽤

架构师必知必会之Netty原理

架构师必知必会之Netty原理

5.5.2.3、HeadHandler

出站处理流程中,申请分配到的 ByteBuf,通过 HeadHandler 完成⾃动释放。

出站处理⽤到的 Bytebuf 缓冲区,⼀般是要发送的消息,一般由应⽤所申请。在出站流程开始的时候,通过调⽤ ctx.writeAndFlush(msg),Bytebuf 缓冲区开始进⼊出站处理的 pipeline 流⽔线 。

在每⼀个出站Handler中的处理完成后,最后消息会来到出站的最后⼀棒 HeadHandler,再经过⼀轮复杂的调⽤,在flush完成后终将被release掉。

示例:

架构师必知必会之Netty原理

架构师必知必会之Netty原理

执⾏⽅法调⽤链:

架构师必知必会之Netty原理

5.5.3、⼩结

  • ⼊站处理流程中,如果对原消息不做处理,调⽤ ctx.fireChannelRead(msg) 把原消息往下传,由流⽔线最后⼀棒 TailHandler 完成⾃动释放。
  • 如果截断了⼊站处理流⽔线,则可以继承 SimpleChannelInboundHandler ,完成⼊站ByteBuf ⾃动释放。
  • 出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成⾃动释放。⼊站处理中,如果将原消息转化为新的消息并调⽤ ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;
  • ⼊站处理中,如果已经不再调⽤ ctx.fireChannelRead(msg) 传递任何消息,也没有继承SimpleChannelInboundHandler 完成⾃动释放,那更要把原消息release掉;
© 版权声明

相关文章

2 条评论

  • 头像
    小鱼妈妈 读者

    netty

    无记录
    回复
  • 头像
    与你共我 读者

    收藏了,感谢分享

    无记录
    回复