Netty源码分析第七章: 编码器和写数据

第四节: 刷新buffer队列

上一小节学习了writeAndFlush的write方法, 这一小节我们剖析flush方法

通过前面的学习我们知道, flush方法通过事件传递, 最终会传递到HeadContext的flush方法:

public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe.flush();
}

这里最终会调用AbstractUnsafe的flush方法:

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

这里首先也是拿到ChannelOutboundBuffer对象

然后我们看这一步:

outboundBuffer.addFlush();

这一步同样也是调整ChannelOutboundBuffer的指针

跟进addFlush方法:

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;
}
}

首先声明一个entry指向unflushedEntry, 也就是第一个未flush的entry

通常情况下unflushedEntry是不为空的, 所以进入if

再未刷新前flushedEntry通常为空, 所以会执行到flushedEntry = entry

也就是flushedEntry指向entry

经过上述操作, 缓冲区的指针情况如图所示:

7-4-1

然后通过do-while将, 不断寻找unflushedEntry后面的节点, 直到没有节点为止

flushed自增代表需要刷新多少个节点

循环中我们关注这一步

decrementPendingOutboundBytes(pending, false, true);

这一步也是统计缓冲区中的字节数, 但是是和上一小节的incrementPendingOutboundBytes正好是相反, 因为这里是刷新, 所以这里要减掉刷新后的字节数,

我们跟到方法中:

private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
if (size == 0) {
return;
}
//从总的大小减去
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
//直到减到小于某一个阈值32个字节
if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) {
//设置写状态
setWritable(invokeLater);
}
}

同样TOTAL_PENDING_SIZE_UPDATER代表缓冲区的字节数, 这里的addAndGet中参数是-size, 也就是减掉size的长度

再看 if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark())

getWriteBufferLowWaterMark()代表写buffer的第水位值, 也就是32k, 如果写buffer的长度小于这个数, 就通过setWritable方法设置写状态

也就是通道由原来的不可写改成可写

回到addFlush方法:

遍历do-while循环结束之后, 将unflushedEntry指为空, 代表所有的entry都是可写的

经过上述操作, 缓冲区的指针情况如下图所示:

7-4-2

回到AbstractUnsafe的flush方法:

指针调整完之后, 我们跟到flush0()方法中:

protected void flush0() {
if (inFlush0) {
return;
}
final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null || outboundBuffer.isEmpty()) {
return;
}
inFlush0 = true;
if (!isActive()) {
try {
if (isOpen()) {
outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
} else {
outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
}
} finally {
inFlush0 = false;
}
return;
}
try {
doWrite(outboundBuffer);
} catch (Throwable t) {
if (t instanceof IOException && config().isAutoClose()) {
close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
} else {
outboundBuffer.failFlushed(t, true);
}
} finally {
inFlush0 = false;
}
}

if (inFlush0) 表示判断当前flush是否在进行中, 如果在进行中, 则返回, 避免重复进入

我们重点关注doWrite方法

跟到AbstractNioByteChannel的doWrite方法中去:

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = -1;
boolean setOpWrite = false;
for (;;) {
//每次拿到当前节点
Object msg = in.current();
if (msg == null) {
clearOpWrite();
return;
}
if (msg instanceof ByteBuf) {
//转化成ByteBuf
ByteBuf buf = (ByteBuf) msg;
//如果没有可写的值
int readableBytes = buf.readableBytes();
if (readableBytes == 0) {
//移除
in.remove();
continue;
}
boolean done = false;
long flushedAmount = 0;
if (writeSpinCount == -1) {
writeSpinCount = config().getWriteSpinCount();
}
for (int i = writeSpinCount - 1; i >= 0; i --) {
//将buf写入到socket里面
//localFlushedAmount代表向jdk底层写了多少字节
int localFlushedAmount = doWriteBytes(buf);
//如果一个字节没写, 直接break
if (localFlushedAmount == 0) {
setOpWrite = true;
break;
}
//统计总共写了多少字节
flushedAmount += localFlushedAmount;
//如果buffer全部写到jdk底层
if (!buf.isReadable()) {
//标记全写道
done = true;
break;
}
}
in.progress(flushedAmount);
if (done) {
//移除当前对象
in.remove();
} else {
break;
}
} else if (msg instanceof FileRegion) {
//代码省略
} else {
throw new Error();
}
}
incompleteWrite(setOpWrite);
}

首先是一个无限for循环

Object msg = in.current() 这一步是拿到flushedEntry指向的entry中的msg

跟到current()方法中:

