gprs发送信号对方如何接收_和接收缓冲区比较:Netty发送缓冲区是如何设计的,why?...

news/2024/7/6 4:08:19
121fd32efff0a2439820f377e82f84c8.png

点击上方蓝字关注我吧!

本篇文章大概3300字,阅读时间大约10分钟

前面文章,透彻分析了Netty的接收缓冲区优化的套路和实现细节,以及写数据和刷新数据的宏观流程和细节:

从源码出发:在宏观上把握Netty写数据到应用层缓冲区的过程

从源码出发:在宏观上把握Netty刷新数据到网络的过程

其中,反复提到了一个组件叫ChannelOutboundBuffer,对这个缓冲区的着墨并不多。本文就单独总结这个发送缓冲区的设计思想和实现细节,以及为何这样设计。

cbb603107e1561f4434a7f0c34858cca.gif

Netty为每个已经创建的Channel都绑定了一个ChannelOutboundBuffer对象,如下一个Netty的Channel的代码片段:黄色1处是Channel聚合的Unsafe,在每个Netty的Channel里,都会通过其unsafe获取一个独立的ChannelOutboundBuffer,如黄色2所示。即它调控的粒度是Channel级。

b5e0c3ce3eda1889298674c65bd76b4a.png

如下是官方对ChannelOutboundBuffer的解释:

