【Netty】模块组件 详解
在之前的博文中,本人讲解了 什么是Netty
,Netty
的基本使用
那么,在本篇博文中,本人就来讲解下 Netty
的核心组件:
首先是 启动对象 —— Bootstrap
与 ServerBootstrap
:
Bootstrap 与 ServerBootstrap:
Bootstrap
意思是 引导,一个 Netty
应用通常由一个 Bootstrap 开始,
主要作用 是:
配置整个 Netty 程序,串联各个组件
Netty
中:
- Bootstrap类 是 客户端程序的启动引导类
- ServerBootstrap 是服务端程序的启动引导类
方法 | 功能 |
---|---|
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) | 用于 服务器端 用来设置 两个EventLoop |
public B group(EventLoopGroup group) | 用于 客户端 用来设置 一个EventLoopGroup |
public B channel(Class<? extends C channelClass) | 用来设置 一个 服务器端的通道实现 |
public |
用来给 ServerChannel 添加配置 |
public |
用来给 接收到的通道 添加配置 |
public ServerBootstrap childHandler(ChannelHandler childHandler) | 用来设置 业务处理类 (自定义的handler) |
public ChannelFuture bind(int inetPort) | 用于 服务器端 用来设置 占用的端口号 |
public ChannelFuture connect(String inetHost, int inetPort) | 用于 客户端 用来 连接服务器 |
Future 与 ChannelFuture:
正如前面介绍,在 Netty
中所有的 IO 操作 都是 异步 的,不能立刻 得知 消息 是否被 正确处理
但是可以 过一会等它执行完成 或者 直接注册一个监听,具体实现 就是通过 Future
和 ChannelFuture
,
它们可以注册一个 监听器,当 操作执行成功或失败 时,监听器 会自动 触发 注册的监听事件
方法 | 功能 |
---|---|
Channel channel() | 返回 当前正在进行操作的通道 |
ChannelFuturesync() | 同步化 当前操作 等待 异步操作执行完毕 |
Channel:
用于 执行网络 I/O 操作
Channel 为用户提供:
- 1)当前网络连接的 通道的状态(例如:是否打开?是否已连接?)
- 2)网络连接 的 配置参数 (例如接收缓冲区大小)
- 3)提供 异步的网络 I/O 操作(如:建立连接、读写、绑定端口)
异步调用 意味着 任何 I/O 调用 都将 立即返回,
并且 不保证 在 调用结束 时 所请求的 I/O 操作 已完成- 4)调用立即返回 一个 ChannelFuture 实例,通过 注册 监听器 到 ChannelFuture 上,可以在 I/O 操作 成功、失败 或 取消 时回调 通知调用方法
- 5)支持 关联 I/O 操作 与 对应的处理程序
不同协议、不同的阻塞类型 的连接都有 不同的Channel类型 与之对应。
下面是一些 常用的 Channel 类型:
这些通道涵盖了 UDP
和 TCP
等 网络IO 以及 文件IO
Selector:
Netty
基于 Selector对象 实现 I/O 多路复用,
通过 Selector,一个线程 可以监听 多个连接 的 Channel事件:
当向一个 Selector 中注册 Channel 后,
Selector 内部的机制 就可以 自动不断地查询(Select) 这些 注册的 Channel 是否有 已就绪的 I/O事件
(例如 可读,可写,网络连接完成 等)
这样程序就可以很简单地使用 一个线程 高效地管理 多个 Channel
NioEventLoop:
NioEventLoop
中维护了一个 线程 和 任务队列,支持 异步 提交执行任务,
线程启动时,会调用 NioEventLoop 的 run()方法,执行 I/O 任务 和 非 I/O 任务:
I/O任务
,即 selectionKey 中 ready 的事件,如:accept、connect、read、write 等
由 processSelectedKeys()方法 触发
非 I/O任务
,添加到 taskQueue 中的任务,如:register0、bind0 等任务
由 runAllTasks()方法 触发
NioEventLoopGroup:
用于 管理 eventLoop
的 生命周期
,
可以理解为一个线程池,内部维护了一组线程,
每个线程(NioEventLoop) 负责处理 多个Channel 上的事件,
而 一个Channel 只对应 一个线程
方法 | 功能 |
---|---|
public NioEventLoopGroup() | 创建获取 NioEventLoopGroup对象 |
public Future<?> shutdownGracefully() | 断开连接,关闭线程 |
ChannelHandler:
用于 处理 I/O事件 或 拦截 I/O操作,并将其 转发到其 ChannelPipeline(业务处理链) 中的 下一个处理程序(责任链模式
)
ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,
因此,我们可以继承它的子类:
- ChannelInboundHandler —— 用于处理 入站 I/O事件
- ChannelOutboundHandler —— 用于处理 出站 I/O操作
或者使用以下 适配器类:
- ChannelInboundHandlerAdapter —— 用于处理 入站 I/O事件
- ChannelOutboundHandlerAdapter —— 用于处理 出站 I/O操作
ChannelHandlerContext:
保存 Channel 相关的所有 上下文信息,同时关联一个 ChannelHandler 对象
方法 | 功能 |
---|---|
ChannelFuture close() | 关闭通道 |
ChannelOutboundInvoker flush() | 刷新 |
ChannelFuture writeAndFlush(Object msg) | 将 数据msg 写到 ChannelPipeline 中 的 当前ChannelHandler的下一个ChannelHandler 开始处理(出站) |
ChannelOption:
- Netty在创建Channel实例后,一 般都需要设置ChannelOption参数。
- ChannelOption 参数如下:
通用配置:
参数 | 意义 |
---|---|
CONNECT_TIMEOUT_MILLIS |
连接超时 毫秒数 默认值:30000毫秒,即30秒 |
MAX_MESSAGES_PER_READ |
一次Loop 读取的 最大消息数 对于ServerChannel 或者 NioByteChannel,默认值 为16 其他Channel 默认值 为 1 因为:ServerChannel需要接受 足够多的连接,保证 大吞吐量 NioByteChannel可以 减少不必要的系统调用select |
WRITE_SPIN_COUNT |
一个Loop写操作 执行的 最大次数 默认值 为 16 也就是说,对于大数据量的写操作至多进行16次 如果 16次 仍没有全部写完数据,此时会提交一个 新的写任务 给 EventLoop,任务将在 下次调度 继续执行 这样,其他的写请求 才能被响应不会因为 单个大数据量写请求 而 耽误 |
ALLOCATOR |
ByteBuf的分配器 默认值:ByteBufAllocator.DEFAULT,4.0版本 为 UnpooledByteBufAllocator 4.1版本 为 PooledByteBufAllocator 该值也可以使用 系统参数io.netty.allocator.type 配置,使用字符串值:" unpooled ","pooled " |
RCVBUF_ALLOCATOR |
用于Channel分配接受Buffer的分配器 默认值 为 AdaptiveRecvByteBufAllocator.DEFAULT 是一个 自适应 的 接受缓冲区分配器,能根据 接受到的数据 来 自动调节大小 可选值为 FixedRecvByteBufAllocator,固定大小 的接受缓冲区分配器 |
AUTO_READ |
自动读取 默认值 为 True Netty只在 必要时候 才 设置 关心 相应的I/O事件 对于 读操作,需要调用 channel.read() 设置关心的I/O事件为 OP_READ,这样若有数据到达才能读取以供用户处理 该值为True时,每次 读操作完毕后 会自动 调用channel.read(),从而有数据到达便能读取 否则,需要用户手动调用 channel.read() 需要注意的是:当调用config.setAutoRead(boolean)方法 时,如果状态由 false 变为 true,将会调用 channel.read()方法读取数据; 由true变为 false,将调用config.autoReadCleared()方法 终止数据读取 |
WRITE_BUFFER_HIGH_WATER_MARK |
写高水位标记 默认值64KB 如果Netty的 写缓冲区 中的字节超过该值,Channel 的 isWritable() 返回 False |
WRITE_BUFFER_LOW_WATER_MARK |
写低水位标记 默认值32KB 当Netty的 写缓冲区 中的字节超过高水位之后若下降到低水位,则Channel的 isWritable() 返回 True 写高低水位标记使用户可以控制 写入数据速度,从而实现 流量控制 推荐做法是:每次调用 channl.write(msg)方法 首先调用channel.isWritable()判断 是否可写 |
MESSAGE_SIZE_ESTIMATOR |
消息大小估算器 默认 为 DefaultMessageSizeEstimator.DEFAULT 估算 ByteBuf、ByteBufHolder 和 FileRegion 的 大小 其中 ByteBuf 和 ByteBufHolder 为 实际大小 FileRegion 估算值为 0 该值估算的字节数在 计算水位 时使用 因此:FileRegion为0,可知 FileRegion不影响高低水位 |
SINGLE_EVENTEXECUTOR_PER_GROUP |
单线程 执行ChannelPipeline中的事件 默认值 为 True 该值控制执行ChannelPipeline中执行ChannelHandler的线程 如果为 True,整个pipeline 由 一个线程 执行,这样不需要进行 线程切换 以及 线程同步,是 Netty4 的推荐做法;如果为 False,ChannelHandler中的处理过程会由 Group中的不同线程 执行 |
配置 SocketChannel:
参数 | 意义 |
---|---|
SO_RCVBUF |
TCP数据 接收 缓冲区大小 该缓冲区 即 TCP接收滑动窗口,linux操作系统可使用命令: cat /proc/sys/net/ipv4/tcp_rmem 查询其 大小一般情况下,该值可由用户在 任意时刻 设置 但当设置值 超过64KB 时,需要在 连接到远端之前 设置 |
SO_SNDBUF |
TCP数据 发送 缓冲区大小 该缓冲区 即 TCP发送滑动窗口,linux操作系统可使用命令: cat /proc/sys/net/ipv4/tcp_smem 查询其 大小 |
TCP_NODELAY |
立即发送数据 默认值 为 Ture (Netty 默认为 True,而 操作系统 默认为 False) 该值设置 Nagle算法 的启用,该算法将 小的碎片数据 连接成 更大的报文 来 最小化 所发送的报文 的数量, 如果需要发送一些 较小的报文,则需要 禁用该算法 Netty 默认 禁用该算法,从而最小化 报文传输延时 |
SO_KEEPALIVE |
一直保持 连接活动状态 默认为 false |
SO_REUSEADDR |
地址复用 默认值 为 False 有 四种情况 可以使用: (1)当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你希望启动的程序的socket2要占用该地址和端口,比如重启服务且保持先前端口 (2)有多块网卡或用IP Alias技术的机器在同一端口启动多个进程,但每个进程绑定的本地IP地址不能相同 (3)单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同 (4)完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP |
SO_LINGER |
Netty对底层Socket参数的简单封装,关闭Socket的延迟时间 默认值 为 -1,表示 禁用该功能 -1 以及 所有<0的数 表示 socket.close()方法 立即返回,但 OS底层 会将 发送缓冲区 全部发送 到 对端 0 表示 socket.close()方法 立即返回,OS底层 会 放弃发送缓冲区的数据,直接 向对端发送 RST包,对端收到 复位错误 非0整数值 表示调用 socket.close()方法 的线程被 阻塞 直到 延迟时间到 或 发送缓冲区中的数据发送完毕;若超时,则对端会收到 复位错误 |
IP_TOS |
设置 IP头部 的 Type-of-Service字段,用于描述 IP包的优先级 和 QoS选项 |
ALLOW_HALF_CLOSURE |
一个连接 的 远端关闭时,本地端是否关闭 默认值为 False 值为 False时,连接自动关闭 为 True 时,触发 ChannelInboundHandler 的 userEventTriggered()方法,事件 为 ChannelInputShutdownEvent |
配置 ServerSocketChannel:
参数 | 意义 |
---|---|
SO_RCVBUF |
见上文 |
SO_REUSEADDR |
见上文 |
SO_BACKLOG |
对应 TCP/IP协议 的 listen函数 中的 backlog参数,用来 初始化服务器可连接队列大小 服务端处理客户端连接请求是 顺序处理 的,所以 同一时间 只能处理 一个客户端连接 多个客户端 来的时候,服务端将 不能处理的客户端连接请求 放在 可连接队列 中 等待处理 默认 为 Windows为200,其他为128 |
配置 DatagramChannel:
参数 | 意义 |
---|---|
SO_BROADCAST |
设置 广播模式 |
SO_RCVBUF |
见上文 |
SO_SNDBUF |
见上文 |
SO_REUSEADDR |
见上文 |
IP_MULTICAST_LOOP_DISABLED |
对应 IP参数 IP_MULTICAST_LOOP ,设置 本地回环接口 的 多播功能由于 IP_MULTICAST_LOOP 返回 True 表示 关闭所以Netty加上 后缀_DISABLED 防止歧义 |
IP_MULTICAST_ADDR |
对应 IP参数 IP_MULTICAST_IF 设置对应地址的网卡为 多播模式 |
IP_MULTICAST_IF |
对应IP参数 IP_MULTICAST_IF2 ,同上但支持IPV6 |
IP_MULTICAST_TTL |
多播数据报的 time-to-live 即 存活跳数 |
IP_TOS |
见上文 |
DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION |
DatagramChannel 注册的 EventLoop,即表示:已激活 |
ChannelPipline:
保存 ChannelHandler 的 List集合,用于 处理 或 拦截 Channel 的 入站事件 和 出站操作(责任链模式
处理)
ChannelPipeline 实现了一种高级形式的 拦截过滤器模式,使用户可以完全控制 事件的处理方式,以及 Channel 中的各个 ChannelHandler 如何 相互交互
在 Netty
中 每个Channel 都 有且仅有一个 ChannelPipeline 与之对应
它们的组成关系如下:
- 一个Channel 包含了 一个 ChannelPipeline,
- 而 ChannelPipeline 中又维护了 一个由 ChannelHandlerContext 组成的双向链表,并且 每个ChannelHandlerContext 中又关联着 一个 ChannelHandler
- read事件(入站事件) 和 write事件(出站事件) 在 一个双向链表 中,
- 入站事件 会从 链表 head 往后传递到 最后一个入站 的 handler,
- 出站事件 会从 链表 tail 往前传递到 最前一个出站 的 handler,
- 两种类型的 handler 互不干扰
如下图:
Unpooled:
描述:
Netty提供一 个专门用来 操作缓冲区(即:Netty的数据容器) 的工具类
常用API:
方法 | 功能 |
---|---|
public static ByteBuf buffer(int initialCapacity) | 创建一个大小为 参数initialCapacity 的 字节缓冲区 |
public static ByteBuf copiedBuffer(CharSequence string, Charset charset) | 通过 参数 数据 和 参数 字符编码 返回一个 ByteBuf 对象( ByteBuf 和 NIO中的 ByteBuff 很类似) |
ByteBuf:
简介:
从结构上来说,ByteBuf 由 一串字节数组 构成,数组中每个字节用于存放信息。
ByteBuf 提供了 两个索引,一个用于 读取数据,一个用于 写入数据。
(这里和本人之前讲解的 Buffer
很相似,但是这里 读写指针 是 分开 的)
这两个索引通过在字节数组中移动,来定位需要 读或者写信息 的 位置
如下图:
当从 ByteBuf 读取时,它的 readerIndex(读索引) 将会根据 读取的字节数 递增
同样,当 写 ByteBuf 时,它的 writerIndex(写索引) 会根据 写入的字节数 递增
需要注意的是情况是:
readerIndex 刚好读到了 writerIndex 写入的地方。
如果 readerIndex 超过了 writerIndex 的时候,会抛出IndexOutOf-BoundsException
异常。
那么,本人来展示下 ByteBuf类
的使用:
使用展示:
package edu.youzg.demo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
public class NettyByteBufDemo {
public static void main(String[] args) {
// 创建 byteBuf对象,该对象内部包含一个 字节数组byte[10]
// 通过 readerindex 和 writerIndex 和 capacity,将buffer分成 三个区域
// 已经读取的区域:[0,readerindex)
// 可读取的区域:[readerindex,writerIndex)
// 可写的区域: [writerIndex,capacity)
ByteBuf byteBuf = Unpooled.buffer(10);
System.out.println("byteBuf=" + byteBuf);
for (int i = 0; i < 8; i++) {
byteBuf.writeByte(i);
}
System.out.println("byteBuf=" + byteBuf);
for (int i = 0; i < 5; i++) {
System.out.println(byteBuf.getByte(i));
}
System.out.println("byteBuf=" + byteBuf);
for (int i = 0; i < 5; i++) {
System.out.println(byteBuf.readByte());
}
System.out.println("byteBuf=" + byteBuf);
//用Unpooled工具类创建ByteBuf
ByteBuf byteBuf2 = Unpooled.copiedBuffer("hello,Youzg!", CharsetUtil.UTF_8);
//使用相关的方法
if (byteBuf2.hasArray()) {
byte[] content = byteBuf2.array();
//将 content 转成字符串
System.out.println(new String(content, CharsetUtil.UTF_8));
System.out.println("byteBuf2=" + byteBuf2);
System.out.println(byteBuf2.getByte(0)); // 获取数组0这个位置的字符h的ascii码,h=104
int len = byteBuf2.readableBytes(); //可读的字节数 12
System.out.println("len=" + len);
//使用for取出各个字节
for (int i = 0; i < len; i++) {
System.out.println((char) byteBuf2.getByte(i));
}
//范围读取
System.out.println(byteBuf2.getCharSequence(0, 6, CharsetUtil.UTF_8));
System.out.println(byteBuf2.getCharSequence(6, 6, CharsetUtil.UTF_8));
}
}
}
现在,本人来展示下 运行结果:
我们可以看到:
随着我们向 缓冲区 中,
写入 或 读取 数据时,读指针 和 写指针 就会相对地 后移