在分布式系统被普遍应用的今天,服务有可能分布在网络中的各个节点中。所以,服务之间的调用对分布式系统来讲,就显得尤其重要。算法
对于高性能的 RPC 框架,Netty 做为异步通讯框架,几乎成为必备品。例如,Dubbo 框架中通讯组件,还有 RocketMQ 中生产者和消费者的通讯,都使用了 Netty。今天,咱们来看看 Netty 的基本架构和原理。segmentfault
Netty 的特色与 NIO数组
Netty 是一个异步的、基于事件驱动的网络应用框架,它能够用来开发高性能服务端和客户端。性能优化
之前编写网络调用程序的时候,咱们都会在客户端建立一个 Socket,经过这个 Socket 链接到服务端。服务器
服务端根据这个 Socket 建立一个 Thread,用来发出请求。客户端在发起调用之后,须要等待服务端处理完成,才能继续后面的操做。这样线程会出现等待的状态。网络
若是客户端请求数越多,服务端建立的处理线程也会越多,JVM 如此多的线程并非一件容易的事。多线程
使用阻赛 I/O 处理多个链接
为了解决上述的问题,推出了 NIO 的概念,也就是(Non-blocking I/O)。其中,Selector 机制就是 NIO 的核心。架构
当每次客户端请求时,会建立一个 Socket Channel,并将其注册到 Selector 上(多路复用器)。并发
而后,Selector 关注服务端 IO 读写事件,此时客户端并不用等待 IO 事件完成,能够继续作接下来的工做。框架
一旦,服务端完成了 IO 读写操做,Selector 会接到通知,同时告诉客户端 IO 操做已经完成。
接到通知的客户端,就能够经过 SocketChannel 获取须要的数据了。
NIO 机制与 Selector
上面描述的过程有点异步的意思,不过,Selector 实现的并非真正意义上的异步操做。
由于 Selector 须要经过线程阻塞的方式监听 IO 事件变动,只是这种方式没有让客户端等待,是 Selector 在等待 IO 返回,而且通知客户端去获取数据。真正“异步 IO”(AIO)这里不展开介绍,有兴趣能够自行查找。
说好了 NIO 再来谈谈 Netty,Netty 做为 NIO 的实现,它适用于服务器/客户端通信的场景,以及针对于 TCP 协议下的高并发应用。
对于开发者来讲,它具备如下特色:
从一个简单的例子开始
开篇讲到了,为了知足高并发下网络请求,引入了 NIO 的概念。Netty 是针对 NIO 的实现,在 NIO 封装,网络调用,数据处理以及性能优化等方面都有不俗的表现。
学习架构最容易的方式就是从实例入手,从客户端访问服务端的代码来看看 Netty 是如何运做的。再一次介绍代码中调用的组件以及组件的工做原理。
假设有一个客户端去调用一个服务端,假设服务端叫作 EchoServer,客户端叫作 EchoClient,用 Netty 架构实现代码以下。
服务端代码
构建服务器端,假设服务器接受客户端传来的信息,而后在控制台打印。首先,生成 EchoServer,在构造函数中传入须要监听的端口号
构造函数中传入须要监听的端口号
接下来就是服务的启动方法:
启动 NettyServer 的 Start 方法
Server 的启动方法涉及到了一些组件的调用,例如 EventLoopGroup,Channel。这些会在后面详细讲解。
这里有个大体的印象就好:
NettyServer 启动之后会监听某个端口的请求,当接受到了请求就须要处理了。在 Netty 中客户端请求服务端,被称为“入站”操做。
能够经过 ChannelInboundHandlerAdapter 实现,具体内容以下:
处理来自客户端的请求
从上面的代码能够看出,服务端处理的代码包含了三个方法。这三个方法都是根据事件触发的。
他们分别是:
客户端代码
客户端和服务端的代码基本类似,在初始化时须要输入服务端的 IP 和 Port。
一样在客户端启动函数中包括如下内容:
客户端启动程序的顺序:
客户端在完成以上操做之后,会与服务端创建链接从而传输数据。一样在接受到 Channel 中触发的事件时,客户端会触发对应事件的操做。
例如 Channel 激活,客户端接受到服务端的消息,或者发生异常的捕获。
从代码结构上看仍是比较简单的。服务端和客户端分别初始化建立监听和链接。而后分别定义各自的 Handler 处理对方的请求。
服务端/客户端初始化和事件处理
Netty 核心组件
经过上面的简单例子,发现有些 Netty 组件在服务初始化以及通信时被用到,下面就来介绍一下这些组件的用途和关系。
①Channel
经过上面例子能够看出,当客户端和服务端链接的时候会创建一个 Channel。
这个 Channel 咱们能够理解为 Socket 链接,它负责基本的 IO 操做,例如:bind(),connect(),read(),write() 等等。
简单的说,Channel 就是表明链接,实体之间的链接,程序之间的链接,文件之间的链接,设备之间的链接。同时它也是数据入站和出站的载体。
②EventLoop 和 EventLoopGroup
既然有了 Channel 链接服务,让信息之间能够流动。若是服务发出的消息称做“出站”消息,服务接受的消息称做“入站”消息。那么消息的“出站”/“入站”就会产生事件(Event)。
例如:链接已激活;数据读取;用户事件;异常事件;打开连接;关闭连接等等。
顺着这个思路往下想,有了数据,数据的流动产生事件,那么就有一个机制去监控和协调事件。
这个机制(组件)就是 EventLoop。在 Netty 中每一个 Channel 都会被分配到一个 EventLoop。一个 EventLoop 能够服务于多个 Channel。
每一个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的全部 IO 操做和事件(Netty 4.0)。
EventLoopGroup,EventLoop 和 Channel 的关系
在异步传输的状况下,一个 EventLoop 是能够处理多个 Channel 中产生的事件的,它主要的工做就是事件的发现以及通知。
相对于之前一个 Channel 就占用一个 Thread 的状况。Netty 的方式就要合理多了。
客户端发送消息到服务端,EventLoop 发现之后会告诉服务端:“你去获取消息”,同时客户端进行其余的工做。
当 EventLoop 检测到服务端返回的消息,也会通知客户端:“消息返回了,你去取吧“。客户端再去获取消息。整个过程 EventLoop 就是监视器+传声筒。
③ChannelHandler,ChannelPipeline 和 ChannelHandlerContext
若是说 EventLoop 是事件的通知者,那么 ChannelHandler 就是事件的处理者。
在 ChannelHandler 中能够添加一些业务代码,例如数据转换,逻辑运算等等。
正如上面例子中展现的,Server 和 Client 分别都有一个 ChannelHandler 来处理,读取信息,网络可用,网络异常之类的信息。
而且,针对出站和入站的事件,有不一样的 ChannelHandler,分别是:
假设每次请求都会触发事件,而由 ChannelHandler 来处理这些事件,这个事件的处理顺序是由 ChannelPipeline 来决定的。
ChannelHanlder 处理,出站/入站的事件
ChannelPipeline 为 ChannelHandler 链提供了容器。到 Channel 被建立的时候,会被 Netty 框架自动分配到 ChannelPipeline 上。
ChannelPipeline 保证 ChannelHandler 按照必定顺序处理事件,当事件触发之后,会将数据经过 ChannelPipeline 按照必定的顺序经过 ChannelHandler。
说白了,ChannelPipeline 是负责“排队”的。这里的“排队”是处理事件的顺序。
同时,ChannelPipeline 也能够添加或者删除 ChannelHandler,管理整个队列。
如上图,ChannelPipeline 使 ChannelHandler 按照前后顺序排列,信息按照箭头所示方向流动而且被 ChannelHandler 处理。
说完了 ChannelPipeline 和 ChannelHandler,前者管理后者的排列顺序。那么它们之间的关联就由 ChannelHandlerContext 来表示了。
每当有 ChannelHandler 添加到 ChannelPipeline 时,同时会建立 ChannelHandlerContext 。
ChannelHandlerContext 的主要功能是管理 ChannelHandler 和 ChannelPipeline 的交互。
不知道你们注意到没有,开始的例子中 ChannelHandler 中处理事件函数,传入的参数就是 ChannelHandlerContext。
ChannelHandlerContext 参数贯穿 ChannelPipeline,将信息传递给每一个 ChannelHandler,是个合格的“通信员”。
ChannelHandlerContext 负责传递消息
把上面提到的几个核心组件概括一下,用下图表示方便记忆他们之间的关系。
Netty 核心组件关系图
Netty 的数据容器
前面介绍了 Netty 的几个核心组件,服务器在数据传输的时候,产生事件,而且对事件进行监控和处理。
接下来看看数据是如何存放以及是如何读写的。Netty 将 ByteBuf 做为数据容器,来存放数据。
ByteBuf 工做原理
从结构上来讲,ByteBuf 由一串字节数组构成。数组中每一个字节用来存放信息。
ByteBuf 提供了两个索引,一个用于读取数据,一个用于写入数据。这两个索引经过在字节数组中移动,来定位须要读或者写信息的位置。
当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。
一样,当写 ByteBuf 时,它的 writerIndex 也会根据写入的字节数进行递增。
ByteBuf 读写索引图例
须要注意的是极限的状况是 readerIndex 恰好读到了 writerIndex 写入的地方。
若是 readerIndex 超过了 writerIndex 的时候,Netty 会抛出 IndexOutOf-BoundsException 异常。
ByteBuf 使用模式
谈了 ByteBuf 的工做原理之后,再来看看它的使用模式。
根据存放缓冲区的不一样分为三类:
ByteBuf 的分配
聊完告终构和使用模式,再来看看 ByteBuf 是如何分配缓冲区的数据的。
Netty 提供了两种 ByteBufAllocator 的实现,他们分别是:
对象池化的技术和线程池,比较类似,主要目的是提升内存的使用率。池化的简单实现思路,是在 JVM 堆内存上构建一层内存池,经过 allocate 方法获取内存池中的空间,经过 release 方法将空间归还给内存池。
对象的生成和销毁,会大量地调用 allocate 和 release 方法,所以内存池面临碎片空间回收的问题,在频繁申请和释放空间后,内存池须要保证连续的内存空间,用于对象的分配。
基于这个需求,有两种算法用于优化这一块的内存分配:伙伴系统和 slab 系统。
伙伴系统,用彻底二叉树管理内存区域,左右节点互为伙伴,每一个节点表明一个内存块。内存分配将大块内存不断二分,直到找到知足所需的最小内存分片。
内存释放会判断释放内存分片的伙伴(左右节点)是否空闲,若是空闲则将左右节点合成更大块内存。
slab 系统,主要解决内存碎片问题,将大块内存按照必定内存大小进行等分,造成相等大小的内存片构成的内存集。
按照内存申请空间的大小,申请尽可能小块内存或者其整数倍的内存,释放内存时,也是将内存分片归还给内存集。
Netty 内存池管理以 Allocate 对象的形式出现。一个 Allocate 对象由多个 Arena 组成,每一个 Arena 能执行内存块的分配和回收。
Arena 内有三类内存块管理单元:
Tiny 和 Small 符合 Slab 系统的管理策略,ChunkList 符合伙伴系统的管理策略。
当用户申请内存介于 tinySize 和 smallSize 之间时,从 tinySubPage 中获取内存块。
申请内存介于 smallSize 和 pageSize 之间时,从 smallSubPage 中获取内存块;介于 pageSize 和 chunkSize 之间时,从 ChunkList 中获取内存;大于 ChunkSize(不知道分配内存的大小)的内存块不经过池化分配。
Netty 的 Bootstrap
说完了 Netty 的核心组件以及数据存储。再回到最开始的例子程序,在程序最开始的时候会 new 一个 Bootstrap 对象,后面全部的配置都是基于这个对象展开的。
生成 Bootstrap 对象
Bootstrap 的做用就是将 Netty 核心组件配置到程序中,而且让他们运行起来。
从 Bootstrap 的继承结构来看,分为两类分别是 Bootstrap 和 ServerBootstrap,一个对应客户端的引导,另外一个对应服务端的引导。
支持客户端和服务端的程序引导
客户端引导 Bootstrap,主要有两个方法 bind() 和 connect()。Bootstrap 经过 bind() 方法建立一个 Channel。
在 bind() 以后,经过调用 connect() 方法来建立 Channel 链接。
Bootstrap 经过 bind 和 connect 方法建立链接
服务端引导 ServerBootstrap,与客户端不一样的是在 Bind() 方法以后会建立一个 ServerChannel,它不只会建立新的 Channel 还会管理已经存在的 Channel。
ServerBootstrap 经过 bind 方法建立/管理链接
经过上面的描述,服务端和客户端的引导存在两个区别:
ServerBootstrap 有两组 EventLoopGroup
总结
咱们从 NIO 入手,谈到了 Selector 的核心机制。而后经过介绍 Netty 客户端和服务端源代码运行流程,让你们对 Netty 编写代码有基本的认识。
在 Netty 的核心组件中,Channel 提供 Socket 的链接通道,EventLoop 会对应 Channel 监听其产生的事件,而且通知执行者。EventloopGroup 的容器,负责生成和管理 EventLoop。
ChannelPipeline 做为 ChannelHandler 的容器会绑定到 Channel 上,而后由 ChannelHandler 提供具体事件处理。另外,ChannelHandlerContext 为 ChannelHandler 和 ChannelPipeline 提供信息共享。
ByteBuf 做为 Netty 的数据容器,经过字节数组的方式存储数据,而且经过读索引和写索引来引导读写操做。
上述的核心组件都是经过 Bootstrap 来配置而且引导启动的,Bootstrap 启动方式虽然一致,可是针对客户端和服务端有些许的区别。