Netty4.1源码分析——服务端发送数据过程

本文只代表笔者一人的理解和叙述,笔者功力尚浅,如有错误,还请各位大神斧正。
阅读本篇文章前需阅读:
Netty4.1源码分析—— 服务端启动

一、引言与结论

Netty的服务启动、构建连接、读取数据都是入栈事件,也就是从HeadContext的handle到TailContext的handle链处理,并且服务启动为构建连接做准备、构建连接为读取数据做准备,这三者分别对应OP_ACCEPTOP_READ事件的处理。而发送数据不同,它的数据是从TailContextHeadContext的处理,也就是从尾部到头部,即出站事件。

Netty在写数据时,将写入步骤分为两个步骤,write和flush,并且write和flush中间还有个发送缓冲区ChannelOutboundBuffer。write将数据组装在ChannelOutboundBuffer内,而flush则将ChannelOutboundBuffer的数据真正的发送出去。

二、ChannelOutboundBuffer类

在深入了解发送过程之前,熟悉ChannelOutboundBuffer类的精髓是很有必要的。只有理解了这个类,才能理解write和flush所做的事情。

2.1 ChannelOutboundBuffer的结构

ChannelOutboundBuffer将每次write的消息包装成一个节点Entry,并且采用链式的方式,将后write的消息接在前者Entrynext节点上:

static final class Entry {
   //...
    Entry next;//记录下一个消息Entry
    Object msg;
    // ...
    long progress; //用作记录写入的进度,可能存在该Entry没写完但是对端不能在写的情况
    long total;//消息的总字节数
    //...
}

并且在ChannelOutboundBuffer类的内部定义了如下几个Entry节点,用于表示未flush节点的位置、已经flush的节点位置等:

public final class ChannelOutboundBuffer {
    private Entry flushedEntry;
    // The Entry which is the first unflushed in the linked-list structure
    private Entry unflushedEntry;
    // The Entry which represents the tail of the buffer
    private Entry tailEntry;
}

再来看addMessage方法:

public void addMessage(Object msg, int size, ChannelPromise promise) {
    Entry entry = Entry.newInstance(msg, size, total(msg), promise);
    if (tailEntry == null) {
        flushedEntry = null;
    } else {
        Entry tail = tailEntry;
        tail.next = entry;
    }
    tailEntry = entry;
    if (unflushedEntry == null) {
        unflushedEntry = entry;
    }

    //判断是否超过高水位线
    incrementPendingOutboundBytes(entry.pendingSize, false);
}

代码很简单,将每一个新增的消息转变成Entry后用单向链表串起来。继续查看addFlush方法:

public void addFlush() {
    Entry entry = unflushedEntry;
    if (entry != null) {
        if (flushedEntry == null) {
            // 将flushedEntry的位置指向unflushedEntry节点的位置
            flushedEntry = entry;
        }
        do {
            // // 已刷新但数据未写入的节点数
            flushed ++;
            // 设置不可取消,设置失败则释放entry节点
            if (!entry.promise.setUncancellable()) {
                //释放掉Entry的内部消息空间,并返回所占字节数
                int pending = entry.cancel();
                //判断低水位线
                decrementPendingOutboundBytes(pending, false, true);
            }
            entry = entry.next;
        } while (entry != null);
        unflushedEntry = null;
    }
}

代码也很简单,将flushedEntry的位置指向unflushedEntry节点的位置,表示从这一个节点开始,后续节点都是未flush的节点。从addMessage方法和addFlush方法两者可以看出,ChannelOutboundBuffer类内部维持着一个单线链表,采用着不同的标记来描述未flush的数据以及已经被flush的数据位置,可以用如下图来描述:


2.2 写入的高低水位线

需要注意的是两个方法内的incrementPendingOutboundBytes方法,该方法是判断未flush的总字节数是不是大于设置的写入高水位线了,如果大于则将channel的可写标记位置为不可写的状态。如果没有高水位线的限制,当出于某种原因,比如网速慢造成flush这一步真正发送的速率较慢时,假如此时又writeChannelOutboundBuffer的数据太多,这样就极有可能造成内存溢出了。看下其源码:

private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
    if (size == 0) {
        return;
    }

    long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
    // 判断是否待写入的数据已超过写入的高水位线,超过则停止写入
    if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
        // 设置不可写
        setUnwritable(invokeLater);
    }
}

