二、Netty源码解读之底层原理

Netty源码解读之底层原理

Netty源码编译

版本:4.1.40.Final-SNAPSHOT

image.png

编译 Netty 常遇问题

image.png

image.png

重新编译一次

Netty 源码核心包

1、工具类:

2、底层协议(transport)

TCP的不同实现(linux(epoll)、Mac(kqueue),unix)

3、人性化协议

包括编解码codec、还有handler

image.png

image.png

image.png

Netty对三种I/O 模式的支持

Netty对BIO,NIO,还有AIO,BIO也称之为OIO(已被弃用)。

AIO也进行了相关的移除。

image.png

但是我们还是来看下Netty是如何做到对三种I/O模式的切换。

代码比较简单:

image.png

ServerSocketChannel:工厂模式+泛型+反射实现

image.png

Netty对Reactor模式的支持

Reactor 是一种开发模式,模式的核心流程:

注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理

NioEventLoop.run()

image.png

mainReactor

image.pngimage.png

image.png

image.png

image.png

image.png

image.png

image.png

这里只会调用一次,也就是这里一般也只会有一个线程

image.png

subReactor

image.pngimage.png

image.png

image.png

Netty 给 Channel 分配 NIO event loop 的规则是什么

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

所以这里可以看到,在选择的算法删,&运算比%运算效率要高,但是必须基数是2的次方(这种跟HashMap中的取模运算是一样),这里是极致的追求高性能!

实现多路复用器是怎么跨平台的

image.png

在不同的操作系统上,NIO怎么去实现多路复用的。我可以看下,

image.png

image.png

image.png

mac系统

jdk8u-jdk/DefaultSelectorProvider.java at master · frohoff/jdk8u-jdk · GitHub

image.png

windows系统

image.png

Netty对处理粘包/半包的支持

什么是粘包和半包?

image.png

假设客户端分别发送了两个数据包ABC和DEF给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况

1、服务端分两次读取到了两个独立的数据包,分别是ABC和DDEF,没有粘包和拆包;

2、服务端一次接收到了两个数据包,ABC和DEF粘合在一起,被称为TCP粘包;

3、服务端分两次读取到了两个数据包,第一次读取到了完整的ABC包和DEF包的部分内容(ABCD),第二次读取到了DEF包的剩余内容(EF),这被称为TCP拆包

4、服务端分两次读取到了两个数据包,第一次读取到了ABC包的部分内容(AB),第二次读取到了CD,第三次读到了EF(这种有粘包和半包问题)

TCP粘包/半包发生的原因

根本原因:TCP 是流式协议,消息无边界。
UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收,所以无粘包、半包问题。

image.png

TCP收发
一个发送可能被多次接收,多个发送可能被一次接收
TCP传输
一个发送可能占用多个传输包,多个发送可能公用一个传输包

image.png

TCP粘包/半包解决实战

image.png

固定长度

消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;

参见com.msb.netty.splicing.fixed下的代码。

消息定长:FixedLengthFrameDecoder类

image.png

不过确定很明显,如果消息不定长,那么空间非常浪费,所以这种方式不推荐!!!

分割符

在包尾增加分割符,比如回车换行符进行分割,例如FTP协议;

参见com.msb.netty.splicing.linebase(回车换行符进行分割)

行分隔符类:LineBasedFrameDecoder

image.png

com.msb.netty.splicing.delimiter(自定义分割符)

自定义分隔符类:DelimiterBasedFrameDecoder

image.png

消息头和消息体

将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度,使用LengthFieldBasedFrameDecoder

Netty的编解码器

解码器: 将字节解码为消息

编码器: 将消息编码为字节

一次解码器

一次解码器: 解决半包粘包问题的常用三种解码器

image.png

io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)

二次解码器

二次解码器: 需要和项目中所使用的对象做转化的编解码器

  • Java 序列化
  • Marshaling
  • XML
  • JSON
  • MessagePack
  • Protobuf
  • 其他

image.png

编解码速度对比:

2017-05-06 10_30_22-Serialization Performance comparison(XML,Binary,JSON,P…) | HowToAutomate.in.th

feature-image-protobuf-101

MessagePack

MessagePack: It's like JSON. but fast and small. (msgpack.org)

image.png

优点:压缩包比高,语言支持丰富,缺点:难懂

编解码器源码分析

NETTY的String编解码器

比较简单,转成字符串image.png

NETTY的Java序列化编解码器

核心类,你可以看出继承了 LengthFieldBasedFrameDecoder

image.png

再到编码器中: ObjectEncoder

image.png

image.png

image.png

通过看源码可知,NETTY针对JDK的序列化也做了优化(虽然JDK的序列化本身效率低)

