【netty系列-08】深入Netty组件底层原理和基本实现

news2024/11/16 23:58:36

Netty系列整体栏目


内容链接地址
【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640
【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478
【三】深入理解NIO的基本原理和底层实现https://zhenghuisheng.blog.csdn.net/article/details/138451491
【四】深入理解反应堆模式的种类和具体实现https://zhenghuisheng.blog.csdn.net/article/details/140113199
【五】深入理解直接内存与零拷贝https://zhenghuisheng.blog.csdn.net/article/details/140721001
【六】select、poll和epoll多路复用的区别https://zhenghuisheng.blog.csdn.net/article/details/140795733
【七】深入理解和使用Netty中组件https://zhenghuisheng.blog.csdn.net/article/details/141166098
【八】深入Netty组件底层原理和基本实现https://zhenghuisheng.blog.csdn.net/article/details/141685088

深入Netty组件底层原理和基本实现

  • 一,深入理解netty组件
    • 1,EventLoopGroup的组成原理
    • 2,EventLoopGroup组件
    • 3,channel组件
    • 4,ChannelPipeline组件
    • 5,ChannelHandlerContext
    • 6,ChannelHandler以及对应适配器
      • 6.1,出站的read事件
      • 6.2,实现Handler共享

一,深入理解netty组件

在上一篇中讲解了netty的基本使用,在代码中用到了多个组件。如BootStarp,EventLoopGroup,以及NioServerSocketChannel,handler以及pipeline等。接下来这篇主要是对这些组再件做一个详细的解释

1,EventLoopGroup的组成原理

如下图所示,一个 EventLoopGroup 可以管理多个 EventLoop ,每一个EventLoop都会对应一个线程,EventLoop会管理所有对应的channel,channel就是封装的socket,一个channel只会对应一个EventLoop,内部需要触发或者执行什么事件都得是通过相应的EventLoop进行管理
在这里插入图片描述

2,EventLoopGroup组件

在前面大概说了一下,这个EventLoopGroup就是类似于nio中反应堆模式的selector,用于循环的去处理事件,接下来通过本篇文章,详细的描述一下到底什么是EventLoopGroup。还是使用的上一个4.1.42.Final版本,后续都是该版本查看内部的源码以及实现,该接口的组成和基本方法和实现如下

public interface EventLoopGroup extends EventExecutorGroup {
    EventLoop next();

    ChannelFuture register(Channel var1);

    ChannelFuture register(ChannelPromise var1);

    /** @deprecated */
    @Deprecated
    ChannelFuture register(Channel var1, ChannelPromise var2);
}

该接口的父接口的实现图如下,最顶层就是一个任务的实现类,因此在没看具体的源码之前,就能知道这个EventLoop底层应该就是一个线程任务

在这里插入图片描述

EventLoop就是对应一个个线程,去执行多个socket对应的事件或者任务。ChannelPromise就是一个具体的ChannelFuture,通过unsafe方法将这个socket注册到对应的EventLoop中,随后返回。

public ChannelFuture register(ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    promise.channel().unsafe().register(this, promise);
    return promise;
}

EventLoop的注册实现如下,在socketChannel绑定对应EventLoop时,需要判断EventLoop是否存在,如果存在则通过unsafe方法将这个socket注册到对应的EventLoop中,如果不存在,则将这个 promise 打包成一个任务,丢到线程池中,随后通过这个EventLoop执行

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            if (eventLoop == null) {
                throw new NullPointerException("eventLoop");
            } else {
                AbstractChannel.this.eventLoop = eventLoop;
                if (eventLoop.inEventLoop()) {
                    this.register0(promise);
                } else {
                    try {
                        eventLoop.execute(new Runnable() {
                            public void run() {
                                //封装成
                                AbstractUnsafe.this.register0(promise);
                            }
                        });
                    } catch (Throwable var4) {
                        ...
                    }
                }

            }
}

