《Netty实战》-学习笔记1

444 阅读7分钟

《Netty实战》-学习笔记1

Chapter 3 Netty的组件及设计

  • Channel:Socket,对应一个连接。

    • 提供基本的 I/O 操作:如 bind()connect()read()write() 等,就是对 Java 中实现的 Socket 进行一定层次的封装。
  • EventLoop:控制流、多线程处理、并发。

    • 在一个 EventLoopGroup 中包含一个或多个 EventLoop
    • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定。
    • 所有 EventLoop 处理的 I/O 事件都在它专有的 Thread 上被处理。
    • 一个 Channel 在它的生命周期内只注册于一个 EventLoop
    • 一个 EventLoop 可能会被分配给一个或多个 Channel
  • ChannelFuture:异步通知。

    • 在 Netty 中所有操作的是异步的,调用时不会立即返回结果,但是返回一个 Future 。能够在之后的某个时间点确定结果,对 Future 添加 Listener 注册回调,再操作完成后通知执行相应的回调。
  • ChannelHandler:在 Channel 上发生事件的处理器。

    • 由网络事件触发的处理器,实际上可以响应任何类型的动作。
    • ChannelInboundHandler:接收入站事件和数据。
    • ChannelOutboundHandler:数据出站处理器。
  • ChannelPipelineChannelHandler 链的容器。

    • ChannelPipeline 就是一些 ChannelHandler 的编排顺序,每个 ChannelHandler 接收事件,执行所实现的处理逻辑,完毕后交给下个一个 ChannelHandler
    • 从客户端角度来说,发送数据到服务器就是出站的。则通过 ChannelPipeline 的编排,在一定次序下调用 ChannelOutboundHandler 。响应的接收服务器发来数据就是入站的,在一个 ChannelPipeline 按顺序调用 ChannelInboundHandler
    • 如下图:客户端操作系统从网络上接收服务器发来数据放入缓存中,接下来,在 Netty 中从 ChannelPipeline 中从头部开始按顺序调用 ChannnelInboundHandler。从客户端发送数据到服务器则是从 ChannelPipeline 的尾部开始执行,直到发送到网络上。
  • ChannelHandlerContext:代表了 ChannelHandlerChannelPipeline 之间的绑定。

    • 可以用于获取底层Channel
  • 两种发送消息的方式:

    • 直接写入 Channel 中:消息从 ChannelPipeline 的尾部开始流动。
    • 写入到 ChannelHandler 相关联的 ChannelHandlerContext 对象中: 消息从 ChannelPipeline 的下一个ChannelHandler开始流动。

Chapter 4 传输API

Channel

每个 Channel 都会被分配一个 ChannelPipelineChannelConfig

  • ChannelConfig:对应 Channel 的配置设置,支持热更新。
  • ChannelPipeline: 持有所有将应用于入站和出站数据以及事件的 ChannelHandler 实例,这些 ChannelHandler 实现了应用程序用于处理状态变化以及数据处理的逻辑。

内置传输

名称 描述
NIO io.netty.channel.socket.nio 使用 java.nio.channels 包作为基础——基于选择器(select)
Epoll io.netty.channel.epoll 由 JNI 驱动的 epoll() 和非阻塞 IO。这个传输支持只有在Linux上可用的多种特性,如 SO_REUSEPORT,比NIO 传输更快,而且是完全非阻塞的
OIO io.netty.channel.socket.oio 使用 java.net 包作为基础——使用阻塞流
Local io.netty.channel.local 可以在 VM 内部通过管道进行通信的本地传输
Embedded io.netty.channel.embedded Embedded 传输,允许使用 ChannelHandler 而又不需要一个真正的基于网络的传输。这在测试 ChannelHandler 实现时非常有用
  • NIO 基于选择器,每个 Channel 在选择器(Selector)上注册,当 Selector 调用 select() 的时候是阻塞的,直到已经注册的 Channel 中有相应的事件发生。
事件 描述
OP_ACCEPT 请求在接受新连接并创建 Channel 时获得通知
OP_CONNECT 请求在建立一个连接时获得通知
OP_READ 请求当数据已经就绪,可以从 Channel 中读取时获得通知
OP_WRITE 请求当可以向 Channel 中写更多的数据时获得通知。这处理了套接字缓冲区被完全填满时的情况,这种情况通常发生在数据的发送速度比远程节点可处理的速度更快的时候

处理模型:

  • Epoll NIO 是基于 Select 的,那么 Epoll 如其名,则是基于 Linux 的 epoll 。好处不言而喻,提供比旧的 select 和 poll 更好的性能。 对应的则是 EpollEventLoopGroupEpollServerSocketChannel

  • OIO 建立在 java.net 包下,不是异步的。能够通过设置 SO_TIMEOUT 来在单线程中处理多个连接。

  • Embedded Embedded 提供了可以将一组 ChannelHandler 作为帮助器嵌入到其他的 ChannelHandler 内部。这样可以扩展一个 ChannelHandler 的功能,但又不需要修改其内部代码。

