netty基础知识梳理和总结
目录标题
- 由来
- netty整体结构
- 核心功能
- 可扩展的事件模型
- 统一的通信 API
- 零拷贝机制与字节缓冲区
- 传输服务
- 协议支持
- netty的IO模型
- netty核心组件
- Channel
- EventLoop、EventLoopGroup
- ChannelHandler
- ChannelPipeline
- Bootstrap
- Future
- netty的bytebuf
- bytebuf的内部构造
- bytebuf的使用模式
- ByteBuf的释放
- netty的零拷贝
- Kafka的零拷贝
- **1. Kafka 的零拷贝**
- **定义**
- **实现原理**
- **优点**
- **缺点**
由来
Netty是由JBOSS公司提供的一个java开源框架。
是一个基于NIO的客户、服务器端编程框架,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
netty整体结构
核心功能
可扩展的事件模型
- 提供灵活且可扩展的事件处理机制,适应多种应用场景。
统一的通信 API
- 统一接口:无论是 HTTP 还是 Socket,均使用统一的 API,简化了操作流程。
- 易用性:通过一致的接口设计,降低了开发复杂度。
零拷贝机制与字节缓冲区
- 高效数据传输:采用零拷贝机制,减少数据在内存中的拷贝次数,提升性能。
- 字节缓冲区:优化数据存储和读取效率。
传输服务
- Socket,基于TCP
- Datagram(数据报),基于UDP
- HTTP 协议
- In-VM Pipe(管道协议)
协议支持
- HTTP 与 WebSocket:支持标准的 HTTP 和 WebSocket 协议。
- SSL 安全套接字协议:提供安全的通信保障。
- Google Protobuf:支持高效的序列化框架。
- 压缩支持:
- 支持
zlib
和gzip
压缩,优化传输效率。
- 支持
- 大文件传输:支持高效的大文件传输能力。
- RTSP(实时流传输协议):支持 TCP/IP 协议体系中的应用层协议,适用于实时流媒体场景。
- 二进制协议支持:支持自定义二进制协议,并提供完整的单元测试。
netty的IO模型
Netty就是使用Java的NIO实现了Reactor线程模型是其实现高性能的一个核心点。
Netty是基于NIO2的IO模型(IO多路复用模型,非循环进行read的系统调用的原始NOBlock IO模型)。
- 常见的Reactor线程模型有三种
Netty就是结合了NIO的特点,应用了Reactor线程模型所实现的。
- 在Netty模型中,负责处理新连接事件的是BossGroup,负责处理其他事件的是WorkGroup。Group就是线程池的概念。
- NioEventLoop表示一个不断循环的执行处理任务的线程,用于监听绑定在其上的读/写事件。NioEventLoop就是Group里面的工作线程。
- 通过Pipeline(管道)执行业务逻辑的处理,Pipeline中会有多个ChannelHandler,真正的业务逻辑是在ChannelHandler中完成的。
netty核心组件
Channel
可以理解为是socket连接,在客户端与服务端连接的时候就会建立一个Channel。
常用的 Channel 类型:
- NioSocketChannel,NIO的客户端 TCP Socket 连接。
- NioServerSocketChannel,NIO的服务器端 TCP Socket 连接。
- NioDatagramChannel, UDP 连接。
- NioSctpChannel,客户端 Sctp 连接。
- NioSctpServerChannel,Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件IO。
EventLoop、EventLoopGroup
- 在 Netty 中每个 Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。
- 每个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件
- EventLoopGroup 是用来生成 EventLoop 的
ChannelHandler
ChannelHandler对使用者而言,可以说是最重要的组件了,因为对于数据的入站和出站的业务逻辑的编写都是在ChannelHandler中完成的。
- ChannelInboundHandler 入站事件处理器
- ChannelOutBoundHandler 出站事件处理器
例如:在 入站事件处理器中可以实现了channelRead方法,获取到客户端传来的数据
ChannelHandler只能完成编码解码处理、读写操作中的一种,所以ChannelHandler都是成组出现来完成功能。
ChannelPipeline
一个Channel包含了一个ChannelPipeline,而ChannelPipeline中维护了一个ChannelHandler的列表。
ChannelHandlerContext进行维护ChannelHandler与Channel和ChannelPipeline之间的映射关系。
ChannelHandler按照加入的顺序会组成一个双向链表
- 入站事件从链表的head往后传递到最后一个ChannelHandler
- 出站事件从链表的tail向前传递,直到最后一个ChannelHandler
- 两种类型的ChannelHandler相互不会影响。
Bootstrap
Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端口、启动Netty服务。
Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务器。
它们的区别在于:
- ServerBootstrap 只需要绑定一个监听连接的端口,而 Bootstrap 则是需要远程节点的IP和端口
- 客户端Bootstrap 只需要一个EventLoopGroup来实现消息的读取和写入,而ServerBootstrap则需要两个EventLoopGroup,一个来实现处理新连接事件,一个用来处理消息的读取和写入等事件。
Future
Future提供了一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问。
netty的bytebuf
bytebuf的内部构造
ByteBuf的三个指针:
- readerIndex(读指针)
指示读取的起始位置, 每读取一个字节, readerIndex自增累加1。 如果readerIndex 与writerIndex 相等,ByteBuf 不可读 。 - writerIndex(写指针)
指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与capacity() 容量相等,表示 ByteBuf 已经不可写。 - maxCapacity(最大容量)
指示ByteBuf 可以扩容的最大容量, 如果向ByteBuf写入数据时, 容量不足, 可以进行扩容
当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。
同样,当写 ByteBuf 时,它的 writerIndex(写索引) 也会根据写入的字节数进行递增。
ByteBuf内部空间结构:
- byteBuf.readByte(),内部通过移动readerIndex进行读取
- byteBuf.writeInt(i);
- discardReadBytes(),可以将已经读取的数据进行丢弃处理,就可以回收已经读取的字节空间,可写空间就会变大
- clear() ,重置readerIndex 、 writerIndex 为0,需要注意的是,重置并没有删除真正的内容
bytebuf的使用模式
- 堆缓冲区(HeapByteBuf)
- 优点:内存的分配和回收速度比较快,可以被JVM自动回收
- 缺点:进行socket的IO读或者写时,需要额外做一次内存复制,,将堆内存对应的缓冲区复制到内核Channel中,性能下降。
- 直接缓冲区(DirectByteBuf),非堆内存
- 优点:它写入或从Socket Channel中读取时,由于减少了一次内存拷贝(零拷贝),速度比堆内存块
- 缺点:内存的分配和回收速度慢,并且如果不注意内存回收,会导致内存泄漏
- 复合缓冲区,顾名思义就是将上述两类缓冲区聚合在一起。
- 复合缓冲区的核心思想是通过逻辑上的组合,而不是物理上的合并,来管理多个缓冲区
- 工作原理
- 数据的逻辑组合:通过维护一个内部的组件列表(components),记录每个子缓冲区的引用和偏移量。每个子缓冲区可以是堆缓冲区或直接缓冲区。
- 统一的读写接口:复合缓冲区对外提供了一个统一的读写接口,用户可以通过索引访问整个缓冲区的内容,而不需要关心底层是由哪些子缓冲区组成的。
- 使用场景:在网络协议中,可能需要将头部(通常较小,适合堆缓冲区)和负载(通常较大,适合直接缓冲区)分开存储,但又希望以统一的方式操作。
ByteBuf的释放
- 手动释放,就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放
- 自动释放
- 入站的TailHandler:Netty的ChannelPipleline的流水线的末端是TailHandler,默认情况下如果每个入站处理器Handler都把消息往下传,TailHandler会释放掉ReferenceCounted类型的消息。
- 继承SimpleChannelInboundHandler,类中包含一个onUnhandledInboundMessage方法,会释放缓存
- HeadHandler的出站释放,类似于入站
netty的零拷贝
所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,都可以通过一种方式,让应用进程向用户空间写入或者读取数据,就如同直接向内核空间写入或者读取数据一样,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。
Netty零拷贝主要体现在三个方面:
- Netty的接收和发送ByteBuffer是采用DIRECT BUFFERS,使用堆外的直接内存(内存对象分配在JVM中堆以外的内存)进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果采用传统堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后写入Socket中。
- Netty提供了组合Buffer对象,也就是CompositeByteBuf 类,可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝。
- Netty的文件传输采用了FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。(类似于kafka的零拷贝)
Kafka的零拷贝
Kafka 和 Netty 都使用了零拷贝技术,但它们的应用场景和实现方式有所不同。以下是对两者的区别、优缺点的详细分析:
1. Kafka 的零拷贝
定义
Kafka 的零拷贝是指在数据传输过程中,尽量减少数据在用户空间和内核空间之间的拷贝次数。它主要依赖于操作系统提供的 sendfile
系统调用(Linux 环境下)来实现。
实现原理
- 当 Kafka 从磁盘读取数据并发送到网络时,传统的方式需要经过多次数据拷贝:
- 数据从磁盘读取到内核缓冲区。
- 数据从内核缓冲区拷贝到用户空间缓冲区。
- 数据从用户空间缓冲区再拷贝回内核缓冲区(用于网络传输)。
- 数据通过网络接口发送出去。
- 使用零拷贝后:
- 数据直接从内核缓冲区通过
sendfile
系统调用传输到网络接口,避免了用户空间的参与,减少了两次拷贝操作。
- 数据直接从内核缓冲区通过
优点
- 性能提升:减少了数据拷贝次数和上下文切换,显著提高了吞吐量。
- 降低 CPU 开销:避免了用户空间和内核空间之间的数据拷贝,降低了 CPU 的负担。
- 适合大数据传输:特别适用于 Kafka 这种需要频繁进行大文件传输的场景。
缺点
- 灵活性较低:零拷贝要求数据是连续存储的,且不能对数据进行修改。如果需要对数据进行处理(如加密、压缩等),则无法直接使用零拷贝。
- 依赖操作系统支持:需要底层操作系统提供
sendfile
或类似机制的支持。