Netty源码-中文注释-高级面试题(源码级刨析)
一、源码环境搭建
👉👉👉中文注释源码地址👈👈👈
👉👉👉中文注释源码地址👈👈👈
👉👉👉中文注释源码地址👈👈👈
拉取代码后需要对Common包执行一下 clean package,或者直接对整个项目构建一下
在netty-example包下,阅读源码入口,每一个方法都需要Debug进去,才能看清楚做了些什么
二、源码阅读
跟着👆👆上面NettyServer类方法一步步Debug下去,再参考文档阅读
三、高级面试题-源码级别刨析
1、 Netty为什么高性能?
Netty的网络模型
Java三种模型
- BIO(阻塞式)
- NIO(非阻塞)
- AIO(异步)
Netty使用的是NIO,为什么呢?
- 在Liunx下NIO和AIO都是使用epoll模型实现,在性能上又没有明显的区别,但AIO到Jdk1.7才发布;
- NIO使用的Reactor模型而AIO使用的是Proactor模型,模型的不同使得在难以在一套组件中共行;
- AIO有个缺点是接收需要预先分配缓存, 而NIO接收时才需要分配缓存, 对连接数量非常大但流量小的情况, 内存浪费多;
NIO的核心
- Buffer
- Channel
- Selector
Netty对Nio的核心的处理-源码解析
👉** 1. Buffer **👈
在默认情况下Netty默认分配的都是分配的直接缓存(0拷贝),只有在Andorid、 IKVM.NET、手动关闭的情况下使用非直接内存;
断点位置👉:io.netty.buffer.AbstractByteBufAllocator#ioBuffer(int)
直接缓存:堆外缓存,读取时不用内核态与用户态之间拷贝,读取速度快但释放比较慢;
非直接缓存区:通过allocate()方法分配的缓存区,将缓存区建立在JVM的内存中;
比较特别的是Netty的缓存分配器会动态的根据内容调节缓存大小来尽量避免粘包/拆包的情况
断点位置👉:io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read
那内存是怎么释放的呢?
handler处理链中,会有一个固定的处理器tail,该处理器会在异常等情况下去释放,还有比如在调用转码器后会清理转码前的数据(前提继承了ReferenceCounted接口,这个接口主要是Buffer对象继承实现)
因为使用的直接内存,直接内存的创建相比非直接内存慢(10-20),且不受JVMGC管理,缓存问题也是需要关注的,所以Netty实现了有内存池 管理buffer的分配与释放,netty内部使用Jemallca 算法管理buffer,简单概括几句:一次性申请16M,每个线程注册到一个Arena(内存管理器),16M会被划分成多个page或者小于page的单位进行管理;也是非常复杂的算法如果感兴趣可以去搜一下Netty内存池。
释放ReferenceCounted对象👉:io.netty.util.ReferenceCountUtil#release(java.lang.Object)
tail释放ReferenceCounted对象👉:io.netty.channel.DefaultChannelPipeline.TailContext#userEventTriggered
解码器释放对象👉:io.netty.handler.codec.MessageToMessageDecoder#channelRead
Netty还的Buffer封装还实现了以下功能
- 通过内置的复合缓冲区类型实现透明的零拷贝
- 支持方法的链式调用
- 支持引用计数
- 支持池化
👉2. Channel👈
Netty对原生的Channel做了一层封装,使得做一些业务操作更加的优雅
Pipeline:维护Handler的双向链表,可以被动态的维护。比如第一次有注册Handler,注册后就可以剔除;
Handler:链式的处理读、写事件,同时Netty也为我们提供了很多处理器的比如解决粘包\粘包、编解码(String、protocol等),又或者默认的头尾处理器;
ChannelPromise:异步回调方法,Netty自己封装的,可以在后续中动态的去添加异步回调;
HeadContext(默认处理器-头):主要做连接、绑定等操作;
TailContext(默认处理器-尾):主要做Buffer的释放;
👉3. Selector👈
Netty比较主要的对selector优化点:select的SelectionKey类型修改、解决Jdk selector无限循环;
select的SelectionKey类型修改:SeletorKey是具体接收到某个的channel事件映射对象,而select模型在系统底层是一个1024长度的数组每个槽位代表一个channel,监听这个就可以获取到产生的事件,而在JDK原生的会映射到 Set中,而Set类型底层是HashMap,在添加发生Hash碰撞的时候时间复杂度就会是O(n),故Netty使用反射的方式将Set类型换成了数组类型,添加时时间复杂度O(1)。
优化代码位置👉:io.netty.channel.nio.NioEventLoop#openSelector
解决Jdk selector无限循环
该bug主要点在于select出现一些无效事件,但系统还是提示有事件存在而导致。
netty的解决方案是设置一个标记值,在无效循环到达一定次数后重建Selector重新注册。
源码解析-看绿色的就好了
优化代码位置👉:io.netty.channel.nio.NioEventLoop#run
Netty的线程模型
Netty采用的是主从Reactor多线程模型。
- 一个线程监听多个线程处理;
- EventLoopGroup管理一组EventLoop;
- 一个EventLoop管理一个线程;
- BossEventLoopGroup组用于监听连接事件,但BossEventLoopGroup里的每一个EventLoop只负责一个地址的监听;
- BossEventLoopGroup里的EventLoop监听到线程后会将Channel注册到ChildEventLoop里的其中一个(默认轮询)EventLoop中(注册的EventLoop自己的seletor中监听处理后续事件)处理后续事件;
在监听连接后会注册到ChildEventLoopGroup中处理后续事件:
Boos添加监听连接位置👉:io.netty.bootstrap.ServerBootstrap#init
注册至Child处理位置👉:io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead
select、poll、epoll
Select和Poll模型基本一直只是在文件描述对象类型不同,poll使用链表的形式保存无上限限制;
Epoll模型,会管理文件描述符所以不用每次都传入,减少内核态与用户态的切换,且他会通过回调将有效事件添加至就绪链表,当就绪链表触发了添加,就会激活等待线程返回就绪链表数据;
调用Jdk方法获取当前可用的Seletor实现:
获取Selector位置👉:io.netty.channel.nio.NioEventLoopGroup#NioEventLoopGroup(int, java.util.concurrent.Executor)
Netty零拷贝
零拷贝主要使用DMA技术实现,DMA可以实现无需CUP的参与,外设直接操控内存;
Netty中对零拷贝的应用主要有三个方面:
-
DirectBuffer(直接内存) netty在为经指定的情况下创建的都是直接内存,并根据jemalloc算法,在一次性申请大量内存后进行内存池管理。
-
Composite Buffers(合并Buffer操作) 这是由netty自己实现的,核心在与保存两个Buffer引用处理,类似数据库的视图;
-
TransferTo(传输) Jdk提供的方法FileChannel.transferTo()也是零拷贝,可以直接将文件缓冲区的数据发送到目标Channel,Linux 的 sendfile实现,在Netty中通过FileRegion包装的FileChannel.transferTo()方法调用;
2、Netty都采用那些设计模式
1.ServerBootstrap-建造者模式&原型模式
2.ChannelPipeline-责任链
一个双向的链表,这个自己Debug的时候会发现每次执行下一个Pipeline里的Handler都会调用fireChannel***方法来触发下一个Handler
ChannelPipeline类的描述👉:io.netty.channel.ChannelPipeline
3.SimpleChannelInboundHandler-模板模式
SimpleChannelInboundHandler的channelRead0只会在消息类型与指定的泛型匹配时执行
模板模式代码位置👉:io.netty.channel.SimpleChannelInboundHandler
4.SelectedSelectionKeySetSelector-装饰模式+委派模式
代码位置👉:
io.netty.channel.nio.SelectedSelectionKeySetSelector
5.SelectedSelectionKeySetSelector-观察者模式
代码位置👉:io.netty.channel.DefaultChannelPipeline#write(java.lang.Object, io.netty.channel.ChannelPromise)
6.ReflectiveChannelFactory-工厂模式
使用类路径来初始化后续Channel的工厂,该工厂用户后续创建新channel对象
代码位置👉:io.netty.bootstrap.AbstractBootstrap#channel
3、其他
Netty怎么保证用户任务都在一个线程下执行
每一个Channel在连接成功后,会被注册到ChildEventLoopGroupd其中的一个EventLoop中,这个EventLoop会跟踪该Channel到结束(EventLoop会被保存到Channel对象中);
EventLoop:一个EventLoop其实就代表了一个线程,一个EventLoop中有一个Selector,一个Selector里可以注册多个Channel,这意味着一个EventLoop会处理多个Channel的任务;
怎么解决粘包/拆包
Netty提供了多个解决的Handler
- FixedLengthFrameDecoder(定长):固定长度发送消息,如果消息不够长使用占位符代替(空格);
- DelimiterBasedFrameDecoder(分隔符):指定消息分隔符,如/n代表消息结束;
- LengthFieldBasedFrameDecoder(记录长度):在消息头部记录长度;
- 自定义解码器:通常情况下会使用-头部加魔数、长度记录。
为什么不适用AIO
- 在Liunx下NIO和AIO都是使用epoll模型实现,在性能上又没有明显的区别,但AIO到Jdk1.7才发布;
- NIO使用的Reactor模型而AIO使用的是Proactor模型,模型的不同使得在难以在一套组件中共行;
- AIO有个缺点是接收需要预先分配缓存, 而NIO接收时才需要分配缓存, 对连接数量非常大但流量小的情况, 内存浪费多;
— EOF—