Java框架-netty

netty优势

  • api简单,开发快
  • 预置很多编码功能,支持多种主流的协议
  • 拓展能力强,ChannelHandler可定制化程度高
  • 使用nio运行,性能高
  • 用的人多

netty特点

  1. 高性能
  • 采用**NIO,基于Reactor**模式实现,解决了传统同步阻塞IO模式

  • 零拷贝,**数据缓冲区使用直接内存**代替堆内存,避免了内存复制,提升了IO读取和写入的性能

  • 内存池的方式循环利用ByteBuf,避免了频繁插件和销毁ByteBuf带来的性能消耗

  • IO线程数、TCP参数可配置,为**不同的用户场景**提供定制化的调优参数,满足不同的性能场景

  • 采用**环形数组缓冲区实现无锁化并发**编程,代替传统的线程安全或锁。

  • **合理**使用线程安全容器,原子类,提升系统的并发处理能力

  • **关键资源**的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和cpu资源消耗

  • 通过**引用计数法**及时地申请释放不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁GC带来的时延增大和CPU损耗

  1. 可靠性
  • 链路有效监测(心跳和空闲检测)
    • 读空闲超时机制
    • 写空闲超时机制
  • 内存保护机制
    • 通过对象引用计数法管理内置对象
    • 可设置的内存容量上限,包括ByteBuf、线程池线程数
  1. 可定制性
  • 责任链模式:channelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展
  • 基于接口的开发:关键的类库都提供了接口或者抽象类,用户可以自定义实现相关接口
  • 提供了大量工厂类,通过重载这些工厂类可以按需创建出用户实现的对象
  • 提供大量的系统参数供用户按需设置,增强系统的场景定制

netty工作原理机制

  • 组件:
    • Channel:通道,底层入口,所有读写最后都是channel进行操作,可以理解为操作系统API的包装类
    • Pipeline:管道,数据读写的通道,用于耦合多个handler成责任链,fire方法会取下个handler执行。每个channel有且只有一个Pipeline
    • ChannelHandler:通道处理器,自定义业务的接口
    • ChannelHandlerContext:通道处理器上下文,每个处理器都有自己的上下文。上下文在Pipeline中成双向链表节点,主要用于执行handler
    • channel执行时委托给Pipeline,Pipeline负责责任链的构建并从head触发操作,操作处理由双向链表节点Context完成,Context执行handler
  • 1、ServerBootstrap配置
    • 监听channel配置:bossGroup(监听线程组)、channel(通道类型,tcp、udp、sctp等)、option(监听channel的可选配置)、attr(监听channel的属性)
    • 工作channel配置:childGroup(工作线程组)、childOption(工作channel的可选配置)、childAttr(工作channel的属性)、childHandler(工作处理器)
    • 调用ServerBootstrap.bind 绑定端口
  • 2、流程
    • ServerBootstrap执行bind时,通过反射创建监听channel,然后执行初始化(设置配置、属性、添加通道初始化器),通道初始化器用于**添加建立连接的ChannelHandler(ServerBootstrapAcceptor)**
    • channel执行注册,包括向操作系统注册、bossGroup开始轮训任务、触发fireChannelRegistered向channel**ServerBootstrapAcceptor**
    • 轮训到SelectedKey时,触发监听channel的read,在read中创建**处理channel,然后通过ServerBootstrapAcceptor处理Channel**注册到childGroup,至此连接建立并交给childGroup处理,开始轮训
  • 3、读写事件传播
    • read事件由netty触发传播,write事件由我们自己传播
    • pipeline持有两个哨兵Handler,即HeadContext、TailContext,我们添加的Handler处于这两个Handler之间
    • 事件的传播起点、方向、目标:
      • 入站事件传播的起点为当前Handler或者HeadContext,方向为next,也就是往下一个,目标是InboundHandler
        • ctx.fireChannelRead
      • 出站事件传播的起点为当前Handler或者TailContext,方向为prev,也就是往回一个,目标是OutboundHandler
        • 当前Handler:ctx.writeAndFlush,TailContext:ctx.channel().writeAndFlush
  • 4、类图
classDiagram
class ServerBootstrap
class Eventloop
Eventloop: +selector
class selector
selector: +Map<SelectorKey, Channel>
class Channel
Channel: +JDKSelectableChannel
Channel: +Pipeline
class Pipeline
Pipeline: +HeadContext->ChannelHandlerContext->...->ChannelHandlerContext->TailContext
class ChannelHandler
class ChannelHandlerContext
class ServerBootstrapAcceptor
ChannelHandler <|.. ServerBootstrapAcceptor
ServerBootstrap --> EventLoopGroup
EventLoopGroup --> Eventloop
Eventloop --> selector
selector o-- Channel
Channel --> Pipeline
Pipeline o-- ChannelHandlerContext
ChannelHandlerContext --> ChannelHandler

FastThreadLocal

零拷贝

Netty的零拷贝是**JVM层面**的零拷贝。ByteBuf贯穿了零拷贝的设计理念: 尽量避免Buffer复制带来的开销

