- 代码示例
- Bootstrap:引导
- EventLoop(Group):控制流、多线程处理、并发
- Channel:Socket
- ChannelFuture:异步通知
- ChannelHandler
- ChannelPipeline
- ChannelHandlerContext
- ChannelOption
- ByteBuf
代码示例
- 为什么Netty 使用 NIO 而不是 AIO:Netty5 已经停止开发了
- Netty 不看重Windows 上的使用,在Linux 系统上,AIO 的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK 封装了一层不容易深度优化
- 而且Linux 上AIO 不够成熟,处理回调结果速度跟不上处理需求
- AIO 还有个缺点是接收数据需要预先分配缓存, 而不是NIO 那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况,内存浪费很多
// Server
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 自定义 EchoServerHandler 做业务处理,可继承 SimpleChannelInboundHandler
ch.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
group.shutdownGracefully().sync();
// Client
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host,port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 自定义 EchoClientHandler 做业务处理,可继承 SimpleChannelInboundHandler
ch.pipeline().addLast(new EchoClientHandler());
}
}
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
group.shutdownGracefully().sync();
Netty 组件
Bootstrap:引导
- Bootstrap 是 Netty 框架的启动类和主入口类,分为客户端类Bootstrap 和服务器类 ServerBootstrap 两种
-
引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个(也可以是同一个实例),因为服务器需要两组不同的Channel
- 第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字
- 第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel
-
与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel 的EventLoop。一旦连接被接受,第二个EventLoopGroup 就会给它的Channel 分配一个EventLoop
EventLoop(Group):控制流、多线程处理、并发
-
EventLoop 暂时可以看成一个线程、EventLoopGroup 自然就可以看成线程组
-
EventLoop 事件循环:循环处理关心的每种事件
- 一个 EventLoop 将由一个永远都不会改变的 Thread 驱动,同时任务(Runnable 或者Callable)可以直接提交给EventLoop 实现,以立即执行或者调度执行
-
线程的分配
- 服务于 Channel 的 I/O 和事件的 EventLoop 包含在 EventLoopGroup 中
- 异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread),它们可能会被多个Channel 所共享
- EventLoopGroup 负责为每个新创建的Channel 分配一个EventLoop
- 使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布
- 并且相同的EventLoop 可能会被分配给多个Channel
- 因为一个EventLoop 通常会被用于支撑多个Channel,所以对于所有相关联的Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择
- 一旦一个Channel 被分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的Thread)
-
线程管理
-
当提交任务到如果(当前)调用线程正是支撑EventLoop 的线程,那么所提交的代码块将会被(直接)执行
-
否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中
- 当EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件
-
内置通信传输模式
- NIO(NioEventLoopGroup NioServerSocketChannel):基于选择器的方式
- Epoll(EpollEventLoopGroup EpollServerSocketChannel):由JNI 驱动的epoll()和非阻塞IO
- 这个传输支持只有在Linux上可用的多种特性,如SO_REUSEPORT,比NIO 传输更快,而且是完全非阻塞的
- OIO:使用阻塞流
- Local:可以在VM 内部通过管道进行通信的本地传输
- Embedded:允许使用ChannelHandler 而又不需要一个真正的基于网络的传输
- 在测试ChannelHandler 实现时非常有用
Channel:Socket
-
Channel 是 Java NIO 的一个基本构造
- 它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O 操作的程序组件)的开放连接,如读操作和写操作
- 可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接
-
- Channel 需要被注册到某个EventLoop 上
- 在Channel 整个生命周期内都由这个EventLoop 处理IO 事件(一个Channel 和一个EventLoop 进行了绑定)
- 但是一个EventLoop 可以同时被多个Channel 绑定
-
Channel 接口
- 基本的I/O 操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语
- 其基本的构造是类Socket
- Netty 的Channel 接口所提供的API,被用于所有的I/O 操作。大大地降低了直接使用Socket 类的复杂性
- 为了保证顺序将Channel 声明为Comparable子接口
- 如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error
- 基本的I/O 操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语
-
Channel 的生命周期状态
- ChannelUnregistered :Channel 已经被创建,但还未注册到EventLoop
- ChannelRegistered :Channel 已经被注册到了EventLoop
- ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)
- 它现在可以接收和发送数据了
- ChannelInactive :Channel 没有连接到远程节点
- 当这些状态发生改变时,将会生成对应的事件
- 这些事件将会被转发给 ChannelPipeline 中的ChannelHandler,其可以随后对它们做出响应
- 关注ChannelActive 和ChannelInactive 会更多一些
-
重要Channel 的方法
- eventLoop: 返回分配给Channel 的EventLoop
- pipeline:返回Channel 的ChannelPipeline,也就是说每个Channel 都有自己的ChannelPipeline
- isActive:如果Channel 是活动的,则返回true;活动的意义可能依赖于底层的传输
- 一个Socket 传输一旦连接到了远程节点便是活动的
- 一个Datagram 传输一旦被打开便是活动的
- localAddress:返回本地的SokcetAddress
- remoteAddress:返回远程的SocketAddress
- write:将数据写到远程节点
- 这个写只是写往Netty 内部的缓存,还没有真正写往socket
- flush:将之前已写的数据冲刷到底层socket 进行传输
- writeAndFlush:一个简便的方法,等同于调用write()并接着调用flush()
ChannelFuture:异步通知
- Netty 提供了它自己的实现 ChannelFuture,用于在执行异步操作的时候使用
- 一般来说,每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture
事件
- Netty 使用不同的事件来通知我们状态的改变或者是操作的状态
- 这使得我们能够基于已经发生的事件来触发适当的动作
- Netty 事件是按照它们与入站或出站数据流的相关性进行分类的
- 可能由入站数据或者相关的状态更改而触发的事件包括:
- 连接已被激活或者连接失活;数据读取;用户事件;错误事件
- 出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
- 打开或者关闭到远程节点的连接;将数据写到或者冲刷到套接字
- 每个事件都可以被分发给ChannelHandler 类中的某个用户实现的方法
- 既然事件分为入站和出站
- 用来处理事件的ChannelHandler 也被分为可以处理入站事件的Handler 和出站事件的Handler
- 当然有些Handler 既可以处理入站也可以处理出站
- Channel、ChannelPipeline 和ChannelHandlerContext 上的事件传播
-
ChannelHandlerContext 有一些方法也存在于Channel 和Channel-Pipeline 上,但是有一点重要的不同
- 如果调用Channel 或者ChannelPipeline 上的这些方法,它们将沿着整个ChannelPipeline 进行传播
- 而调用位于ChannelHandlerContext上的相同方法,则将从当前所关联的ChannelHandler 开始,并且只会传播给位于该ChannelPipeline 中的下一个(入站下一个,出站上一个)能够处理该事件的ChannelHandler
ChannelHandler
-
Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,也可以自行开发
- Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器
- ChannelHandler 的方法是由网络事件触发的
- ChannelHandler 可专门用于几乎任何类型的动作
-
ChannelHandler 都放在 ChannelPipeline 中统一管理
- 事件就会在ChannelPipeline 中流动,并被其中一个或者多个ChannelHandler 处理
-
ChannelHandler 的生命周期:在ChannelHandler 被添加到ChannelPipeline 中或者被从ChannelPipeline 中移除时会调用下面这些方法。这些方法中的每一个都接受一个ChannelHandlerContext 参数
- handlerAdded 当把ChannelHandler 添加到ChannelPipeline 中时被调用
- handlerRemoved 当从ChannelPipeline 中移除ChannelHandler 时被调用
- exceptionCaught 当处理过程中在ChannelPipeline 中有错误产生时被调用
-
入站和出站ChannelHandler 被安装到同一个ChannelPipeline 中,ChannelPipeline 是双向链表的形式
-
- 如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,但是只被处理入站事件的Handler 处理
- 数据的出站运动(即正在被写的数据)在概念上也是一样的
-
Netty 能区分入站事件的Handler 和出站事件的Handler,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递
-
分属出站和入站不同的Handler ,在业务没特殊要求的情况下是无所谓顺序的
-
而同属一个方向的Handler 则是有顺序的
- 上一个Handler 处理的结果往往是下一个Handler 的要求的输入
-
-
ChannelHandler 接口:这些方法和Channel的生命周期密切相关
- 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 状态发生改变时被调用
- ChannelOutboundHandler 接口处理出站数据并且允许拦截所有的操作
- bind 当请求将Channel 绑定到本地地址时被调用
- connect 当请求将Channel 连接到远程节点时被调用
- disconnect 当请求将Channel 从远程节点断开时被调用
- close 当请求关闭Channel 时被调用
- deregister 当请求将Channel 从它的EventLoop 注销时被调用
- read 当请求从Channel 读取更多的数据时被调用
- flush 当请求通过Channel 将入队数据冲刷到远程节点时被调用
- write 当请求通过Channel 将数据写到远程节点时被调用
- 处理出站操作和数据。它的方法将被Channel、ChannelPipeline 以及ChannelHandlerContext 调用
- ChannelInboundHandler 接口处理入站数据以及各种状态变化
-
ChannelHandler 的适配器
-
ChannelInboundHandlerAdapter(处理入站)
-
ChannelOutboundHandlerAdapter(处理出站)
- read 方法不是表示读数据,而是表示业务发出了读(read)数据的要求
- 这个要求也会封装为一个事件进行传播,这个事件因为是业务发出到网络的,自然就是个出站事件
- read 方法不是表示读数据,而是表示业务发出了读(read)数据的要求
-
ChannelDuplexHandler(既要处理入站又要处理出站)
-
ChannelInitializer:ChannelInboundHandlerAdapter 子类
- initChannel 提供了一种将多个ChannelHandler 添加到一个ChannelPipeline 中的简便方法
- 只需要简单地向 Bootstrap 或 ServerBootstrap 的实例提供你的 ChannelInitializer 实现即可
- 并且一旦Channel 被注册到了它的EventLoop 之后,就会调用你的 initChannel()
- 在该方法返回之后,ChannelInitializer 的实例将会从ChannelPipeline 中移除它自己
- 在我们自己的应用程序中,如果存在着某个handler 只使用一次的情况,也可以仿造ChannelInitializer,用完以后将自己从ChannelPipeline 中移除自己,比如授权handler
-
Handler 的共享和并发安全性:
- 每个socketChannel 有自己的pipeline 而且每个socketChannel 又是和线程绑定的,所以这些Handler 的实例之间完全独立的,只要Handler 的实例之间不是共享了全局变量, Handler 的实例是线程安全的
- 但是如果业务需要我们在多个socketChannel 之间共享一个Handler 的实例
- ChannelHandlerAdapter 还提供了实用方法 isSharable()
- 如果其对应的实现被标注为
@Sharable
,那么这个方法将返回 true,表示它可以被添加到多个ChannelPipeline
-
资源管理和 SimpleChannelInboundHandler:会在数据被channelRead0()方法消费之后自动释放数据
- Netty 在处理网络数据时,同样也需要 Buffer:在Read 网络数据时由Netty 创建Buffer;Write 网络数据时Buffer 往往是由业务方创建的
- Buffer 用完后都必须进行释放,否则可能会造成内存泄露
- 在Write 网络数据时,可以确保数据被写往网络了,Netty 会自动进行Buffer 的释放,但是如果Write 网络数据时,我们有outBoundHandler 处理了write()操作并丢弃了数据,没有继续往下写,要由我们负责释放这个Buffer,就必须调用ReferenceCountUtil.release 方法,否则就可能会造成内存泄露
- 在Read 网络数据时,如果我们可以确保每个InboundHandler 都把数据往后传递了,也就是调用了相关的fireChannelRead 方法,Netty 也会帮我们释放,同样的,如果我们有InboundHandler 处理了数据,又不继续往后传递,又不调用负责释放的ReferenceCountUtil.release 方法,就可能会造成内存泄露
- 自行在编写业务Handler 时,也需要注意这一点:要么继续传递,要么自行释放
- Netty 在处理网络数据时,同样也需要 Buffer:在Read 网络数据时由Netty 创建Buffer;Write 网络数据时Buffer 往往是由业务方创建的
ChannelPipeline
- 当Channel 被创建时,它将会被自动地分配一个新的ChannelPipeline,每个Channel 都有自己的ChannelPipeline
- 这项关联是永久性的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预
- ChannelPipeline 提供了ChannelHandler 链的容器
-
并定义了用于在该链上传播入站(也就是从网络到业务处理)和出站(也就是从业务处理到网络)各种事件流的API,我们代码中的ChannelHandler 都是放在ChannelPipeline 中的
-
- 使得事件流经ChannelPipeline 是ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的
- 这些ChannelHandler 对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler
- 而且ChannelHandler 对象也完全可以拦截事件不让事件继续传递
- 它们的执行顺序是由它们被添加的顺序所决定的
-
- ChannelPipeline 上的方法:
- addFirst、addBefore、addAfter、addLast
- remove 将一个ChannelHandler 从ChannelPipeline 中移除
- replace 将ChannelPipeline 中的一个ChannelHandler 替换为另一个ChannelHandler
- get 通过类型或者名称返回ChannelHandler
- context 返回和ChannelHandler 绑定的ChannelHandlerContext
- names 返回ChannelPipeline 中所有ChannelHandler 的名称
ChannelHandlerContext
-
ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联
- 每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandlerContext
- ChannelHandlerContext 的主要作用就和LinkedList 内部的类Node 类似
-
不过ChannelHandlerContext 不仅仅只是个包装类,它还提供了很多的方法
- 比如让事件从当前ChannelHandler 传递给链中的下一个ChannelHandler
- 还可以被用于获取底层的Channel
- 还可以用于写出站数据
-
ChannelHandlerContext 的API
- alloc 返回和这个实例相关联的Channel 所配置的ByteBufAllocator
- bind 绑定到给定的SocketAddress,并返回ChannelFuture
- channel 返回绑定到这个实例的Channel
- close 关闭Channel,并返回ChannelFuture
- connect 连接给定的SocketAddress,并返回ChannelFuture
- deregister 从之前分配的EventExecutor 注销,并返回ChannelFuture
- disconnect 从远程节点断开,并返回ChannelFuture
- executor 返回调度事件的EventExecutor
- fireChannelActive fireChannelInactive fireChannelRead fireChannelReadComplete fireChannelRegistered fireChannelUnregistered fireChannelWritabilityChanged fireExceptionCaught fireUserEventTriggered
- 触发对下一个ChannelInboundHandler 上的特定方法
- handler 返回绑定到这个实例的ChannelHandler
- isRemoved 如果所关联的ChannelHandler 已经被从ChannelPipeline 中移除则返回true
- name 返回这个实例的唯一名称
- pipeline 返回这个实例所关联的ChannelPipeline
- read 将数据从Channel 读取到第一个入站缓冲区
- 如果读取成功则触发一个channelRead 事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler 的channelReadComplete(ctx)方法
- write 通过这个实例写入消息并经过ChannelPipeline
- writeAndFlush 通过这个实例写入并冲刷消息并经过ChannelPipeline
-
ChannelHandlerContext和ChannelHandler之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的
-
相对于其他类的同名方法,ChannelHandlerContext 的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能
ChannelOption
- ChannelOption.SO_BACKLOG 对应的是tcp/ip 协议listen 函数中的backlog 参数
- 操作系统里一般有两个队列
- ACCEPT 队列,保存着已经完成了TCP 的三次握手的连接
- SYN 队列,服务器正在等待TCP 的三次握手完成的队列
- BSD 派生系统里backlog 指的就是SYN 队列的大小
- 在Linux 的实现里backlog 相对来说,从Linux 2.2 开始,指的是 ACCEPT 队列的长度
- SYN 队列可以使用
/proc/sys/net/ipv4/tcp_max_syn_backlog
设置,默认值为128
- SYN 队列可以使用
- 如果 backlog 参数大于
/proc/sys/net/core/somaxconn
中的值,那么它会被静默截断为值128/etc/sysctl.conf
修改这个默认值,包括tcp_max_syn_backlog
也可以在此处修改sysctl -p
生效
- 操作系统里一般有两个队列
- ChannelOption.SO_REUSEADDR 对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口
- 比如,多网卡(IP)绑定相同端口,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR 就无法正常使用该端口
- 这个参数无法做到让应用绑定完全相同IP + Port 来重复启动
- ChannelOption.SO_KEEPALIVE 应于套接字选项中的SO_KEEPALIVE
- 该参数用于设置TCP 连接,当设置该选项以后,连接会测试链接的状态
- 这个选项用于可能长时间没有数据交流的连接
- 当设置该选项以后,如果在两小时内没有数据的通信时,TCP 会自动发送一个活动探测数据报文
- ChannelOption.SO_SNDBUF 对应于套接字选项中的SO_SNDBUF
- 用于操作发送缓冲区的大小
- 发送缓冲区用于保存发送数据,直到发送成功
- ChannelOption.SO_RCVBUF 对应于套接字选项中的SO_RCVBUF
- 用于操作接收缓冲区的大小
- 接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功
- ChannelOption.SO_LINGER 对应于套接字选项中的SO_LINGER
- Linux 内核默认的处理方式是当用户调用close方法的时候,函数返回
- 在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性
- 使用SO_LINGER 可以阻塞close()的调用时间,直到数据完全发送
- Linux 内核默认的处理方式是当用户调用close方法的时候,函数返回
- ChannelOption.TCP_NODELAY 对应于套接字选项中的TCP_NODELAY
- 该参数的使用与Nagle 算法有关,Nagle 算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次
- 因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送
- 虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle 算法,使用于小数据即时传输
- ChannelOption.TCP_CORK 对应于套接字选项中的TCP_CORK
- 该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输
ByteBuf
- 优点
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长
- 在读和写这两种模式之间切换不需要调用ByteBuffer 的flip()方法
- 读和写使用了不同的索引
- 名称以read 或者write 开头的ByteBuf 方法,将会推进其对应的索引
- 而名称以set 或者get 开头的操作则不会
- 如果打算读取字节直到readerIndex 达到和writerIndex会触发一个IndexOutOf-BoundsException
- 可以指定ByteBuf 的最大容量(默认的限制是Integer.MAX_VALUE)
- 试图移动写索引(即writerIndex)超过这个值将会触发一个异常
- 支持方法的链式调用
- 支持引用计数
- 支持池化
- 使用模式
- 堆缓冲区:最常用的ByteBuf 模式是将数据存储在JVM 的堆空间中
- 这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放
- 可以由hasArray()来判断检查ByteBuf 是否由数组支撑
- 如果不是,则这是一个直接缓冲区
- 直接缓冲区
- 直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵
- 复合缓冲区:它为多个ByteBuf 提供一个聚合视图
- 比如HTTP 协议,分为消息头和消息体,这两部分可能由应用程序的不同模块产生,各有各的ByteBuf,将会在消息被发送的时候组装为一个ByteBuf,此时可以将这两个ByteBuf 聚合为一个CompositeByteBuf,然后使用统一和通用的ByteBuf API 来操作
- 堆缓冲区:最常用的ByteBuf 模式是将数据存储在JVM 的堆空间中
- 分配
- ByteBufAllocator 接口
- buffer() 返回一个基于堆或者直接内存存储的ByteBuf
- heapBuffer() 返回一个基于堆内存存储的ByteBuf
- directBuffer() 返回一个基于直接内存存储的ByteBuf
- compositeBuffer() 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf
- ioBuffer() 返回一个用于套接字的I/O 操作的ByteBuf
- 当所运行的环境具有sun.misc.Unsafe 支持时,返回基于直接内存存储的ByteBuf
- 否则返回基于堆内存存储的ByteBuf
- 当指定使用PreferHeapByteBufAllocator 时,则只会返回基于堆内存存储的ByteBuf
- 可以通过Channel(每个都可以有一个不同的ByteBufAllocator 实例)或者绑定到ChannelHandler 的ChannelHandlerContext 获取一个到ByteBufAllocator 的引用
- Netty 提供了两种ByteBufAllocator 的实现
- PooledByteBufAllocator(默认)
- 池化了ByteBuf 的实例以提高性能并最大限度地减少内存碎片
- Unpooled-ByteBufAllocator
- 实现不池化ByteBuf 实例,并且在每次它被调用时都会返回一个新的实例
- PooledByteBufAllocator(默认)
- ByteBufAllocator 接口
- Unpooled 缓冲区:Unpooled 工具类提供了静态的辅助方法来创建未池化的ByteBuf 实例
- buffer() 返回一个未池化的基于堆内存存储的ByteBuf
- directBuffer()返回一个未池化的基于直接内存存储的ByteBuf
- wrappedBuffer() 返回一个包装了给定数据的ByteBuf
- copiedBuffer() 返回一个复制了给定数据的ByteBuf
- 随机访问索引:使用那些需要一个索引值参数的方法来访问数据既不会改变readerIndex 也不会改变writerIndex
- 可以通过调用readerIndex(index)或者writerIndex(index)来手动移动这两者
- 顺序访问索引:
- get()和set()操作,从给定的索引开始,并且保持索引不变
- get+数据字长(bool.byte,int,short,long,bytes)
- read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整
- get()和set()操作,从给定的索引开始,并且保持索引不变
- 读写操作
- isReadable() 如果至少有一个字节可供读取,则返回true
- isWritable() 如果至少有一个字节可被写入,则返回true
- readableBytes() 返回可被读取的字节数
- writableBytes() 返回可被写入的字节数
- capacity() 返回ByteBuf 可容纳的字节数
- 在此之后,它会尝试再次扩展直到达到 maxCapacity()
- maxCapacity() 返回ByteBuf 可以容纳的最大字节数
- hasArray() 如果ByteBuf 由一个字节数组支撑,则返回true
- array() 如果ByteBuf 由一个字节数组支撑则返回该数组
- 否则,它将抛出一个UnsupportedOperationException 异常
- 可丢弃字节
-
可丢弃字节的分段包含了已经被读过的字节
-
通过调用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
- 这并不会清除内存中的内容
- 查找操作:用来确定指定值的索引的方法
- indexOf()
- forEachByte()
ByteBuf buffer = .. .;
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
- 派生缓冲区:其内部存储和 JDK 的ByteBuffer 一样也是共享的
- 派生缓冲区为ByteBuf 提供了以专门的方式来呈现其内容的视图
- 创建方法
- duplicate();slice();slice(int, int);Unpooled.unmodifiableBuffer(…);order(ByteOrder);readSlice(int)
- 每个这些方法都将返回一个新的ByteBuf 实例,它具有自己的读索引、写索引和标记索引
- 真实副本:不同于派生缓冲区,由这个调用所返回的ByteBuf 拥有独立的数据副本
- ByteBuf 复制如果需要一个现有缓冲区的真实副本,请使用copy()或者copy(int, int)方法
- 引用计数:通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能
- 工具类:ByteBufUtil 提供了用于操作ByteBuf 的静态的辅助方法
- hexdump()方法,它以十六进制的表示形式打印ByteBuf 的内容
boolean equals(ByteBuf, ByteBuf)
用来判断两个ByteBuf 实例的相等性
- 资源释放
- 当某个ChannelInboundHandler 的实现重写channelRead()方法时,它要负责显式地释放与池化的ByteBuf 实例相关的内存
ReferenceCountUtil.release()
- Netty 将使用WARN 级别的日志消息记录未释放的资源
- 更加简单的方式是使用SimpleChannelInboundHandler自动释放资源
- 对于入站请求,Netty 的EventLoop 在处理Channel 的读操作时进行分配ByteBuf(需要我们自行进行释放)
- 使用SimpleChannelInboundHandler
- 在重写channelRead()方法使用ReferenceCountUtil.release()
- 在重写channelRead()方法使用使用ctx.fireChannelRead 继续向后传递
- 对于出站请求,不管ByteBuf 是否由我们的业务创建的,当调用了write 或者writeAndFlush 方法后,Netty 会自动替我们释放,不需要我们业务代码自行释放
- 当某个ChannelInboundHandler 的实现重写channelRead()方法时,它要负责显式地释放与池化的ByteBuf 实例相关的内存