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 的布局。
如果,您希望更容易地发现我的新博客,不妨点击一下【关注我】。
我的写作热情也离不开您的肯定支持,感谢您的阅读,我是【老梁】!