Netty源码分析
2024-07-20 18:26:20 0 举报
AI智能生成
Netty是一个高性能、异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能服务器和客户端。它使用NIO(非阻塞IO)进行网络通信,提供了简化网络编程的抽象层。源码分析揭示了Netty的核心组件,如Channel、EventLoop、Buffer等,以及其高效的线程模型。通过分析源代码,可以深入了解Netty的事件驱动机制、负载均衡策略、并发处理等关键技术。此外,源码分析还有助于理解Netty的配置和调优,以提高应用程序的性能和稳定性。
作者其他创作
大纲/内容
参数【在Linux内核中的原理】
Backlog【连接队列】
含义
半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态为:SYN_RCVD)。
全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一直保留在半连接状态中;当服务器接收到客户端的ACK报文后,该条目将从半连接队列搬到全连接队列尾部,即 accept queue (服务器端口状态为:ESTABLISHED)
配置
SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为20
队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。
如果accpet queue队列满了,server将发送一个ECONNREFUSED错误信息Connection refused到client。
如果accpet queue队列满了,server将发送一个ECONNREFUSED错误信息Connection refused到client。
KeepAlive
含义
tcp连接空闲时,就是2小时内没发送过任何网络包
此时是否需要向对方发送探测包,防止发生意外
此时是否需要向对方发送探测包,防止发生意外
配置
net.ipv4.tcp_keepalive_time = 7200 (空闲多长时间,则发送探测包,3600,7200s)
net.ipv4.tcp_keepalive_intvl = 75 (发送探测包的周期)
net.ipv4.tcp_keepalive_probes = 9 (探测失败的重试次数,发送探测包达次数限制对方依旧没有回应,则关闭自己这端的连接)
课程截图
2
1
4
5
6
7
核心分析
ChannelPipeline
管道
含义
是ChannelHandler实例对象的链表,组装了很多handler, 用于处理或截获通道的接收和发送数据
每个新的通道Channel,都会创建一个新的ChannelPipeline
流向
入站处理器流向是从头到尾【接收到远端传递来的缓存区ByteBuf类型数据】
出站处理器流向是从尾到头【要将应用数据转换成缓存区ByteBuf类型数据发送到远端】
channelhandler添加到channelpipeline的过程
在ServerBootstrap初始化时,为监听端口accept事件的Channel添加ServerBootstrapAcceptor
在有新链接进入时,为监听客户端read/write事件的Channel添加用户自定义的ChannelHandler
源码图
图
是ChannelHandlerContext实例
成员
ChannelHandler
ChannnelInboundHandler【入站,输出是从尾到头,顺序执行】
处理输入数据和Channel状态类型改变
适配器: ChannelInboundHandlerAdapter(适配器设计模式)
常用的: SimpleChannelInboundHandler
ChannelOutboundHandler【出站,输出是从头到尾,逆序执行】
处理输出数据
适配器: ChannelOutboundHandlerAdapter
负责管理处理连接、数据接收、异常处理、数据转换一类
客户端是发起请求再接受数据,先outbound再inbound,服务端则相反。
ChannelHandlerContext实例
是ChannelHandler实例与ChannelPipeline之间的桥梁
双线程池模型设计【reactor模型】
Netty EventLoop与Channel的关系
channel
代表了网络通信管道,网络连接,bind、connect、read、write,底层封装了java nio socketchannel网络编程
eventloop
关系
EventLoopGroup(一个)
包含多个EventLoop
EventLoop(一个)
在生命周期里就绑定一个Thread
可能分配给多个Channel
Channel(一个)
只能注册到一个EventLoop来
所有的ChannelHandler事件都是由EventLoop对象里的IO线程处理的
流程
一个新的连接建立后,注册一个channel
EventLoopGroup分配一个EventLoop绑定到channel来
启动和绑定流程
netty server启动,会构建一个serversocketchannel,绑定监听指定端口
boss group里,拿出来一个event loop,接着会把server socket channel注册到这个event loop里的selector中去
event loop里就有一个io线程会监听这个selector里注册的server socket chanenl,是否有accept连接事件到来
监听accept流程【boss group#event loop】
io线程启动事件循环模式while循环不停的监听selector里是否有事件产生
每次有一个客户端发起连接,底层三次握手然后触发一个accept事件,就会被selector监听到了
io线程就会通过accept方法,拿到跟这个客户端的连接,socket channel
把这个socket channel注册到worker group里的一个event loop中的selector
监听read流程【worker group#event loop】
io线程会循环监听selector里的read事件
等待客户端发送数据过来进行读取
堆外内存与缓冲池管理机制
ByteBuff的writeAndFlush
调用write方法并没有将数据写到Socket缓冲区中,而是写到了一个单向链表的数据结构中,flush才是真正的写出
writeAndFlush等价于先将数据写到netty的缓冲区,再将netty缓冲区中的数据写到Socket缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功
原因
框架运行的过程中,输入输出数据是需要放在内存里的,因为需要对框架使用的内存去做管理
Java NIO原生ByteBuffer的缺陷分析
缓冲区内存是可以复用的,下次有数据来的时候,复用他就可以了,不要老是搞频繁回收的短期对象
数据长度是固定的,一旦分配,就不能动态扩容和收缩,所以在复用的时候,如果要是超出这个范围了,就没法复用了
只有一个标识位置的指针,每次读写,都要手工调整指针位置
设计
申请一块较大内存区域,内存分批额和回收,用类似和数据库表结构一类的记录,
监控内存使用状态,随着内存申请和释放,更新状态【堆外内存。避免GC】
监控内存使用状态,随着内存申请和释放,更新状态【堆外内存。避免GC】
ByteBuf结构设计
两个指针
writerIndex
刚开始都是0,capacity是可以写入的字节数量,写入数据以后,writerIndex增加n个字节,
可读取的数据就是readerIndex到writerIndex,
剩余可以写入的就是writerIndex到capacity
剩余可以写入的就是writerIndex到capacity
readerIndex
读取m个字节后,readerIndex会增加m个字节,这个时候0到readerIndex就被锁住了,没法读写
readerIndex是不能超过writerIndex的,有一个discardReadBytes的操作,释放掉0到readerIndex的空间,让readerIndex归零
写入时动态扩展分析
两个指针,我们不用去做flip()动作,如果说内存长度不够了,他会自动去做一个扩容
零拷贝数据传输实现原理分析
方式
内核->hard disk、read buffer
用户->application buffer
内核->socket buffer
零拷贝:hard disk、read buffer、socket buffer、nic buffer
零拷贝
含义
零拷贝就是避免了第一次复制,直接内核里走socket
流程
就是把数据包从内核复制到用户
再复制到内核通过socket传输到另外一条机器的内核
再复制到用户空间
ps
java nio,FileChannel.transferTo()就是零拷贝
netty底层也是基于java nio零拷贝支持去实现的
按维度划分
Heap/Direct:堆内和堆外内存
Pooled/Unpooled:池化还是非池化内存
Unsafe/非Unsafe:操作方式是否安全
内存分配
PooledByteBufAllocator:分配内存的入口
PoolThreadLocalCache:PooledByteBufAllocator持有的一个threadLocal
PoolThreadCache:每个线程独有。内部持有MemoryRegionCache集合和PoolArena
PoolThreadLocalCache:PooledByteBufAllocator持有的一个threadLocal
PoolThreadCache:每个线程独有。内部持有MemoryRegionCache集合和PoolArena
模型图
Chunk:申请内存的单位,为 Page 的集合
Page:管理内存Arena的单位,大小为8K
Subpage:page内的内存分配
Page:管理内存Arena的单位,大小为8K
Subpage:page内的内存分配
内存回收
如何回收
利用虚引用的cleaner和GC来顺带回收堆外内存
回收哪些
引用计数机制
内存泄漏检测机制
LeakAwareByteBuf 感知内存泄漏
详设
Cleaner:
创建一个用于自动回收内存的Cleaner(虚引用类型 创建时会被放入引入队列中,当DirectByteBuffer被GC回收后,cleaner的引用就会不可达,然后监听该引用队列的后台线程 就会移除该cleaner 并且自动执行该cleaner的异步线程任务 在这里是回收内存的任务)
所以NIO中的DirectByteBuffer无需手动释放,当这个对象被GC回收时就会异步自动释放堆外内存,若是不小心进入了老年期,则会等待该方法中的System.gc()触发FullGC 来尝试回收
主动回收:Netty就为ByteBuf提供了一种引用计数的机制,以及基于引用计数的回收机制,可以通过调用release让引用次数-1,也可以调用retain让引用次数+1,当ByteBuf的引用次数为0时就会被回收(直接释放或者返回内存池)
内存泄漏检测机制(只是检测,并不会帮你回收)
Netty每当创建一个池化的ByteBuf时,都会将其包装成一个LeakAwareByteBuf(可感知泄漏的ByteBuf)
创建一个用于自动回收内存的Cleaner(虚引用类型 创建时会被放入引入队列中,当DirectByteBuffer被GC回收后,cleaner的引用就会不可达,然后监听该引用队列的后台线程 就会移除该cleaner 并且自动执行该cleaner的异步线程任务 在这里是回收内存的任务)
所以NIO中的DirectByteBuffer无需手动释放,当这个对象被GC回收时就会异步自动释放堆外内存,若是不小心进入了老年期,则会等待该方法中的System.gc()触发FullGC 来尝试回收
主动回收:Netty就为ByteBuf提供了一种引用计数的机制,以及基于引用计数的回收机制,可以通过调用release让引用次数-1,也可以调用retain让引用次数+1,当ByteBuf的引用次数为0时就会被回收(直接释放或者返回内存池)
内存泄漏检测机制(只是检测,并不会帮你回收)
Netty每当创建一个池化的ByteBuf时,都会将其包装成一个LeakAwareByteBuf(可感知泄漏的ByteBuf)
`
编解器
MessageToByteEncoder
将符合条件的类型数据转成缓存区对象ByteBuf。
MessageToMessageEncoder
将符合条件的一种类型数据转成另一种类型数据。
解码器
MessageToMessageDecoder【二次解码】
将符合条件的一种类型数据转成另一种类型数据。
ByteToMessageDecoder【一次解码】
将缓存区数据转成另一种类型数据。
如何解决沾包,半包问题
原因
TPC 是一个数据流,会将应用层的数据拆成一个一个帧包发送,不会按照应用层的数据格式进行分割。
应用层发送10条数据,有可能被当成一个帧包发送;或者发送一条数据,被拆成几个帧包发送。
模型图
核心逻辑
让子类去解析累计缓存区中的数据;
将解析成功的数据添加到 out 中;
将解析成功的数据添加到 out 中;
子类
FixedLengthFrameDecoder
固定长度分割数据的解码器
LineBasedFrameDecoder
使用换行符 \n 或 \r\n 分割数据的解码器
DelimiterBasedFrameDecoder
自定义多种分隔符的解码器
LengthFieldBasedFrameDecoder
根据消息中长度字段的值动态分割数据的解码器
实现要求
如果你解析成功数据,将它们添加到 out,记住一定要改变缓存区索引。
如果你能解析部分数据,但是不够拼接成完成对象,添加到out;这时你可以选择改变缓存区读索引,或者不改变。
PS:一次解码是将数据正确的拆分或者拼合 , 二次解码是将数据正确的序列化或者反序列化 。
内存管理
内存规格划分
为了分配的内存块尽可能保持连续、为了内存块能尽大程度的被利用、为了减少内部碎片
整体流程图
问题
粘包拆包
组件
子主题
0 条评论
下一页