Netty基础入门与组件介绍

Netty简介

  • Netty是由JBOSS提供的一个java开源框架。
  • Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

为什么要用 Netty

  1. 虽然 JAVA NIO 框架支持多路复用 IO ,但是并没有提供上层“信息格式” 的良好封装。例如没有提供针对 Protocol Buffer、JSON 这些信息格式的封装,但是 Netty 框架提供了这些数据格式封装(基于责任链模式的编码和解码功能);
  2. NIO 的类库和 API 相当复杂,使用它来开发,需要非常熟练地掌握 Selector、ByteBuffer、ServerSocketChannel、SocketChannel 等,需要很多额外的编程技能来辅助使用 NIO,例如,因为 NIO 涉及了 Reactor 线程模型,所以必须必须对多线程和网络编程非常熟悉才能写出高质量的 NIO 程序;
  3. 要编写一个可靠的、易维护的、高性能的 NIO 服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如:客户端的权限、还有上面提到的信息格式封装、简单的数据读取,断连重连,半包读写,心跳等等, 这些 Netty 框架都提供了相应的支持;
  4. JAVA NIO 框架存在一个 poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能 block 意味着 CPU 的使用率会变成 100%(这是底层 JNI 的问题, 上层要处理这个异常实际上也好办)。当然这个 bug 只有在 Linux 内核上才能重现。
    这个问题在 JDK 1.7 版本中还没有被完全解决,但是 Netty 已经将这个 bug 进行了处理。 这个 Bug 与操作系统机制有关系的,JDK 虽然仅仅是一个兼容各个操作系统平台的软件, 但在 JDK5 和 JDK6 最初的版本中,这个问题并没有解决,而将这个帽子抛给了操作系统方,这也就是这个 bug 最终一直到 2013 年才最终修复的原因(JDK7 和 JDK8 之间)。

为什么 Netty 使用 NIO 而不是 AIO?

  • Netty 不看重 Windows 上的使用,在 Linux 系统上,AIO 的底层实现仍使用 EPOLL,没有很好实现 AIO,因此在性能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化。
  • AIO 还有个缺点是接收数据需要预先分配缓存, 而不是 NIO 那种需要接收时才需要分配缓存,所以对连接数量非常大但流量小的情况,内存浪费很多。
  • Netty5 就是使用的 AIO,现在已经停止开发了,官网上最新的是netty4。

在这里插入图片描述

EventLoop(Group) 、Channel

  • Channel 是 Java NIO 的一个基本构造。
  • 它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的 I/O 操作的程序组件)的开放连接,如读操作和写操作。
  • 可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
  • EventLoop 暂时可以看成一个线程、EventLoopGroup 就可以看成线程组。

事件和 ChannelHandler、ChannelPipeline

  • Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。
  • Netty 事件是按照它们与入站或出站数据流的相关性进行分类的。
  • 由入站数据或者相关的状态更改而触发的事件包括:连接已被激活或者连接失活;数据读取;用户事件;错误事件。
  • 出站事件是未来将会触发的某个动作的操作结果,这些动作包括:打开或者关闭到远程节点的连接;将数据写到或者冲刷到 Socket 套接字。
  • 每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法。
  • Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。

ChannelFuture

  • Netty 中所有的 I/O 操作都是异步的。
  • JDK 预置了 interface java.util.concurrent.Future,Future 提供了一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 Netty 提供了它自己的实现-ChannelFuture,用于在执行异步操作的时候使用。
  • 每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture。

Netty 组件详解

Channel、EventLoop(Group)和 ChannelFuture

  • Channel—Socket;
  • EventLoop—控制流、多线程处理、并发;
  • ChannelFuture—异步通知。
  • Channel 和 EventLoop 关系如图:

在这里插入图片描述

