栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

服务器之Netty分析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

服务器之Netty分析

目录

一、Netty的基本介绍

二、Netty的编解码拦截器;

三、Netty的ByteBuf;

四、Netty的心跳检测

五、Netty的Future和Promise


一、Netty的基本介绍
  • Boss Thread:
    • NioEventLoop 中的 selector 监听连接事件(OP_ACCEPT):
    • 创建 socket channel
    • 初始化 socket channel 并从 worker group 中选择一个 NioEventLoop
  • Worker Thread: 
    • 将 socket channel 注册到选择的 NioEventLoop 的 selector
    • 注册读事件(OP_READ)到 selector 上
  • WriteAndFlush: 
    • 不管在 write 还是 flush ,都是在 Head 节点 HeadContext 实现的。
    • write 方法并没有将数据写入 Socket 缓冲区,只是将数据写入到 ChannelOutboundBuffer 缓存中,内部是由单向链表实现的。
    • flush 方法才最终将数据写入到 Socket 缓冲区。
    • write 方法:
      • 过滤msg:如果 msg 使用的不是 DirectByteBuf,那么它会将 msg 转换成 DirectByteBuf。
      • 给buffer添加数据:
        • ChannelOutboundBuffer 缓存是一个链表结构,每次传入的数据都会被封装成一个 Entry 对象添加到链表中。ChannelOutboundBuffer 包含三个非常重要的指针:第一个被写到缓冲区的节点 flushedEntry、第一个未被写到缓冲区的节点 unflushedEntry和最后一个节点 tailEntry
        • tailEntry 指针会不断指向新加入的 msgN,unflushedEntry 依然保持不变,unflushedEntry 和 tailEntry 指针之间的数据都是未写入 Socket 缓冲区的。 
        • 但是我们不可能一直向缓存中写入数据,所以 addMessage 方法中每次写入数据后都会判断缓存的水位线 ,判断缓存大小是否超过所设置的高水位线 64KB,如果超过了高水位,那么 Channel 会被设置为不可写状态。直到缓存的数据大小低于低水位线 32KB 以后,Channel 才恢复成可写状态。
    • flush 方法:
      • 准备待发送数据:
        • flushedEntry 指针指向的数据才会被真正发送到 Socket 缓冲区。decrementPendingOutboundBytes 主要作用是减去待发送的数据字节,如果缓存的大小已经小于低水位,那么 Channel 会恢复为可写状态。
      • doWrite 方法:
        • 当我们向 Socket 底层写数据的时候,如果每次要写入的数据量很大,是不可能一次将数据写完的,所以只能分批写入。Netty 在不断调用执行写入逻辑的时候,EventLoop 线程可能一直在等待,这样有可能会阻塞其他事件处理。所以需要控制一次写入数据的最大的循环执行次数,如果超过所设置的自旋锁次数,那么写操作将会被暂时中断。
        • 删除缓存中的链表节点以及调用底层 API 发送数据。
        • 调用 incompleteWrite 方法确保数据能够全部发送出去:
          • 因为自旋锁次数的限制,可能数据并没有写完,所以需要继续 OP_WRITE 事件;如果数据已经写完,清除 OP_WRITE 事件即可。
  • ChannelPipeline的介绍:
    • ChannelPipeline 的双向链表分别维护了 HeadContext 和 TailContext 的头尾节点。我们自定义的 ChannelHandler 会插入到 Head 和 Tail 之间,这两个节点在 Netty 中已经默认实现了,它们在 ChannelPipeline 中起到了至关重要的作用。
      • HeadContext 既是 Inbound 处理器,也是 Outbound 处理器。它分别实现了 ChannelInboundHandler 和 ChannelOutboundHandler。网络数据写入操作的入口就是由 HeadContext 节点完成的。
      • HeadContext 作为 Pipeline 的头结点负责读取数据并开始传递 InBound 事件,当数据处理完成后,数据会反方向经过 Outbound 处理器,最终传递到 HeadContext,所以 HeadContext 又是处理 Outbound 事件的最后一站。
      • TailContext 只实现了 ChannelInboundHandler 接口。它会在 ChannelInboundHandler 调用链路的最后一步执行,主要用于终止 Inbound 事件传播,例如释放 Message 数据资源等。TailContext 节点作为 OutBound 事件传播的第一站,仅仅是将 OutBound 事件传递给上一个节点。
      • Inbound 事件的传播方向为 Head -> Tail,而 Outbound 事件传播方向是 Tail -> Head。
    • 异常会按顺序从 Head 节点传播到 Tail 节点:
      • 如果用户没有对异常进行拦截处理,最后将由 Tail 节点统一处理。
      • 虽然 Netty 中 TailContext 提供了兜底的异常处理逻辑,但是在很多场景下,并不能满足我们的需求。假如你需要拦截指定的异常类型,并做出相应的异常处理
      • 异常处理的最佳实践
        在 Netty 应用开发的过程中,良好的异常处理机制会让排查问题的过程事半功倍。所以推荐用户对异常进行统一拦截,然后根据实际业务场景实现更加完善的异常处理机制。在 ChannelPipeline 自定义处理器的末端添加统一的异常处理器
      • public class ExceptionHandler extends ChannelDuplexHandler {
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                if (cause instanceof RuntimeException) {
                    System.out.println("Handle Business Exception Success.");
                }
            }
        }

  • Netty启动的流程:
    • 创建服务端 Channel:本质是创建 JDK 底层原生的 Channel,并初始化几个重要的属性,包括 id、unsafe、pipeline 等。
    • 初始化服务端 Channel:设置 Socket 参数以及用户自定义属性,并给Boos Group添加特殊的处理器ServerBootstrapAcceptor。
    • 注册服务端 Channel:实际是调用 JDK 底层将 Channel 注册到 Selector 上并监听OP_ACCEPT 事件。
    • 端口绑定:实际是调用 JDK 底层进行端口绑定,并触发 channelActive 事件。
    • 注册和绑定:不会阻塞当前线程,提交给EventLoop执行,得到Future并且添加回调,
  • Netty处理客户端新建连接:
    • Boss NioEventLoop 线程轮询客户端新连接 OP_ACCEPT 事件;
    • 构造 NioSocketChannel,添加childHandler、设置channel参数;
    • 注册 NioSocketChannel 到 Worker 工作线程中;
    • 注册 OP_READ 事件到 NioSocketChannel 的事件集合。
    • 整个过程是由ServerBootstrapAcceptor触发执行的;

