前面文章说了,ChannelHandlerContext#write只是将数据缓存到ChannelOutboundBuffer,等到ChannelHandlerContext#flush时,再将ChannelOutboundBuffer缓存的数据写到Channel中。

本文分享Netty中ChannelOutboundBuffer的实现以及Flush过程。

源码分析基于Netty 4.1

每个Channel的AbstractUnsafe#outboundBuffer 都维护了一个ChannelOutboundBuffer。

ChannelOutboundBuffer,出站数据缓冲区,负责缓存ChannelHandlerContext#write​的数据。通过链表管理数据,链表节点为内部类Entry。

关键字段如下

Entry tailEntry;		// 链表最后一个节点,新增的节点添加其后。
Entry unflushedEntry; // 链表中第一个未刷新的节点
Entry flushedEntry; // 链表中第一个已刷新但数据未写入的节点
int flushed; // 已刷新但数据未写入的节点数

ChannelHandlerContext#flush操作前,需要先刷新一遍待处理的节点(主要是统计本次ChannelHandlerContext#flush操作可以写入多少个节点数据),从unflushedEntry开始。刷新完成后使用flushedEntry标志第一个待写入的节点,flushed为待写入节点数。

前面分享Netty读写过程的文章说过,AbstractUnsafe#write处理写操作时,会调用ChannelOutboundBuffer#addMessage将数据缓存起来

public void addMessage(Object msg, int size, ChannelPromise promise) {
// #1
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
if (tailEntry == null) {
flushedEntry = null;
} else {
Entry tail = tailEntry;
tail.next = entry;
}
tailEntry = entry;
if (unflushedEntry == null) {
unflushedEntry = entry;
} incrementPendingOutboundBytes(entry.pendingSize, false);
}

#1 构建一个Entry,注意,这里使用了对象池RECYCLER,后面有文章详细解析。

主要是更新tailEntry和unflushedEntry

#2 如果当前缓存数量超过阀值WriteBufferWaterMark#high,更新unwritable标志为true,并触发pipeline.fireChannelWritabilityChanged()方法。

由于ChannelOutboundBuffer链表没有大小限制,不断累积数据可能导致 OOM,

为了避免这个问题,我们可以在unwritable标志为true时,不再继续缓存数据。

Netty只会更新unwritable标志,并不阻止数据缓存,我们可以根据需要实现该功能。示例如下

if (ctx.channel().isActive() && ctx.channel().isWritable()) {
ctx.writeAndFlush(responseMessage);
} else {
...
}