可以得知 EventLoop 继承于OrderedEventExecutor,结合OrderedEventExecutor的实现类图分析,可以得知该类的父类就是一个Executor的的线程池

public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
    EventLoopGroup parent();
}

在这里插入图片描述

以一个具体实现的单例的EventLoop为例,该接口继承了 SingleThreadEventExecutor 任务执行器和 EventLoop 接口

public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop {
    ...
}

在这个 SingleThreadEventExecutor 抽象类中,我把一些重要的属性和参数列在下面,如一默认最大的线程任务数,存放的任务队列等

//默认的最大线程任务数
static final int DEFAULT_MAX_PENDING_EXECUTOR_TASKS = Math.max(16,
      SystemPropertyUtil.getInt("io.netty.eventexecutor.maxPendingTasks", Integer.MAX_VALUE));
//存放线程的队列
private final Queue<Runnable> taskQueue;
//可见线程
private volatile Thread thread;
//计数器
private final CountDownLatch threadLock = new CountDownLatch(1);

除此之外,还有线程的一些状态等,如未启动,就绪,运行,阻塞,终止等状态

    private static final int ST_NOT_STARTED = 1;
    private static final int ST_STARTED = 2;
    private static final int ST_SHUTTING_DOWN = 3;
    private static final int ST_SHUTDOWN = 4;
    private static final int ST_TERMINATED = 5;

因此EventLoop的线程实现,就是类似于在jdk中的线程池的实现,里面既有线程,又有队列。因此在使用这个Event Loop的流程大致如下:首先会判断当前执行的线程是否为EventLoop线程,如果是则直接将当前的Channel注册到该EventLoop中,这样可以减少这个上下文的切换;如果当前执行的线程不是EventLoop线程,那么就会将这个注册的任务打包成一个队列去完成,有点类似于加入到线程池中去排队异步执行。

为什么要通过队列去通过这个EventLoop去执行这个任务,而不是直接使用主线程去执行任务,原因是为了解决这个并发问题。netty为了解决这个问题,特意指定了每个channel只能由对应的EventLoop去管理和执行,因此就不能由其他线程或者当前主线程去执行注册或者执行任务的事件。每次由一个EventLoop去管理多个channel,这样就可以保证每个channel执行的安全性。

总而言之就是一句话:EventLoopGroup负责管理EventLoop,EventLoop负责管理Channel

3,channel组件

在上面提到了Channel所有的动作和行为都得由对应的EventLoop去触发对应的事件,接下来分析在Netty中的这个Channel的具体实现。在分析之前,不管是Netty的channel还是Nio中ServerSocket,其内部都是对底层的 Socket 进行操作。

public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable<Channel> {
    ...
}

在该接口中,其内部的有的方法如下,其部分方法详细描述如下,如是否注册,绑定地址,绑定eventLoop等

在这里插入图片描述

方法详情
EventLoop eventLoop()返回与该 Channel 关联的 EventLoop,负责处理该 Channel 的所有 I/O 操作
Channel parent()返回父 Channel,例如,ServerSocketChannel 是父 Channel
ChannelConfig config()用于配置 Channel 的参数,如 TCP_NODELAY, SO_KEEPALIVE
boolean isOpen()判断 Channel 是否打开(未关闭)。一个打开的 Channel 是可以接收和发送数据的
boolean isRegistered()判断 Channel 是否已经注册到 EventLoop
ChannelFuture bind(SocketAddress localAddress)绑定到一个本地地址,用于服务器端 Channel
ChannelFuture connect(SocketAddress remoteAddress)连接到远程地址,用于客户端 Channel
ChannelFuture close()关闭 Channel,释放资源。
Channel read()请求从 Channel 中读取数据,通常是由 ChannelHandler 自动触发的。
ChannelFuture write(Object msg)Channel 中写入数据,writeAndFlush() 会立即将消息刷出到远程对端。
ChannelPipeline pipeline()返回与 Channel 关联的 ChannelPipeline

