zoukankan      html  css  js  c++  java
  • 网络编程-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中有以下几个重要的指针:
    1357217-6c645e2c5d0876d4.png

    • Entry代表了我们发送的数据
    • flushedEntry代表需要写入到发送缓冲区的第一个Entry
    • unflushedEntry代表第一个等待写入发送缓冲区的Entry

    当第一次调用addMessage方法往ChannelOutBoundBuffer中添加数据时

    1357217-2fd7d3a45b0de35d.png

    第二次调用addMessage方法时,数据指针如下
    1357217-01d5fe86bd63437f.png

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

    Flush操作

    当调用Flush操作后,指针情况如图:
    1357217-24c29eedd72aeb23.png

    之后的代码,就是遍历这段节点数据,写入到发送缓冲区中,并且写入后释放节点内存。

    判断缓冲区是否可写(小知识)

    在实际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是否为空,为空则不发送,并设置失败,到此我们的问题就研究明白了。

  • 相关阅读:
    element ui 表单清空
    element ui 覆盖样式 方法
    element ui 修改表单值 提交无效
    element ui 抽屉里的表单输入框无法修改值
    element ui 抽屉首次显示 闪烁
    css 左侧高度 跟随右侧内容高度 自适应
    PICNUF框架
    elementui 抽屉组件标题 出现黑色边框
    vue 子组件跨多层调用父组件中方法
    vue 编辑table 数据 未点击提交,table里的数据就发生了改变(深拷贝处理)
  • 原文地址:https://www.cnblogs.com/ging/p/13467886.html
Copyright © 2011-2022 走看看