Netty源码阅读6——Write And Flush

Posted by 皮皮潘 on 12-05,2021

内核管理的每一个TCP文件描述符都有一个读缓冲区和一个写缓冲区

在BIO的情况下,当读缓冲区为空时,Socket.read会一直阻塞,当写缓冲区满了时,Socket.write会一直阻塞,这里需要注意的是,所有的读和写不是针对NIC(网卡)的,而是针对内核管理的TCP文件描述符的缓冲区的,而NIO对于BIO的改善就是,当读缓冲区为空或者写缓冲区满了时都立即返回,并提示读取/写入数据量为0

对于write和flush而言,不论是调用channel还是ctx的方法,最后都会落地到pipeline的head节点(如果outboundhandler没有把对应的数据截获的话),然后调用unsafe对应的write和flush方法

Netty的write实现

Netty的write实际上并没有调用Channel的实际写入,而是将对应的数据缓存到用户态的缓冲区中——ChannelOutboundBuffer,其中ChannelOutboundBuffer是采用链单表结构进行存储的,每个节点是一个Entry,Entry里面包含了待写出的ByteBuf以及消息回调promise,另外还有三个指针:

  1. flushedEntry指针表示第一个被写到操作系统Socket缓冲区中的节点
  2. unFlushedEntry指针表示第一个未被写入到操作系统Socket缓冲区中的节点
  3. tailEntry指针表示ChannelOutboundBuffer缓冲区的最后一个节点

另外在调用outboundBuffer.addMessage(msg, size, promise)之前会将msg转换为DirectByteBuf类型,从而加快在flush方法中Channel的写入,因为在 JVM 内部执行 I/O 操作时,都必须将数据拷贝到堆外内存,才能执行系统调用,这是所有 VM 语言都会存在的问题,因为操作系统并不感知 JVM 的堆内存,而且 JVM 的内存布局与操作系统所分配的是不一样的,操作系统并不会按照 JVM 的行为来读写数据,所以Netty 在进行 I/O 操作时都是使用的堆外内存,可以避免数据从 JVM 堆内存到堆外内存的拷贝

Netty的flush实现

Channel的实际写入是在flush中执行的,在flush方法中,首先调用ChannelOutboundBuffer的addflush方法,遍历所有的unflushed的entry执行一定的操作一直到unFlushedEntry指向null,接下来调用flush0方法,其中主要调用doWrite(ChannelOutboundBuffer in)方法,其主要操作如下:

  1. 调用in.current()拿到第一个需要flush的节点的数据

  2. 调用writeSpinCount = config().getWriteSpinCount()拿到自旋锁迭代数(默认为16次)

  3. 通过自旋的方式,将对应的数据写入Channel,其中为什么要使用自旋是为了防止写缓冲区快要满了的情况下不能写入所有的数据,此时就需要自旋等待数据写入完毕,当发现写入数据为0时(写缓冲区满了)则直接跳出大循环,并且去Selector中注册write事件,当发生write事件了(socket可以重新write了)则再次flush数据同时设置ChannelWritability为false,并且向ChannelPipeline触发ChannelWritabilityChanged事件

  4. 如果数据写入完毕调用in.remove()清理数据并且通过safeSuccess(promise)回调对应的注册事件,反之16次之后数据还未发送完则跳出大循环,并给EventLoop提交flushTask等待之后再次flush

  5. 重复上述操作直到flush完所有的节点