Chapter 5 ByteBuf

ByteBuf工作原理

维护了两个不同的索引:readerIndexwriterIndex ,一个用于读取,一个用于写入。当从 ByteBuf 读取时,readerIndex 递增。同样当从 ByteBuf 写入时, writerIndex 递增。 如果 readerIndex 递增到和 writerIndex 相同的值,那么到达“可读取数据的末尾”,这时再进行读取则会像读取超出数组末尾的数据一样,抛出 IndexOutOfBoundsException。 另外,名称以 read 或者 write 开头的 ByteBuf 方法会推进相应的索引值,而以 set 或者 get 开头的方法则不会(这样只是在相应的索引值上进行操作)。

使用模式

在弄清楚 ByteBuf 的使用模式之前,应该理解 Java 中的直接缓冲区和非直接缓冲区:

  • 直接缓冲区:缓冲区建立在物理内存中,可以提高效率。

  • 非直接缓冲区:缓冲区建立在 JVM 的内存中。

  • 堆缓冲区(backing 模式) ByteBuf 数据存储在 JVM 的堆空间(非直接缓冲区)中。这种模式也被称为“支撑数组(backing array)”,能够在没有使用池化的的情况下提供快速的分配和释放。

  • 直接缓冲区(direct 模式) 通过本地调用分配内存,直接分配的是物理内存。

  • 复合缓冲区 为多个 ByteBuf 提供一个聚合视图。

    • CompositeByteBuf:能够提供将多个缓冲区(每个 ByteBuf 可以是 backing 模式或者 direct 模式)表示为单个合并缓冲区的虚拟表示。

字节级操作

  1. 随机访问索引 ByteBuf 的索引是从 0 开始到 capacity() - 1。 获取每个字节的方法则是:byte getByte(int i); 这样倒不会改变 readerIndexwriterIndex 的值。 当然可以通过:ByteBuf readerIndex(int index)ByteBuf writerIndex(int index)来手动移动读取/写入索引值。
  2. 顺序访问索引 有三个区域:
  3. 可丢弃字节 通过调用discardReadBytes()方法,可以丢弃已经被读过的字节以回收空间。是的,调用discardReadBytes()可以确保可写分段的最大化,但是有可能导致内存复制(可读字节会移动到缓冲区的开始位置)。同样的可丢弃字节空间也只是相应的更改了 writerIndex 的值,并不会保证空间中值的擦写。
  4. 可读字节 ByteBuf 的可读字节分段(CONTENT)存储了实际数据。任何名称以 read 或者 skip 开头的操作都将检索或者跳过位于当前 readerIndex 的数据,并且将它增加已读字节数。
  5. 可写字节 可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。
  6. 索引管理 在 JDK 中 InputStream定义了mark(int readlimit)reset()方法,来标记流中指定位置,以及重置。在 ByteBuf 中也有相应的方法来标记和重置 ByteBuf 中的 readerIndexwriterIndex
    • markReaderIndex()
    • markWriterIndex()
    • resetReaderIndex()
    • resetWriterIndex() 还有clear()方法只是重置索引,且不会复制内存。
  7. 查找操作 最简单的,可以使用indexOf()方法。 或者使用ByteProcessor类。
  8. 派生缓冲区 提供了专门的方法来呈现器内容的视图:
    • duplicate()
    • slice()
    • slice(int , int)
    • Unpooled.unmodifiableBuffer(...)
    • order(ByteOrder)
    • readSlice(int) 以上每个方法都会返回一个新的ByteBuf实例,具有自己的读索引、写索引和标记索引。但是内部存储是共享的。所以如果修改了它的内容,也同时修改了其对应的源实例。 如果要深拷贝获得真实副本,使用copy()或者copy(int ,int)函数。
  9. 读/写操作 两种类别的读/写操作:
    • get()set()操作,从给定的索引值开始,且保持索引值不变。
    • read()write()操作,从给定的索引开始,且会根据以及访问过的字节数堆索引进行调整。 get()方法:
      set()方法:
      read()方法:
      write()方法:
  10. 更多的操作

ByteBuf分配

  1. 按需分配:ByteBufAllocator接口 ByteBufAllocator的方法:
    可以通过Channel或者ChannelHandlerContext获取到一个ByteBufAllocator的引用。 Netty提供了两种ByteBufAllocator的实现:
  • PooledByteBufAllocator:池化了ByteBuf的实例以提高性能并且最大限度的减少内存碎片。
  • UnpooledByteBufAllocator:不池化ByteBuf实例,但是每次被调用时会返回一个新的实例。
  1. Unpooled缓冲区 无法获取到ByteBufAllocator的引用时,有一个简单的Unpooled工具类,提供静态的辅助方法来创建未池化的ByteBuf实例。
  2. ByteBufUtil类 这个类提供了用于操作ByteBuf的静态辅助方法。