Channel 接口

  • 基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原语。在基于 Java 的网络编程中,其基本的构造是类 Socket。Netty 的 Channel 接口所提供的 API,被用于所有的 I/O 操作。大大地降低了直接使用 Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
  • 由于 Channel 是独一无二的,所以为了保证顺序将 Channel 声明为 java.lang.Comparable 的一个子接口。因此,如果两个不同的 Channel 实例都返回了相同的散列码,那么 AbstractChannel 中的 compareTo()方法的实现将会抛出一个 Error。

Channel 的生命周期状态

  • ChannelUnregistered :Channel 已经被创建,但还未注册到 EventLoop;
  • ChannelRegistered :Channel 已经被注册到了 EventLoop;
  • ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了;
  • ChannelInactive :Channel 没有连接到远程节点;
  • 当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 ChannelPipeline 中的 ChannelHandler,其可以随后对它们做出响应。

Channel 的方法

  • eventLoop:返回分配给 Channel 的 EventLoop;
  • pipeline:返回分配给 Channel 的 ChannelPipeline;
  • isActive:如果 Channel 是活动的,则返回 true。活动的意义可能依赖于底层的传输。 例如,一个 Socket 传输一旦连接到了远程节点便是活动的,而一个 Datagram 传输一旦被打开便是活动的;
  • localAddress:返回本地的 SokcetAddress;
  • remoteAddress:返回远程的 SocketAddress;
  • write:将数据写到远程节点。这个数据将被传递给 ChannelPipeline,并且排队直到它被冲刷;
  • flush:将之前已写的数据冲刷到底层传输,如一个 Socket;
  • writeAndFlush: 一个简便的方法,等同于调用 write()并接着调用 flush()。

EventLoop 和 EventLoopGroup

  • 在 NIO 中是处理我们关心的事件,通过在一个 while 循环中 select 出事件,然后依次处理每种事件。我们可以把它称为事件循环,这就是 EventLoop。interface io.netty.channel. EventLoop 定义了 Netty 的核心抽象,用于处理网络连接的生命周期中所发生的事件。
  • io.netty.util.concurrent 包构建在 JDK 的 java.util.concurrent 包上。而 io.netty.channel 包中的类,为了与 Channel 的事件进行交互,扩展了这些接口/类。一个 EventLoop 将由一个 永远都不会改变的 Thread 驱动,同时任务(Runnable 或者 Callable)可以直接提交给 EventLoop 实现,以立即执行或者调度执行。
  • 根据配置和可用核心的不同,可能会创建多个 EventLoop 实例用以优化资源的使用,并且单个 EventLoop 可能会被指派用于服务多个 Channel。
  • Netty 的 EventLoop 在继承了 ScheduledExecutorService 的同时,只定义了一个方法, parent()。在 Netty 4 中,所有的 I/O 操作和事件都由已经被分配给了 EventLoop 的那个 Thread 来处理。

任务调度

  • 偶尔,你将需要调度一个任务以便延迟执行或者周期性地执行。例如,你可能想要注册一个在客户端已经连接了 5 分钟之后触发的任务。比如,发送心跳消息到远程节点,以检查连接是否仍然还活着。如果没有响应,就关闭该 Channel。

线程管理

  • 在内部,当提交任务到如果当前调用线程正是支撑 EventLoop 的线程,那么所提交的代码块将会被直接执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当 EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件。

在这里插入图片描述
线程的分配

  • 服务于 Channel 的 I/O 和事件的 EventLoop 则包含在 EventLoopGroup 中。
  • 异步传输实现只使用了少量的 EventLoop(以及和它们相关联的 Thread),而且在当前的线程模型中,它们可能会被多个 Channel 所共享。这使得可以通过尽可能少量的 Thread 来支撑大量的 Channel,而不是每个 Channel 分配一个 Thread。EventLoopGroup 负责为每个新创建的 Channel 分配一个 EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的 EventLoop 可能会被分配给多个 Channel。
  • 一旦一个 Channel 被分配给一个 EventLoop,它将在它的整个生命周期中都使用这个 EventLoop(以及相关联的 Thread)。它可以使你从 ChannelHandler 实现中的线程安全和同步问题中解脱出来。

