网络编程-Netty-writeAndFlush方法原理分析 以及 close以后是否还能写入数据?
前言
在上一讲网络编程-关闭连接(2)-Java的NIO在关闭socket时,究竟用了哪个系统调用函数?中,我们做了个实验,研究了java nio的close函数究竟调用了哪个系统调用,答案是close,但在真实的测试代码中,其实我犯了一个小错误,在close之后并没有return,所以在测试close之后,还做了writeAndFlush操作发送了一条数据,并且执行过程并没有报错。这件事让我关注起了close和之后的writeAndFlush之间的关系。为什么在close之后”看起来“还可以继续写入呢?
原始代码如下:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//写入本地文件测试字符,然后关闭channel
FileWriter fileWriter = new FileWriter("/root/test.txt");
fileWriter.write("test test hold on");
fileWriter.flush();
fileWriter.close();
//调用同步方法关闭
ChannelFuture sync = ctx.channel().close().sync();
if(sync.isSuccess()){
System.out.println("关闭成功!");
}else{
System.out.println("关闭失败!");
}
//这里开始,是误执行的语句
this.ctx = ctx;
//发送心跳指令
if (count.intValue() > 150) {
count.set(1);
}
Command0C04 command0C04 = new Command0C04(count.intValue());
byte[] encode = command0C04.encode();
logger.info("心跳指令:" + HexStringUtils.toHexString(encode));
ctx.channel().writeAndFlush(encode).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
System.out.println("success:"+channelFuture.isSuccess());
System.out.println("cancelled:"+channelFuture.isCancelled());
System.out.println("done:"+channelFuture.isDone());
System.out.println("isCancellable:"+channelFuture.isCancellable());
}
});
count.getAndIncrement();
}
我们知道,close系统调用会关闭读和写两个方向的操作,那么writeAndFlush在close之后具体是如何执行的?netty是怎么确保不会写入到发送缓冲区中呢?
想研究清楚这个问题,需要先看writeAndFlush操作做了什么,涉及到什么底层的数据结构。
writeAndFlush原理
简言之,writeAndFlush,在底层会做两个操作
- write操作
- flush操作
首先分析write操作。
write操作
netty底层会维护一个重要的数据结构,ChannelOutboundBuffer,这是一个单向链表。我们调用写的方法其实会把数据先缓存到这个数据结构中,等调用flush之后,就会真正的把数据写入到发送缓冲区当中。
ChannelOutBoundBuffer中有以下几个重要的指针:

- Entry代表了我们发送的数据
- flushedEntry代表需要写入到发送缓冲区的第一个Entry
- unflushedEntry代表第一个等待写入发送缓冲区的Entry
当第一次调用addMessage方法往ChannelOutBoundBuffer中添加数据时

第二次调用addMessage方法时,数据指针如下

如果不调用Flush,那么flushedEntry指针一直为null,数据会一直写入到后面的链表中。
Flush操作
当调用Flush操作后,指针情况如图:

