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

  • 相关阅读:
    创业指南:如何实现打工族的老板梦
    在C#中压缩解压缩文件(适合.Net1.x)
    35岁之前成功的12条黄金法则
    郑州DOTNET俱乐部《DotNet实战之旅》活动邀请
    1baiwan.com你能走多远?(原创,请任意转载,作者:小张.net)
    MongoDB实战开发 【零基础学习,附完整Asp.net示例】
    TFS2010强制撤签锁定项
    持续集成理论和实践的新进展
    JQuery最佳实践
    IE下实现全屏两方法
  • 原文地址:https://www.cnblogs.com/ging/p/13467886.html
Copyright © 2011-2022 走看看