在这里插入图片描述

  • 需要注意,EventLoop 的分配方式对 ThreadLocal 的使用的影响。因为一个 EventLoop 通常会被用于支撑多个 Channel,所以对于所有相关联的 Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上下文中,它仍然可以被用于在多个 Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。

ChannelFuture 接口

  • Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了 ChannelFuture 接口, 其 addListener() 方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
  • 可以将 ChannelFuture 看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。

ChannelHandler

ChannelHandler 接口

  • 从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。 事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,例如各种编解码,或者处理转换过程中所抛出的异常。
  • 举例来说,ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的 ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。 当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 直接冲刷数据然后输出到对端。应用程序的业务逻辑通常实现在一个或者多个 ChannelInboundHandler 中。
  • 这种类型的 ChannelHandler 接收入站事件和数据,这些数据随后将会被应用程序的业务逻辑所处理。
  • Netty 定义了两个重要的 ChannelHandler 子接口:
    ChannelInboundHandler——处理入站数据以及各种状态变化;
    ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。

ChannelInboundHandler 接口

  • channelRegistered 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用;
  • channelUnregistered 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用;
  • channelActive 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就 绪;
  • channelInactive 当 Channel 离开活动状态并且不再连接它的远程节点时被调用;
  • channelReadComplete 当 Channel 上的一个读操作完成时被调用;
  • channelRead 当从 Channel 读取数据时被调用;
  • ChannelWritabilityChanged 当 Channel 的可写状态发生改变时被调用。可以通过调用 Channel 的 isWritable()方法来检测 Channel 的可写性。与可写性相关的阈值可以通过Channel.config().setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来设置;
  • userEventTriggered 当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用。
  • 这些方法将会在数据被接收时或者与其对应的 Channel 状态发生改变时被调用,和 Channel 的生命周期密切相关。

ChannelOutboundHandler 接口

  • 出站操作和数据将由 ChannelOutboundHandler 处理。它的方法将被 Channel、Channel- Pipeline 以及 ChannelHandlerContext 调用。
  • 所有由 ChannelOutboundHandler 本身所定义的方法:bind(ChannelHandlerContext,SocketAddress,ChannelPromise) 当请求将 Channel 绑定到本地地址时被调用;
  • connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise) 当请求将 Channel 连接到远程节点时被调用;
  • disconnect(ChannelHandlerContext,ChannelPromise) 当请求将 Channel 从远程节点断开时被调用;
  • close(ChannelHandlerContext,ChannelPromise) 当请求关闭 Channel 时被调用;
  • deregister(ChannelHandlerContext,ChannelPromise) 当请求将 Channel 从它的 EventLoop 注销时被调用;
  • read(ChannelHandlerContext) 当请求从 Channel 读取更多的数据时被调用;
  • flush(ChannelHandlerContext) 当请求通过 Channel 将入队数据冲刷到远程节点时被调 用;
  • write(ChannelHandlerContext,Object,ChannelPromise) 当请求通过 Channel 将数据写到远程节点时被调用。

ChannelHandler 的适配器

  • 有一些适配器类可以将编写自定义的 ChannelHandler 所需要的工作降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的事件,所以 Netty 提供了抽象基类 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter。
  • 你可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 类作为自己的 ChannelHandler 的起始点。这两个适配器分别提供了 ChannelInboundHandler 和 ChannelOutboundHandler 的基本实现。通过扩展抽象类 ChannelHandlerAdapter,它们获得了它们共同的顶层接口 ChannelHandler 的方法。
  • ChannelHandlerAdapter 还提供了实用方法 isSharable()。如果其对应的实现被标注为 Sharable,那么这个方法将返回 true,表示它可以被添加到多个 ChannelPipeline。

在这里插入图片描述

