目录:

前言

  1. ChannelOutboundBuffer 介绍
  2. addMessage 方法
  3. addFlush 方法
  4. flush0 方法
  5. 缓冲区扩展思考
  6. 总结

每个 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);
}

说说方法步骤:

  1. 根据 ByteBuf 相互属性和 promise 创建一个 Entry 节点。
  2. 将新的节点追加到 tailEntry 节点上。如果考虑之前的全部被清空了话,则新节点就是唯一节点,unflushedEntry 属性就是新的节点。可对照上面的图来看。
  3. 使用 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;
}
}

结合上面的图:

  1. 首先拿到未刷新的头节点。
  2. 判 null 之后,将这个 unflushedEntry 赋值给 flushedEntry,而这里的判 null 是做什么呢?防止多次调用 flush 。
  3. 循环尝试设置这些节点,告诉他们不能做取消操作了,如果尝试失败了,就将这个节点取消,在调用 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 属性的重要性)的更多相关文章

  1. netty服务端启动--ServerBootstrap源码解析

    netty服务端启动--ServerBootstrap源码解析 前面的第一篇文章中,我以spark中的netty客户端的创建为切入点,分析了netty的客户端引导类Bootstrap的参数设置以及启动 ...

  2. Netty(四):AbstractChannel源码解析

    首先我们通过一张继承关系的图来认识下AbstractChannel在Netty中的位置. 除了Comaprable接口来自java自带的包,其他都是Netty包中提供的. Comparable接口定义 ...

  3. Netty(三):IdleStateHandler源码解析

    IdleStateHandler是Netty为我们提供的检测连接有效性的处理器,一共有读空闲,写空闲,读/写空闲三种监测机制. 将其添加到我们的ChannelPipline中,便可以用来检测空闲. 先 ...

  4. Netty(六):NioServerSocketChannel源码解析

    我们在Netty学习系列五的最后提出了一些问题还没得到回答,今天来通过学习NioServerSocketChannel的源码来帮我们找到之前问题的答案. 先看一下NioServerSocketChan ...

  5. Netty 解码器抽象父类 ByteToMessageDecoder 源码解析

    前言 Netty 的解码器有很多种,比如基于长度的,基于分割符的,私有协议的.但是,总体的思路都是一致的. 拆包思路:当数据满足了 解码条件时,将其拆开.放到数组.然后发送到业务 handler 处理 ...

  6. sprin源码解析之属性编辑器propertyEditor

    目录 异常信息 造成此异常的原因 bean 配置文件 调用代码 特别说明: 异常解决 注册springt自带的属性编辑器 CustomDateEditor 控制台输出 属性编辑器是何时并如何被注册到s ...

  7. spring源码解析之属性编辑器propertyEditor

    异常信息造成此异常的原因bean配置文件调用代码特别说明:异常解决注册springt自带的属性编辑器 CustomDateEditor控制台输出属性编辑器是何时并如何被注册到spring容器中的?查看 ...

  8. 5.2 dubbo-compiler源码解析

    ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class); final P ...

  9. (一)ArrayList集合源码解析

    一.ArrayList的集合特点 问题 结      论 ArrayList是否允许空 允许 ArrayList是否允许重复数据 允许 ArrayList是否有序 有序 ArrayList是否线程安全 ...

随机推荐

  1. css中box-sizing简单说明(标准盒式模型和怪异盒式模型)

    今天写程序做布局的时候,遇到关于css中盒式模型的问题,百度了下这属性的解释,脑大啊,文字太绕看不懂.怎么办,于是自己动动手写了段程序测试,嗯,不错,一看效果就恍然大明白了.这里简单说明下,也可能说得 ...

  2. 10 个免费的Bootstrap Admin 主题,模板收集

    In designing websites today, one of the must have frameworks is the twitter bootstrap. To those who ...

  3. 解决.net+steeltoe服务客户端被服务调用出现400BadRequst错误

    一直尝试用steeltoe的官方示例被调用,一直报400BadRequst错误,换用Java写了一个简单client服务,却能正常被调用. 百思不得其解,用了一晚上填坑,开始觉得是不是IP没绑定,服务 ...

  4. .NET高级代码审计(第四课) JavaScriptSerializer反序列化漏洞

    0X00 前言 在.NET处理 Ajax应用的时候,通常序列化功能由JavaScriptSerializer类提供,它是.NET2.0之后内部实现的序列化功能的类,位于命名空间System.Web.S ...

  5. C# Winform 换肤

    本来计划接着上篇 C# Winform模仿百度日历,发现一时半会写不完,只写了一小半还不全,暂且搁置下.现在计划下班后每天至少写一篇博客,未能完成的等周末(不加班都情况)补充完整. 本篇博客窗体换肤, ...

  6. 开机或联网时自启动gunicorn

    网站部署完后,如果每次使用gunicorn启动网站会很麻烦,因此必须使gunicorn自启动. 环境 ubuntu 16. 参考: http://www.yangfan.cc/zhanzhang/14 ...

  7. Tomcat在Linux下的安装

    按部就班的把 tomcat 上传到 Linux 我创建了一个文件夹用作存放解压文件 ( tomcat只要解压就可以使用 ) 解压  :  tar -xvf apache-tomcat-7.0.52.t ...

  8. 利用CVE-2018-0950漏洞自动窃取Windows密码

    i春秋作家:浅安 0×00 前言 记得还在2016年的时候,我的一个同事那时还在使用CERT BFF(自动化模糊测试工具),他向我询问到如何将微软办公软件中看起来可利用的漏洞转化成可以运行的病毒程序并 ...

  9. UPX源码分析——加壳篇

    0x00 前言 UPX作为一个跨平台的著名开源压缩壳,随着Android的兴起,许多开发者和公司将其和其变种应用在.so库的加密防护中.虽然针对UPX及其变种的使用和脱壳都有教程可查,但是至少在中文网 ...

  10. java.lang.System.setProperty()方法实例

    java.lang.System.setProperty() 方法设置指定键指定的系统属性. 声明 以下是java.lang.System.setProperty()方法的声明 public stat ...