比如派生缓冲区(Derived buffers)的操作,duplicate()(复制),slice()(切分),order()(排序)等,虽然都会返回一个新的ByteBuf实例, 但它们****只是具有**自己独立的读索引、写索引和标记索引而已,内部存储(Buffer数据)是共享**的,也就是过程中并没有复制操作。

由此使用这些操作的时候需要**注意**:修改原对象会影响派生对象, 修改派生对象也会影响原对象。

netty零拷贝体现在几个方面:

  • ByteBuf组合报文数据时,通过 CompositeByteBuf 实现零拷贝通过组合多个ByteBuf避免复制

    ByteBuf header = ... 
    ByteBuf body = ...  
    // 合成ByteBuf
    ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes()); 
    allBuf.writeBytes(header); 
    allBuf.writeBytes(body);  
  • 通过 wrap 操作实现零拷贝,byte[]数据需要转换成ByteBuf时,对现有数据进行包装;此时修改byte[],ByteBuf也会被修改

    byte[] bytes = ... 
    ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);  
  • 通过 slice 操作实现零拷贝。当需要进行数据裁剪时,通过slice可以生成裁剪后的ByteBuf。新ByteBuf与旧ByteBuf共享数据,但是读写等控制指针独立维护

  • 通过 FileRegion 实现零拷贝,FileRegion是基于**OS的零拷贝实现,上述的是程序上的零拷贝,OS是在IO时的零拷贝**

    • OS的零拷贝有两种方式:sendfile、mmap内存地址映射,两者都是系统调用
      • mmap系统调采用**内存映射的方式,让内核空间和用户控件共享同一块内存,省去了从内核空间往用户空间复制的开销**。
      • sendfile系统调用可以**将文件直接从硬盘经由DMA传输到套接字缓冲区**,而无需经过用户空间。如果网卡支持收集操作(scatter-gather),那么可以做到真正意义上的零拷贝。
      • NIO中FileChannel的map()和transferTo()方法**封装了底层的mmap和sendfile系统调用**,从而在Java语言上提供了系统层面零拷贝的支持。

什么是 TCP 粘包/拆包

  • 待发送数据大于发送缓冲去大小或者最大报文长度时会出现拆包
  • 相反,待发送数据小于发送缓冲区大小,多次写入缓冲区后才发送数据则出现粘包;接收端没有及时读取数据也会出现粘包
  • 处理方法:
  • 1、数据包添加包信息,比如包长度,接收端接收时根据长度进行包长度进行处理
  • 2、数据包使用固定长度进行发送,接收端每次读取固定长度。不够长度使用0填充
  • 3、使用特殊字符对包进行标记分隔

netty的线程模型

  • 使用的是Reactor的多路复用线程模型,一般服务端启动的时候会创建两个线程组:BossGroup、WorkerGroup。BossGroup是用来监听并建立客户端连接,workerGroup则是用来读写连接数据和处理逻辑的。
  • 每个EventLoopGroup可以包含一个或多个EventLoop,每个EventLoop包含一个nio selector,一个队列,一个线程。其中线程负责轮询selector和处理队列里面的事件

Netty 的零拷贝

  • 零拷贝是指netty中使用直接内存进行数据的接收和发送,worker线程进行数据读写时,相比传统io的需要先将数据复制到堆内存,netty并不需要数据读取到jvm内存中,从而减少数据在内存中的复制。

说说操作系统的io模型

  • 操作系统io时并不是直接和外部io直接数据交换,而是通过数据缓冲区进行数据交换。缓冲区又分为内核缓冲区和用户缓冲区,当进程需要进行io read时,需要将数据从内核缓冲区拷贝到用户缓冲区;同样io write时,需要将数据从用户缓冲区拷贝到内核缓冲区。
  • 不同的io模型,主要围绕进程状态,数据从准备到内核缓冲区的«数据准备»阶段、数据从内核缓冲区到用户缓冲区的«数据复制»阶段两个阶段展开
  • 阻塞io:java的socket默认为bio。从数据准备阶段到数据复制阶段都是进程状态都是阻塞,其中数据准备阶段cpu并不需要参与
  • 非阻塞io:数据准备阶段,进程不阻塞。进程发起read调用时若数据还没准备好会立即返回进程不会被阻塞。当数据准备好时,则会进入数据拷贝阶段,这个阶段仍然会阻塞线程。由于不知道什么时候数据准备好,所以需要用户进程自己通过轮询检查数据准备情况。
  • 多路io复用:非阻塞io中的轮询过程进程是脱不开身的,如果有线程专门处理轮询则可以将进程解放出来。多路io复用select\poll检查io状态,检查到io就绪后随后进行相应的io调用。线程可以同时轮询成百上千的io请求,达到复用的目的。
  • 异步io:数据准备阶段、数据拷贝阶段都是不阻塞的。进程发起io请求后直接返回,等待内核数据准备和拷贝,内核处理完后通过发送信号量或者回调通知用户线程。目前只要windows的iocp支持