资源管理和 SimpleChannelInboundHandler

  • 在NIO 中接收和发送网络数据,都是首先创建了一个 Buffer,应用程序中的业务部分和 Channel 之间通过 Buffer 进行数据的交换。
  • Netty 在处理网络数据时,同样也需要 Buffer,在 Read 网络数据时由 Netty 创建 Buffer, Write 网络数据时 Buffer 往往是由业务方创建的。不管是读和写,Buffer 用完后都必须进行释放,否则可能会造成内存泄露。
  • 在 Write 网络数据时,可以确保数据被写往网络了,Netty 会自动进行 Buffer 的释放, 但是如果 Write 网络数据时,我们有 outBoundHandler 处理了 write() 操作并丢弃了数据,没有继续往下写,要由我们负责释放这个 Buffer,就必须调用 ReferenceCountUtil.release 方法, 否则就可能会造成内存泄露。
  • 在 Read 网络数据时,如果我们可以确保每个 InboundHandler 都把数据往后传递了,也就是调用了相关的 fireChannelRead 方法,Netty 也会帮我们释放,同样的,如果我们有 InboundHandler 处理了数据,又不继续往后传递,又不调用负责释放的 ReferenceCountUtil.release 方法,就可能会造成内存泄露。
  • 但是由于消费入站数据是一项常规任务,所以 Netty 提供了一个特殊的被称为 SimpleChannelInboundHandler 的 ChannelInboundHandler 实现。这个实现会在数据被 channelRead0() 方法消费之后自动释放数据。
  • 同时系统为我们提供的各种预定义 Handler 实现,都实现了数据的正确处理,所以我们自行在编写业务 Handler 时,也需要注意这一点:要么继续传递,要么自行释放。

ChannelPipeline 和 ChannelHandlerContext

ChannelPipeline 接口

  • 当 Channel 被创建时,它将会被自动地分配一个新的 ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。在 Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。
  • 使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。

ChannelHandler 的生命周期

  • 在 ChannelHandler 被添加到 ChannelPipeline 中或者被从 ChannelPipeline 中移除时会调用下面这些方法。这些方法中的每一个都接受一个 ChannelHandlerContext 参数。
  • handlerAdded 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用;
  • handlerRemoved 当从 ChannelPipeline 中移除 ChannelHandler 时被调用;
  • exceptionCaught 当处理过程中在 ChannelPipeline 中有错误产生时被调用。

ChannelPipeline 中 ChannelHandler

  • 入站和出站 ChannelHandler 可以被安装到同一个 ChannelPipeline 中。如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部开始流动,最终,数据将会到达 ChannelPipeline 的尾端,届时,所有处理就都结束了。
  • 数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为 Socket。通常情况下,这将触发一个写操作。
  • 如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline 中会发生什么。 虽然 ChannelInboundHandle 和 ChannelOutboundHandle 都扩展自 ChannelHandler,但是 Netty 能区分 ChannelInboundHandler 实现和 ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个 ChannelHandler 之间传递。

在这里插入图片描述

  • 站在逻辑视图的角度,分属出站和入站不同的Handler ,是无所谓顺序的。
    而同属一个方向的Handler则是有顺序的,因为上一个Handler处理的结果往往是下一个Handler的要求的输入。将图中的处理器(ChannelHandler)从左到右进行编号,那么入站事件按顺序看到的ChannelHandler 将是1,2,4,而出站事件按顺序看到的ChannelHandler 将是5,3。

ChannelPipeline 上的方法

  • addFirst、addBefore、addAfter、addLast 将一个 ChannelHandler 添加到 ChannelPipeline 中;
  • remove 将一个 ChannelHandler 从 ChannelPipeline 中移除;
  • replace 将 ChannelPipeline 中的一个 ChannelHandler 替换为另一个 ChannelHandler;
  • get 通过类型或者名称返回 ChannelHandler;
  • context 返回和 ChannelHandler 绑定的 ChannelHandlerContext;
  • names 返回 ChannelPipeline 中所有 ChannelHandler 的名称;