public Object current() {
Entry entry = flushedEntry;
if (entry == null) {
return null;
}
return entry.msg;
}

这里直接拿到flushedEntry指向的entry中关联的msg, 也就是一个ByteBuf

回到doWrite方法:

如果msg为null, 说明没有可以刷新的entry, 则调用clearOpWrite()方法清除写标识

如果msg不为null, 则会判断是否是ByteBuf类型, 如果是ByteBuf, 就进入if块中的逻辑

if块中首先将msg转化为ByteBuf, 然后判断ByteBuf是否可读, 如果不可读, 则通过in.remove()将当前的byteBuf所关联的entry移除, 然后跳过这次循环进入下次循环

remove方法稍后分析, 这里我们先继续往下看

boolean done = false 这里设置一个标识, 标识刷新操作是否执行完成, 这里默认值为false代表走到这里没有执行完成

writeSpinCount = config().getWriteSpinCount() 这里是获得一个写操作的循环次数, 默认是16

然后根据这个循环次数, 进行循环的写操作

在循环中, 关注这一步:

int localFlushedAmount = doWriteBytes(buf);

这一步就是将buf的内容写到channel中, 并返回写的字节数, 这里会调用NioSocketChannel的doWriteBytes

我们跟到doWriteBytes方法中:

protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}

这里首先拿到buf的可读字节数, 然后通过readBytes将可读字节写入到jdk底层的channel中

回到doWrite方法:

将内容写的jdk底层的channel之后, 如果一个字节都没写, 说明现在channel可能不可写, 将setOpWrite设置为true, 用于标识写操作位, 并退出循环

如果已经写出字节, 则通过 flushedAmount += localFlushedAmount 累加写出的字节数

然后根据是buf是否没有可读字节数判断是否buf的数据已经写完, 如果写完, 将done设置为true, 说明写操作完成, 并退出循环

因为有时候不一定一次就能将byteBuf所有的字节写完, 所以这里会继续通过循环进行写出, 直到循环到16次

如果ByteBuf内容完全写完, 会通过in.remove()将当前entry移除掉

我们跟到remove方法中:

public boolean remove() {
//拿到当前第一个flush的entry
Entry e = flushedEntry;
if (e == null) {
clearNioBuffers();
return false;
}
Object msg = e.msg;
ChannelPromise promise = e.promise;
int size = e.pendingSize;
removeEntry(e);
if (!e.cancelled) {
ReferenceCountUtil.safeRelease(msg);
safeSuccess(promise);
decrementPendingOutboundBytes(size, false, true);
}
e.recycle();
return true;
}

首先拿到当前的flushedEntry

我们重点关注removeEntry这步, 跟进去:

private void removeEntry(Entry e) {
if (-- flushed == 0) {
//位置为空
flushedEntry = null;
//如果是最后一个节点
if (e == tailEntry) {
//全部设置为空
tailEntry = null;
unflushedEntry = null;
}
} else {
//移动到下一个节点
flushedEntry = e.next;
}
}

if (-- flushed == 0) 表示当前节点是否为需要刷新的最后一个节点, 如果是, 则flushedEntry指针设置为空

如果当前节点是tailEntry节点, 说明当前节点是最后一个节点, 将tailEntry和unflushedEntry两个指针全部设置为空

如果当前节点不是需要刷新的最后的一个节点, 则通过 flushedEntry = e.nex t这步将flushedEntry指针移动到下一个节点

以上就是flush操作的相关逻辑

上一节: 写buffer队列

下一节: Future和Promies

