Book(10) Netty实战

100 阅读13分钟

BIO 和 NIO

  • Java阻塞API 早期的JAVA API(java.net)只支持由本地系统套接字库提供的所谓的阻塞函数。服务器持续监听连接事件,直到连接建立,随后返回一个新的Socket用于客户端和服务器之间的通信。 读写方法如readLine() 将会阻塞,直到某个字符串被读取。

缺点: 1、在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据,造成资源浪费。 2、需要为每个线程分配内存,并发量大的时候容易达到系统瓶颈。

  • Java非阻塞的API 除BIO之外,本地套接字库还提供了非阻塞调用,Java对于非阻塞的I/O支持是在2002年引入的,位于JDK1.4的java nio包。 1,可以使用setsockopt()方法配置套接字,在读写没有数据的时候可以立刻返回。 2,可以使用操作系统的事件通知API注册一组非阻塞的套接字,已确定他们中是否有任何的套接字已经有数据可以读写。
Netty的基本概念
  • channel Java NIO的一个基本构造,可以看作是传入和传出数据的载体,可以被打开或者关闭,连接或者断开连接。

  • 回调 一个回调就是一个方法,将一个方法的引用传入另一个方法,以供它调用。 Netty在内部使用了回调来处理事件,例如新的连接建立的时候,ChannelHandler的channelActive()方法将会被调用。

  • Future Future提供了另一种在操作完成时通知应用程序的方式。这个对象是一个一步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问。 JDK 预置了interface java.utils.concurrent.Future, 但其所提供的实现,只允许手动检查对应的操作是否完成,或者一直阻塞到它完成。Netty提供了对他的实现--ChannelFuture,用于在执行异步操作的时候使用。 ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听的回调方法operationComplete(),将会在对应的操作完成的时候被调用。然后监听可以判断该操作是否成功的完成了还是出错了。如果出错了,我们可以检索产生的Throwable。消除了手动检查对应操作是否完成的必要。 每个Netty的出站I/O操作都将返回一个ChannelFuture;也就是说,他们都不会阻塞。Netty完全是异步和事件驱动的。

  • 事件 Netty使用不同的事件来通知我们状态的改变或者是操作的状态。使得我们能够基于已经发生的事件来触发相应的动作。

  • ChannelHandler Netty的ChannelHandler为处理器提供了基本的抽象。 Netty提供了大量与定义的可以开箱即用的ChannelHandler实现,包括用于各种协议的ChannelHandler。在内部,ChannelHandler自己也使用了事件和Future,是的他们也成了你的应用程序将使用的相同抽象的消费者。

  • EventLoop Netty通过触发事件将Selector从应用程序中抽象出来,消除了所有本来将需要手动编写的派发代码。在内部,将会为每个Channel分配一个EventLoop,用以处理所有事件。

Netty的核心组件
  • channel 接口 基本的I/O操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语。在Java网络编程中,其基本的构造是class Socket。Netty的Channel接口所提供的API,大大降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单。 EmbeddedChannel、LocalServerChannel、NioDatagramChannel、NioSctpChannel、NioSocketChannel
  • EventLoop 接口 EventLoop中定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。 Channel、EventLoop、Thread以及EventLoopGroup之间的关系:
    • 一个EventLoopGroup包含一个或者多个EventLoop
    • 一个EventLoop在它的生命周期内只和一个Thread绑定
    • 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理
    • 一个Channel在它的生命周期内只注册于一个EventLoop
    • 一个EventLoop可能会被分配给一个或者多个Chennel。
  • ChannelFuture接口 Netty所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后某个时间点确定其结果的方法。为此Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知
  • ChannelHandler 接口 从应用程序开发人员的角度来看,Netty的主要组件时ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为ChannelHandler的方法是由网络事件触发的。
  • ChannelPipeline

ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,它会自动分配到专属的ChannelPipeline

ByteBuf 的 API

Netty的数据处理API通过两个组件暴露--abstract class ByteBuf 和 interface ByteBufHolder 下面是一些ButeBuf API的优点:

  • 它可以被用户自定义的缓冲区类型扩展。
  • 通过内置的复合缓冲区类型是吸纳了透明的零拷贝。
  • 容易可以按需增长(类似于JDK的StringBuilder)
  • 在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法
  • 读和写使用了不同的索引
  • 支持方法的链式调用
  • 支持引用计数
  • 支持池化
ByteBuf类 -Netty的数据容器

