《Netty实战》-学习笔记1
Chapter 3 Netty的组件及设计
-
Channel:Socket,对应一个连接。- 提供基本的 I/O 操作:如
bind()、connect()、read()、write()等,就是对 Java 中实现的 Socket 进行一定层次的封装。
- 提供基本的 I/O 操作:如
-
EventLoop:控制流、多线程处理、并发。- 在一个
EventLoopGroup中包含一个或多个EventLoop。 - 一个
EventLoop在它的生命周期内只和一个Thread绑定。 - 所有
EventLoop处理的 I/O 事件都在它专有的Thread上被处理。 - 一个
Channel在它的生命周期内只注册于一个EventLoop。 - 一个
EventLoop可能会被分配给一个或多个Channel。
- 在一个
-
ChannelFuture:异步通知。- 在 Netty 中所有操作的是异步的,调用时不会立即返回结果,但是返回一个
Future。能够在之后的某个时间点确定结果,对Future添加Listener注册回调,再操作完成后通知执行相应的回调。
- 在 Netty 中所有操作的是异步的,调用时不会立即返回结果,但是返回一个
-
ChannelHandler:在Channel上发生事件的处理器。- 由网络事件触发的处理器,实际上可以响应任何类型的动作。
ChannelInboundHandler:接收入站事件和数据。ChannelOutboundHandler:数据出站处理器。
-
ChannelPipeline:ChannelHandler链的容器。ChannelPipeline就是一些ChannelHandler的编排顺序,每个ChannelHandler接收事件,执行所实现的处理逻辑,完毕后交给下个一个ChannelHandler。- 从客户端角度来说,发送数据到服务器就是出站的。则通过
ChannelPipeline的编排,在一定次序下调用ChannelOutboundHandler。响应的接收服务器发来数据就是入站的,在一个ChannelPipeline按顺序调用ChannelInboundHandler。 - 如下图:客户端操作系统从网络上接收服务器发来数据放入缓存中,接下来,在 Netty 中从
ChannelPipeline中从头部开始按顺序调用ChannnelInboundHandler。从客户端发送数据到服务器则是从 ChannelPipeline 的尾部开始执行,直到发送到网络上。
-
ChannelHandlerContext:代表了ChannelHandler和ChannelPipeline之间的绑定。- 可以用于获取底层
Channel。
- 可以用于获取底层
-
两种发送消息的方式:
- 直接写入
Channel中:消息从ChannelPipeline的尾部开始流动。 - 写入到
ChannelHandler相关联的ChannelHandlerContext对象中: 消息从ChannelPipeline的下一个ChannelHandler开始流动。
- 直接写入
Chapter 4 传输API
Channel

Channel 都会被分配一个 ChannelPipeline 和 ChannelConfig。
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 更好的性能。 对应的则是
EpollEventLoopGroup和EpollServerSocketChannel。 -
OIO 建立在
java.net包下,不是异步的。能够通过设置SO_TIMEOUT来在单线程中处理多个连接。 -
Embedded Embedded 提供了可以将一组
ChannelHandler作为帮助器嵌入到其他的ChannelHandler内部。这样可以扩展一个ChannelHandler的功能,但又不需要修改其内部代码。
Chapter 5 ByteBuf
ByteBuf工作原理
维护了两个不同的索引:readerIndex 和 writerIndex ,一个用于读取,一个用于写入。当从 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 模式)表示为单个合并缓冲区的虚拟表示。
字节级操作
- 随机访问索引
ByteBuf的索引是从 0 开始到 capacity() - 1。 获取每个字节的方法则是:byte getByte(int i);这样倒不会改变readerIndex和writerIndex的值。 当然可以通过:ByteBuf readerIndex(int index)和ByteBuf writerIndex(int index)来手动移动读取/写入索引值。 - 顺序访问索引
有三个区域:

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

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