4,ChannelPipeline组件

channelPipeline是用于存放channelhandler的容器,每一个channel都有一个自身对应的channelPipeline。

public interface ChannelPipeline
        extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable<Entry<String, ChannelHandler>> {}

依旧是如同上面的channel一样,先了解一下这个channelPipeline接口部分api的使用

在这里插入图片描述

方法详情
addFirst / addLast将Handler处理器添加到头部或者尾部
remove / removelast / removefirst将Handler移除 / 移除首个 / 者最后一个
get / first() / last()获取任意一个 / 获取第一个 / 获取最后一个
fireChannelRegistered入站事件,入站handler处理器注册事件
fireChannelRead入站事件,入站handler处理器读取事件
bind /connect /write /flush出站事件,handler绑定、连接、写、刷新事件

ChannelPipeline 中,支持出站事件和入站事件,顾名思义,对应的就是接收并处理请求以及处理并想要请求。

举个例子,如客户端想服务端发起请求,然后向服务端发送gzip压缩的base64编码的数据,服务端需要获取并解压,解码数据,然后做出对应的响应,然后将数据就行base64编码,再gzip压缩。入站事件就是接收请求,gzip解压,base64解码;出战事件就是做出响应,base64编码,gzip压缩。每一个事件对应的就是一个Handler,需要在对应的 ChannelHandler 中编写具体的事件

在这里插入图片描述

5,ChannelHandlerContext

在pipeline中,内部采用的是双向链表的结构,除了灵活的插入和删除的操作之外呢,最主要的是可以在一个pipeline中支持出站和入站事件,这样在入站时可以从前往后的遍历所有Handler结点,出站时可以从后往前的遍历所有Handler的结点,这样就支持双向遍历。除此之外,由于内部采用的是责任链模式,在执行next结点或者指定结点时,支持直接从当前结点往前找或者往后找,不需要每次都从前往后找。因此双向链表的优势远大于单向链表。

当然这个双向链表是如何实现的呢,其实他也是借助了Lisked链表的方式,具体的实体数据存在列表中,前驱指针和后驱指针等存放在Node节点中。在netty中,使用了 Context 上下文的方式存储着对应的结点,例如 AbstractChannelHandlerContext 抽象类中,就定义类next和prev。

abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {
    volatile AbstractChannelHandlerContext next;
    volatile AbstractChannelHandlerContext prev;
}

上面的gzip解压,base64编码等就是一个具体的入站Handler;base64编码,gzip压缩就是一个具体的出站事件。

在这里插入图片描述

当然在这个 AbstractChannelHandlerContext 抽象类中,这个上下文也不仅仅是维护上下文链表的关系,同时也有数据在这个pipeline中流动。 如在上一篇文章中有讲到,通过ctx的 writeAndFlush 写事件将数据写入到上下文中,然后将数据发送给对端。

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty", CharsetUtil.UTF_8));
}

除了上面这种写数据之外,还能通过以下两种方式将数据写入到上下文中发送给对端。也就是说既可以直接通过本上将数据写入到上下文中发送给对端,也可以通过pipeline或者channel的方式将数据写入到上下文中发送给对端

ctx.pipeline()
ctx.channel()    

真正的区别在于是否要遍历整个pipiline中的出站事件。举个例子,依旧是下图三个pipeline,假设入站事件就是一个gzip的解压事件,如果此时解压成功,那么流程继续往下走没问题,如果此时解压失败,那么就会涉及到是否直接将报错返回,还是得继续往下走,把所有的出站事件走一遍的问题。

在这里插入图片描述

如果业务有强制要求,就是说就算报错,也得将报错信息先base64编码然后gzip压缩将数据返回,如果业务没这种要求,那么就可以直接找前面出站handler将事件返回即可。如果是直接通过context将数据写入到上下文的话,那么在发生gzip报错的时候,那么就会直接往前找对应的出站Handler即可,这样可以提高整个流程的效率;如果是使用的pipeline或者channel的话,就算第一步的入站handler出现异常报错,也得从后往前将全部的出站Handler的事件走一遍,再将结果返回,这种方式可以使得整体的返回结果更统一和规范,缺点就是耗时长。当然无论使用哪种方式r,都能体现出使用双向链表的优势。