1,ByteBuf如何工作 ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当 从BytebBuf读取时,readerIndex将会被递增已经读区的字数。 同样地,当你写入ByteBuf时,它的writeIndex也会被递增。 2,ByteBuf的使用模式 堆缓冲区:常见的ByteBuf模式是将数据存储在JVM的堆空间中,这种模式被称为支撑数组(backing array), 它能在没有使用池化的情况下提供快速的分配和释放。 直接缓冲区:直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外。缺点是,相对于基于堆的缓冲区,他们的分配和释放都较为昂贵。 复合缓冲区:为多个ByteBuf提供一个聚合视图。在之类可以天际啊或者删除ByteBuf实例,这是一个JDK的ByteBuffer完全缺失的特性。

字节级的操作

ButeBuf提供了许多超出基本读、写操作的方法用于修改它的数据。

  • 随机访问索引 ByteBuf的索引范围是从0到capacity()-1

  • 顺序访问索引 ByteBuf被划分为3个区,分别是已经被读过的可丢弃字节,尚未被读过的字节,可以添加更多字节的空间

  • 可丢弃字节 标有“可丢弃字节”的段包含已经被读取的字节。他们可以被丢弃,通过调用discardReadBytes() 来回收空间。这个段的初始大小存储在readerIndex,为 0,当“read”操作被执行时递增(“get”操作不会移动 readerIndex)。

  • 可读字节 ByteBuf 的“可读字节”分段存储的是实际数据。新分配,包装,或复制的缓冲区的 readerIndex 的默认值为 0 。任何操作,其名称以 "read" 或 "skip" 开头的都将检索或跳过该数据在当前 readerIndex ,并且通过读取的字节数来递增。

  • 索引管理 在 JDK 的 InputStream 定义了 mark(int readlimit) 和 reset()方法。这些是分别用来标记流中的当前位置和复位流到该位置。同样,您可以设置和重新定位ByteBuf readerIndex 和 writerIndex 通过调用markReaderIndex(),markWriterIndex(),resetReaderIndex() 和 resetWriterIndex()。这些类似于InputStream 的调用,所不同的是,没有 readlimit 参数来指定当标志变为无效。 您也可以通过调用 readerIndex(int) 或 writerIndex(int) 将指标移动到指定的位置。在尝试任何无效位置上设置一个索引将导致 IndexOutOfBoundsException 异常。 调用 clear() 可以同时设置 readerIndex 和 writerIndex 为 0。

  • 查询操作 有几种方法,以确定在所述缓冲器中的指定值的索引。最简单的是使用 indexOf() 方法。更复杂的搜索执行以 ByteBufProcessor 为参数的方法。这个接口定义了一个方法,boolean process(byte value),它用来报告输入值是否是一个正在寻求的值。

  • 读/写操作 读/写操作主要由2类: gget()/set() 操作从给定的索引开始,保持不变 read()/write() 操作从给定的索引开始,与字节访问的数量来适用,递增当前的写索引或读索引

  • ByteBufHolder 接口 我们时不时的会遇到这样的情况:即需要另外存储除有效的实际数据各种属性值。Netty 提供的 ByteBufHolder 可以对这种常见情况进行处理。 ByteBufHolder 还提供了对于 Netty 的高级功能,如缓冲池,其中保存实际数据的 ByteBuf 可以从池中借用,如果需要还可以自动释放。

  • ByteBuf 分配 ByteBufAllocator 为了减少分配和释放内存的开销,Netty 通过支持池类 ByteBufAllocator,可用于分配的任何 ByteBuf 我们已经描述过的类型的实例。是否使用池是由应用程序决定的

  • Unpooled (非池化)缓存 当未引用 ByteBufAllocator 时,上面的方法无法访问到 ByteBuf。对于这个用例 Netty 提供一个实用工具类称为 Unpooled,,它提供了静态辅助方法来创建非池化的 ByteBuf 实例。

  • ByteBufUtil ByteBufUtil 静态辅助方法来操作 ByteBuf,因为这个 API 是通用的,与使用池无关,这些方法已经在外面的分配类实现。

ChannelHandler
  • channel的生命周期 1 channelUnregistered channel已创建但未注册到一个 EventLoop. 2 channelRegistered channel 注册到一个 EventLoop. 3 channelActive channel 变为活跃状态(连接到了远程主机),现在可以接收和发送数据了 4 channelInactive channel 处于非活跃状态,没有连接到远程主机

  • channelHandler 的生命周期 1 handlerAdded 当 ChannelHandler 添加到 ChannelPipeline 调用 2 handlerRemoved 当 ChannelHandler 从 ChannelPipeline 移除时调用 3 exceptionCaught 当 ChannelPipeline 执行抛出异常时调用

  • ChannelHandler 子接口 Netty 提供2个重要的 ChannelHandler 子接口: ChannelInboundHandler - 处理进站数据和所有状态更改事件 ChannelOutboundHandler - 处理出站数据,允许拦截各种操作

  • 资源管理 当你通过 ChannelInboundHandler.channelRead(...) 或者 ChannelOutboundHandler.write(...) 来处理数据,重要的是在处理资源时要确保资源不要泄漏。为了让用户更加简单的找到遗漏的释放,Netty 包含了一个 ResourceLeakDetector ,将会从已分配的缓冲区 1% 作为样品来检查是否存在在应用程序泄漏。因为 1% 的抽样,开销很小。