protobuf编解码器

Release Protocol Buffers v21.2 · protocolbuffers/protobuf · GitHub

image.png

image.png

image.png

image.png

image.png

源码分析

ProtobufVarint32FrameDecoder

image.png

这里可以看出来,protobuf采用的是不定长,报文头不是定长的,站在性能角度来说,是可以不浪费空间的。

这样看也大致大知道protobuf非常高效

Netty中的零拷贝

什么是零拷贝?

零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率

➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销

可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝。

下面这些组件、框架中均使用了零拷贝技术:Kafka、Netty、Rocketmq、Nginx、Apache。

Linux的I/O机制与DMA

在早期计算机中,用户进程需要读取磁盘数据,需要CPU中断和CPU参与,因此效率比较低,发起IO请求,每次的IO中断,都带来CPU的上下文切换。因此出现了——DMA。

DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。DMA控制器,接管了数据读写请求,减少CPU的负担。这样一来,CPU能高效工作了。现代硬盘基本都支持DMA。

实际因此IO读取,涉及两个过程:

1、DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;

2、用户进程,将内核缓冲区的数据copy到用户空间。

这两个过程,都是阻塞的。

传统数据传送机制

比如:读取文件,再用socket发送出去,实际经过四次copy。

伪码实现如下:

buffer = File.read()
Socket.send(buffer)

1、第一次:将磁盘文件,读取到操作系统内核缓冲区;

2、第二次:将内核缓冲区的数据,copy到应用程序的buffer;

3、第三步:将应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);

4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。

image.png

分析上述的过程,虽然引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。

显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。

sendfile零拷贝

linux 2.1支持的sendfile

当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接拷贝到socket buffer。在硬件支持的情况下,甚至数据都并不需要被真正复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中,DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制(CPU copy)。

image.png

一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、代表数据转化的完成。socket buffer里的数据就能在网络传输了。

sendfile会经历:3次拷贝,1次CPU copy ,2次DMA copy;

硬件支持的情况下,则是2次拷贝,0次CPU copy, 2次DMA copy。

Netty源码分析

Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实现了零拷贝:io.netty.channel.DefaultFileRegion#transferTo

image.png

案例性能对比

代码如下,使用传统的四次传输,对比使用了sendfile技术的零拷贝技术

image.png

image.png

image.png

传统四次传输:耗时

image.png

senfile技术:耗时

image.png

Netty中的锁机制

锁优化技术:

1、减少锁的粒度

2、减少锁对象的空间占用

3、提高锁的性能

4、根据不同的业务场景选择适合的锁

5、能不用锁则不用锁

减少锁的粒度

io.netty.bootstrap.ServerBootstrap#init

请注意,因为这个方法已经被重构了,但是这个地方比较经典:展示需要找老版本

netty/Bootstrap.java at netty-4.1.15.Final · netty/netty · GitHub

image.png

减少锁对象的空间占用

注意锁的对象本身大小 -> 减少空间占用

io.netty.channel.ChannelOutboundBuffer

image.png

Atomic long VS long

Atomic long:是一个对象,包含对象头(object header)以用来保存 hashcode、lock 等信息,32 位系统占
用8字节;64 位系统占 16 字节。

AtomicLong = 8 bytes (volatile long)+ 16bytes (对象头)+ 8 bytes (引用) = 32 bytes

而如果使用long,则只有8个字节。

虽然代码实现麻烦一些,但是能够节约至少24个字节。

image.png

提高锁的性能

io.netty.util.internal.PlatformDependent#newLongCounter()

记录内存分配字节数

image.png

image.png

高并发时:java.util.concurrent.atomic.AtomicLong -> java.util.concurrent.atomic.LongAdder (JDK1.8)

image.png

另外阿里的开发手册中也明确的说明这点:

image.png

根据不同的业务场景选择适合的锁

io.netty.util.concurrent.SingleThreadEventExecutor#threadLock

关闭和等待关闭事件执行器

image.png

image.png

image.png

请注意这里,这里为什么不简单使用Object.wait/notify 而使用CountDownLatch

在实际运用过程中Object.wait/notify,这个用起来比较简单,但是实际使用的时候,往往非常复杂。因为你使用的时候,必须要放到一个同步代码块中(监视器中),如果不放的话,它就会直接报错。对于新人来说非常不友好。

而使用CountDownLatch它的业务就非常简单了。

能不用锁则不用锁

io.netty.util.Recycler#threadLocal

用 ThreadLocal 来避免资源竞争,轻量级的线程池实现

image.png

image.png

image.png

ThreadLocal使用的时候要避免内存泄漏,具体的在并发编程中有详细的讲解,这里不再重复述说。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JavaHxg

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值