6,ChannelHandler以及对应适配器

在谈完上面的这些基本的固定组件,在实际开发中,我们最主要写的就是一个个 channelHandler 事件。如前面文章例子中定义了一个实现接口ChannelInboundHandlerAdapter的 NettyServerChannelHandler

public class NettyServerChannelHandler extends ChannelInboundHandlerAdapter {
	...
}

在这里插入图片描述

ChannelInboundHandlerAdapter 顾名思义就是一个均衡的适配器,即实现了入站事件,也实现了出站事件的一些方法。通过适配器方式,可以在开发中只需要去继承这个适配器类,而不需要去就行实现对应的ChannelHandler接口

在该适配器中,只做了一件事情,就是将一些数据或者动作就行传递。里面所有的方法中,都用了fire开头,如fireChannelRegistered

public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelRegistered();
    }
}

6.1,出站的read事件

一般入站事件中调用read方法,出站事件中调用write写方法。但是在出站的接口中,同时也提供了一个read方法,这就得回归到这个pipeline中的这张图,在pipeline中流转的不仅仅是数据,而且可能是动作。理解这点还是得回归到nio,也就是说客户端先在服务端注册一个感兴趣的事件,然后通过selector轮询器一直去扫描这些事件,当服务端这边有线程空闲的时候就会去触发这个事件,那么就会将这个事件交给感兴趣的线程去操作。

public interface ChannelOutboundHandler extends ChannelHandler {
    ...
    void read(ChannelHandlerContext ctx) throws Exception;
}

在这里插入图片描述

在netty中是通过 EventLoop 去触发这个感兴趣的事件的,那么当事件被触发时,就需要从EventLoop去通知对应的感兴趣的线程,那么这个过程中,eventLoop就类似于一个服务端,对应的感兴趣的线程就是一个服务端,那么就会将这个触发事件的通知打包成一个出站的 ChannelOutboundHandler 事件,因为这个操作时起点是一个发送者,因此在出站事件中,也提供了这个 read 事件。这个事件是一个比较特殊的用法,因为不像上面的数据流动主要针对的是双端的Socket之间的通信,而这个出站的read事件主要是针对服务端内部通过 EventLoop 去通知内部线程去响应对应的事件。

6.2,实现Handler共享

上面谈到了channelHandler会由对应的Eventoop进行管理,因此每一个Handler内部都相互隔离,属于是线程安全的。但是如果有需求需要设计一个共享的handler如何实现,其实在ChannerHandler内部已实现。在 ChannelHandler 接口中,写了一个自定义注解的 Sharable 接口,当在handler上面声明了是共享之后,那么所有的handler都能用这个共享Handler

public interface ChannelHandler {
    ...
        
    @Inherited
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface Sharable {
        // no value
    }
}

如在下面的 NettyShareChannelHandler 类中加上这个 @ChannelHandler.Sharable 接口,那么该类就能成为一个共享handler。在该接口中的read方法中,对所有的请求数量进行统计

@ChannelHandler.Sharable
@Slf4j
public class NettyShareChannelHandler extends ChannelDuplexHandler {
    AtomicInteger increment = new AtomicInteger(0);

    @Override
    public void read(ChannelHandlerContext ctx) throws Exception {
        int count = increment.incrementAndGet();
        log.info("接收到的请求总数为:" + count);
        super.read(ctx);
    }
}

在服务端中,也不需要再去手动的new Handler,只需要外部定义好将该对象加入即可。