ChannelPipeline

ChannelPipeline 是一系列的ChannelHandler 实例,流经一个 Channel 的入站和出站事件可以被ChannelPipeline 拦截,ChannelPipeline能够让用户自己对入站/出站事件的处理逻辑,以及pipeline里的各个Handler之间的交互进行定义。

每当一个新的Channel被创建了,都会建立一个新的 ChannelPipeline,并且这个新的 ChannelPipeline 还会绑定到Channel上。这个关联是永久性的;Channel 既不能附上另一个 ChannelPipeline 也不能分离当前这个。这些都由Netty负责完成,,而无需开发人员的特别处理。 一个 ChannelPipeline 是用来保存关联到一个 Channel 的ChannelHandler 可以修改 ChannelPipeline 通过动态添加和删除 ChannelHandler ChannelPipeline 有着丰富的API调用动作来回应入站和出站事件。

ChannelHandlerContext

在ChannelHandler 添加到 ChannelPipeline 时会创建一个实例,就是接口 ChannelHandlerContext,它代表了 ChannelHandler 和ChannelPipeline 之间的关联。接口ChannelHandlerContext 主要是对通过同一个 ChannelPipeline 关联的 ChannelHandler 之间的交互进行管理

ChannelHandlerContext 中包含了有许多方法,其中一些方法也出现在 Channel 和ChannelPipeline 本身。如果您通过Channel 或ChannelPipeline 的实例来调用这些方法,他们就会在整个 pipeline中传播 。相比之下,一样的方法在 ChannelHandlerContext 的实例上调用, 就只会从当前的 ChannelHandler 开始并传播到相关管道中的下一个有处理事件能力的 ChannelHandler 。

EventLoop接口

在连续的生命周期内发生连续的事件是任何网络框架的基本功能。Netty使用了interface io.netty.EventLoop。 Netty使用了两个基本的API:并发和网络编程。 一个EventLoop将由一个永远都不会改变的Thread来驱动,同时任务(Runnable或者Callable)可以直接提交给EventLoop实现,以立即执行或者调度执行。根据配置和可用核心的不同,可能会创建多个Eventloop实例用以优化资源的使用,并且单个Eventloop可能会被指派用于多个Channel。

Netty提供的Decoder(解码器)

Netty 提供了丰富的解码器抽象基类,我们可以很容易的实现这些基类来自定义解码器。主要分两类:

  • 解码字节到消息(ByteToMessageDecoder 和 ReplayingDecoder)
  • 解码消息到消息(MessageToMessageDecoder) decoder 负责将“入站”数据从一种格式转换到另一种格式,Netty的解码器是一种 ChannelInboundHandler 的抽象实现。实践中使用解码器很简单,就是将入站数据转换格式后传递到 ChannelPipeline 中的下一个ChannelInboundHandler 进行处理;这样的处理是很灵活的,我们可以将解码器放在 ChannelPipeline 中,重用逻辑。
Netty的Encoder(编码器)

Netty 也为你提供了一组类来写 encoder ,当然这些类提供的是与 decoder 完全相反的方法,如下所示:

编码从消息到字节 MessageToByteEncoder 编码从消息到消息 MessageToMessageEncoder

Netty抽象 Codec(编解码器)类

我们在讨论解码器和编码器的时候,都是把它们当成不同的实体的,但是有时候如果在同一个类中同时放入入站和出站的数据和信息转换的话,发现会更加实用。而Netty中的抽象Codec(编解码器)类就能达到这个目的,它们成对地组合解码器和编码器,以此提供对于字节和消息都相同的操作(这些类实现了 ChannelInboundHandler 和 ChannelOutboundHandler )。

  • 随机
  • ByteBuf相对于ByteBuffer的优势
  • 如何分配和访问由ByteBuf所使用的内存。
  • Netty线程模型
  • interface EventLoop

自定义协议经常用到的编解码器

  • 编码器和解码器
  • Netty的编码器和解码器

netty对应用层高级协议的支持

  • 如何使用WebSocket协议来实现Web服务器和客户端之间的双向通信