欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

netty入门(一)

程序员文章站 2022-07-01 23:11:38
1. netty入门(一) 1.1. 传统socket编程 1. 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种 资源浪费 。 2. 需要 为每个线程的调用栈都分配内存 ,其默认值大小区间为 64 KB 到 1 MB,具体取决于操作系统。 3. 即使 Java ......

1. netty入门(一)

1.1. 传统socket编程

netty入门(一)

  1. 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费
  2. 需要为每个线程的调用栈都分配内存,其默认值大小区间为 64 kb 到 1 mb,具体取决于操作系统。
  3. 即使 java 虚拟机(jvm)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦

1.2. nio

netty入门(一)

  1. class java.nio.channels.selector 是java 的非阻塞 i/o 实现的关键。它使用了事件通知 api以确定在一组非阻塞套接字中有哪些已经就绪能够进行 i/o 相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态,所以如图 1-2 所示,一个单一的线程便可以处理多个并发的连接

1.3. netty核心组件

1.3.1. channel

它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执
行一个或者多个不同的i/o操作的程序组件)的开放连接,如读操作和写操作
  1. 目前,可以把 channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以
    被打开或者被关闭,连接或者断开连接。

1.3.2. 回调

  1. 一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一
  2. netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interfacechannelhandler 的实现处理。代码清单 1-2 展示了一个例子:当一个新的连接已经被建立时,
    channelhandler 的 channelactive()回调方法将会被调用,并将打印出一条信息
    netty入门(一)

    1.3.3. future

  3. future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问
  4. jdk 预置了 interface java.util.concurrent.future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 netty提供了它自己的实现——channelfuture,用于在执行异步操作的时候使用
  5. channelfuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个channelfuturelistener实例。监听器的回调方法operationcomplete(),将会在对应的操作完成时被调用
  6. 简而 言之 ,由channelfuturelistener提供的通知机制消除了手动检查对应的操作是否完成的必要
  7. 每个 netty 的出站 i/o 操作都将返回一个 channelfuture;也就是说,它们都不会阻塞。正如我们前面所提到过的一样,netty 完全是异步和事件驱动的。
    netty入门(一)

1.3.4. 事件和 channelhandler

  1. netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经
    发生的事件来触发适当的动作。这些动作可能是:
    • 记录日志;
    • 数据转换;
    • 流控制;
    • 应用程序逻辑
  2. 每个事件都可以被分发给 channelhandler 类中的某个用户实现的方法。这是一个很好的将事件驱动范式直接转换为应用程序构件块的例子。
    netty入门(一)

  3. netty 的 channelhandler 为处理器提供了基本的抽象,如图 1-3 所示的那些

1.4. 服务端核心流程

netty入门(一)

netty入门(一)

  • echoserverhandler 实现了业务逻辑;
  • main()方法引导了服务器;

    引导过程中所需要的步骤如下:
    • 创建一个 serverbootstrap 的实例以引导和绑定服务器;
    • 创建并分配一个 nioeventloopgroup 实例以进行事件的处理,如接受新连接以及读/
      写数据;
    • 指定服务器绑定的本地的 inetsocketaddress;
    • 使用一个 echoserverhandler 的实例初始化每一个新的 channel;
    • 调用 serverbootstrap.bind()方法以绑定服务器

1.5. 客户端核心流程

  1. echo 客户端将会:
  • 连接到服务器;
  • 发送一个或者多个消息;
  • 对于每个消息,等待并接收从服务器发回的相同的消息;
  • 关闭连接。
    netty入门(一)

netty入门(一)

  1. 流程
    • 为初始化客户端,创建了一个 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 接口

  1. 基本的 i/o 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原语。
  2. netty 的 channel 接口所提供的 api,大大地降低了直接使用 socket 类的复杂性。

1.6.1.2. eventloop 接口

  1. eventloop 定义了 netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
  2. 图 3-1在高层次上说明了 channel、eventloop、thread 以及 eventloopgroup 之间的关系。
    netty入门(一)

  3. 这些关系是:
  • 一个 eventloopgroup 包含一个或者多个 eventloop;
  • 一个 eventloop 在它的生命周期内只和一个 thread 绑定;
  • 所有由 eventloop 处理的 i/o 事件都将在它专有的 thread 上被处理;
  • 一个 channel 在它的生命周期内只注册于一个 eventloop;
  • 一个 eventloop 可能会被分配给一个或多个 channel