二、Netty的编解码拦截器;
  • 通过拦截器的机制,可以让数据的处理方式扩展性更好,比如:TCP协议的粘包拆包问题、HTTP协议的数据处理、数据的压缩和解压缩、数据的序列化和反序列化。
  • 基于TCP协议的粘包拆包通过编解码器的解决方式:
    • 特定分隔符法:使用基于特殊分割符的方式;
      • 由于在发送报文时尾部需要添加特定分隔符,所以对于分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。
      • 比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。特定分隔符法在消息协议足够简单的场景下比较高效,例如 Redis 在通信过程中采用的就是换行分隔符。
    • 消息长度+消息内容:先写入一个数据长度,然后在写入消息即可,比如心跳信息就约定长度为-1;
      • 消息长度 + 消息内容是项目开发中最常用的一种协议。消息头中存放消息的总长度,例如使用 4 字节的 int 值记录消息的长度,消息体实际的二进制的字节数据。
      • 接收方在解析数据时,首先读取消息头的长度字段 Len,然后紧接着读取长度为 Len 的字节数据,该数据即判定为一个完整的数据报文。消息长度 + 消息内容的使用方式非常灵活,且不会存在消息定长法和特定分隔符法的明显缺陷。

三、Netty的ByteBuf;
  • 索引指针更细粒度的划分:
    • JDK原生的buffer底层使用了pos指针、limit指针、capacity指针来实现读写,在写模式下调用flip方法,那么limit就设置为了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读。
    • Netty进行了改进,使用了readerIndex指针、writerIndex指针、capacity指针来进行了实现,不需要进行flip重置指针,更加高效。
    • 分为三部分,第一部分为已经读取过的数据可以进行废弃,第二部分是还没有读取但是可以读取的数据,第三部分是还没有写但是可以写的区域。
  • 动态扩容机制:
  • 这是JDK的ByteBuffer对象中用于存储数据的对象声明,其字节数组是被声明为final的,也就是长度是固定不变的。一旦分配好后不能动态扩容,如果ByteBufer的空间不足,我们需要自己实现动态扩容,创建一个全新的ByteBuffer对象,然后再将之前的ByteBuffer中的数据复制过去。      
  • Netty存储字节的数组是动态的,其最大值默认是Integer.anx_VALB。这里的动态性是体现在write方法中的, write方法在执行时会判断butfer容量,如果不足则自动扩容。
    •  默认门限阈值为4MB(这个阈值是一个经验值,不同场景,可能取值不同);
    • 当需要的容量等于门限阈值,使用阈值作为新的缓存区容量 目标容量;
    • 如果大于阈值,采用每次步进4MB的方式进行内存扩张((需要扩容值/4MB)*4MB),扩张后需要和最大内存(maxCapacity)进行比较,大于maxCapacity的话就用maxCapacity,否则使用扩容值 目标容量;
    • 如果小于阈值,采用倍增的方式,以64(字节)作为基本数值,每次翻倍增长64-》128-》256,直到倍增后的结果大于或等于需要的容量值。
  • ByteBuf大致分为两种Pooled和Unpooled:
    • pooled(池化)类型的bytebuf是在已经申请好的内存块取一块内存,而Unpooled(未池化)是直接通过JDK底层代码申请。
    • ByteBuf提供了heap buffer堆缓冲、direct buffer非堆缓冲、composite buffer复合缓冲(可以组装堆缓冲和非堆缓冲)这三种,