ChannelHandlerContext

  • 通过使用作为参数传递到每个方法的 ChannelHandlerContext,事件可以被传递给当前 ChannelHandler 链中的下一个 ChannelHandler。虽然这个对象可以被用于获取底层的 Channel,但是它主要还是被用于写出站数据。
  • ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。 ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。

在这里插入图片描述

  • ChannelHandlerContext 有很多的方法,其中一些方法也存在于 Channel 和 Channel-Pipeline 本身上,但是有一点重要的不同。如果调用Channel 或者ChannelPipeline 上的这些方法,它们将沿着整个 ChannelPipeline 进行传播。而调用位于 ChannelHandlerContext 上的相同方法,则将从当前所关联的 ChannelHandler 开始,并且只会传播给位于该 ChannelPipeline 中的下一个(入站下一个,出站上一个)能够处理该事件的 ChannelHandler。

在这里插入图片描述
ChannelHandlerContext 的 API

  • alloc 返回和这个实例相关联的 Channel 所配置的 ByteBufAllocator;
  • bind 绑定到给定的 SocketAddress,并返回 ChannelFuture ;
  • channel 返回绑定到这个实例的 Channel ;
  • close 关闭 Channel,并返回 ChannelFuture ;
  • connect 连接给定的 SocketAddress,并返回 ChannelFuture ;
  • deregister 从之前分配的 EventExecutor 注销,并返回 ChannelFuture ;
  • disconnect 从远程节点断开,并返回 ChannelFuture ;
  • executor 返回调度事件的 EventExecutor ;
  • fireChannelActive 触发对下一个 ChannelInboundHandler 上的 channelActive()方法(已连接)的调用 ;
  • fireChannelInactive 触发对下一个 ChannelInboundHandler 上的 channelInactive()方法 (已关闭)的调用 ;
  • fireChannelRead 触发对下一个 ChannelInboundHandler 上的 channelRead()方法(已接收的消息)的调用 ;
  • fireChannelReadComplete 触发对下一个 ChannelInboundHandler 上的 channelReadComplete()方法的调用 ;
  • fireChannelRegistered 触发对下一个 ChannelInboundHandler 上的 fireChannelRegistered()方法的调用 ;
  • fireChannelUnregistered 触发对下一个 ChannelInboundHandler 上的 fireChannelUnregistered()方法的调用 ;
  • fireChannelWritabilityChanged 触发对下一个 ChannelInboundHandler 上的 fireChannelWritabilityChanged()方法的调用 ;
  • fireExceptionCaught 触发对下一个 ChannelInboundHandler 上的 fireExceptionCaught(Throwable)方法的调用 ;
  • fireUserEventTriggered 触发对下一个 ChannelInboundHandler 上的 fireUserEventTriggered(Object evt)方法的调用;
  • handler 返回绑定到这个实例的 ChannelHandler ;
  • isRemoved 如果所关联的 ChannelHandler 已经被从 ChannelPipeline 中移除则返回 true ;
  • name 返回这个实例的唯一名称 pipeline 返回这个实例所关联的 ChannelPipeline ;
  • read 将数据从 Channel 读取到第一个入站缓冲区;如果读取成功则触发一个 ;channelRead 事件,并(在最后一个消息被读取完成后)通知 ChannelInboundHandler 的 channelReadComplete(ctx)方法 ;
  • write 通过这个实例写入消息并经过 ChannelPipeline ;
  • writeAndFlush 通过这个实例写入并冲刷消息并经过 ChannelPipeline;

当使用 ChannelHandlerContext 的 API 的时候,有以下两点:

  • ChannelHandlerContext 和 ChannelHandler 之间的关联(绑定)是永远不会改变的, 所以缓存对它的引用是安全的;
  • 相对于其他类的同名方法,ChannelHandler Context 的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。