private void setUnwritable(boolean invokeLater) {
    for (;;) {
        final int oldValue = unwritable;
        final int newValue = oldValue | 1;
        //将可写入标记位置为不可写
        if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
            if (oldValue == 0) {
                fireChannelWritabilityChanged(invokeLater);
            }
            break;
        }
    }
}

当超过高水位线时,会设置写入标记位为不可写状态,即不可写的状态,并通过fireChannelWritabilityChanged将可写入状态改变通知给其它handler。需要注意的是,不可写状态表示Netty认为此状态下写入发送成功是不确定的,但是写不写入还是交由开发者决定,只是Netty不保证它的成功性。

有高水位线,自然就有低水位线,当ChannelOutboundBuffer里待发送的数据总大小小于低水位线时,Netty将可写标记位置为可写状态:

private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
    if (size == 0) {
        return;
    }

    long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
    if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) {
        //设为可写
        setWritable(invokeLater);
    }
}

三、写出过程

3.1 write过程

平常在我们写入数据的时候用的最多的是两种写法:ChannelHandlerContext的write方法和ChannelHandlerContext#channel的write方法。前者和后者的处理其实是一样的,只不过前者每个消息都会从TailContextHeadContext走一遍,而后者只会从当前的handle走到HeadContext,所以本篇只着重分析ChannelHandlerContext的write方法以及ChannelHandlerContextflush方法。追踪ChannelHandlerContext的write方法可以看到:

public final ChannelFuture write(Object msg) {
    return tail.write(msg);
}

ChannelHandlerContext#write方法在pipline#write方法内调用的是TailContext,也就是消息是从pipiline的尾部开始处理。接着往下看:

private void write(Object msg, boolean flush, ChannelPromise promise) {
        //....
        //找出被标记write和flush的handleContext
        final AbstractChannelHandlerContext next = findContextOutbound(flush ?
                (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
        //暂不清楚意义,还请大神告知。。。
        final Object m = pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        //判断是否需要write和flush一并完成
        if (executor.inEventLoop()) {
            if (flush) {
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        } 
        //.....
    }

首先找出被标有WRITEFLUSH标记的第一个HandlerContext,如果你是用Netty官方定义的EchoServer来启动Debug,那么此处就是EchoServerHandler类。当然这里的HandlerContext都是ChannelOutboundHandler的实现,根据传参调用其write方法或者writeAndFlush方法,这两者的流程其实是一样的,后者是在前者的基础上完成后直接flush,而不是让开发者来决定如何进行flush的操作。本篇着重分析前者:

void invokeWrite(Object msg, ChannelPromise promise) {
    //判断是否handler就绪
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
    } else {
        write(msg, promise);
    }
}

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}

代码在此处进行了判断,invokeHandler方法的作用是判断该handler是否已经被完全加载就绪了。该判断是因为存在handler已被添加,但是可能handlerAdded事件还未完全执行完的情况。如果没有就绪,那么继续执行write,进行一个循环,找到下一个ChannelOutboundHandler;如果就绪,则调用该ChannelOutboundHandler的write方法,将消息写入。

需要注意的是,在ChannelOutboundHandler的实现当中,write往往对应着消息的编码,可以查看一下常用ChannelOutboundHandler的实现,比如ByteToMessageCodec类的write方法:

public abstract class ByteToMessageCodec<I> extends ChannelDuplexHandler {
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        encoder.write(ctx, msg, promise);
    }
}

pipiline上的ChannelOutboundHandler都会执行一遍write方法,主要是用于Netty自身的一些编码和开发者自定义的一些编码操作,执行到最后最终都会执行到pipiline的头部HeadContext的write方法内:

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    unsafe.write(msg, promise);
}

public final void write(Object msg, ChannelPromise promise) {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    //...

    int size;
    try {
        // 判断是哪种内存,然后转化为直接内存
        msg = filterOutboundMessage(msg);
        // 读取有多少可写入字节
        size = pipeline.estimatorHandle().size(msg);
        if (size < 0) {
            size = 0;
        }
    } 
    //....
    outboundBuffer.addMessage(msg, size, promise);
}

HeadContext内部调用的是AbstractChannelAbstractUnsafe的方法,其最终调用的是本篇上文所说的ChannelOutboundBufferaddMessage方法,也就是将消息临时存储在写出缓冲区ChannelOutboundBuffer内部。

3.2 flush过程

在Netty中,提供了两种flush让开发者选择:writeAndFlush或者先write等待writeComplete事件发生在flush数据。前者将所有的操作都交由框架,而后者将何时写入数据交由开发者来决定。