//创建共享对象
NettyShareChannelHandler nettyShareChannelHandler = new NettyShareChannelHandler();
//部分伪代码
socketChannel.pipeline()
.addLast(nettyShareChannelHandler) 		//加入共享事件
.addLast(new NettyServerChannelHandler());     //将事件加入到管道中

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2085835.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第一个go程序

package main import "fmt" func main(){fmt.Println("Hello World") }hello.go所在目录 运行go程序

美团代付多模版三合一源码 附教程

简介 美团代付多模版三合一源码 附教程 界面

这一轮医疗数字化,沃可趣以医疗专业人员交流成长为中心

沃可趣看见医疗行业人员需求痛点&#xff0c;量身打造数字服务平台&#xff0c;促进知识分享&#xff0c;赋能活动组织。 从电子病历的普及到远程医疗的兴起&#xff0c;从人工智能辅助诊断到大数据在医疗管理中的应用&#xff0c;科技进步正在大力推动医疗领域的发展。然而&a…

Ubuntu系统本地搭建WordPress网站并一键发布内网站点至公网实战

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

嵌入式技术文件、学习资料、在线工具、学习网站、技术论坛,非常全面的分享~~~

一、数据手册查询及下载网站 1、【ALLDATASHEET 自称是最大的在线电子元件数据的搜索引擎】ALLDATASHEETCN.COM - 电子元件和半导体及其他半导体的数据表搜索网站。电子元件和半导体, 集成电路, 二极管, 三端双向可控硅 和其他半导体的https://www.alldatasheetcn.com/ 2、【…

defineProps、defineEmits、 defineExpose的TS写法

小满视频 1 defineProps&#xff1a;父向子传递数据 作用&#xff1a;父组件向子组件传递数据 1.1 传递纯类型参数的方式来声明 父组件中的代码&#xff1a; 父组件App.vue <template><div><span>传递给子组件的响应式数据&#xff1a;</span>&l…

【循环顺序队的实现】

1.队列的逻辑结构 与 抽象数据类型定义 先进先出的线性表 在顺序队列中&#xff0c;我们使用头指针front指向队首元素&#xff1b;用尾指针rear指向队尾元素的下一个位置&#xff08;当然这里的指针是用下标模拟出来的&#xff09; 同时顺序队列中的元素当然是用数组来存储的 …

【系统架构设计】嵌入式系统设计(1)

【系统架构设计】嵌入式系统设计&#xff08;1&#xff09; 嵌入式系统概论嵌入式系统的组成硬件嵌入式处理器总线存储器I/O 设备与接口 软件 嵌入式开发平台与调试环境交叉平台开发环境交叉编译环境调试 嵌入式网络系统嵌入式数据库管理系统实时系统与嵌入式操作系统嵌入式系统…

【Qt笔记】QToolButton控件详解

目录 一、前言 二、QToolButton的基本特性 2.1 图标和文本 2.2 自动提升 2.3 下拉菜单 2.4 工具提示 2.5 弹出模式 三、高级功能 3.1 自定义大小与形状 3.2 检查框与单选按钮 3.3 动画效果 四、常用方法与信号槽 常用方法 信号槽 五、实际应用示例 说明 六、总…

ESP32 CYD 使用 LVGL 在屏幕上显示图像 | Random Nerd Tutorials

在本指南中&#xff0c;你将学习如何使用LVGL在ESP32 Cheap Yellow Display (CYD) 板上处理和加载图像。ESP32将使用Arduino IDE进行编程。 对ESP32 Cheap Yellow Display不熟悉&#xff1f; 从这里开始&#xff1a;开始使用ESP32 Cheap Yellow Display Board – CYD (ESP32-2…

线性代数 第三讲 线性相关无关 线性表示

线性代数 第三讲 线性相关无关 线性表示 文章目录 线性代数 第三讲 线性相关无关 线性表示1.向量运算1.线性相关与线性无关1.1 线性相关与线性无关基本概念 2.线性表示&#xff08;线性组合&#xff09;3.线性相关无关与线性表示的定理大总结3.1 向量β可由向量组线性表出的同义…

