netty入门(一)
1. netty入门(一)
1.1. 传统socket编程
- 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
- 需要为每个线程的调用栈都分配内存,其默认值大小区间为 64 kb 到 1 mb,具体取决于操作系统。
- 即使 java 虚拟机(jvm)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦
1.2. nio
- class java.nio.channels.selector 是java 的非阻塞 i/o 实现的关键。它使用了事件通知 api以确定在一组非阻塞套接字中有哪些已经就绪能够进行 i/o 相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态,所以如图 1-2 所示,一个单一的线程便可以处理多个并发的连接。
1.3. netty核心组件
1.3.1. channel
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执 行一个或者多个不同的i/o操作的程序组件)的开放连接,如读操作和写操作
- 目前,可以把 channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以
被打开或者被关闭,连接或者断开连接。
1.3.2. 回调
- 一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一
-
netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interfacechannelhandler 的实现处理。代码清单 1-2 展示了一个例子:当一个新的连接已经被建立时,
channelhandler 的 channelactive()回调方法将会被调用,并将打印出一条信息1.3.3. future
- future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问
- jdk 预置了 interface java.util.concurrent.future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 netty提供了它自己的实现——channelfuture,用于在执行异步操作的时候使用。
- channelfuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个channelfuturelistener实例。监听器的回调方法operationcomplete(),将会在对应的操作完成时被调用
- 简而 言之 ,由channelfuturelistener提供的通知机制消除了手动检查对应的操作是否完成的必要
每个 netty 的出站 i/o 操作都将返回一个 channelfuture;也就是说,它们都不会阻塞。正如我们前面所提到过的一样,netty 完全是异步和事件驱动的。
1.3.4. 事件和 channelhandler
- netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经
发生的事件来触发适当的动作。这些动作可能是:- 记录日志;
- 数据转换;
- 流控制;
- 应用程序逻辑
每个事件都可以被分发给 channelhandler 类中的某个用户实现的方法。这是一个很好的将事件驱动范式直接转换为应用程序构件块的例子。
netty 的 channelhandler 为处理器提供了基本的抽象,如图 1-3 所示的那些
1.4. 服务端核心流程
- echoserverhandler 实现了业务逻辑;
-
main()方法引导了服务器;
引导过程中所需要的步骤如下:- 创建一个 serverbootstrap 的实例以引导和绑定服务器;
- 创建并分配一个 nioeventloopgroup 实例以进行事件的处理,如接受新连接以及读/
写数据; - 指定服务器绑定的本地的 inetsocketaddress;
- 使用一个 echoserverhandler 的实例初始化每一个新的 channel;
- 调用 serverbootstrap.bind()方法以绑定服务器
1.5. 客户端核心流程
- echo 客户端将会:
- 连接到服务器;
- 发送一个或者多个消息;
- 对于每个消息,等待并接收从服务器发回的相同的消息;
- 关闭连接。
- 流程
- 为初始化客户端,创建了一个 bootstrap 实例;
- 为进行事件处理分配了一个 nioeventloopgroup 实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
- 为服务器连接创建了一个 inetsocketaddress 实例;
- 当连接被建立时,一个 echoclienthandler 实例会被安装到(该 channel 的)channelpipeline 中;
- 在一切都设置完成后,调用 bootstrap.connect()方法连接到远程节点;
1.6. netty 的组件和设计
1.6.1. channel、eventloop 和 channelfuture
- channel—socket;
- eventloop—控制流、多线程处理、并发;
-
channelfuture—异步通知
1.6.1.1. channel 接口
- 基本的 i/o 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原语。
- netty 的 channel 接口所提供的 api,大大地降低了直接使用 socket 类的复杂性。
1.6.1.2. eventloop 接口
- eventloop 定义了 netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
图 3-1在高层次上说明了 channel、eventloop、thread 以及 eventloopgroup 之间的关系。
- 这些关系是:
- 一个 eventloopgroup 包含一个或者多个 eventloop;
- 一个 eventloop 在它的生命周期内只和一个 thread 绑定;
- 所有由 eventloop 处理的 i/o 事件都将在它专有的 thread 上被处理;
- 一个 channel 在它的生命周期内只注册于一个 eventloop;
- 一个 eventloop 可能会被分配给一个或多个 channel
1.6.1.3. channelfuture 接口
- netty 中所有的 i/o 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,netty 提供了channelfuture 接口,其 addlistener()方法注册了一个 channelfuturelistener,以便在某个操作完成时(无论是否成功)得到通知。
1.6.2. channelhandler 和 channelpipeline
1.6.2.1. channelhandler 接口
- 从应用程序开发人员的角度来看,netty 的主要组件是 channelhandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器
1.6.2.2. channelpipeline 接口
- channelpipeline 提供了 channelhandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 api。当 channel 被创建时,它会被自动地分配到它专属的 channelpipeline。
- channelhandler 安装到 channelpipeline 中的过程如下所示:
- 一个channelinitializer的实现被注册到了serverbootstrap中
- 当 channelinitializer.initchannel()方法被调用时,channelinitializer将在 channelpipeline 中安装一组自定义的 channelhandler;
- channelinitializer 将它自己从 channelpipeline 中移除。
- 图 3-3 说明了一个 netty 应用程序中入站和出站数据流之间的区别。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。
1.6.2.3. 编码器和解码器
- 当你通过 netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个 java 对象。
- 如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。
1.6.2.4. 抽象类 simplechannelinboundhandler
- 最常见的情况是,你的应用程序会利用一个 channelhandler 来接收解码消息,并对该数据应用业务逻辑。
- 要创建一个这样的 channelhandler,你只需要扩展基类 simplechannelinboundhandler
,其中 t 是你要处理的消息的 java 类型 。
1.7. 传输
- 流经网络的数据总是具有相同的类型:字节。这些字节是如何流动的主要取决于我们所说的网络传输—一个帮助我们抽象底层数据传输机制的概念。
- channelhandler 的典型用途包括:
- 将数据从一种格式转换为另一种格式;
- 提供异常的通知;
- 提供 channel 变为活动的或者非活动的通知;
- 提供当 channel 注册到 eventloop 或者从 eventloop 注销时的通知;
- 提供有关用户自定义事件的通知
1.8. bytebuf
- 网络数据的基本单位总是字节。java nio 提供了 bytebuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐
- netty 的 bytebuffer 替代品是 bytebuf,一个强大的实现,既解决了 jdk api 的局限性,又为网络应用程序的开发者提供了更好的 api。
- 下面是一些 bytebuf api 的优点:
- 它可以被用户自定义的缓冲区类型扩展;
- 通过内置的复合缓冲区类型实现了透明的零拷贝;
- 容量可以按需增长(类似于 jdk 的 stringbuilder);
- 在读和写这两种模式之间切换不需要调用 bytebuffer 的 flip()方法;
- 读和写使用了不同的索引;
- 支持方法的链式调用;
- 支持引用计数;
- 支持池化
1.8.1. 如何工作
- bytebuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 bytebuf 读取时,它的readerindex 将会被递增已经被读取的字节数。同样地,当你写入 bytebuf 时,它的writerindex 也会被递增。图 5-1 展示了一个空 bytebuf 的布局结构和状态。
1.8.2. bytebuf 的使用模式
1.8.2.1. 堆缓冲区
- 最常用的 bytebuf 模式是将数据存储在 jvm 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,如代码清单5-1 所示,非常适合于有遗留的数据需要处理的情况。
1.8.2.2. 直接缓冲区
- 直接缓冲区是另外一种 bytebuf 模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的——nio 在 jdk 1.4 中引入的 bytebuffer 类允许 jvm 实现通过本地调用来分配内存。这主要是为了避免在每次调用本地 i/o 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
- 直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一次复制,如代码清单 5-2 所示。
1.8.2.3. 复合缓冲区
- 第三种也是最后一种模式使用的是复合缓冲区,它为多个 bytebuf 提供一个聚合视图。在这里你可以根据需要添加或者删除 bytebuf 实例,这是一个 jdk 的 bytebuffer 实现完全缺失的特性。
- netty 通过一个 bytebuf 子类——compositebytebuf——实现了这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示
- 代码清单 5-3 展示了如何通过使用 jdk 的 bytebuffer 来实现这一需求。创建了一个包含两个 bytebuffer 的数组用来保存这些消息组件,同时创建了第三个 bytebuffer 用来保存所有这些数据的副本。
- 分配和复制操作,以及伴随着对数组管理的需要,使得这个版本的实现效率低下而且笨拙。代码清单 5-4 展示了一个使用了 compositebytebuf 的版本
- compositebytebuf 可能不支持访问其支撑数组,因此访问 compositebytebuf 中的数据类似于(访问)直接缓冲区的模式,如代码清单 5-5 所示。
1.8.3. unpooled 缓冲区
- 可能某些情况下,你未能获取一个到 bytebufallocator 的引用。对于这种情况,netty 提供了一个简单的称为 unpooled 的工具类,它提供了静态的辅助方法来创建未池化的 bytebuf实例。表 5-8 列举了这些中最重要的方法。
1.9. channelhandler和channelpipeline
1.9.1. channelhandler 家族
1.9.1.1. channel 的生命周期
- interface channel 定义了一组和 channelinboundhandler api 密切相关的简单但功能强大的状态模型
- channelunregistered channel 已经被创建,但还未注册到 eventloop
- channelregistered channel 已经被注册到了 eventloop
- channelactive channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
- channelinactive channel 没有连接到远程节点
- channel 的正常生命周期如图 6-1 所示。当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 channelpipeline 中的 channelhandler,其可以随后对它们做出响应
1.9.1.2. channelhandler 的生命周期
- 表 6-2 中列出了 interface channelhandler 定义的生命周期操作,在 channelhandler被添加到 channelpipeline 中或者被从 channelpipeline 中移除时会调用这些操作。这些方法中的每一个都接受一个 channelhandlercontext 参数。
- netty 定义了下面两个重要的 channelhandler 子接口:
- channelinboundhandler——处理入站数据以及各种状态变化;
- channeloutboundhandler——处理出站数据并且允许拦截所有的操作。
1.9.1.3. channelinboundhandler 接口
- 当某个 channelinboundhandler 的实现重写 channelread()方法时,它将负责显式地释放与池化的 bytebuf 实例相关的内存。netty 为此提供了一个实用方法 referencecountutil.release(),如代码清单 6-1 所示。
- netty 将使用 warn 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用simplechannelinboundhandler。代码清单 6-2 是代码清单 6-1 的一个变体,说明了这一点
1.9.1.4. channeloutboundhandler 接口
- 出站操作和数据将由 channeloutboundhandler 处理。它的方法将被 channel、channelpipeline 以及 channelhandlercontext 调用
- channeloutboundhandler 的一个强大的功能是可以按需推迟操作或者事件,这使得可以通过一些复杂的方法来处理请求。例如,如果到远程节点的写入被暂停了,那么你可以推迟冲刷操作并在稍后继续
- 表6-4显示了所有由channeloutboundhandler本身所定义的方法(忽略了那些从channelhandler 继承的方法)。
1.9.1.5. channelhandler 适配器
你可以使用 channelinboundhandleradapter 和 channeloutboundhandleradapter类作为自己的 channelhandler 的起始点。这两个适配器分别提供了 channelinboundhandler和channeloutboundhandler 的基本实现。
channelhandleradapter 还提供了实用方法 issharable()。如果其对应的实现被标注为 sharable,那么这个方法将返回 true,表示它可以被添加到多个 channelpipeline中
1.9.2. channelpipeline 接口
- channelhandler 可以通过添加、删除或者替换其他的 channelhandler 来实时地修改
channelpipeline 的布局。
上一篇: SpringCloud学习笔记(6):使用Zuul构建服务网关
下一篇: 吃饭忘带钱了