1.6.1.3. channelfuture 接口

  1. netty 中所有的 i/o 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,netty 提供了channelfuture 接口,其 addlistener()方法注册了一个 channelfuturelistener,以便在某个操作完成时(无论是否成功)得到通知。

1.6.2. channelhandler 和 channelpipeline

1.6.2.1. channelhandler 接口

  1. 从应用程序开发人员的角度来看,netty 的主要组件是 channelhandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器

1.6.2.2. channelpipeline 接口

  1. channelpipeline 提供了 channelhandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 api。当 channel 被创建时,它会被自动地分配到它专属的 channelpipeline。
  2. channelhandler 安装到 channelpipeline 中的过程如下所示:
  • 一个channelinitializer的实现被注册到了serverbootstrap中
  • 当 channelinitializer.initchannel()方法被调用时,channelinitializer将在 channelpipeline 中安装一组自定义的 channelhandler;
  • channelinitializer 将它自己从 channelpipeline 中移除。
  1. 图 3-3 说明了一个 netty 应用程序中入站和出站数据流之间的区别。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。
    netty入门(一)

1.6.2.3. 编码器和解码器

  1. 当你通过 netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个 java 对象。
  2. 如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节

1.6.2.4. 抽象类 simplechannelinboundhandler

  1. 最常见的情况是,你的应用程序会利用一个 channelhandler 来接收解码消息,并对该数据应用业务逻辑。
  2. 要创建一个这样的 channelhandler,你只需要扩展基类 simplechannelinboundhandler,其中 t 是你要处理的消息的 java 类型 。

1.7. 传输

  1. 流经网络的数据总是具有相同的类型:字节。这些字节是如何流动的主要取决于我们所说的网络传输—一个帮助我们抽象底层数据传输机制的概念。
  2. channelhandler 的典型用途包括:
  • 将数据从一种格式转换为另一种格式;
  • 提供异常的通知;
  • 提供 channel 变为活动的或者非活动的通知;
  • 提供当 channel 注册到 eventloop 或者从 eventloop 注销时的通知;
  • 提供有关用户自定义事件的通知

1.8. bytebuf

  1. 网络数据的基本单位总是字节。java nio 提供了 bytebuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐
  2. netty 的 bytebuffer 替代品是 bytebuf,一个强大的实现,既解决了 jdk api 的局限性,又为网络应用程序的开发者提供了更好的 api。
  3. 下面是一些 bytebuf api 的优点:
  • 它可以被用户自定义的缓冲区类型扩展
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于 jdk 的 stringbuilder);
  • 在读和写这两种模式之间切换不需要调用 bytebuffer 的 flip()方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用
  • 支持引用计数
  • 支持池化

1.8.1. 如何工作

  1. bytebuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 bytebuf 读取时,它的readerindex 将会被递增已经被读取的字节数。同样地,当你写入 bytebuf 时,它的writerindex 也会被递增。图 5-1 展示了一个空 bytebuf 的布局结构和状态。
    netty入门(一)

1.8.2. bytebuf 的使用模式

1.8.2.1. 堆缓冲区

  1. 最常用的 bytebuf 模式是将数据存储在 jvm 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,如代码清单5-1 所示,非常适合于有遗留的数据需要处理的情况。
    netty入门(一)

1.8.2.2. 直接缓冲区

  1. 直接缓冲区是另外一种 bytebuf 模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的——nio 在 jdk 1.4 中引入的 bytebuffer 类允许 jvm 实现通过本地调用来分配内存。这主要是为了避免在每次调用本地 i/o 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
  2. 直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一次复制,如代码清单 5-2 所示。

netty入门(一)

