Netty源码分析系列之writeAndFlush()下

扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,即可关注微信公众号,阅读更多Spring源码分析Java并发编程Netty源码系列文章。

微信公众号

前言

在上一篇文章中(Netty 源码分析系列之 writeAndFlush()上)分析了 netty 将数据写出流程的前半部分:write()方法源码,知道了在这个过程中,数据只是被存放到了 NioSocketChannel 对象的 ChannelOutboundBuffer 缓冲区中,还没有被发送到操作系统的套接字中。只有当调用了 flush()方法后,才会真正将数据发送到套接字中。那么 flush()方法的源码又是如何执行的呢?这就是本文分析的重点。

还是以上篇文章的 demo 为例,客户端 channel 的 pipeline 的结构图如下所示。(关于 demo 的代码可以直接去查看上篇文章)

Demo示例pipeline结构图

源码分析

从 tail 节点的 writeAndFlush()方法开始,从 tail 节点的 write()方法向前传播执行,当 write()方法执行完之后,就会调用 invokeFlush0() 方法,该方法就是触发执行 flush()方法。上面的 pipeline 的结构中,由于 BizHandler 是 InBound 类型,在写数据的过程中不会触发执行它,另外由于 UserEncoder 我们没有重写 flush()方法,因此默认情况下,它啥也不干,直接再往前一个节点传播执行 flush 方法,因此最终调用的是 head 节点的 flush()方法。head 节点的 flush()方法源码如下。

public void flush(ChannelHandlerContext ctx) {
   
    unsafe.flush();
}

直接调用的是 unsafe 对象的 flush()方法,由于此时是 NioSocketChannel 对象,因此 unsafe 是 NioSocketChannelUnsafe 对象的实例,又因为 NioSocketChannelUnsafe 继承自抽象类 AbstrractUnsafe,且 flush()方法定义在抽象类中,因此最终执行的是如下代码。

public final void flush() {
   
    assertEventLoop();
    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
   
        return;
    }
    // 改变待写队列的指针指向,
    outboundBuffer.addFlush();
    flush0();
}

在上面的代码片段中,主要有两行核心逻辑代码,第一处是调用 outboundBuffer 对象的 addFlush() 方法,改变写队列的三个指针指向;第二处是调用 flush0() 方法,真正将数据写到套接字缓冲区中。下面详细分析下这两处核心逻辑。

在上一篇文章中((Netty 源码分析系列之 writeAndFlush()上))我们从源码中知道,outboundBuffer 这个缓冲区内部实际上是维护了一个队列,这个队列依靠三个指针来维护。初始状态下,这三个指针都是指向 null,当调用一次 write()方法向 outboundBuffer 缓冲区中写入一次数据对象后(这个数据对象会被封装成一个 Entry),就会将 unflushedEntrytailEntry 这两个指针指向刚刚写进来的这个数据所代表的 Entry,而此时 flushedEntry 这个指针仍然是指向 null。

那么什么时候会改变 flushedEntry 指针的指向呢? 那就是当调用 addFlush() 方法的时候,就会改变 flushedEntry 指针。addFlush()方法的作用就是将前面 write()写进到缓冲区的数据移动到刷新队列中,即:将 unflushedEntry 指针指向的数据改变成由 flushedEntry 指针指向,只有 flushedEntry 指针指向的数据才会真正地被发送到套接字中。用文字描述可能有点难以理解,可以结合下面的图来理解。

指针变化示意图

(上一篇文章中也画了一张三个指针变化的关系图,但是由于疏忽,那张图的最后一行的指针指向画的不对,微信公众号没有提供修改的功能,所以阅读到这儿朋友注意下。)

下面看下 addFlush()方法的具体源码实现。

public void addFlush() {
   
    Entry entry = unflushedEntry;
    if (entry != null) {
   
        if (flushedEntry == null) {
   
            // 将flushed的指针指向第一个unFlushed的数据
            flushedEntry = entry;
        }
        do {
   
            // 记录目前有多少个数据等待被Flush
            flushed ++;
            // 现将promise设置为不可取消状态
            if (!entry.promise.setUncancellable()) {
   
                int pending = entry.cancel();
                // 递减待写的字节数,如果待写的字节数低于了最低水位线,那么就将channel的写状态设置为可写状态
                decrementPendingOutboundBytes(pending, false, true);
            }
            entry = entry.next;
        } while (entry != null);

        // 所有unFlushed的数据均已经被标识成flushed了,所以unFlushed可以指针可以指向null了
        unflushedEntry = null;
    }
}

这段代码中,通过循环,将 unflushedEntry 指向的数据变为由 flushedEntry 指向,由于 unflushedEntry 执行的是第一个被 write()写进来的数据,因为 write()可能被多次调用,这样队列中就会有多个数据,因此使用了 do…while 循环,这样让 flushedEntry 指针指向第一个数据,然后再使用 flushed 这个变量来记录一下有多少个数据被从 unflushedEntry 指向的队列中移动到了 flushedEntry 指向的队列中,最后将 unflushedEntry 指向 null。

在数据被 write()进队列之后,被 flush()之前,数据是可以被取消的,可以通过 promise.cancle() 方法,取消数据的写出。但是当开始调用 flush()方法后,就不能再取消了,因为这一步即将将数据写入到操作系统的套接字中,所以再改变三个指针之前,需要将 promise 的状态设置为不可取消状态:promise.setUncancellable()

当 setUncancellable()返回 false 时,表示的是 promise 之前已经被取消了,所以此时需要递减待写的字节数,如果缓冲区中待写的字节数低于了最低水位线,那么就将 channel 的写状态设置为可写状态。(前面在调用 write()方法向缓冲区中写数据时,会累计字节数,当超过最高水位线的时候,会将 channel 设置为不可写状态)。这里递减字节数使用的方法是 decrementPendingOutboundBytes()方法,其中 netty 默认的最高水位线是 64KB,最低水位线是 32KB。

第一处逻辑:addFlush()方法分析完了,接下来分析第二处核心逻辑:flush0()方法。当调用 flush0()方法时,首先会调用 AbstractNioUnsafe 类的 flush0()方法。源码如下。

protected final void flush0() {
   
	// 如果channel注册了OP_WRITE事件,那么就会返回true
    
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值