flush的流程前半部分和write相同,就不在此展开叙述了,所以直接从AbstractUnsafe类的flush方法看起:

public final void flush() {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
        return;
    }

    outboundBuffer.addFlush();
    flush0();
}

在这一步,调用了ChannelOutboundBufferaddFlush方法,得到了ChannelOutboundBuffer内部从哪个节点开始flush的信息(flushedEntry节点)。而flush0方法最终调用的是NioSocketChanneldoWrite方法。在分析该方法之前,先了解一下Netty对写出数据的处理机制。

3.2.1 写出数据大小的动态调整

首先来了解writeSpinCountmaxBytesPerGatheringWrite两个特殊变量。这两者其实都表示Netty在一次写入的过程中尽可能的写多一点的思想,也就是如果对端能接受的更多,那么我每次写入都尽可能的根据对端最大能接受的数据大小来写。

  • writeSpinCount:默认为16,表示每次flush都会尝试进行16次的循环写出数据动作。
  • maxBytesPerGatheringWrite:表示一次写出数据动作的最大数据大小,这个值是动态变化的,在每次写完后会根据具体写入了多少来判断是否需要增加该值还是缩小该值。

doWrite方法中,adjustMaxBytesPerGatheringWrite方法就是maxBytesPerGatheringWrite变量需要增加还是减小的判断:

private void adjustMaxBytesPerGatheringWrite(int attempted, int written, int oldMaxBytesPerGatheringWrite) {
    // 尝试写出的量等于真实写出的量,表示可以下一次可以写入更多(因为全部写入了)
    if (attempted == written) {
        // 每次扩大一倍
        if (attempted << 1 > oldMaxBytesPerGatheringWrite) {
            ((NioSocketChannelConfig) config).setMaxBytesPerGatheringWrite(attempted << 1);
        }
    } else if (attempted > MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD && written < attempted >>> 1) {
        ((NioSocketChannelConfig) config).setMaxBytesPerGatheringWrite(attempted >>> 1);
    }
}

adjustMaxBytesPerGatheringWrite方法中,如果真实的写出数据大小等于尝试写出的数据大小(就是消息的数据大小,所以真正的意思就是全部写出去了),那么Netty认为对端可以接受更大的数据量而加大写出的数据大小,每次都是扩大至原来的两倍。当尝试写出的大小大于尝试写出的最低阈值(MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD = 4096)并且真实的写出量小于尝试写出量的一半,那么缩小maxBytesPerGatheringWrite的值为原来的一半。

3.2.2 写出数据时的特殊处理

再来看incompleteWrite方法,该方法是对写出数据特殊的两种处理,即对端不能写入数据的情况和16次写出动作执行完,但是没有将数据全部写完的情况:

protected final void incompleteWrite(boolean setOpWrite) {
    if (setOpWrite) {
        //注册OP_WRITE事件
        setOpWrite();
    } else {
        clearOpWrite();
        //没写出的数据采用execute执行
        eventLoop().execute(flushTask);
    }
}

protected final void setOpWrite() {
    final SelectionKey key = selectionKey();
    // Check first if the key is still valid as it may be canceled as part of the deregistration
    // from the EventLoop
    // See https://github.com/netty/netty/issues/2104
    if (!key.isValid()) {
        return;
    }
    final int interestOps = key.interestOps();
    if ((interestOps & SelectionKey.OP_WRITE) == 0) {
        key.interestOps(interestOps | SelectionKey.OP_WRITE);
    }
}

private final Runnable flushTask = new Runnable() {
    @Override
    public void run() {
        // Calling flush0 directly to ensure we not try to flush messages that were added via write(...) in the
        // meantime.
        ((AbstractNioUnsafe) unsafe()).flush0();
    }
};

setOpWrite为true时,即对端不能写入数据的情况,此时注册OP_WRITE事件到selector上。需要注意的是OP_WRITE事件表示的意义是对端如果可以写入,那么就会发送OP_WRITE事件到本端,也就是OP_WRITE事件起到的是监听对端是否可以写入的作用。而OP_WRITE事件的处理就是在EventLoop事件循环里处理:

if ((readyOps & SelectionKey.OP_WRITE) != 0) {
    // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
    ch.unsafe().forceFlush();
}

在事件循环里,当收到OP_WRITE事件时,通知调用channelAbStractUnsafe类继续进行flush的操作。

