前言

在上一讲网络编程-关闭连接(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. CDS标准视图:技术对象检验级别 I_TechObjInspectionLevelCode

    视图名称:技术对象检验级别 I_TechObjInspectionLevelCode 视图类型:基础 视图代码: 点击查看代码 @AbapCatalog: { sqlViewName: 'ITECHO ...

  2. c# set Webbowser version with WPF/Winform app

    <Window x:Class="TestWPF.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 ...

  3. mybatis中的数据源和连接池

    1.核心配置文件中配置数据库相关属性 <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE con ...

  4. springBoot(2)--初步理解

    一.定时任务 1.步骤: 1:在启动类上写@EnableScheduling注解 2:在要定时任务的类上写@component 3:在要定时执行的方法上写@Scheduled(fixedRate=毫秒 ...

  5. 基于FATE的可验证秘密分享算法详解及应用场景分享:学习

    内容来自"光大科技-基于FATE的可验证秘密分享算法详解及应用场景分享" 理论 基于Shamir的秘密共享方案,通过多项式插值实现. 加入可验证功能,即发送多项式系数的模数给对方作 ...

  6. 自定义Ollama安装路径

    由于Ollama的exe安装软件双击安装的时候默认是在C盘,以及后续的模型数据下载也在C盘,导致会占用C盘空间,所以这里单独写了一个自定义安装Ollama安装目录的教程. Ollama官网地址:htt ...

  7. C# Winform 实现静态变量属性的值变了,触发事件,类似WPF的双向绑定

    在C# WinForms中,虽然没有像WPF那样内置的双向绑定机制,但你可以通过事件和属性封装来实现类似的功能.具体来说,你可以在静态属性的set访问器中触发一个自定义事件,然后在需要的地方订阅这个事 ...

  8. FLink自定义Source,不停生产数据

    一.代码模板 VideoOrder.java package net.xdclass.model; import java.util.Date; import lombok.AllArgsConstr ...

  9. GUI编程之Swing

    窗口 面板  package com.yeyue.lesson04; ​ import javax.swing.*; import java.awt.*; ​ public class JFrameD ...

  10. FreeSql学习笔记——4.联表

    前言   上一章节是查询,记录了简单的查询,比较看好的是分块.Dto映射和分页,除了简单的单表查询,更多的时候要用到联表查询,毕竟设计数据库是按照范式设计,FreeSql的联表操作有导航属性.Join ...