四、Netty的心跳检测
  • 客户端和服务端都可以检测每隔一段时间是否有读写请求,如果没有就会触发回调,自己可以设定一些逻辑。
  • Netty 中的心跳检测机制_rickiyang的博客-CSDN博客_netty心跳检测

五、Netty的Future和Promise
  • Netty的ChannelFuture在JDK的Future上增加了监听器功能;
    • 当置为完成状态的时候,就会调用ChannelFuture对应的Listener,是在执行任务的线程执行的。
    • 通过GenericFutureListener代替ChannelFuture的 get等,当我们进行异步IO操作时,完成的时间是无法预测的,利用异步通知机制回调GenericFutureListener,其他线程不需要被get一直阻塞,这样的实现非常优雅。
  • Netty是通过Promise对Future进行扩展的;

扩展:HTTP和RPC协议的区别;

  • 两者其实都是建立在传输层之上的,都属于应用层的协议;
  • HTTP协议更适用于和用户交互,定义了请求行、请求体、请求头等规范;
  • RPC协议更适用于远程方法调用的场景,不需要严格的请求行、请求头、请求体,一般只需要头部记录版本号、消息长度等、body记录方法信息、参数实体信息等;
  • 基于RPC协议引出的RPC框架实现:
    • RPC 框架可以基于不同的协议实现,大部分主流 RPC 框架会选择 TCP、HTTP 协议,出名的 gRPC 框架使用的则是 HTTP2。
      • 基于TCP协议,需要设计消息的结构,需要处理粘包拆包问题,可以写入一个消息长度来解决,然后读出数据进行解码即可。
      • 基于HTTP协议,不需要设计消息的结构,也不需要处理粘包拆包问题,按照顺序和换行分隔符解析出请求行、请求头、请求体即可,然后对数据进行解码。
    • TCP、HTTP、HTTP2 都是稳定可靠的,但其实使用 UDP 协议也是可以的,具体看业务使用的场景。
    • 成熟的 RCP 框架能够支持多种协议,例如阿里开源的 Dubbo 框架被很多互联网公司广泛使用,其中可插拔的协议支持是 Dubbo 的一大特色,这样不仅可以给开发者提供多种不同的选择,而且为接入异构系统提供了便利。
    • 主流的 RPC 框架并且都都会自定义通信协议,相比于 HTTP、HTTPS、JSON 等通用的协议,自定义协议可以实现更好的性能、扩展性以及安全性。我们设计了一个简易版的 RPC 自定义协议,如下所示:
    • +---------------------------------------------------------------+
      | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte  |
      +---------------------------------------------------------------+
      | 状态 1byte |        消息 ID 8byte     |      数据长度 4byte     |
      +---------------------------------------------------------------+
      |                   数据内容 (长度不定)                          |
      +---------------------------------------------------------------+

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/282248.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号