Netty源码分析第7章(编码器和写数据)---->第4节: 刷新buffer队列的更多相关文章

  1. Netty源码分析第7章(编码器和写数据)---->第5节: Future和Promies

    Netty源码分析第七章: 编码器和写数据 第五节: Future和Promise Netty中的Future, 其实类似于jdk的Future, 用于异步获取执行结果 Promise则相当于一个被观 ...

  2. Netty源码分析第7章(编码器和写数据)---->第1节: writeAndFlush的事件传播

    Netty源码分析第七章: 编码器和写数据 概述: 上一小章我们介绍了解码器, 这一章我们介绍编码器 其实编码器和解码器比较类似, 编码器也是一个handler, 并且属于outbounfHandle ...

  3. Netty源码分析第7章(编码器和写数据)---->第2节: MessageToByteEncoder

    Netty源码分析第七章: Netty源码分析 第二节: MessageToByteEncoder 同解码器一样, 编码器中也有一个抽象类叫MessageToByteEncoder, 其中定义了编码器 ...

  4. Netty源码分析第7章(编码器和写数据)---->第3节: 写buffer队列

    Netty源码分析七章: 编码器和写数据 第三节: 写buffer队列 之前的小节我们介绍过, writeAndFlush方法其实最终会调用write和flush方法 write方法最终会传递到hea ...

  5. Netty源码分析第6章(解码器)---->第1节: ByteToMessageDecoder

    Netty源码分析第六章: 解码器 概述: 在我们上一个章节遗留过一个问题, 就是如果Server在读取客户端的数据的时候, 如果一次读取不完整, 就触发channelRead事件, 那么Netty是 ...

  6. Netty源码分析第3章(客户端接入流程)---->第1节: 初始化NioSockectChannelConfig

    Netty源码分析第三章: 客户端接入流程 概述: 之前的章节学习了server启动以及eventLoop相关的逻辑, eventLoop轮询到客户端接入事件之后是如何处理的?这一章我们循序渐进, 带 ...

  7. Netty源码分析第3章(客户端接入流程)---->第2节: 处理接入事件之handle的创建

    Netty源码分析第三章: 客户端接入流程 第二节: 处理接入事件之handle的创建 上一小节我们剖析完成了与channel绑定的ChannelConfig初始化相关的流程, 这一小节继续剖析客户端 ...

  8. Netty源码分析第3章(客户端接入流程)---->第3节: NioSocketChannel的创建

    Netty源码分析第三章: 客户端接入流程 第三节: NioSocketChannel的创建 回到上一小节的read()方法: public void read() { //必须是NioEventLo ...

  9. Netty源码分析第3章(客户端接入流程)---->第4节: NioSocketChannel注册到selector

    Netty源码分析第三章: 客户端接入流程 第四节: NioSocketChannel注册到selector 我们回到最初的NioMessageUnsafe的read()方法: public void ...

随机推荐

  1. Yii设置Cache缓存的方法

    先在配置文件components数组中加上: 'cache'=>array( 'class'=>'CFileCache'), 设置Cache: Yii::app()->cache-& ...

  2. java学习笔记-JavaWeb篇三

    63 JSTL_表达式操作64 JSTL_流程控制操作 65 JSTL_迭代操作 66 JSTL_URL操作67 JSTL_改写MVC案例68 Filter概述 69 创建HttpFilter 70 ...

  3. kubernetes pvc pv 坑

    这里遇到一个问题,开始建立的pv死活claim为空,查看pv以及pvc的配置发现并没有任何名称上的关联,继续研究,发现纯粹是通过storage大小进行匹配的,之前因为照抄书本,一个是5G,一个是8G所 ...

  4. kubernetes 比较好的案例-创建tomcat-mysql集群

    安装部署一个tomcat+mysql应用 apiVersion: v1 kind: ReplicationController metadata: name: myweb spec:   //spec ...

  5. 学习JavaWeb aop两种配置方式

    aop aop:面向切面编程,它可以解决重复代码. aop有两种方式: 一..xml方式 1.在springmvc-servlet.xml中配置aop,应用bean文件: <!--aop配置-- ...

  6. mac上cocoapods安装与卸载

    安装 # 安装最新beta版 sudo gem install cocoapods --pre -n /usr/local/bin # 安装最新稳定版 sudo gem install cocoapo ...

  7. HBase基础概念

    定义 非关系型分布式列式数据库,支持大数据量查询(百万,上亿行) 概要 数据存储:HDFS 数据计算:MapReduce/Spark 服务协调:Zookeeper 特征 列式存储(列只有一种类型byt ...

  8. rtthread移植到jz2440之BootLoader

    从2016年第一次接触rtthread,感觉很容易上手,记得一个项目是小飞行器上的IPC,趁着空闲,手里有一块jz2440的板子,准备在这块板子上跑起来rtthread,查了很多资料,最后决定自己写一 ...

  9. Bugku Writeup —文件上传2(湖湘杯)

    我们先来看下题目,题目说明是文件上传 我们可以尝试通过构造payload来进行测试 php://filter/read=convert.base64-encode/resource=flag 获取到f ...

  10. 如何防止网页被植入广告,内容被监控-HTTPS

    前几天一朋友说访问网站页面底部怎么出现小广告了呢,内容有点不雅,朋友截图发给我,调侃我说怎么放这种广告,我一听纳闷,网站运行伊始,从来没有投放过任何广告,更别说不雅广告了. 最近还遇到一个问题就是,网 ...