*(Transport implementors only)an internal data structure used by{@link AbstractChannel}to store its pending*outbound write requests.*<p>*All methods must be called by a transport implementation from an I/O thread,except the following ones:*<ul>*<li>{@link#size()}and{@link#isEmpty()}li>*<li>{@link#isWritable()}li>*<li>{@link#getUserDefinedWritability(int)}and{@link#setUserDefinedWritability(int,boolean)}li>*ul>*p>*/

即它是一个Netty内部的数据结构,由每个Channel独立保存一份,并且只能被Netty的I/O线程调度,除了一些get方法,比如size()方法,isEmpty方法等。回忆文章从源码出发:在宏观上把握Netty写数据到应用层缓冲区的过程中拆解的流程,当用户调用writeAndFlush方法发消息时,首先添加消息到ChannelOutboundBuffer,最终会层层调用到pipeline的头结点的write方法,内部会调用unsafe的write方法,如下由于unsafe的write和flush方法会被客户端,服务端复用,故被实现在了AbstractUnsafe类:

005bf1ae2c0640095e6fc1dda7c60678.png

首先在黄色1处,拿到发送缓冲区的一个快照,正常情况黄色2不会执行,进入黄色3处的filter0utboundMessage(msg)方法——对非堆外内存进行转换,转换为堆外内存,即Netty的设计理念是——所有的I/O操作都走堆外内存,以提升性能。接着下一步是执行黄色4处代码——拿到当前msg消息对象的大小(单位是字节),接着看最后一行——黄色5处调用了ChannelOutboundBuffer的addMessage方法,目的是将待发送消息msg加入应用层的发送缓冲区,并且一旦加入就会通过promise回调用户的监听器。

下面进入addMessage方法细致的看下它的实现,该方法将本次待发送的msg以及msg的字节数,和异步对象promise当做参数传入:

0961a0ee7e8ffc942756a53bc9bb016a.png

这里知道一个结论——ChannelOutboundBuffer的数据结构是一个单向Entry链表+Netty对象池,故上面的addMessage方法主要就是在操作单向链表的指针。

基本思路是:首先将待发送的msg——ByteBuf对象封装为ChannelOutboundBuffer内部的Entry节点,然后使用尾插法将该新节点加入Entry链表。最后在addMessage方法的结束处调用一个incrementPendingOutboundBytes方法,这是Netty的流控机制,前面也简单提过,后续专题总结。

下面梳理ChannelOutboundBuffer的单向Entry链表结构,如下是创建Entry节点的过程:

Entry entry=Entry.newInstance(msg,size,total(msg),promise);

total方法会计算当前发送消息的字节数,本demo为6,然后进入newInstance方法实例化一个Entry:

d45ed6745de751dcf79a94fe65e84bc5.png

1、RECYCLER对象,它涉及到了Netty的对象池技术,此处先抓主要矛盾,不深究,后续专题总结,只需要知道它实现了一个线程安全的轻量级的对象池——通过复用已有的对象来避免频繁创建和回收对象带来的性能损耗。所以这个Entry节点可以被重复使用,即每次ChannelOutboundBuffer用完一个Entry,便将Entry的参数都置为null,然后仍回到对象池RECYCLER,下次可以拿出来重复利用,并且使用了本地线程存储来保证线程安全

2、属性pendingSize记录的是当前待发送消息msg的一个估计值,这里有一个容易费解的地方,即CHANNEL_OUTBOUND_BUFFER_ENTRY_OVERHEAD常量,它默认是96(单位字节),即Netty会给当前待发送的消息msg额外加上96字节,算作msg的估计值,后续会通过这个值去影响流量控制机制,关于这里的设计想法暂且不表,后续专题总结。属性total是当前待发送消息本身的大小。

对Entry有了一个理解后,下面梳理一下Entry链表的几个指针和属性:

4390b97910903331fc8794f61d63624f.png

1、flushedEntry:代表被flush方法标记为已刷新的消息节点,即可以认为该Entry马上或者已经被发到网络了,它指向的是链表里第一个要被刷新出去的节点

2、unflushedEntry:代表只是通过write方法添加到了Entry链表的消息节点。它是链表里第一个等待刷新的节点

3、tailEntry:Entry链表的最后一个节点

4、next:链表中节点的下一个节点

5、属性flushed代表当前缓冲区的大小,即链表中还没有被刷新出去的节点的数量

下面看Entry链表是如何被组织的,回忆addMessage方法:

1cf14106877f7b2c711ef05bdbea31ab.png

调用addMessage方法之前,Entry链表的样子如下:

2572d25e8e00f7e35cbb2d07b908fe24.png

调用addMessage方法后,将待发送的消息节点,即新的Entry节点,通过尾插法追加到tailEntry节点后,同时tailEntry指向新Entry,unflushedEntry也指向新Entry,其结构变化为如下模样:

6633acc38e07011f9f4ce4c9ce3910ee.png

当第二个待发送消息被加入Entry链表时,它的结构变为如下模样:

d596445b974ab44fda0bf24ed031620b.png

以此类推,用户只要没有执行flush操作,最终当前Channel的发送缓冲区的待发送消息的堆积形式如下,直到触发了流控保护后才停止堆积:

68b47b9fdea263d54f04ddd6b2c1932b.png

以上,也能看出这是一个FIFO的添加顺序,即Netty保证通过write添加的消息,会严格按照它的调用顺序缓存,其Entry链表被消费时总是先消费最老的节点。接着回忆flush的流程,当用户调用flush方法后,会调用到pipeline的头结点的flush方法,即unsafe的flush方法:

c36ac00c83a288e66df6c18e573afd66.png

当调用黄色1处的addFlush方法时,会将unflushedEntry的引用赋给flushedEntry,然后将unflushedEntry置为null,即设置刷新消息的标记。如下:

7106514e2412c522f3dd9b3bee7aac7b.png

通过unflushedEntry指针拿到第一个待发送的消息节点,并且判null后,将这个unflushedEntry赋值给flushedEntry,然后开一个循环设置这些节点的标记,并且告诉用户不能做取消操作了,如果当前某个消息节点在设置之前就已经取消发送了,那么就将这个节点过滤(后续不会flush),同时将totalPendingSize减小。此时Entry链表的结构如下:

9df4f74544612ad8f14015aa90e90ec5.png

由于addFlush方法里会循环的设置待发送节点的标记指针,最终Entry链表变为如下的样子;

4cadf6d185bb6d53fc16428b6f8555d7.png

此时addFlush结束,但是以上操作仅仅对待发送消息节点做了标记和过滤,紧接着调用的flush0方法才会执行真正的刷新操作,回忆前面的文章从源码出发:在宏观上把握Netty刷新数据到网络的过程。

此时要真的刷新数据到Socket缓冲区里,会从flushedEntry节点开始,循环的将每个flushed状态的节点取出,存入一个JDK的ByteBuffer数组,通过nioBuffers方法做转化:

1ff8b4b982190acc5fd3137bc3194f06.png

得到ByteBuffer数组后,后续分策略刷新。如果消息发送成功,那么调用ChannelOutboundBuffer的remove方法,将已发送消息从Entry链表中删除,此步骤也包括清理堆外内存,如果是非池化分配,那么直接释放——通过调用UNSAFE.freeMemory(address);,如果是池化内存分配,那么将内存回收到池子里,如下是Netty清理内存的核心方法free:

f01e5790b8df3e6f266ba39e060b6854.png

清理内存后,同时更新待发送的消息节点指针flushedEntry=flushedEntry.next。如下是此时链表的样子,红色方块代表已经刷新到了网络的消息:

30072c2a438f2476122fcd8ad28670ec.png

以上循环往复,直到Entry链表里已经没有flushed的消息节点,就把flushedEntry设置为null,最终缓冲区被排干:

a334b394797599f88a6597104ee17bf2.png

做个小结——设计的意义

a)一方面是吞吐量和延时的考虑。可以联系TCP协议的Nagle算法,默认都是启用的,Java可以通过setTcpNoDelay(true)来禁用。Netty就是如下配置,参考Netty里配置的TCP_NODELAY参数作用是啥,应该如何调优?:

.childOption(ChannelOption.TCP_NODELAY,true)

如果开启了Nagle算法(关闭TCP_NODELAY),那么在网络应用里就可能出现频繁的延时,用户体验极差。不过连续进行多次对小数据包的发送操作,本身就不是一个好的编程模式,在应用层就应该进行优化。对于既要求低延时,又有大量小数据传输,还同时想提高网络利用率的应用,只能用UDP自己在应用层来实现可靠性保证了。更合理的方案还是应该使用一次大数据的写操作,而不是多次小数据的写操作,所以Netty也在应用层设计实现了发送缓冲区。

b)给用户取消写操作的时间上的缓冲