setOpWrite为false时,表示16次写出动作已经写完了,但是还有数据没有写出去,此时会调用EventLoopexecute方法来执行一个任务,任务的动作就是再次调用一遍flush0方法,直到数据完全写出。

3.2.3 doWrite方法

了解完上述的思想和处理过程后,再来看doWrite的整个流程:

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    SocketChannel ch = javaChannel();
    // 每次写入都试图写满16次
    int writeSpinCount = config().getWriteSpinCount();
    do {
        // 判断是否有数据可写
        if (in.isEmpty()) {
            //....
            return;
        }
        // 该变量表示一次尽可能写入的最大数据量
        int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
        ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
        // 有多少个ByteBuf
        int nioBufferCnt = in.nioBufferCount();
        switch (nioBufferCnt) {
            case 0:
                // 暂不清楚该分支的意思。。。
                // We have something else beside ByteBuffers to write so fallback to normal writes.
                writeSpinCount -= doWrite0(in);
                break;
            case 1: {
                ChannelOutboundBuffer, so there is no need
                ByteBuffer buffer = nioBuffers[0];
                //获取本次写入的数据
                int attemptedBytes = buffer.remaining();
                final int localWrittenBytes = ch.write(buffer);
                // 对端不能写入数据了
                if (localWrittenBytes <= 0) {
                    // 注册OP_WRITE事件,OP_WRITE事件表示对端可以写数据了
                    incompleteWrite(true);
                    return;
                }
                // 动态更改maxBytesPerGatheringWrite的大小,让每次都尽可能更多的写入
                adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
                // 移除已被写入的数据,没有被写完则标记写入进度
                in.removeBytes(localWrittenBytes);
                --writeSpinCount;
                break;
            }
            default: {
                long attemptedBytes = in.nioBufferSize();
                final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
                //后续代码与case:1分支相同
            }
        }
    } while (writeSpinCount > 0);

    // 写满了16次,但是还是没有写完,调用execute继续写
    incompleteWrite(writeSpinCount < 0);
}
  • 第一步,通过nioBuffers方法将ChannelOutboundBuffer缓冲区里的数据转化为NIO的ByteBuf数组。
  • 第二步,判断NIO ByteBuf数组里有多少个ByteBuf。根据不同的数量来决定走哪一个分支,需要注意的是数量为1和多个的情况其实是走的相同的代码,只不过前者对应NIO 单个ByteBuf的写入,后者对应NIO多个ByteBuf的写入,本篇只分析单个的写入流程。
  • 第三步,分支1中,首先获得ByteBuf的真实数据量大小,也就是上文说的attemptedBytes变量。调用NIO的write写入数据到socket中,并获得真实写入的数据量大小(localWrittenBytes)。当该值小于等于0时,表示对端不能写入了,并调用incompleteWrite方法注册一个OP_WRITE事件,等待对端通知何时能写。
  • 第四步,如果写入的数据量大于0,那么调用adjustMaxBytesPerGatheringWrite方法来动态的更改每次写入的数据量大小(maxBytesPerGatheringWrite)
  • 第五步,移除ChannelOutboundBuffer内当前Entry已经写入的数据,如果没有写完,则标记一下写入的进度(progress),写入动作次数(writeSpinCount)减一,进行下一次循环。
  • 第六步,循环结束后,假如写入的动作次数小于0,意味着ChannelOutboundBuffer内还有缓存的数据没写完,就调用incompleteWrite方法在内部起一个flush的任务,继续调用flush0方法接着写,直到写完。

四、总结

  1. 写的本质是调用了NIO的write方法,在数据写不进去的时候,会注册一个OP_WRITE事件,来监听什么时候可以写了。
  2. OP_WRITE事件表示对端可以写的情况,不是有数据可写。OP_WRITE事件的处理是在事件循环内处理,其内部是调用flush0方法继续写数据。
  3. Netty会尽可能尝试写多数据,每一次写入数据的大小是会动态变化的,每一轮写入的次数是16次。
  4. 当16次数据还没写完的时候,会利用EventLoop起一个flush0的任务,继续写入。
  5. Netty写入是分为两步骤的,第一步将数据缓存在ChannelOutboundBuffer内,第二步则将据缓存在ChannelOutboundBuffer内的数据写入到socket
  6. 写入的时候会实时比较写入的高低水位线,来判断标记该socketChannel是否可写。但是不可写状态表示Netty认为此状态下写入发送成功是不确定的,但是写不写入还是交由开发者决定,只是Netty不保证它的成功性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值