零丶引入

系列文章目录和关于我

经过《Netty源码学习4——服务端是处理新连接的&netty的reactor模式《Netty源码学习5——服务端是如何读取数据的》,我们了解了netty服务端是如何建立连接,读取客户端数据的,通过《Netty源码学习6——netty编码解码器&粘包半包问题的解决》我们认识到编解码在网络编程中的作用以及netty是如何解决TCP粘包,半包问题的。

那么netty客户端是如何发送数据的,以及服务端是如何将响应发送给客户端的昵?

在我们之前编写的小demo当中,有如下代码:

可以看到无论是客户端还是服务端发送数据均可调用Channel#writeAndFlush,在此之外netty还可以直接使用ChannelHandlerContext#writeAndFlush写数据,二者有什么区别昵?

这篇文章,我们将深入源码看看netty是如何write和flush的

一丶Write事件的产生和传播

在业务逻辑处理完毕后,需要调用write 或者 writeAndFlush方法

  • ChannelHandlerContext#write or writeAndFlush方法会从当前 ChannelHandler 开始在 pipeline 中向前传播 write 事件直到 HeadContext。
  • ChannelHandlerContext.channel()#write or writeAndFlush 方法则会从 pipeline 的尾结点 TailContext 开始在 pipeline 中向前传播 write 事件直到 HeadContext 。

write 方法并不是真将数据写到socket缓存区,而是写道Netty的ChannelOutBoundBuffer中,调用flush方法才会真正调用JDK SockectChannel将数据写入。

如下是pipeline#writeAndFlush,可以看到直接调用TailContext#writeAndFlush进行处理

关键源码如下:

