Netty 出站缓冲区 ChannelOutboundBuffer 源码解析(isWritable 属性的重要性)
目录:
前言
- ChannelOutboundBuffer 介绍
- addMessage 方法
- addFlush 方法
- flush0 方法
- 缓冲区扩展思考
- 总结
每个 ChannelSocket 的 Unsafe 都有一个绑定的 ChannelOutboundBuffer , Netty 向站外输出数据的过程统一通过 ChannelOutboundBuffer 类进行封装,目的是为了提高网络的吞吐量,在外面调用 write 的时候,数据并没有写到 Socket,而是写到了 ChannelOutboundBuffer 这里,当调用 flush 的时候,才真正的向 Socket 写出。同时,本文也关注当缓冲区满了的时候,Netty 如何处理。
1. ChannelOutboundBuffer 介绍
官方文档这么介绍的:
(Transport implementors only) an internal data structure used by AbstractChannel to store its pending outbound write requests.
All methods must be called by a transport implementation from an I/O thread。
意思是,这个一个数据传输的实现者,一个内部的数据结构用于存储等待的出站写请求。所有的方法都必有由 IO 线程来调用。
既然该类有一个内部的数据结构,我们就看看他的数据结构的样子,有以下几个属性:
private Entry flushedEntry; // 即将被消费的开始节点
private Entry unflushedEntry;// 被添加的开始节点,但没有准备好被消费。
private Entry tailEntry;// 最后一个节点
从上面的属性可以看出,这他么就是个链表。不过,这个链表有2个头,在调用 addFlush 方法的时候会将 unflushedEntry 赋值给 flushedEntry。表示即将从这里开始刷新。具体如下图:
调用 addMessage 方法的时候,创建一个 Entry ,将这个 Entry 追加到 TailEntry 节点后面,调用 addFlush 的时候,将 unflushedEntry 的引用赋给 flushedEntry,然后将 unflushedEntry 置为 null。
当数据被写进 Socket 后,从 flushedEntry(current) 节点开始,循环将每个节点删除。
关于这 3 个方法,我们后面详细解释。
2. addMessage 方法
该方法 doc 文档:
Add given message to this ChannelOutboundBuffer. The given ChannelPromise will be notified once the message was written.
将给定的消息添加到 ChannelOutboundBuffer,一旦消息被写入,就会通知 promise。
代码如下:
public void addMessage(Object msg, int size, ChannelPromise promise) {
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
if (tailEntry == null) {
flushedEntry = null;
tailEntry = entry;
} else {
Entry tail = tailEntry;
tail.next = entry;
tailEntry = entry;
}
if (unflushedEntry == null) {
unflushedEntry = entry;
}
incrementPendingOutboundBytes(entry.pendingSize, false);
}
说说方法步骤:
- 根据 ByteBuf 相互属性和 promise 创建一个 Entry 节点。
- 将新的节点追加到 tailEntry 节点上。如果考虑之前的全部被清空了话,则新节点就是唯一节点,unflushedEntry 属性就是新的节点。可对照上面的图来看。
- 使用 CAS 将 totalPendingSize(总的数据大小) 属性增加 Entry 实例的大小(96 字节) + 真实数据的大小。
主要这个 Entry 节点的创建有点意思:
Netty 将在 ThreadLocalMap 中存储了一个 Stack (栈)对象,存储重复使用的 DefaultHandle 实例,该实例的 value 属性就是 Entry ,所以这个 Entry 也是重复使用的,每次用完所有参数置为 null,再返回到栈中,下次再用,从这个栈中弹出。重复利用。对象池的最佳实践。而且是保存再线程中,速度更快,不会有线程竞争。这个设计倒是可以学习以下。
看完了 addMessage ,再看看 addFlush 方法。
3. addFlush 方法
当 addMessage 成功添加进 ChannelOutboundBuffer 后,就需要 flush 刷新到 Socket 中去。但是这个方法并不是做刷新到 Socket 的操作。而是将 unflushedEntry 的引用转移到 flushedEntry 引用中,表示即将刷新这个 flushedEntry,至于为什么这么做?
答:因为 Netty 提供了 promise,这个对象可以做取消操作,例如,不发送这个 ByteBuf 了,所以,在 write 之后,flush 之前需要告诉 promise 不能做取消操作了。
代码如下:
public void addFlush() {
Entry entry = unflushedEntry;
if (entry != null) {
if (flushedEntry == null) {
flushedEntry = entry;
}
do {
flushed ++;
if (!entry.promise.setUncancellable()) {
int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
} while (entry != null);
unflushedEntry = null;
}
}
结合上面的图:
- 首先拿到未刷新的头节点。
- 判 null 之后,将这个 unflushedEntry 赋值给 flushedEntry,而这里的判 null 是做什么呢?防止多次调用 flush 。
- 循环尝试设置这些节点,告诉他们不能做取消操作了,如果尝试失败了,就将这个节点取消,在调用 nioBuffers 方法的时候,这个节点会被忽略。同时将 totalPendingSize 相应的减小。
设置之后,promise 调用 cancel 方法就会返回 false。
在调用完 outboundBuffer.addFlush() 方法后,Channel 会调用 flush0 方法做真正的刷新。
4. flush0 方法
flush0 的核心是调用 dowrite 方法并传入 outboundBuffer。
每种类型的 Channel 都实现都不一样。我们看的是 NioSocketChannel 的实现,方法很长,楼主截取重要逻辑:
// 拿到NIO Socket
SocketChannel ch = javaChannel();
// 获取自旋的次数,默认16
int writeSpinCount = config().getWriteSpinCount();
// 获取设置的每个 ByteBuf 的最大字节数,这个数字来自操作系统的 so_sndbuf 定义
int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
// 调用 ChannelOutboundBuffer 的 nioBuffers 方法获取 ByteBuffer 数组,从flushedEntry开始,循环获取
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
// ByteBuffer 的数量
int nioBufferCnt = in.nioBufferCount();
// 使用 NIO 写入 Socket
ch.write(buffer);
// 调整最大字节数
adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
// 删除 ChannelOutboundBuffer 中的 Entry
in.removeBytes(localWrittenBytes);
// 自旋减一,直到自旋小于0停止循环,当然如果 ChannelOutboundBuffer 空了,也会停止。
--writeSpinCount;
// 如果自旋16次还没有完成 flush,则创建一个任务放进mpsc 队列中执行。
incompleteWrite(writeSpinCount < 0);
上面的注释基本就是 flush 的逻辑。
- 当然 flush0 方法在 NIO 的具体实现中,还加入了对注册事件的判断:
protected final void flush0() {
if (!isFlushPending()) {
super.flush0();
}
}
private boolean isFlushPending() {
SelectionKey selectionKey = selectionKey();
return selectionKey.isValid() && (selectionKey.interestOps() & SelectionKey.OP_WRITE) != 0;
}
这里的判断是:如果注册了写事件,就暂时不写了,因为缓冲区到了水位线了,所以这次直接返回,等会再写。等到 EventLoop 触发写事件了,就会调用 ch.unsafe().forceFlush()
方法将数据刷新到 TCP 缓冲区。
- 这里有一个小知识点:
NIO 的写事件大部分时候是不需要注册的,只有当 TCP 缓冲区达到水位线了,不能写入了,才需要注册写事件。当缓冲区有空间了,NIO 就会触发写事件。
5. 缓冲区扩展思考
从上面的逻辑上来看,不直到大家有没有发现一个问题:如果对方 Socket 接收很慢,ChannelOutboundBuffer 就会积累很多的数据。并且这个 ChannelOutboundBuffer 是没有大小限制的链表。可能会导致 OOM,Netty 已经考虑了这个问题,在 addMessage 方法的最后一行,incrementPendingOutboundBytes方法,会判断 totalPendingSize 的大小是否超过了高水位阈值(默认64 kb),如果超过,关闭写开关,调用 piepeline 的 fireChannelWritabilityChanged 方法可改变 flush 策略。
关于 channelWritabilityChanged API,Netty 这样解释:
当 Channel 的可写状态发生改变时被调用。用户可以确保写操作不会完成的太快(以避免发生 OOM)或者可以在 Channel 变为再次可写时恢复写入。可以通过调用 Channel 的 isWritable 方法来检测 Channel 的可写性。与可写性相关的阈值可以通过 Channel.config().setWriteBufferHighWaterMark 和 Channel.config().setWriteBufferLowWaterMark 方法来设置,默认最小 32 kb,最大 64 kb。
那么,上面时候恢复可写状态呢?remove 的时候,或者 addFlush 是丢弃了某个节点,会对 totalPendingSize 进行削减,削减之后进行判断。如果 totalPendingSize 小于最低水位了。就恢复写入。
也就是说,默认的情况下,ChannelOutboundBuffer 缓存区的大小最大是 64 kb,最小是 32 kb,哪里看出来的呢?
当然了,可以在 option 选项中进行修改,API 文档也说过了。
当不能写的时候,就会调用 ChannelWritabilityChanged 方法,用户可以在代码中,让写操作进行的慢一点。
6. 总结
到了总结的时刻。
Netty 的 write 的操作不会立即写入,而是存储在了 ChannelOutboundBuffer 缓冲区里,这个缓冲区内部是 Entry 节点组成的链表结构,通过 addMessage 方法添加进链表,通过 addFlush 方法表示可以开始写入了,最后通过 SocketChannel 的 flush0 方法真正的写入到 JDK 的 Socket 中。同时需要注意如果 TCP 缓冲区到达一个水位线了,不能写入 TCP 缓冲区了,就需要晚点写入,这里的方法判断是 isFlushPending()。
其中,有一个需要注意的点就是,如果对方接收数据较慢,可能导致缓冲区存在大量的数据无法释放,导致OOM,Netty 通过一个 isWritable 开关尝试解决此问题,但用户需要重写 ChannelWritabilityChanged 方法,因为一旦超过默认的高水位阈值,Netty 就会调用 ChannelWritabilityChanged 方法,执行完毕后,继续进行 flush。用户可以在该方法中尝试慢一点的操作。等到缓冲区的数据小于低水位的值时,开关就关闭了,就不会调用 ChannelWritabilityChanged 方法。因此,合理设置这两个数值也挺重要的。
好,限于篇幅,关于 ChannelOutboundBuffer 的分析就到这里,今天说的这几个方法算是这个类的主要方法,因为 Netty 的写操作都是围绕这三个方法来的。
good luck!!!!!
Netty 出站缓冲区 ChannelOutboundBuffer 源码解析(isWritable 属性的重要性)的更多相关文章
- netty服务端启动--ServerBootstrap源码解析
netty服务端启动--ServerBootstrap源码解析 前面的第一篇文章中,我以spark中的netty客户端的创建为切入点,分析了netty的客户端引导类Bootstrap的参数设置以及启动 ...
- Netty(四):AbstractChannel源码解析
首先我们通过一张继承关系的图来认识下AbstractChannel在Netty中的位置. 除了Comaprable接口来自java自带的包,其他都是Netty包中提供的. Comparable接口定义 ...
- Netty(三):IdleStateHandler源码解析
IdleStateHandler是Netty为我们提供的检测连接有效性的处理器,一共有读空闲,写空闲,读/写空闲三种监测机制. 将其添加到我们的ChannelPipline中,便可以用来检测空闲. 先 ...
- Netty(六):NioServerSocketChannel源码解析
我们在Netty学习系列五的最后提出了一些问题还没得到回答,今天来通过学习NioServerSocketChannel的源码来帮我们找到之前问题的答案. 先看一下NioServerSocketChan ...
- Netty 解码器抽象父类 ByteToMessageDecoder 源码解析
前言 Netty 的解码器有很多种,比如基于长度的,基于分割符的,私有协议的.但是,总体的思路都是一致的. 拆包思路:当数据满足了 解码条件时,将其拆开.放到数组.然后发送到业务 handler 处理 ...
- sprin源码解析之属性编辑器propertyEditor
目录 异常信息 造成此异常的原因 bean 配置文件 调用代码 特别说明: 异常解决 注册springt自带的属性编辑器 CustomDateEditor 控制台输出 属性编辑器是何时并如何被注册到s ...
- spring源码解析之属性编辑器propertyEditor
异常信息造成此异常的原因bean配置文件调用代码特别说明:异常解决注册springt自带的属性编辑器 CustomDateEditor控制台输出属性编辑器是何时并如何被注册到spring容器中的?查看 ...
- 5.2 dubbo-compiler源码解析
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class); final P ...
- (一)ArrayList集合源码解析
一.ArrayList的集合特点 问题 结 论 ArrayList是否允许空 允许 ArrayList是否允许重复数据 允许 ArrayList是否有序 有序 ArrayList是否线程安全 ...
随机推荐
- 调用DLL窗体-Delphi实例
(一)通过向导DLL Wizard新建一个动态链接库,取名为:DLLPro.dpr.说明:当在DLL工程文件中使用了String类型时,要有 uses ShareMem ,不过建议使用PChar类型. ...
- Ocelot——初识基于.Net Core的API网关
前言 前不久看到一篇<.NET Core 在腾讯财付通的企业级应用开发实践>,给现在研究.Net Core及想往微服务方向发展的人来了一剂强心针.于是我也就立刻去下Ocelot的源码及去阅 ...
- Resolving SharePoint Application Authentication Error: Login Failed
Check event viewer log Click Start, click Run, type eventvwr, and then click OK. Click on Security u ...
- AJPFX:外汇的技术面分析
AJPFX平台:开设外汇保证金交易账户以及入金之后,通常就可以开始交易了,但是在选择买卖时点时通常会依据两种分析,两种主要分析方法通常会被称为基本面分析和技术分析.基本面分析注重金融,经济理论和政局发 ...
- jQuery基础(4)- 位置信息、事件流、事件对象、事件代理、jquery事件
一.jQuery的位置信息 jQuery的位置信是JS的client系列.offset系列.scroll系列封装好的一些简便api. 1.宽度和高度 a.获取宽度和高度,例如: .width() // ...
- 用代码来细说Csrf漏洞危害以及防御
开头: 废话不多说,直接进主题. 0x01 CSRF介绍:CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session ...
- 从工程化角度讨论如何快速构建可靠React组件
前言 React 的开发也已经有2年时间了,先从QQ的家校群,转成做互动直播,主要是花样直播这一块.切换过来的时候,业务非常繁忙,接手过来的业务比较凌乱,也没有任何组件复用可言. 为了提高开发效率,去 ...
- Spring Boot中使用@Async实现异步调用
在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数,为了让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsyn ...
- 【Vue】【Router】手动跳转用 this.$router.push() 时 $router 未定义的问题
初入Vue,手写路由跳转时的问题: toXxxRoute: () => { this.$router.push({'path': '/xxx', 'name': 'xxx'}) } 由于使用了箭 ...
- Redis 入门知识
Redis 的前世今生 Redis的诞生于2008年,由Salvatore Sanfilippo开发.最初作者在开发一个网站时,需要实现一个高性能的队列功能,在使用Mysql无果后,决定自己造一个轮子 ...