netty入门(一)

匿名 (未验证) 提交于 2019-12-02 21:35:18

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

1.2. NIO

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

1.3.1. Channel

它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执 行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作
  1. 目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以
    被打开或者被关闭,连接或者断开连接。
  1. 一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一
  2. Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interfaceChannelHandler 的实现处理。代码清单 1-2 展示了一个例子:当一个新的连接已经被建立时,
    ChannelHandler 的 channelActive()回调方法将会被调用,并将打印出一条信息

    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 完全是异步和事件驱动的。

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

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

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

    引导过程中所需要的步骤如下:
    • 创建一个 ServerBootstrap 的实例以引导和绑定服务器;
    • 创建并分配一个 NioEventLoopGroup 实例以进行事件的处理,如接受新连接以及读/
      写数据;
    • 指定服务器绑定的本地的 InetSocketAddress;
    • 使用一个 EchoServerHandler 的实例初始化每一个新的 Channel;
    • 调用 ServerBootstrap.bind()方法以绑定服务器
  1. Echo 客户端将会:
  • 连接到服务器;
  • 发送一个或者多个消息;
  • 对于每个消息,等待并接收从服务器发回的相同的消息;
  • 关闭连接。

  1. 流程
    • 为初始化客户端,创建了一个 Bootstrap 实例;
    • 为进行事件处理分配了一个 NioEventLoopGroup 实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
    • 为服务器连接创建了一个 InetSocketAddress 实例;
    • 当连接被建立时,一个 EchoClientHandler 实例会被安装到(该 Channel 的)ChannelPipeline 中;
    • 在一切都设置完成后,调用 Bootstrap.connect()方法连接到远程节点;
  • Channel―Socket;
  • EventLoop―控制流、多线程处理、并发;
  • ChannelFuture―异步通知

  1. 基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原语。
  2. Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
  1. EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
  2. 图 3-1在高层次上说明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之间的关系。

  3. 这些关系是:
  • 一个 EventLoopGroup 包含一个或者多个 EventLoop;
  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
  • 一个 EventLoop 可能会被分配给一个或多个 Channel
  1. Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了ChannelFuture 接口,其 addListener()方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
  1. 从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器
  1. ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
  2. ChannelHandler 安装到 ChannelPipeline 中的过程如下所示:
  • 一个ChannelInitializer的实现被注册到了ServerBootstrap中
  • 当 ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler;
  • ChannelInitializer 将它自己从 ChannelPipeline 中移除。
  1. 图 3-3 说明了一个 Netty 应用程序中入站和出站数据流之间的区别。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。
  1. 当你通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个 Java 对象。
  2. 如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节
  1. 最常见的情况是,你的应用程序会利用一个 ChannelHandler 来接收解码消息,并对该数据应用业务逻辑。
  2. 要创建一个这样的 ChannelHandler,你只需要扩展基类 SimpleChannelInboundHandler,其中 T 是你要处理的消息的 Java 类型 。
  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. ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 ByteBuf 读取时,它的readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的writerIndex 也会被递增。图 5-1 展示了一个空 ByteBuf 的布局结构和状态。
  1. 最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,如代码清单5-1 所示,非常适合于有遗留的数据需要处理的情况。
  1. 直接缓冲区是另外一种 ByteBuf 模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的――NIO 在 JDK 1.4 中引入的 ByteBuffer 类允许 JVM 实现通过本地调用来分配内存。这主要是为了避免在每次调用本地 I/O 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
  2. 直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一次复制,如代码清单 5-2 所示。

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

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

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

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

  1. Interface Channel 定义了一组和 ChannelInboundHandler API 密切相关的简单但功能强大的状态模型
  • ChannelUnregistered Channel 已经被创建,但还未注册到 EventLoop
  • ChannelRegistered Channel 已经被注册到了 EventLoop
  • ChannelActive Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
  • ChannelInactive Channel 没有连接到远程节点
  1. Channel 的正常生命周期如图 6-1 所示。当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 ChannelPipeline 中的 ChannelHandler,其可以随后对它们做出响应
  1. 表 6-2 中列出了 interface ChannelHandler 定义的生命周期操作,在 ChannelHandler被添加到 ChannelPipeline 中或者被从 ChannelPipeline 中移除时会调用这些操作。这些方法中的每一个都接受一个 ChannelHandlerContext 参数。

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

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

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

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

  1. 你可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter类作为自己的 ChannelHandler 的起始点。这两个适配器分别提供了 ChannelInboundHandler和ChannelOutboundHandler 的基本实现。

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

  1. ChannelHandler 可以通过添加、删除或者替换其他的 ChannelHandler 来实时地修改
    ChannelPipeline 的布局。

文章来源: netty入门(一)
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!