addFlush方法负责刷新节点(ChannelHandlerContext#flush操作前调用该方法统计可写入节点数据数)

public void addFlush() {
// #1
Entry entry = unflushedEntry;
if (entry != null) {
// #2
if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}
do {
// #3
flushed ++;
if (!entry.promise.setUncancellable()) {
// Was cancelled so make sure we free up memory and notify about the freed bytes
int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
// #4
} while (entry != null); // All flushed so reset unflushedEntry
// #5
unflushedEntry = null;
}
}

#1 从unflushedEntry节点开始处理

#2 赋值flushedEntry为unflushedEntry。

ChannelHandlerContext#flush写入完成后会置空flushedEntry

#3 增加flushed

设置节点的ChannelPromise不可取消

#4 从unflushedEntry开始,遍历后面节点

#5 置空unflushedEntry,表示当前所有节点都已刷新。

nioBuffers方法负责将当前缓存的ByteBuf转发为(jvm)ByteBuffer

public ByteBuffer[] nioBuffers(int maxCount, long maxBytes) {
assert maxCount > 0;
assert maxBytes > 0;
long nioBufferSize = 0;
int nioBufferCount = 0;
// #1
final InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
ByteBuffer[] nioBuffers = NIO_BUFFERS.get(threadLocalMap);
Entry entry = flushedEntry;
while (isFlushedEntry(entry) && entry.msg instanceof ByteBuf) {
if (!entry.cancelled) {
ByteBuf buf = (ByteBuf) entry.msg;
final int readerIndex = buf.readerIndex();
final int readableBytes = buf.writerIndex() - readerIndex; if (readableBytes > 0) {
// #2
if (maxBytes - readableBytes < nioBufferSize && nioBufferCount != 0) {
break;
}
nioBufferSize += readableBytes;
// #3
int count = entry.count;
if (count == -1) {
//noinspection ConstantValueVariableUse
entry.count = count = buf.nioBufferCount();
} int neededSpace = min(maxCount, nioBufferCount + count);
if (neededSpace > nioBuffers.length) {
nioBuffers = expandNioBufferArray(nioBuffers, neededSpace, nioBufferCount);
NIO_BUFFERS.set(threadLocalMap, nioBuffers);
}
// #4
if (count == 1) {
ByteBuffer nioBuf = entry.buf;
if (nioBuf == null) {
// cache ByteBuffer as it may need to create a new ByteBuffer instance if its a
// derived buffer
entry.buf = nioBuf = buf.internalNioBuffer(readerIndex, readableBytes);
}
nioBuffers[nioBufferCount++] = nioBuf;
} else {
...
}
if (nioBufferCount == maxCount) {
break;
}
}
}
entry = entry.next;
}
this.nioBufferCount = nioBufferCount;
this.nioBufferSize = nioBufferSize; return nioBuffers;
}

#1 从线程缓存中获取nioBuffers变量,这样可以避免反复构造ByteBuffer数组的性能损耗

#2 maxBytes,即本次操作最大的字节数。

maxBytes - readableBytes < nioBufferSize,表示如果本次操作后将超出maxBytes,退出

#3

buf.nioBufferCount(),获取ByteBuffer数量,CompositeByteBuf可能有多个ByteBuffer组成。

neededSpace,即nioBuffers数组中ByteBuffer数量,nioBuffers长度不够时需要扩容。

#4

buf.internalNioBuffer(readerIndex, readableBytes),使用readerIndex, readableBytes构造一个ByteBuffer。

这里涉及ByteBuf相关知识,后面有文章详细解析。

ChannelHandlerContext#flush完成后,需要移除对应的缓存节点。

public void removeBytes(long writtenBytes) {

	for (;;) {
// #1
Object msg = current();
if (!(msg instanceof ByteBuf)) {
assert writtenBytes == 0;
break;
} final ByteBuf buf = (ByteBuf) msg;
final int readerIndex = buf.readerIndex();
final int readableBytes = buf.writerIndex() - readerIndex;
// #2
if (readableBytes <= writtenBytes) {
if (writtenBytes != 0) {
progress(readableBytes);
writtenBytes -= readableBytes;
}
remove();
} else { // readableBytes > writtenBytes
// #3
if (writtenBytes != 0) {
buf.readerIndex(readerIndex + (int) writtenBytes);
progress(writtenBytes);
}
break;
}
}
clearNioBuffers();
}

#1

current方法返回flushedEntry节点缓存数据。

结果null时,退出循环

#2 当前节点的数据已经全部写入,

progress方法唤醒数据节点上ChannelProgressivePromise的监听者

writtenBytes减去对应字节数

remove()方法移除节点,释放ByteBuf,flushedEntry标志后移。

#3 当前节点的数据部分写入,它应该是本次ChannelHandlerContext#flush操作的最后一个节点

更新ByteBuf的readerIndex,下次从这里开始读取数据。

退出

移除数据节点

public boolean remove() {
Entry e = flushedEntry;
if (e == null) {
clearNioBuffers();
return false;
}
Object msg = e.msg; ChannelPromise promise = e.promise;
int size = e.pendingSize;
// #1
removeEntry(e); if (!e.cancelled) {
// only release message, notify and decrement if it was not canceled before.
// #2
ReferenceCountUtil.safeRelease(msg);
safeSuccess(promise);
decrementPendingOutboundBytes(size, false, true);
} // recycle the entry
// #3
e.recycle(); return true;
}

#1

flushed减1

当flushed为0时,flushedEntry赋值为null,否则flushedEntry指向后一个节点。

#2 释放ByteBuf

#3 当前节点返回对象池中,以便复用。

下面来看一下ChannelHandlerContext#flush操作过程。

ChannelHandlerContext#flush -> HeadContext#flush -> AbstractUnsafe#flush

public final void flush() {
assertEventLoop(); ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
// #1
outboundBuffer.addFlush();
// #2
flush0();
}

#1 刷新outboundBuffer中数据节点

#2 写入操作

flush -> NioSocketChannel#doWrite

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
SocketChannel ch = javaChannel();
int writeSpinCount = config().getWriteSpinCount();
do {
// #1
if (in.isEmpty()) {
clearOpWrite();
return;
} // #2
int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
int nioBufferCnt = in.nioBufferCount(); switch (nioBufferCnt) {
case 0:
// #3
writeSpinCount -= doWrite0(in);
break;
case 1: {
// #4
ByteBuffer buffer = nioBuffers[0];
int attemptedBytes = buffer.remaining();
final int localWrittenBytes = ch.write(buffer);
if (localWrittenBytes <= 0) {
// #5
incompleteWrite(true);
return;
}
adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
// #6
in.removeBytes(localWrittenBytes);
--writeSpinCount;
break;
}
default: {
// #7
...
}
}
} while (writeSpinCount > 0); incompleteWrite(writeSpinCount < 0);
}

#1 通过ChannelOutboundBuffer#flushed判断是否没有数据可以写,没有数据则清除关注事件OP_WRITE,直接返回。

#2 获取ChannelOutboundBuffer中ByteBuf维护的(jvm)ByteBuffer,并统计nioBufferSize,nioBufferCount。

#3 这时没有ByteBuffer,但是可能有其他类型的数据(如FileRegion类型),调用doWrite0继续处理,这里不再深入

#4 只有一个ByteBuffer,调用SocketChannel#write将数据写入Channel。