心觉:潜意识显化很简单,只是很多人想复杂了

很多人知道潜意识的力量很大&#xff0c;是意识力量的30000倍以上 也知道该怎么显化自己的潜意识 但是就是做不到 这就像很多肥胖的人知道运动可以减肥 知道减肥之后就可以穿漂亮的衣服 知道减肥之后自己有多帅多美 但是就是迈不开腿 根本原因是你的潜意识和意识上的认知不…

RenderMan v26.2更新内容!云渲染平台支持新版本

皮克斯的最新RenderMan v26.2版本带来了一系列激动人心的新特性和改进&#xff0c;进一步巩固了其在高端渲染领域的领导地位&#xff0c;为艺术家们提供了更丰富的创意工具和更流畅的工作流程。作为老牌的云渲染农场&#xff0c;瑞云依然支持新版本的使用。 RenderMan v26.2更新…

移动端视频编辑SDK解决方案,关键帧曲线塑造生动效果

美摄科技&#xff0c;作为移动视频编辑技术的领航者&#xff0c;携其革命性的移动端视频编辑SDK解决方案&#xff0c;正以前所未有的创新力&#xff0c;为视频创作者们开启了一扇通往无限创意的大门。 重塑视频编辑体验&#xff0c;让创意触手可及 美摄科技的移动端视频编辑S…

公网信息泄露监测(网盘、暗网、搜索引擎、文档平台)思路分享

一、背景 众测项目中白帽可能会提交一些信息泄露漏洞&#xff0c;同时甲方可会收到一些白帽提交的公网信息泄露文件漏洞&#xff0c;例如百度网盘被员工分享某些文件或者某些包含敏感信息的文件可以通过如谷歌、百度等搜索引擎通过特定语法搜索到。为了可以及时发现泄露的文件…

九泰智库 | 医械周刊- Vol.54

⚖️ 法规动态 国家药监局综合司发布医疗器械管理法草案征求意见稿 国家药监局综合司发布了《中华人民共和国医疗器械管理法&#xff08;草案征求意见稿&#xff09;》&#xff0c;公开征求意见&#xff0c;以加强医疗器械的管理并推动产业高质量发展。该草案共十一章190条&a…

深入解析财务报表:掌握重要财务指标的技巧

一、概述 财务报表中有大量信息&#xff0c;如果我们在分析时缺乏明确的方向或忽视了重点&#xff0c;就很容易在繁杂的数据中迷失方向。本文将深入探讨财务报表中的几个重要指标&#xff0c;帮助大家更有针对性地理解这些内容&#xff0c;包括如何分析资产负债率、解读净资产…

基于python的web框架 Flask 入门基础知识【1】

Flask是一个轻量级的可定制框架&#xff0c;使用Python语言编写&#xff0c;较其他同类型框架更为灵活、轻便、安全且容易上手。 目录 一、项目环境搭配以及安装运行 1.下载安装 2.最小的应用 3.运行应用 4.运行结果 4.1 外部可见的服务器 二、路由 三、http请求 3.1静…

无人机的核心技术!!!

无人机的核心技术涵盖了多个关键领域&#xff0c;这些技术共同支撑了无人机的稳定飞行、精准控制、高效数据传输以及多样化的应用功能。 1. 飞行控制技术 核心地位&#xff1a;飞行控制技术是无人机的核心关键技术之一&#xff0c;它确保了无人机在复杂飞行环境下的稳定性和安…

论文AI生成软件大PK!揭秘学术界的高效神器,选对了让你研究效率翻倍

在当今的学术界&#xff0c;研究与写作的压力不断攀升&#xff0c;论文的质量与数量往往成为衡量学者成就的重要指标。 然而&#xff0c;面对繁杂的研究任务和紧张的时间线&#xff0c;如何提升学术生产力成为了广大研究人员、学生及教育工作者的共同诉求。 在这样的背景下&a…