选择合适的内置通信传输模式

  • NIO io.netty.channel.socket.nio 使用 java.nio.channels 包作为基础——基于选择器的方式。
  • Epoll io.netty.channel.epoll 由 JNI 驱动的 epoll()和非阻塞 IO。这个传输只支持在 Linux 上可用的多种特性,如 SO_REUSEPORT,比 NIO 传输更快,而且是完全非阻塞的。将 NioEventLoopGroup 替换为 EpollEventLoopGroup ,并且将 NioServerSocketChannel.class 替换为 EpollServerSocketChannel.class 即可。
  • OIO io.netty.channel.socket.oio 使用 java.net 包作为基础——使用阻塞流。
  • Local io.netty.channel.local 可以在 VM 内部通过管道进行通信的本地传输。
  • Embedded io.netty.channel.embedded Embedded 传输,允许使用 ChannelHandler 而又不需要一个真正的基于网络的传输。在测试 ChannelHandler 实现时非常有用。

引导 Bootstrap

  • 网络编程里,“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。
  • 因此,有两种类型的引导:一种用于客户端(简单地称为 Bootstrap),而另一种 (ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据, 唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
  • 比较 Bootstrap 类

在这里插入图片描述

  • ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
  • 第二个区别可能更加明显。Bootstrap 引导一个客户端只需要一个 EventLoopGroup,但是一个 ServerBootstrap 则需要两个(也可以是同一个实例)。
  • 因为服务器需要两组不同的 Channel。第一组将只包含一个 ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel。

在这里插入图片描述

  • 与 ServerChannel 相关联的 EventLoopGroup 将分配一个负责为传入连接请求创建 Channel 的 EventLoop。一旦连接被接受,第二个 EventLoopGroup 就会给它的 Channel 分配 一个 EventLoop。
  • 在引导过程中添加多个 ChannelHandler
  • Netty 提供了一个特殊的 ChannelInboundHandlerAdapter 子类:public abstract class ChannelInitializer< C extends Channel> extends ChannelInboundHandlerAdapter
  • 它定义了下面的方法:protect ed abstract void initChannel(C ch) throws Exception;
  • 这个方法提供了一种将多个 ChannelHandler 添加到一个 ChannelPipeline 中的简便方法。 你只需要简单地向 Bootstrap 或 ServerBootstrap 的实例提供你的 ChannelInitializer 实现即可,并且一旦 Channel 被注册到了它的 EventLoop 之后,就会调用你的 initChannel()版本。 在该方法返回之后,ChannelInitializer 的实例将会从 ChannelPipeline 中移除它自己。

ChannelOption

  • ChannelOption 的各种属性在套接字选项中都有对应。
    1、ChannelOption.SO_BACKLOG
    ChannelOption.SO_BACKLOG 对应的是 tcp/ip 协议 listen 函数中的 backlog 参数,函数 listen(int socketfd,int backlog)用来初始化服务端可连接队列;
    服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小。
    2、ChannelOption.SO_REUSEADDR
    ChanneOption.SO_REUSEADDR 对应于套接字选项中的 SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口;
    比如,某个服务器进程占用了 TCP 的 80 端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置 SO_REUSEADDR 就无法正常使用该端口。
    3、ChannelOption.SO_KEEPALIVE
    Channeloption.SO_KEEPALIVE 参数对应于套接字选项中的 SO_KEEPALIVE,该参数用于设置 TCP 连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP 会自动发送一个活动探测数据报文。
    4、ChannelOption.SO_SNDBUF 和 ChannelOption.SO_RCVBUF
    ChannelOption.SO_SNDBUF 参数对应于套接字选项中的 SO_SNDBUF, ChannelOption.SO_RCVBUF 参数对应于套接字选项中的 SO_RCVBUF 这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
    5、ChannelOption.SO_LINGER
    ChannelOption.SO_LINGER 参数对应于套接字选项中的 SO_LINGER,Linux 内核默认的处理方式是当用户调用 close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用 SO_LINGER 可以阻塞 close()的调用时间,直到数据完全发送。
    6、ChannelOption.TCP_NODELAY
    ChannelOption.TCP_NODELAY 参数对应于套接字选项中的 TCP_NODELAY,该参数的使用与 Nagle 算法有关,Nagle 算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用 Nagle 算法,使用于小数据即时传输,与 TCP_NODELAY 相对应的是 TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

ByteBuf

  • ByteBuf API 的优点:
  1. 它可以被用户自定义的缓冲区类型扩展;
  2. 通过内置的复合缓冲区类型实现了透明的零拷贝;
  3. 容量可以按需增长(类似于 JDK 的 StringBuilder);
  4. 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
  5. 读和写使用了不同的索引;
  6. 支持方法的链式调用;
  7. 支持引用计数;
  8. 支持池化。
  • ByteBuf 维护了两个不同的索引,名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或者 get 开头的操作则不会。
  • 如果打算读取字节直到 readerIndex 达到和 writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个 IndexOutOf-BoundsException。
  • 可以指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触发一个异常。(默认的限制是 Integer.MAX_VALUE。)

使用模式

堆缓冲区

  • 最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组 (backing array),它能在没有使用池化的情况下提供快速的分配和释放。可以由 hasArray() 来判断检查 ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区。

直接缓冲区

  • 直接缓冲区是另外一种 ByteBuf 模式。 直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。

复合缓冲区

  • 复合缓冲区 CompositeByteBuf,它为多个 ByteBuf 提供一个聚合视图。比如 HTTP 协议, 分为消息头和消息体,这两部分可能由应用程序的不同模块产生,各有各的 ByteBuf,将会在消息被发送的时候组装为一个 ByteBuf,此时可以将这两个 ByteBuf 聚合为一个 CompositeByteBuf,然后使用统一和通用的 ByteBuf API 来操作。

分配

ByteBufAllocator 接口

  • Netty 通过 interface ByteBufAllocator 分配我们所描述过的任意类型的 ByteBuf 实例。
名称 描述
buffer() 返回一个基于堆或者直接内存存储的 ByteBuf
heapBuffer() 返回一个基于堆内存存储的 ByteBuf
directBuffer() 返回一个基于直接内存存储的 ByteBuf
compositeBuffer() 返回一个可以通过添加最大到指定数目的基于堆的或者直接 内存存储的缓冲区来扩展的 CompositeByteBuf
ioBuffer() 返回一个用于套接字的 I/O 操作的 ByteBuf,当所运行的环境具有 sun.misc.Unsafe 支持时,返回基于直接内存存储的 ByteBuf, 否则返回基于堆内存存储的 ByteBuf;当指定使用 PreferHeapByteBufAllocator 时,则只会返回基于堆内存存储的 ByteBuf。
  • 可以通过 Channel(每个都可以有一个不同的 ByteBufAllocator 实例)或者绑定到 ChannelHandler 的 ChannelHandlerContext 获取一个到 ByteBufAllocator 的引用。
Channel channel=...;
ByteBufAllocator alloc = channel.alloc();

ChannelHandlerContext ctx=...;
ByteBufAllocator alloc = ctx.alloc();
  • Netty 提供了两种 ByteBufAllocator 的实现:PooledByteBufAllocator 和 Unpooled-ByteBufAllocator。
  • 前者池化了 ByteBuf 的实例以提高性能并最大限度地减少内存碎片。后者的实现不池化 ByteBuf 实例,并且在每次它被调用时都会返回一个新的实例。
  • Netty4.1 默认使用了 PooledByteBufAllocator。

Unpooled 缓冲区

  • Netty 提供了一个简单的称为 Unpooled 的工具类,它提供了静态的辅助方法来创建未池化的 ByteBuf 实例。
名称 描述
buffer() 返回一个未池化的基于堆内存存储的 ByteBuf
directBuffer() 返回一个未池化的基于直接内存存储的 ByteBuf
wrappedBuffer() 返回一个包装了给定数据的 ByteBuf
copiedBuffer() 返回一个复制了给定数据的 ByteBuf

随机访问索引/顺序访问索引/读写操作

  • 如同在普通的 Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是 0,最后一个字节的索引总是 capacity() - 1。使用那些需要一个索引值参数(随机访问, 也即是数组下标)的方法之一来访问数据既不会改变 readerIndex 也不会改变 writerIndex。如果有需要,也可以通过调用 readerIndex(index)或者 writerIndex(index)来手动移动这两者。
  • 顺序访问通过索引访问。
  • 有两种类别的读/写操作:
    get()和 set()操作,从给定的索引开始,并且保持索引不变;get+数据字长 (bool.byte,int,short,long,bytes)
    read()和 write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。

可丢弃字节

  • 可丢弃字节的分段包含了已经被读过的字节。通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为 0,存储在 readerIndex 中,会随着 read 操作的执行而增加(get*操作不会移动 readerIndex)。
  • 缓冲区上调用 discardReadBytes()方法后,可丢弃字节分段中的空间已经变为可写的了。 频繁地调用 discardReadBytes()方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节必须被移动到缓冲区的开始位置。建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。

在这里插入图片描述

可读字节

  • ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的 readerIndex 值为 0。

可写字节

  • 可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的 writerIndex 的默认值为 0。任何名称以 write 开头的操作都将从当前的 writerIndex 处开始写数据,并将它增加已经写入的字节数。

在这里插入图片描述

索引管理

  • 调用 markReaderIndex()、markWriterIndex()、resetWriterIndex()和 resetReaderIndex()来标记和重置 ByteBuf 的 readerIndex 和 writerIndex。
  • 也可以通过调用 readerIndex(int)或者 writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个 IndexOutOfBoundsException。
  • 可以通过调用 clear()方法来将 readerIndex 和 writerIndex 都设置为 0。注意,这并不会清除内存中的内容。

查找操作

  • 在 ByteBuf 中有多种可以用来确定指定值的索引的方法。最简单的是使用 indexOf()方法。
  • 较复杂的查找可以通过调用 forEachByte()
ByteBuf buf=...;
int index = buf.forEachByte(ByteBufProcessor.FIND_CR);

派生缓冲区

  • 派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
  • duplicate(); slice(); slice(int, int); Unpooled.unmodifiableBuffer(…); order(ByteOrder); readSlice(int)
  • 每个这些方法都将返回一个新的 ByteBuf 实例,它具有自己的读索引、写索引和标记索引。其内部存储和 JDK 的 ByteBuffer 一样也是共享的。
  • ByteBuf 复制,如果需要一个现有缓冲区的真实副本,请使用 copy()或者 copy(int, int)方 法。不同于派生缓冲区,由这个调用所返回的 ByteBuf 拥有独立的数据副本。

引用计数

  • 引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第 4 版中为 ByteBuf 引入了引用计数技术, interface ReferenceCounted。

工具类

  • ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法。因为这个 API 是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。
  • 这些静态方法中最有价值的可能就是 hexdump()方法,它以十六进制的表示形式打印 ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录 ByteBuf 的内容。 十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。
  • 另一个有用的方法是 boolean equals(ByteBuf, ByteBuf),它被用来判断两个 ByteBuf 实例的相等性。

资源释放

  • 当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它要负责显式地释放与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法 ReferenceCountUtil.release()。
  • Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用 SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。

1、对于入站请求,Netty 的 EventLoop 在处理 Channel 的读操作时进行分配 ByteBuf,对于这类 ByteBuf,需要我们自行进行释放,有三种方式,或者使用 SimpleChannelInboundHandler,或者在重写 channelRead()方法使用 ReferenceCountUtil.release()或者使用 ctx.fireChannelRead 继续向后传递;

2、对于出站请求,不管 ByteBuf 是否由我们的业务创建的,当调用了 write 或者 writeAndFlush 方法后,Netty 会自动替我们释放,不需要我们业务代码自行释放。

猜你喜欢

转载自blog.csdn.net/qq_40977118/article/details/109450617