之后的代码,就是遍历这段节点数据,写入到发送缓冲区中,并且写入后释放节点内存。
判断缓冲区是否可写(小知识)
在实际flush之前,netty调用isFlushPending判断,这个channel是否注册了可写事件,如果有可写事件就等会再发送。如果没有,就会调用父类的flush0方法直接写。
- 注:如果到达发送缓冲区的水位线了,发送缓冲区本身就不可写了,这个时候会(XX会)注册一个可写事件到selector中,netty就是使用这个可写判断是否可以真正的发送。
protected final void flush0() {
if (!isFlushPending()) {
super.flush0();
}
}
private boolean isFlushPending() {
SelectionKey selectionKey = selectionKey();
return selectionKey.isValid() && (selectionKey.interestOps() & SelectionKey.OP_WRITE) != 0;
}
OOM?
如果接收端消费速度很慢,接收缓冲区满了以后,会导致发送缓冲区无法继续发送数据,在一直发送数据的前提下,ChannelOutboundBuffer会一直上涨,可能会引起OOM问题。
Netty官方提供了两个ChannelOutBoundBuffer配置参数、一个Channel属性和一个用户回调方法来帮助我们识别和解决这件事。
两个ChannelOutBoundBuffer配置参数:
Channel.config().setWriteBufferHighWaterMark:高水位,默认64 kb
Channel.config().setWriteBufferLowWaterMark :低水位:默认32 kb
一个Channel属性:isWritable
一个用户回调方法:fireChannelWritabilityChanged
内部逻辑如下:
- 当本次需要添加到ChannelOutBoundBuffer的数据量超过了高水位,会改变isWritable对应的属性值从0变为1,并且触发一个ChannelWritabilityChanged事件。
- 当flush或者remove后,如果数据恢复到最低水位下了,会改变isWritable对应的属性值从1变为0,并且触发一个ChannelWritabilityChanged事件。
用户可以通过属性和回调方法来检查是否可写,做相关的业务处理。
writeAndFlush总结
在调用写入方法后,netty并不会直接把数据写入到发送缓冲区中,而是存储在了ChannelOutboundBuffer中,等到调用flush操作后,再把数据真正写入Socket的发送缓冲区中。
close以后是否还能写入数据?
跟踪close源码,最后会跟踪到io.netty.channel.AbstractChannel 的内部类 AbstractUnsafe中的close方法,方法代码如下(部分代码省略,只保留这个问题相关的核心代码):
private void close(final ChannelPromise promise, final Throwable cause,
final ClosedChannelException closeCause, final boolean notify) {
final boolean wasActive = isActive();
final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer.
}
可以看到,这里有一句this.outboundBuffer = null; 相当于把上文分析的ChannelOutboundBuffer置空。
结合同在AbstractUnsafe中的write代码中的这一部分来看(同样省略了非问题关注的代码)
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
// If the outboundBuffer is null we know the channel was closed and so
// need to fail the future right away. If it is not null the handling of the rest
// will be done in flush0()
// See https://github.com/netty/netty/issues/2362
safeSetFailure(promise, newWriteException(initialCloseCause));
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg);
return;
}
}
在write之前,会做判断,如果如果ChannelOutboundBuffer为空为空,那么释放内存,不发送数据并返回。
总结
首先我们了解了,在发送过程中比较重要的数据结构ChannelOutboundBuffer,然后我们了解了在close的时候,会把如果ChannelOutboundBuffer置空,并且在write的时候,会判断该buffer是否为空,为空则不发送,并设置失败,到此我们的问题就研究明白了。
网络编程-Netty-writeAndFlush方法原理分析 以及 close以后是否还能写入数据?的更多相关文章
- 网络编程Netty入门:ByteBuf分析
目录 Netty中的ByteBuf优势 NIO使用的ByteBuffer有哪些缺点 ByteBuf的优势和做了哪些增强 ByteBuf操作示例 ByteBuf操作 简单的Demo示例 堆内和堆外内存 ...
- 网络编程Netty入门:EventLoopGroup分析
目录 Netty线程模型 代码示例 NioEventLoopGroup初始化过程 NioEventLoopGroup启动过程 channel的初始化过程 Netty线程模型 Netty实现了React ...
- 并发编程 —— ConcurrentHashMap size 方法原理分析
前言 ConcurrentHashMap 博大精深,从他的 50 多个内部类就能看出来,似乎 JDK 的并发精髓都在里面了.但他依然拥有体验良好的 API 给我们使用,程序员根本感觉不到他内部的复杂. ...
- Python网络编程04 /recv工作原理、展示收发问题、粘包现象
Python网络编程04 /recv工作原理.展示收发问题.粘包现象 目录 Python网络编程04 /recv工作原理.展示收发问题.粘包现象 1. recv工作原理 2. 展示收发问题示例 发多次 ...
- C语言C++编程学习:排序原理分析
C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...
- socket网络编程 的基本方法:--ongoing
https://blog.csdn.net/shuxiaogd/article/details/50366039在学习网络编程时,我们总是从最简单的Server程序写起:socket -> bi ...
- 网络编程Netty入门:责任链模式介绍
目录 责任链模式 责任链模式的简单实现 Netty中的ChannelPipeline责任链 服务端接收客户端连接 pipeline初始化 入站事件和出站事件 Pipeline中的Handler Pip ...
- 网络编程Netty入门:Netty简介及其特性
目录 Netty的简介 Netty的特性 Netty的整体结构 Netty的核心组件 Netty的线程模型 结束语 Netty的简介 Netty是一个java开源框架,是基于NIO的高性能.高可扩展性 ...
- Java网络编程 -- Netty入门
Netty简介 Netty是一个高性能,高可扩展性的异步事件驱动的网络应用程序框架,它极大的简化了TCP和UDP客户端和服务器端网络开发.它是一个NIO框架,对Java NIO进行了良好的封装.作为一 ...
- Java网络编程--Netty中的责任链
Netty中的责任链 设计模式 - 责任链模式 责任链模式(Chain of Responsibility Pattern)是一种是行为型设计模式,它为请求创建了一个处理对象的链.其链中每一个节点都看 ...
随机推荐
- .Net程序员机会来了,微软官方新推出一个面向Windows开发者本地运行AI模型的开源工具
想要开发AI产品的.Net程序员机会来了,这个项目应该好好研究. 虽然说大模型基本都有提供网络API,但肯定没有直接使用本地模型速度快. 最近微软官方新推出AI Dev Gallery开源项目,可以帮 ...
- Python 潮流周刊#84:2024 年 Python 的最佳实践(摘要)
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...
- x86平台SIMD编程入门(2):通用指令
1.重解释转换 虽然128位的XMM寄存器在硬件上只是256位YMM寄存器的下半部分,但在C++中它们是不同的类型.有一些intrinsic函数可以将它们重新解释为不同的类型,如下表所示,行代表源类型 ...
- c代码部分封装为lib
需求:将一个C工程中的核心代码封装为静态文件:lib. 环境 工具:VC6.0++ 语言:c 以封装一个DES工程为例 封装 (1)新建一个静态工程 (2)新建c文件和h文件 (3)挑选封装内容 在原 ...
- 452:管理压缩的TAR存档
- Spaghetti pg walkthrough Intermediate
nmap ┌──(root㉿kali)-[~] └─# nmap -p- -A 192.168.170.160 Starting Nmap 7.94SVN ( https://nmap.org ) a ...
- .NET9 AOT部署
概念 AOT是Ahead-of-Time的缩写,AOT是将C#程序提前编译为机器代码(通常为平台特定的二进制文件),在应用程序启动之前完成编译过程. 微软官方文档是这样说的 将应用程序发布为本机 AO ...
- C# as 和 is 运算符区别和用法
前言 在C#中,as 和 is 关键字都用于处理类型转换的运算符,但它们有不同的用途和行为.本文我们将详细解释这两个运算符的区别和用法. is 运算符 is 运算符用于检查对象是否是某个特定类型,或者 ...
- 独“数”一帜 双证加冕!TeleDB亮相可信数据库发展大会
近日,2024可信数据库发展大会在北京召开,主题为"自主.创新.引领".大会重磅发布多项中国信通院及中国通信标准化协会大数据技术标准推进委员会(CCSA TC601)在数据库领域最 ...
- V-Control 开箱即用的.NET MAUI组件库发布了!
之前写过挺多的MAUI Sample,其中有很多代码可以打包成组件,当组件完善到一定程度,我会把控件封装起来放到控件库中. 今天,在这个仓库建立一年零八个月后,我觉得可以考虑将其作为开源库发布. 有很 ...