前言

在上一讲网络编程-关闭连接(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以后是否还能写入数据?的更多相关文章

  1. 网络编程Netty入门:ByteBuf分析

    目录 Netty中的ByteBuf优势 NIO使用的ByteBuffer有哪些缺点 ByteBuf的优势和做了哪些增强 ByteBuf操作示例 ByteBuf操作 简单的Demo示例 堆内和堆外内存 ...

  2. 网络编程Netty入门:EventLoopGroup分析

    目录 Netty线程模型 代码示例 NioEventLoopGroup初始化过程 NioEventLoopGroup启动过程 channel的初始化过程 Netty线程模型 Netty实现了React ...

  3. 并发编程 —— ConcurrentHashMap size 方法原理分析

    前言 ConcurrentHashMap 博大精深,从他的 50 多个内部类就能看出来,似乎 JDK 的并发精髓都在里面了.但他依然拥有体验良好的 API 给我们使用,程序员根本感觉不到他内部的复杂. ...

  4. Python网络编程04 /recv工作原理、展示收发问题、粘包现象

    Python网络编程04 /recv工作原理.展示收发问题.粘包现象 目录 Python网络编程04 /recv工作原理.展示收发问题.粘包现象 1. recv工作原理 2. 展示收发问题示例 发多次 ...

  5. C语言C++编程学习:排序原理分析

    C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...

  6. socket网络编程 的基本方法:--ongoing

    https://blog.csdn.net/shuxiaogd/article/details/50366039在学习网络编程时,我们总是从最简单的Server程序写起:socket -> bi ...

  7. 网络编程Netty入门:责任链模式介绍

    目录 责任链模式 责任链模式的简单实现 Netty中的ChannelPipeline责任链 服务端接收客户端连接 pipeline初始化 入站事件和出站事件 Pipeline中的Handler Pip ...

  8. 网络编程Netty入门:Netty简介及其特性

    目录 Netty的简介 Netty的特性 Netty的整体结构 Netty的核心组件 Netty的线程模型 结束语 Netty的简介 Netty是一个java开源框架,是基于NIO的高性能.高可扩展性 ...

  9. Java网络编程 -- Netty入门

    Netty简介 Netty是一个高性能,高可扩展性的异步事件驱动的网络应用程序框架,它极大的简化了TCP和UDP客户端和服务器端网络开发.它是一个NIO框架,对Java NIO进行了良好的封装.作为一 ...

  10. Java网络编程--Netty中的责任链

    Netty中的责任链 设计模式 - 责任链模式 责任链模式(Chain of Responsibility Pattern)是一种是行为型设计模式,它为请求创建了一个处理对象的链.其链中每一个节点都看 ...

随机推荐

  1. 数组 | 切片 | map | Go语言

    数组 1.数组的长度需要声明 2.存储的数据类型必须一致 3.可以通过下标来访问,超出长度问报访问越界的错误 4.不支持负数索引 5.数组是值类型,传递的都是拷贝,不会对原来的对象进行修改 6.Go中 ...

  2. 前端学习openLayers配合vue3(偏移动画效果,限制范围)

    我们原来的偏移感觉比较生硬,我们来学习一下偏移的动画,先列一下这节的知识点 限制经纬度范围和缩放范围(view层) view = new View({ center:[114.305469,30.59 ...

  3. 2021 年万圣节 Github 彩蛋

    记录每年 Github 万圣节彩蛋,也记录有来项目成长历程. 2021 万圣节彩蛋 2020 万圣节彩蛋

  4. Pod的优雅上下线

    Pod的优雅上下线依赖k8s的监控检查机制,以及 Pod lifecycle Hooks,通过这些kubernetes的机制,配合服务发现的流量管理机制,实现业务的优雅上下线. 基础概念 Pod 健康 ...

  5. java多线程与线程池-copy

    1. 场景描述 以前多线程也常用,这次因需再页面上用到多线程,如下图,总结下,有需要的朋友可以参考下. 2. 解决方案 2.1 线程池概念 线程池官方定义不说了,通俗说下:池子的概念,事先(预定义)创 ...

  6. nvim及插件安装配置

    1. install neovim 1 sudo apt install neovim After installing neovim, we can delete old vi. 3. instal ...

  7. ctfshow--web10 php代码逻辑漏洞with rollup注入

    dirsearch没有扫到文件 查看源代码发现 有个style.css文件点击查看 查看index.phps代码 又是代码审计 点击查看代码 <?php $flag=""; ...

  8. 认识soui4js(第2篇):代码编辑及调试

    开始 假定您使用向导在d:\jsdemo目录创建一个工程,您也已经安装好了vscode, 那么您应该可以看到下面的界面效果: 工程生成后,主要包含一个soui资源包及一个main.js 要运行这个程序 ...

  9. Jenkins使用maven打包项目

    Jenkins使用maven打包项目 作为一名软件测试工程师,在日常工作中,我们经常需要使用Jenkins进行持续集成和持续部署(CI/CD).而Maven作为Java项目的构建工具,更是不可或缺.今 ...

  10. JMeter下载与环境配置

    前置 使用JMeter前需要先配置好Java的环境变量,不会的朋友可以阅读:https://www.cnblogs.com/test-gang/p/18144441 JMeter下载 JMeter官网 ...