Netty源码-中文注释-高级面试题深入解析

Netty源码-中文注释-高级面试题(源码级刨析)

一、源码环境搭建

👉👉👉中文注释源码地址👈👈👈
👉👉👉中文注释源码地址👈👈👈
👉👉👉中文注释源码地址👈👈👈

拉取代码后需要对Common包执行一下 clean package,或者直接对整个项目构建一下

在这里插入图片描述

在netty-example包下,阅读源码入口,每一个方法都需要Debug进去,才能看清楚做了些什么

在这里插入图片描述

二、源码阅读

跟着👆👆上面NettyServer类方法一步步Debug下去,再参考文档阅读

三、高级面试题-源码级别刨析

1、 Netty为什么高性能?

Netty的网络模型

Java三种模型

  1. BIO(阻塞式)
  2. NIO(非阻塞)
  3. AIO(异步)

Netty使用的是NIO,为什么呢?

  1. 在Liunx下NIO和AIO都是使用epoll模型实现,在性能上又没有明显的区别,但AIO到Jdk1.7才发布;
  2. NIO使用的Reactor模型而AIO使用的是Proactor模型,模型的不同使得在难以在一套组件中共行;
  3. AIO有个缺点是接收需要预先分配缓存, 而NIO接收时才需要分配缓存, 对连接数量非常大但流量小的情况, 内存浪费多;

NIO的核心

  1. Buffer
  2. Channel
  3. 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封装还实现了以下功能

  1. 通过内置的复合缓冲区类型实现透明的零拷贝
  2. 支持方法的链式调用
  3. 支持引用计数
  4. 支持池化

👉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多线程模型

  1. 一个线程监听多个线程处理;
  2. EventLoopGroup管理一组EventLoop;
  3. 一个EventLoop管理一个线程;
  4. BossEventLoopGroup组用于监听连接事件,但BossEventLoopGroup里的每一个EventLoop只负责一个地址的监听;
  5. 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中对零拷贝的应用主要有三个方面:

  1. DirectBuffer(直接内存) netty在为经指定的情况下创建的都是直接内存,并根据jemalloc算法,在一次性申请大量内存后进行内存池管理。
    在这里插入图片描述

  2. Composite Buffers(合并Buffer操作) 这是由netty自己实现的,核心在与保存两个Buffer引用处理,类似数据库的视图;在这里插入图片描述

  3. 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

  1. FixedLengthFrameDecoder(定长):固定长度发送消息,如果消息不够长使用占位符代替(空格);
  2. DelimiterBasedFrameDecoder(分隔符):指定消息分隔符,如/n代表消息结束;
  3. LengthFieldBasedFrameDecoder(记录长度):在消息头部记录长度;
  4. 自定义解码器:通常情况下会使用-头部加魔数、长度记录。
为什么不适用AIO
  1. 在Liunx下NIO和AIO都是使用epoll模型实现,在性能上又没有明显的区别,但AIO到Jdk1.7才发布;
  2. NIO使用的Reactor模型而AIO使用的是Proactor模型,模型的不同使得在难以在一套组件中共行;
  3. AIO有个缺点是接收需要预先分配缓存, 而NIO接收时才需要分配缓存, 对连接数量非常大但流量小的情况, 内存浪费多;

— EOF—

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值