private void write(Object msg, boolean flush, ChannelPromise promise) {
// 省略 部分 //flush 表示是否需要flush,调用writeAndFlush的时候为true
// 找到下一个ChannelHandlerContext
final AbstractChannelHandlerContext next = findContextOutbound(flush ?
(MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
// 在eventLoop 中
if (executor.inEventLoop()) {
// 需要flush 那么调用invokeWriteAndFlush
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
// 在eventLoop 中 那么提交一个WriteTask到eventLoop中
final WriteTask task = WriteTask.newInstance(next, m, promise, flush);
if (!safeExecute(executor, task, promise, m, !flush)) {
task.cancel();
}
}
}

可以看到TailContext会通过 findContextOutbound 方法在当前 ChannelHandler 的前边找到 ChannelOutboundHandler 类型并且覆盖实现 write 回调方法的 ChannelHandler 作为下一个要执行的对象。

然后如果当前执行的线程就是EventLoop线程,那么直接调用,反之提交一个异步任务,从而保证执行write的一定是 reactor 线程——保证线程安全性

如下是next.invokeWriteAndFlush的源码

最终事件会传播到HeadContext进行处理(如果中间的ChannelHandler不截胡的话)

二丶write 源码解析

write 事件最终会由HeadContext进行处理

可以看到HeadContext#write其实就是使用Channel的Unsafe#write,其主要逻辑如下

ChannelOutboundBuffer#addMessage

ChannelOutboundBuffer 是 Netty 内部使用的一个数据结构,它用于存储待发送的出站数据。在 Netty 的网络框架中,当需要写数据到网络时,数据并不会立即被发送出去,而是首先被放入一个出站缓冲区中,即 ChannelOutboundBuffer。这个缓冲区负责管理和存储所有待写入通道的数据。

  • 批量发送优化: ChannelOutboundBuffer 允许 Netty 批量地发送数据,而不是每次写操作都立即进行网络发送。这样可以减少系统调用次数,提高网络效率。
  • 流量控制: 它有助于实现流量控制,防止数据发送过快,导致接收方处理不过来。
  • 缓冲区管理: 可以有效地管理内存,当数据被写入网络后,及时释放相应的内存。
  • 异步处理: Netty 是异步事件驱动的框架,使用 ChannelOutboundBuffer 可以将数据发送的异步化,提升处理性能

下面是向ChannelOutboundBuffer写入messge的源码

可与看到ChannelOutboundBuffer会将msg和promise包装为Entry,然后改变tailEntry,flushedEntry,unflushedEntry指针的指向

然后incrementPendingOutboundBytes将记录下待写出数据size,如果大于高水位还会触发channelWritabilityChanged事件

channelWritabilityChanged会在pipeline上传播,并触发ChannelInboundHandler#channelWritabilityChanged,我们可以实现此方法调用flush将数据写出

三丶flush源码解析

上面看了write将待发送的数据缓存到ChannelOutboundBuffer中,正真将数据写到SocketChannel中的是flush方法

1.addFlush

此方法只是负责更改flushedEntry 和 unflushedEntry 指针指向

将 flushedEntry 指针指向 unflushedEntry 指针表示的第一个未被 flush 的 Entry 节点。并将 unflushedEntry 指针置为空,准备开始 flush 发送数据流程。

这样在 flushedEntry 与 tailEntry 之间的 Entry 节点即为本次 flush 操作需要发送的数据范围。

public void addFlush() {
Entry entry = unflushedEntry;
if (entry != null) {
if (flushedEntry == null) {
flushedEntry = entry;
}
do {
flushed ++;
//如果当前entry对应的write操作被用户取消,则释放msg,并降低channelOutboundBuffer水位线
if (!entry.promise.setUncancellable()) { int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
} while (entry != null); // All flushed so reset unflushedEntry
unflushedEntry = null;
}
}

2.flush0

可以看到如果注册了write到selector上,那么不会进行flush,

如下是NioSockectChannel发送数据的源码

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
//获取jdk nio底层socketChannel
SocketChannel ch = javaChannel();
//最大写入次数 默认为16 ,因为EventLoop可能单线程处理多Channel,需要雨露均沾
int writeSpinCount = config().getWriteSpinCount();
do {
if (in.isEmpty()) {
// 如果全部数据已经写完 则移除OP_WRITE事件并直接退出writeLoop
clearOpWrite();
return;
} // 获取单次发送最大字节数
int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
//Netty的DirectBuffer底层就是JDK的DirectByteBuffer
// 将ChannelOutboundBuffer中缓存的DirectBuffer转换成JDK的ByteBuffer,
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
// ChannelOutboundBuffer中总共的DirectBuffer数
int nioBufferCnt = in.nioBufferCount(); switch (nioBufferCnt) {
// 真正进行发送
//java.nio.channels.SocketChannel#write(java.nio.ByteBuffer)进行写回
}
} while (writeSpinCount > 0);
// 处理Socket可写但已经写满16次还没写完的情况
incompleteWrite(writeSpinCount < 0);
}

可以看到

  • 如果数据全部写完了,会调用clearOpWrite清除当前 Channel 在 Reactor 上注册的 OP_WRITE 事件

    这意味着,不需要再监听write来触发flush了

  • 写入的过程会写入多次,并控制自旋次数,做到雨露均沾

如上是写入的过程

  • 如果ByteBuffer个数为0,说明发送的是FileRegion 类型,case 0 的分支主要就是用于处理网络文件传输的情况

  • case1 和 default则调用jdk SocketChannel#write进行数据发送,如果写入的数据小于等于0,说明当前Socket发送缓冲区满了写不进去了,则注册OP_WRITE事件,等待Socket发送缓冲区可写时再写

    触发Write后,再Sockect写缓冲区可写后,会触发对应事件,即可再NioEventLoop中进行处理,如下图中会直接调用forceFlush

  • 完成发送会调用adjustMaxBytesPerGatheringWrite进行调整

两个分支分别表示

  • 期望写入和真正写入的相等,说明数据能全部写入到 Socket 的写缓冲区中了,那么下次 write loop 就应该尝试去写入更多的数据。

    本次写入的数量x2>maxBytesPerGatheringWrite 说明要写的数据很多,那么更新为本次 write loop 两倍的写入量大小

  • 如果本次写入的数据还不及尝试写入数据的一半,说明Socket写缓冲区容量不多了,尝试缩容为一半

  • 处理

    protected final void incompleteWrite(boolean setOpWrite) {
    
            if (setOpWrite) {
    //socket缓冲区已满写不进去的情况 注册write事件
    setOpWrite();
    } else {
    //处理socket缓冲区依然可写,但是写了16次还没写完,提交flushTask异步写
    clearOpWrite();
    eventLoop().execute(flushTask); }

四丶总结

这一节中我们学习了netty写入数据的流程,写入数据时出站事件,一般最终将有HeadContext进行处理

  • write方法将写入的数据转换为DirectByteBuf包装到ChannelOutboundBuffer中,并且记录了对应的Promise实现异步驱动,还可以减少系统调用

  • flush方法,调用jdk SocketChannel#write进行写入,使用自旋次数控制,让多个Channel的处理得到平衡,如果Socket 缓冲区满无法在继续写入那么会OP_WRITE 事件,等 Socket 缓冲区变的可写时,epoll 通知 EventLoop线程继续发送。

    Socket 缓冲区可写,写满 16 次但依然没有写完,这时候注册异步任务使用EventLoop线程进行异步发送。如果写的时FileRegion类型,那么会使用transferTo进行零拷贝写入。

Netty源码学习7——netty是如何发送数据的的更多相关文章

  1. 【Netty源码学习】DefaultChannelPipeline(三)

    上一篇博客中[Netty源码学习]ChannelPipeline(二)我们介绍了接口ChannelPipeline的提供的方法,接下来我们分析一下其实现类DefaultChannelPipeline具 ...

  2. 【Netty源码学习】ChannelPipeline(一)

    ChannelPipeline类似于一个管道,管道中存放的是一系列对读取数据进行业务操作的ChannelHandler. 1.ChannelPipeline的结构图: 在之前的博客[Netty源码学习 ...

  3. 【Netty源码学习】ServerBootStrap

    上一篇博客[Netty源码学习]BootStrap中我们介绍了客户端使用的启动服务,接下来我们介绍一下服务端使用的启动服务. 总体来说ServerBootStrap有两个主要功能: (1)调用父类Ab ...

  4. Netty 源码学习——EventLoop

    Netty 源码学习--EventLoop 在前面 Netty 源码学习--客户端流程分析中我们已经知道了一个 EventLoop 大概的流程,这一章我们来详细的看一看. NioEventLoopGr ...

  5. Netty 源码学习——客户端流程分析

    Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...

  6. Netty源码学习系列之4-ServerBootstrap的bind方法

    前言 今天研究ServerBootstrap的bind方法,该方法可以说是netty的重中之重.核心中的核心.前两节的NioEventLoopGroup和ServerBootstrap的初始化就是为b ...

  7. 【Netty源码学习】EventLoopGroup

    在上一篇博客[Netty源码解析]入门示例中我们介绍了一个Netty入门的示例代码,接下来的博客我们会分析一下整个demo工程运行过程的运行机制. 无论在Netty应用的客户端还是服务端都首先会初始化 ...

  8. (一)Netty源码学习笔记之概念解读

    尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6121065.html  博主最近在做网络相关的项目,因此有契机学习netty,先 ...

  9. netty源码学习

    概述 Netty is an asynchronous event-driven network application framework for rapid development of main ...

  10. Netty源码学习(零)前言

    本系列文章将介绍Netty的工作机制,以及分析Netty的主要源码. 基于的版本是4.1.15.Final(2017.08.24发布) 水平有限,如有谬误请留言指正 参考资料 the_flash的简书 ...

随机推荐

  1. 【Azure K8S | AKS】在不丢失文件/不影响POD运行的情况下增加PVC的大小

    问题描述 在前两篇文章中,创建了Disk + PV + PVC + POD 方案后,并且进入POD中增加文件. [Azure K8S | AKS]在AKS集群中创建 PVC(PersistentVol ...

  2. 3.你不知道的go语言控制语句

    目录 本篇前瞻 Leetcode习题9 题目描述 题目分析 代码编写 知识点归纳 控制结构 顺序结构(Sequence) 声明和赋值 算术运算符 位运算符 逻辑运算 分支结构 if 语句 switch ...

  3. 论文解读(KD-UDA)《Joint Progressive Knowledge Distillation and Unsupervised Domain Adaptation》

    Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:Joint Progressive Knowledge Distillation and Unsuperv ...

  4. 数据api接口就是应用集成吗?

    ​ 数据 API 接口和应用集成是两个不同的概念,但是它们之间有一定的联系.数据 API 接口是一种用于访问和传输数据的标准化接口,而应用集成则是将不同的应用程序和系统整合在一起,实现数据和业务流程的 ...

  5. 关于API数据接口获取商品的数据的说明

    ​ 获取商品数据已经成为许多应用程序的重要组成部分.为了实现这一目标,许多公司和技术开发者使用API数据接口来获取相关数据.本文将详细介绍如何使用API数据接口获取商品数据,并使用Python作为编程 ...

  6. 从零开发Java入门项目--十天掌握

    ​ 原文网址:从零开发Java入门项目--十天掌握_IT利刃出鞘的博客-CSDN博客 简介 这是一个靠谱的Java入门项目实战,名字叫蚂蚁爱购.从零开发项目,视频加文档,十天就能学会开发Java项目, ...

  7. linux下查找文件中某字符串出现的行以及该行前后n行

    linux下查找文件中某字符串出现的行以及该行前后n行 查找指定字符串的前后n行 grep -A 100 -B 100 "要查找的字符串" 被查找的文件 -A after 后面 - ...

  8. 【Qt6】列表模型——树形列表

    QStandardItemModel 类作为标准模型,主打"类型通用",前一篇水文中,老周还没提到树形结构的列表,本篇咱们就好好探讨一下这货. 还是老办法,咱们先做示例,然后再聊知 ...

  9. 解决因对EFCore执行SQL方法不熟练而引起的问题

    前言 本文测试环境:VS2022+.Net7+MySQL 因为我想要实现使用EFCore去执行sql文件,所以就用到了方法ExecuteSqlAsync,然后就产生了下面的问题,首先因为方法接收的参数 ...

  10. 2023_10_09_MYSQL_DAY_01_课后题

    2023_10_09_MYSQL_DAY_01_课后题 #第三章 #1. 查询每名员工的员工姓名,入职时间. SELECT ename, hiredate FROM emp; #2. 查询部门表中部门 ...