#5 如果写入数据数量小于等于0,说明数据没有被写出去(可能是因为套接字的缓冲区满了等原因),那么就需要关注该Channel上的OP_WRITE事件,方便下次EventLoop将Channel轮询出来的时候,能继续写数据。

#6 移除ChannelOutboundBuffer缓存数据节点。

#7 有多个ByteBuffer,调用SocketChannel#write(ByteBuffer[] srcs, int offset, int length),批量写入,与上一种情况处理类似

回顾之前文章《事件循环机制实现原理》中对NioEventLoop#processSelectedKey方法的解析

	...
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}

这里会调用forceFlush方法,再次写入数据。

FlushConsolidationHandler

ChannelHandlerContext#flush是很昂贵的操作,可能触发系统调用,但数据又不能缓存太久,使用FlushConsolidationHandler可以尽量达到写入延迟与吞吐量之间的权衡。

FlushConsolidationHandler中维护了explicitFlushAfterFlushes变量,

在ChannelOutboundHandler#channelRead中调用flush,如果调用次数小于explicitFlushAfterFlushes, 会拦截flush操作不执行。

在channelReadComplete后调用flush,则不会拦截flush操作。

本文涉及ByteBuf组件,它是Netty中的内存缓冲区,后面有文章解析。

如果您觉得本文不错,欢迎关注我的微信公众号,系列文章持续更新中。您的关注是我坚持的动力!

Netty源码解析 -- ChannelOutboundBuffer实现与Flush过程的更多相关文章

  1. Netty 源码解析(九): connect 过程和 bind 过程分析

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第九篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  2. Netty源码解析 -- ChannelPipeline机制与读写过程

    本文继续阅读Netty源码,解析ChannelPipeline事件传播原理,以及Netty读写过程. 源码分析基于Netty 4.1 ChannelPipeline Netty中的ChannelPip ...

  3. Netty 源码解析(四): Netty 的 ChannelPipeline

    今天是猿灯塔“365篇原创计划”第四篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty ...

  4. Netty 源码解析(三): Netty 的 Future 和 Promise

    今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...

  5. Netty 源码解析(八): 回到 Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第八篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  6. Netty 源码解析(七): NioEventLoop 工作流程

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第七篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  7. Netty 源码解析(六): Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第六篇. 接下来的时间灯塔君持续更新Netty系列一共九篇   Netty 源码解析(一 ):开始 Netty ...

  8. Netty 源码解析(五): Netty 的线程池分析

    今天是猿灯塔“365篇原创计划”第五篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty ...

  9. Netty 源码解析(二):Netty 的 Channel

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty源码解析(一):开始 当前:Netty 源码解析(二): Netty 的 Channel ...

随机推荐

  1. OpenCV中threshold函数的使用

    转自:https://blog.csdn.net/u012566751/article/details/77046445 一篇很好的介绍threshold文章: 图像的二值化就是将图像上的像素点的灰度 ...

  2. 【题解】CF1207E XOR Guessing

    Link 这是一道交互题. \(\text{Solution:}\) 观察到猜的数范围只有\(2^{14}.\) 我第一次想到的方法是,我们可以确定系统选择的两个数的异或和,用这个异或和去穷举所有目标 ...

  3. Linux软件漏洞-1

    RHSA-2018:3107-中危: wpa_supplicant 安全和BUG修复更新 漏洞编号:CVE-2018-14526 漏洞公告:wpa_supplicant中未经身份验证的EAPOL-Ke ...

  4. TP5发送邮件

    1,前提去qq邮箱开启smtp 2,生成授权码 2,发送短信给 3,附上代码 贴上代码如下 <?phpnamespace app\mails\controller;use \think\Cont ...

  5. 微信小程序中使用 npm包管理 (保姆式教程)

    打开自己的微信小程序项目,在勾选这个选项 然后在第一次应该是失败的提示"没有找到可以构建的npm包". 在 小程序的根目录下比如我的项目如图: 右击鼠标在终端中打开. 然后输入:n ...

  6. 多测师讲解接口测试 _接口测试思路_高级讲师肖sir

  7. Android开发Settings源码分析之主界面加载(二)

    现在都说互联网寒冬,其实只要自身技术能力够强,咱们就不怕!我这边专门针对Android开发工程师整理了一套[Android进阶学习视频].[全套Android面试秘籍].[Android知识点PDF] ...

  8. Java常见的一些经典面试题(附答案解析)

    前言: 我想每个程序员比较头疼的事情都是:工作拧螺丝,面试造火箭吧.但是又必须经历这个过程,尤其是弄不清面试官问的问题,如果你准备的不是很充分,会导致面试的时候手足无措.今天这篇文章是从已工作5年的程 ...

  9. golang xpath解析网页

    https://github.com/antchfx/htmlquery package main import ( "fmt" "github.com/antchfx/ ...

  10. PHP SPL标准库-数据结构

    SPL是用于解决典型问题的一组接口与类的集合. 双向链表 SplDoublyLinkedList SplStack SplQueue 双链表是一种重要的线性存储结构,对于双链表中的每个节点,不仅仅存储 ...