【Netty】模块组件 详解

shadowLogo

在之前的博文中,本人讲解了 什么是NettyNetty的基本使用
那么,在本篇博文中,本人就来讲解下 Netty核心组件


首先是 启动对象 —— BootstrapServerBootstrap

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 B option(ChannelOption option, T value) 用来给 ServerChannel 添加配置
public ServerBootstrap childOption(ChannelOption childOption, Tvalue) 用来给 接收到的通道 添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler) 用来设置 业务处理类
(自定义的handler)
public ChannelFuture bind(int inetPort) 用于 服务器端
用来设置 占用的端口号
public ChannelFuture connect(String inetHost, int inetPort) 用于 客户端
用来 连接服务器

Future 与 ChannelFuture:

正如前面介绍,在 Netty 中所有的 IO 操作 都是 异步 的,不能立刻 得知 消息 是否被 正确处理
但是可以 过一会等它执行完成 或者 直接注册一个监听具体实现 就是通过 FutureChannelFuture
它们可以注册一个 监听器,当 操作执行成功或失败 时,监听器 会自动 触发 注册的监听事件

方法 功能
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 类型

  • NioSocketChannel —— 异步的客户端 TCP Socket 连接
  • NioServerSocketChannel —— 异步的服务端 TCP Socket 连接
  • NioDatagramChannel —— 异步的 UDP 连接
  • NioSctpChannel —— 异步的 客户端 Sctp 连接
  • NioSctpServerChannel —— 异步的 服务端 Sctp 连接

这些通道涵盖了 UDPTCP 等 网络IO 以及 文件IO


Selector:

Netty 基于 Selector对象 实现 I/O 多路复用
通过 Selector,一个线程 可以监听 多个连接 的 Channel事件

当向一个 Selector 中注册 Channel 后,
Selector 内部的机制 就可以 自动不断地查询(Select) 这些 注册的 Channel 是否有 已就绪的 I/O事件
(例如 可读,可写,网络连接完成 等)

这样程序就可以很简单地使用 一个线程 高效地管理 多个 Channel


NioEventLoop:

NioEventLoop 中维护了一个 线程任务队列,支持 异步 提交执行任务,
线程启动时,会调用 NioEventLooprun()方法,执行 I/O 任务非 I/O 任务

  • I/O任务,即 selectionKeyready 的事件,如:

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:

  1. Netty在创建Channel实例后,一 般都需要设置ChannelOption参数。
  2. 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.DEFAULT4.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的 写缓冲区 中的字节超过该值,ChannelisWritable() 返回 False
WRITE_BUFFER_LOW_WATER_MARK 写低水位标记
默认值32KB
当Netty的 写缓冲区 中的字节超过高水位之后若下降到低水位,则ChannelisWritable() 返回 True
写高低水位标记使用户可以控制 写入数据速度,从而实现 流量控制
推荐做法是:每次调用 channl.write(msg)方法 首先调用channel.isWritable()判断 是否可写
MESSAGE_SIZE_ESTIMATOR 消息大小估算器
默认DefaultMessageSizeEstimator.DEFAULT
估算 ByteBufByteBufHolderFileRegion大小
其中 ByteBufByteBufHolder实际大小
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 时,触发 ChannelInboundHandleruserEventTriggered()方法事件ChannelInputShutdownEvent

配置 ServerSocketChannel:

参数 意义
SO_RCVBUF 见上文
SO_REUSEADDR 见上文
SO_BACKLOG 对应 TCP/IP协议listen函数 中的 backlog参数,用来 初始化服务器可连接队列大小
服务端处理客户端连接请求是 顺序处理 的,所以 同一时间 只能处理 一个客户端连接
多个客户端 来的时候,服务端将 不能处理的客户端连接请求 放在 可连接队列 中 等待处理
默认 为 Windows200其他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:

保存 ChannelHandlerList集合,用于 处理拦截 Channel 的 入站事件出站操作(责任链模式 处理)

ChannelPipeline 实现了一种高级形式的 拦截过滤器模式,使用户可以完全控制 事件的处理方式,以及 Channel 中的各个 ChannelHandler 如何 相互交互

Netty每个Channel有且仅有一个 ChannelPipeline 与之对应

它们的组成关系如下:

  • 一个Channel 包含了 一个 ChannelPipeline
  • ChannelPipeline 中又维护了 一个由 ChannelHandlerContext 组成的双向链表,并且 每个ChannelHandlerContext 中又关联着 一个 ChannelHandler
  • read事件(入站事件)write事件(出站事件)一个双向链表 中,
  • 入站事件 会从 链表 head 往后传递到 最后一个入站 的 handler,
  • 出站事件 会从 链表 tail 往前传递到 最前一个出站 的 handler,
  • 两种类型的 handler 互不干扰

如下图:
model


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

当从 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));
        }
    }

}

现在,本人来展示下 运行结果
结果2

运行结果
我们可以看到:

随着我们向 缓冲区 中,
写入读取 数据时,读指针写指针 就会相对地 后移


posted @ 2021-05-04 17:34  在下右转,有何贵干  阅读(234)  评论(0编辑  收藏  举报