1.8.2.3. 复合缓冲区

  1. 第三种也是最后一种模式使用的是复合缓冲区,它为多个 bytebuf 提供一个聚合视图。在这里你可以根据需要添加或者删除 bytebuf 实例,这是一个 jdk 的 bytebuffer 实现完全缺失的特性。
  2. netty 通过一个 bytebuf 子类——compositebytebuf——实现了这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示
  3. 代码清单 5-3 展示了如何通过使用 jdk 的 bytebuffer 来实现这一需求。创建了一个包含两个 bytebuffer 的数组用来保存这些消息组件,同时创建了第三个 bytebuffer 用来保存所有这些数据的副本。

netty入门(一)

  1. 分配和复制操作,以及伴随着对数组管理的需要,使得这个版本的实现效率低下而且笨拙。代码清单 5-4 展示了一个使用了 compositebytebuf 的版本

netty入门(一)

  1. compositebytebuf 可能不支持访问其支撑数组,因此访问 compositebytebuf 中的数据类似于(访问)直接缓冲区的模式,如代码清单 5-5 所示。

netty入门(一)

1.8.3. unpooled 缓冲区

  1. 可能某些情况下,你未能获取一个到 bytebufallocator 的引用。对于这种情况,netty 提供了一个简单的称为 unpooled 的工具类,它提供了静态的辅助方法来创建未池化的 bytebuf实例。表 5-8 列举了这些中最重要的方法。

netty入门(一)

1.9. channelhandler和channelpipeline

1.9.1. channelhandler 家族

1.9.1.1. channel 的生命周期

  1. interface channel 定义了一组和 channelinboundhandler api 密切相关的简单但功能强大的状态模型
  • channelunregistered channel 已经被创建,但还未注册到 eventloop
  • channelregistered channel 已经被注册到了 eventloop
  • channelactive channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
  • channelinactive channel 没有连接到远程节点
  1. channel 的正常生命周期如图 6-1 所示。当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 channelpipeline 中的 channelhandler,其可以随后对它们做出响应
    netty入门(一)

1.9.1.2. channelhandler 的生命周期

  1. 表 6-2 中列出了 interface channelhandler 定义的生命周期操作,在 channelhandler被添加到 channelpipeline 中或者被从 channelpipeline 中移除时会调用这些操作。这些方法中的每一个都接受一个 channelhandlercontext 参数。

netty入门(一)

  1. netty 定义了下面两个重要的 channelhandler 子接口:
  • channelinboundhandler——处理入站数据以及各种状态变化;
  • channeloutboundhandler——处理出站数据并且允许拦截所有的操作。

1.9.1.3. channelinboundhandler 接口

netty入门(一)

  1. 当某个 channelinboundhandler 的实现重写 channelread()方法时,它将负责显式地释放与池化的 bytebuf 实例相关的内存。netty 为此提供了一个实用方法 referencecountutil.release(),如代码清单 6-1 所示。

netty入门(一)

  1. netty 将使用 warn 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用simplechannelinboundhandler。代码清单 6-2 是代码清单 6-1 的一个变体,说明了这一点

netty入门(一)

1.9.1.4. channeloutboundhandler 接口

  1. 出站操作和数据将由 channeloutboundhandler 处理。它的方法将被 channel、channelpipeline 以及 channelhandlercontext 调用
  2. channeloutboundhandler 的一个强大的功能是可以按需推迟操作或者事件,这使得可以通过一些复杂的方法来处理请求。例如,如果到远程节点的写入被暂停了,那么你可以推迟冲刷操作并在稍后继续
  3. 表6-4显示了所有由channeloutboundhandler本身所定义的方法(忽略了那些从channelhandler 继承的方法)。

netty入门(一)

1.9.1.5. channelhandler 适配器

  1. 你可以使用 channelinboundhandleradapter 和 channeloutboundhandleradapter类作为自己的 channelhandler 的起始点。这两个适配器分别提供了 channelinboundhandler和channeloutboundhandler 的基本实现。
    netty入门(一)

  2. channelhandleradapter 还提供了实用方法 issharable()。如果其对应的实现被标注为 sharable,那么这个方法将返回 true,表示它可以被添加到多个 channelpipeline中

1.9.2. channelpipeline 接口

  1. channelhandler 可以通过添加、删除或者替换其他的 channelhandler 来实时地修改
    channelpipeline 的布局。
    netty入门(一)
    netty入门(一)