Netty设计的write方法,在内部有一个addMessage操作,它会添加待发消息进ChannelOutboundBuffer,并且这个方法并不会刷新数据到Socket,只有调用了flush方法,才会将unflushedEntry的引用转移到flushedEntry引用中,表示即将刷新这个flushedEntry,至于为什么这么做?

因为Netty提供了实现了promise模式的API,使得每个I/O操作都可以被取消,例如,“我”在一些条件下,不打算发这个ByteBuf了,所以flush之前,都是可以反悔的。

c)方便实现流控

众所周知,TCP协议在发端也有发送缓冲区,在我理解,Netty在应用层搞一个缓冲区和传输层TCP协议设计缓冲区的目的一致,即在应用层提高吞吐量和实现应用层的流控。如下语句:

ctx.channel().write("hello dashuaiRPC!\r\n");

只是将hello dashuaiRPC!\r\n消息写到了Netty的ChannelOutboundBuffer对象,此处并不涉及到网络相关的操作。这样就可以在一定程度上提高服务的吞吐量,当累计当一定数量,在调用flush方法实现一次性批量发送。即write方法可以调用多次,flush才是真正的写入到Socket。并且由于发送缓冲区的存在,Netty还能方便的在应用层实现限流保护,以防止内存被打爆。

d)底层实现=单向链表+对象池

一方面可以节省内存,减少GC次数,另一方面链表实现缓冲区,也能减少数据的移动,只需要修改指针的指向即可。

e)在其它的Netty的功能实现上,起到一个辅助作用,比如辅助记录某个Channel的发消息的进度和总量,为空闲检测等做支持。可以参考文章:Netty进化之路:赏析空闲检测处理器对写检测的优化

END

点亮在看,你最好看

点击此处写留言~

4a3ad1ed975edc69d37782874a0cb712.png2be3256ba9eabb84ab49bb4ac902a469.gif


http://lihuaxi.xjx100.cn/news/236585.html

相关文章

animate默认时长所带来的问题及解决

一、需求描述 做一个进度条长度逐渐减少的动画&#xff0c;当进度条长度小于等于0时&#xff0c;关闭动画&#xff0c;并弹出透明底板显示新提示。 二、问题描述 初始代码如下&#xff1a; //设置进度条初始长度 var progressLength 180; //设置一个定时器 var timer …

使用 spring boot 开发通用程序

2019独角兽企业重金招聘Python工程师标准>>> tag: spring 学习笔记date: 2018-03spring 是什么&#xff1f;spring 核心是应用组件容器&#xff0c;管理组件生命周期&#xff0c;依赖关系&#xff0c;并提倡面向接口编程实现模块间松耦合。 spring boot 是什么&…

蒙特卡洛粒子滤波定位算法_粒子滤波——来自哈佛的详细的粒子滤波器教程【1】...

本文原版链接&#xff1a;https://www.seas.harvard.edu/courses/cs281/papers/doucet-johansen.pdf本文是哈佛大学相关研究人员于2008年发表的一篇关于粒子滤波的详细教程&#xff0c;至今已被引用1687次。目录&#xff1a;介绍Introduction1.1 序言Preliminary remarks1.2 教…

微软开源数据处理引擎 Trill,每天可分析万亿次事件

微软近日开源了数据处理引擎 Trill&#xff0c;它每天能够分析万亿次事件。项目地址&#xff1a;https://github.com/Microsoft/trill当下每毫秒处理大量数据正成为一种常见的业务需求&#xff0c;此次微软开源的 Trill&#xff0c;据说每秒能够处理高达数十亿事件&#xff0c;…

搭建本地https

生成证书 1. 使用openssl生成密钥privkey.pem&#xff1a; openssl genrsa -out privkey.pem 1024/2038 2. 使用密钥生成证书server.pem&#xff1a; openssl req -new -x509 -key privkey.pem -out server.pem -days 365 证书信息可以随便填或者留空&#xff0c;只有Common Na…

python怎么切片提取_彻底搞懂Python切片操作

在利用Python解决各种实际问题的过程中&#xff0c;经常会遇到从某个对象中抽取部分值的情况&#xff0c;切片操作正是专门用于完成这一操作的有力武器。理论上而言&#xff0c;只要条件表达式得当&#xff0c;可以通过单次或多次切片操作实现任意切取目标值。切片操作的基本语…

从0到1构建网易云信IM私有化

本文来源于MOT技术管理课堂杭州站演讲实录&#xff0c;全文 2410 字&#xff0c;阅读约需 5分钟。网易云信资深研发工程师张翱从私有化面临的问题及需求说起&#xff0c;分享了网易云信IM私有化的解决方案和具体实践。想要阅读更多技术干货、行业洞察&#xff0c;欢迎关注网易云…

vue中动态样式不起作用? scoped了解一下

vue中style标签使用属性scoped的注意事项 style上添加属性scoped可以实现样式私有化&#xff0c;但是在使用动态样式时&#xff0c;样式会不起作用。可以先去掉scoped转载于:https://www.cnblogs.com/zuojiayi/p/9364347.html