Netty源码分析一启动流程剖析

我们知道Netty框架是基于NIO网络编程模型实现的,本篇文章就基于NIO的启动流程来剖析Netty启动流程的源码

NIO启动流程

首先我们先来看一下NIO的启动流程

//1 netty 中使用 NioEventLoopGroup (简称 nio boss 线程)来封装线程和 selector
Selector selector = Selector.open(); //2 创建 NioServerSocketChannel,同时会初始化它关联的 handler,以及为原生 ssc 存储 config
NioServerSocketChannel attachment = new NioServerSocketChannel();//3 创建 NioServerSocketChannel 时,创建了 java 原生的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
serverSocketChannel.configureBlocking(false);//4 启动 nio boss 线程执行接下来的操作//5 注册(仅关联 selector 和 NioServerSocketChannel),未关注事件
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);//6 head -> 初始化器 -> ServerBootstrapAcceptor -> tail,初始化器是一次性的,只为添加 acceptor//7 绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));//8 触发 channel active 事件,在 head 中关注 op_accept 事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);

我们剖析Netty启动流程就是分析一下Netty中对上面的代码是如何处理的。

Netty启动流程剖析

我们以一段简单的Netty服务端代码为例

package cn.kjz.source;import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;public class TestSourceServer {public static void main(String[] args) {new ServerBootstrap().group(new NioEventLoopGroup()).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) {ch.pipeline().addLast(new LoggingHandler());}}).bind(8080);}
}

NIO中的Selector selector = Selector.open();就封装在new NioEventLoopGroup()这个类中,NioEventLoopGroup这个类本文就不展开分析了,后面会有详细的博文分析。

在此处打个断点debug运行一下。

F7进入到断点处,我在这里打了三个断点,分别对应了NIO中的几个重要步骤,下面我来详细说明一下。

分析代码我们可以得出第一个断点处执行的代码就相当于创建 java 原生的ServerSocketChannel   ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();对应方法的init,把serverSocketChannel注册到selector上, SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);对应方法的Register。剩下的两个断点就是对应的绑定端口serverSocketChannel.bind(new InetSocketAddress(8080));我们可以看到第一个断点的方法返回的对象是ChannelFuture,这个过程是非阻塞的,如果regFuture.isDone(),就是注册完成比较快,会调用第二个断点处的方法,否则就会进入到第三个断点处。在第三个断点处是将绑定端口的任务交给了其他线程去完成,这个线程就是NIO线程。

private ChannelFuture doBind(final SocketAddress localAddress) {// 1. 执行初始化和注册 regFuture 会由 initAndRegister 设置其是否完成,从而回调 3.2 处代码final ChannelFuture regFuture = initAndRegister();final Channel channel = regFuture.channel();if (regFuture.cause() != null) {return regFuture;}// 2. 因为是 initAndRegister 异步执行,需要分两种情况来看,调试时也需要通过 suspend 断点类型加以区分// 2.1 如果已经完成if (regFuture.isDone()) {ChannelPromise promise = channel.newPromise();// 3.1 立刻调用 doBind0doBind0(regFuture, channel, localAddress, promise);return promise;} // 2.2 还没有完成else {final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);// 3.2 回调 doBind0regFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {Throwable cause = future.cause();if (cause != null) {// 处理异常...promise.setFailure(cause);} else {promise.registered();// 3. 由注册线程去执行 doBind0doBind0(regFuture, channel, localAddress, promise);}}});return promise;}
}

接下来我们先进入到initAndRegister()方法中。

关键代码如下:

final ChannelFuture initAndRegister() {Channel channel = null;try {channel = channelFactory.newChannel();// 1.1 初始化 - 做的事就是添加一个初始化器 ChannelInitializerinit(channel);} catch (Throwable t) {// 处理异常...return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);}// 1.2 注册 - 做的事就是将原生 channel 注册到 selector 上ChannelFuture regFuture = config().group().register(channel);if (regFuture.cause() != null) {// 处理异常...}return regFuture;
}

先创建了channel,channel = channelFactory.newChannel();那么是怎么创建的channel呢?我们进入到这个方法去看一下

我们可以看到是通过反射机制创建的channel。

接下来我们再回去看init(channel),这个方法做的就是将创建的channel进行了初始化,给channel添加了一个初始化器ChannelInitializer

我们进入到这个方法中去看一下

我将上面的代码注释了一下

// 这里 channel 实际上是 NioServerSocketChannel
void init(Channel channel) throws Exception {final Map<ChannelOption<?>, Object> options = options0();synchronized (options) {setChannelOptions(channel, options, logger);}final Map<AttributeKey<?>, Object> attrs = attrs0();synchronized (attrs) {for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {@SuppressWarnings("unchecked")AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();channel.attr(key).set(e.getValue());}}ChannelPipeline p = channel.pipeline();final EventLoopGroup currentChildGroup = childGroup;final ChannelHandler currentChildHandler = childHandler;final Entry<ChannelOption<?>, Object>[] currentChildOptions;final Entry<AttributeKey<?>, Object>[] currentChildAttrs;synchronized (childOptions) {currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));}synchronized (childAttrs) {currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));}// 为 NioServerSocketChannel 添加初始化器,第一次执行到此处时,只是添加了这个初始化器,
//还没有执行初始化器中的代码p.addLast(new ChannelInitializer<Channel>() {@Overridepublic void initChannel(final Channel ch) throws Exception {final ChannelPipeline pipeline = ch.pipeline();ChannelHandler handler = config.handler();if (handler != null) {pipeline.addLast(handler);}// 初始化器的职责是将 ServerBootstrapAcceptor 加入至 NioServerSocketChannelch.eventLoop().execute(new Runnable() {@Overridepublic void run() {pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));}});}});
}

接下来我们来分析注册部分的源码,跟进ChannelFuture regFuture = config().group().register(channel);断点处,经过一系列的调用链,来到了本质的地方。

此处涉及了主线程和NIO线程的切换,由于当前线程不是NIO线程,因此代码会运行到else代码块中,下面验证一下:

我们可以看到确实进入到了else代码块中,同时将真正用于注册的方法register0(promise);封装成了一个任务对象,交给了eventLoop去执行,我们知道eventLoop中封装的就是NIO线程,此处完成了main->nio boss线程的切换

上面的代码注释如下:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {// 一些检查,略...AbstractChannel.this.eventLoop = eventLoop;if (eventLoop.inEventLoop()) {register0(promise);} else {try {// 首次执行 execute 方法时,会启动 nio 线程,之后注册等操作在 nio 线程上执行// 因为只有一个 NioServerSocketChannel 因此,也只会有一个 boss nio 线程// 这行代码完成的事实是 main -> nio boss 线程的切换eventLoop.execute(new Runnable() {@Overridepublic void run() {register0(promise);}});} catch (Throwable t) {// 日志记录...closeForcibly();closeFuture.setClosed();safeSetFailure(promise, t);}}
}

我们继续跟进register0(promise)方法

register0方法的注释如下:

​
private void register0(ChannelPromise promise) {try {if (!promise.setUncancellable() || !ensureOpen(promise)) {return;}boolean firstRegistration = neverRegistered;// 1.2.1 原生的 nio channel 绑定到 selector 上,注意此时没有注册 selector 关注事件,附件为 NioServerSocketChanneldoRegister();neverRegistered = false;registered = true;// 1.2.2 执行 NioServerSocketChannel 初始化器的 initChannelpipeline.invokeHandlerAddedIfNeeded();// 回调 3.2 io.netty.bootstrap.AbstractBootstrap#doBind0safeSetSuccess(promise);pipeline.fireChannelRegistered();// 对应 server socket channel 还未绑定,isActive 为 falseif (isActive()) {if (firstRegistration) {pipeline.fireChannelActive();} else if (config().isAutoRead()) {beginRead();}}} catch (Throwable t) {// Close the channel directly to avoid FD leak.closeForcibly();closeFuture.setClosed();safeSetFailure(promise, t);}
}​

doRegister();将原生的 nio channel 绑定到 selector 上,pipeline.invokeHandlerAddedIfNeeded();上面说过在NioServerSocketChannel 进行初始化时,给channel添加了一个初始化器ChannelInitializer,但是没有执行初始化器中的代码,此处就要执行初始化器中的代码,下面来验证一下

我们可以看到代码又回来执行了初始化器中的代码,initChannel方法在注册完毕后调用,用于初始化channel。

下面是initChannel方法的注释

private boolean initChannel(ChannelHandlerContext ctx) throws Exception {if (initMap.add(ctx)) { // Guard against re-entrance.try {// 1.2.2.1 执行初始化initChannel((C) ctx.channel());} catch (Throwable cause) {exceptionCaught(ctx, cause);} finally {// 1.2.2.2 移除初始化器ChannelPipeline pipeline = ctx.pipeline();if (pipeline.context(this) != null) {pipeline.remove(this);}}return true;}return false;
}

下面我们回到register0方法

上面我加断点处的这行代码就是给initAndRegister()方法设置结果的,我上面有讲到initAndRegister()方法是一个异步方法,返回的是一个promise对象,

上面的safeSetSuccess(promise)就是给promise设置方法执行的结果,下面我们需要验证一下safeSetSuccess(promise)中的promise是不是initAndRegister()方法返回的对象,我将initAndRegister()方法的返回值做一个标记

运行到safeSetSuccess(promise);我们可以发现是同一个对象。

promise设置成功后进入doBind()方法的else代码块中执行回调函数

我们继续跟进到doBind0()方法中,进过一系列的调用链,我们直接来到核心部分

此处的doBind(localAddress);就是真正绑定端口的方法,我们跟进去看一下

绑定好后进入到上面的第二个断点,此处代码的作用就是使channel上绑定的handler都触发channelActive方法。初始化的channel拥有三个handler,head->acceptor->tail,实际上主要的工作都是由headHandler完成的,所以我直接把断点加在headHandler的channelActive上

headHandler的断点

断点处readIfIsAutoRead();方法的作用就是将serverSocketChannel设置关注accept事件,我们跟进readIfIsAutoRead();方法,进过一系列的调用链来到本质部分

我们可以看到目前为止,并没有关注任何事件,进入到if代码块后serverSocketChannel设置关注accept事件。

到此Netty启动流程就剖析完毕了。

为了使整个流程更加清晰,下面我会对上面的流程进行总结

Netty启动流程总结

1.initAndRegister()对channel进行初始化、注册,返回regFuture(promise对象)

    1.1 init  main(主线程中完成)

          创建NioServerSocketChannel  (相当于Nio中的ServerSocketChannel                  serverSocketChannel = ServerSocketChannel.open(); ) mian

           往NioServerSocketChannel添加初始化器handler main

           初始化handler等待调用 (初始化handler使accept事件发生后建立连接)nio-thread调用

    1.2 register(切换线程)

           启动nio boss  线程 main

           原始serverSocketChannel注册至Selector,此时还未关注事件 nio-thread

           执行NioServerScokerChannel初始化handler nio-thread

2. regFuture的回调doBind() nio-thread

    原生ServerSocketChannel绑定端口 nio-thread(相当于nio中的  serverSocketChannel.bind(new InetSocketAddress(8080)))nio-thread

    触发NioServerSocketChannel绑定的handler的channelActive方法。nio-thread

    

           

          

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[C++初阶]初识C++(二)

建议先看完上篇&#xff1a;[C初阶]初识C(一)—————命名空间和缺省函数-CSDN博客 本篇部分代码和文案来源&#xff1a;百度文库&#xff0c;知乎&#xff0c;比特就业课 1.函数重载 自然语言中&#xff0c;一个词可以有多重含义&#xff0c;人们可以通过上下文来判断该词真…

Linux目录结构知识

一、认识Linux目录 1) Linux目录结构知识 1&#xff09; win: 目录顶点是盘符 C/D/E 。所有的目录结构都在不同的盘符下面&#xff0c;不同的盘之间不能沟通的。 2&#xff09; Linux: 目录顶点是 / &#xff0c;称为根。所有的目录结构都在根下面&#xff0c;他的目录之间都…

基于SpringBoot Vue养老院管理

一、&#x1f4dd;功能介绍 基于SpringBoot Vue养老院管理 角色&#xff1a;管理员、企业、老人子女、老人 管理员&#xff1a;管理员登录进入养老院管理系统可以对系统首页、个人中心、服务人员管理、老人管理、老人子女管理、老人档案管理、社区活动管理、活动记录管理、床…

LogicFlow 在HTML中的引入与使用

LogicFlow 在HTML中的引入与使用 LogicFlow的引入与使用&#xff0c;相较于BPMNJS相对容易一些&#xff0c;更加灵活一些&#xff0c;但是扩展代码可能写得更多一些。 示例展示 使用方式 这个的使用方式就简单很多了&#xff0c;利用cdn把js下载下来&#xff0c;引入到HTML文…

【Linux】HTTP协议

HTTP协议 1.认识URL2.urlencode和urldecode3.HTTP协议格式4.HTTP协议基本工作流程5.HTTP的方法6.HTTP的状态码7.HTTP常见Header8.长连接9.cookie&&session会话保持10.基本工具(postman,fiddler) 喜欢的点赞&#xff0c;收藏&#xff0c;关注一下把&#xff01; 目前基本…

JDK安全剖析之安全处理入门

0.前言 Java 安全包括大量 API、工具以及常用安全算法、机制和协议的实现。Java 安全 API 涵盖了广泛的领域&#xff0c;包括加密、公钥基础设施、安全通信、身份验证和访问控制。Java 安全技术为开发人员提供了编写应用程序的全面安全框架&#xff0c;还为用户或管理员提供了…

相对论中关于光速不变理解的补充

近几个月在物理直播间聊爱因斯坦相对论&#xff0c;发现好多人在理解爱因斯坦相对论关于基本假设&#xff0c;普遍认为光速是不变的&#xff0c;质能方程 中光速的光速不变的&#xff0c;在这里我对这个假设需要做一个补充&#xff0c;他是基于质能方程将光速C 在真是光速变化曲…

平衡二叉树,红黑树,B树和B+树的区别及其应用场景

平衡二叉树 基础数据结构左右平衡高度差大于1会自旋每个节点记录一个数据 平衡二叉树&#xff08;AVL&#xff09; AVL树全称G.M. Adelson-Velsky和E.M. Landis&#xff0c;这是两个人的人名。 平衡二叉树也叫平衡二叉搜索树&#xff08;Self-balancing binary search tree…

计算机视觉新巅峰,微软牛津联合提出MVSplat登顶3D重建

开篇&#xff1a;探索稀疏多视图图像的3D场景重建与新视角合成的挑战 3D场景重建和新视角合成是计算机视觉领域的一项基础挑战&#xff0c;尤其是当输入图像非常稀疏&#xff08;例如&#xff0c;只有两张&#xff09;时。尽管利用神经场景表示&#xff0c;例如场景表示网络&a…

Spring Boot 接入 Redis

Spring Boot 接入 Redis 简介 Redis 是一种访问速度非常快的内存数据结构存储&#xff0c;用作数据库、缓存、消息代理和流引擎。提供 strings、hashes、lists、sets 等数据结构。可以解决会话缓存、消息队列、分布式锁、定期将数据集存储到硬盘等功能。 通过 Redis 设计实现…

云原生架构(微服务、容器云、DevOps、不可变基础设施、声明式API、Serverless、Service Mesh)

前言 读完本文&#xff0c;你将对云原生下的核心概念微服务、容器云、DevOps、Immutable Infrastructure、Declarative-API、Serverless、Service Mesh 等有一个相对详细的了解&#xff0c;帮助你快速掌握云原生的核心和要点。 因题主资源有限, 这里会选用部分云服务商的组件进…

Aurora8b10b(2)上板验证

文章目录 前言一、AXI_Stream数据产生模块二、上板效果总结 前言 上一篇内容我们已经详细介绍了基于aurora8b10b IP核的设计&#xff0c;本文将基于此进一步完善并且进行上板验证。 设计思路及代码思路参考FPGA奇哥系列网课 一、AXI_Stream数据产生模块 AXIS协议是非常简单的…

小林coding图解计算机网络|TCP篇06|如何理解TCP面向字节流协议、为什么UDP是面向报文的协议、如何解决TCP的粘包问题?

小林coding网站通道&#xff1a;入口 本篇文章摘抄应付面试的重点内容&#xff0c;详细内容还请移步&#xff1a;小林coding网站通道 文章目录 如何理解UDP 是面向报文的协议如何理解字节流如何解决粘包固定长度的消息 特殊字符作为边界自定义消息结构 如何理解UDP 是面向报文的…

第20次修改了可删除可持久保存的前端html备忘录:重新布局

第20次修改了可删除可持久保存的前端html备忘录&#xff1a;重新布局 <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"…

Elasticsearch:我们如何演化处理二进制文档格式

作者&#xff1a;来自 Elastic Sean Story 从二进制文件中提取内容是一个常见的用例。一些 PDF 文件可能非常庞大 — 考虑到几 GB 甚至更多。Elastic 在处理此类文档方面已经取得了长足的进步&#xff0c;今天&#xff0c;我们很高兴地介绍我们的新工具 —— 数据提取服务&…

[从零开始学习Redis | 第九篇] 深入了解Redis数据类型

前言&#xff1a; 在现代软件开发中&#xff0c;数据存储和处理是至关重要的一环。为了高效地管理数据&#xff0c;并实现快速的读写操作&#xff0c;各种数据库技术应运而生。其中&#xff0c;Redis作为一种高性能的内存数据库&#xff0c;广泛应用于缓存、会话存储、消息队列…

重读Java设计模式: 桥接模式详解

引言 在软件开发中&#xff0c;经常会遇到需要在抽象与实现之间建立连接的情况。当系统需要支持多个维度的变化时&#xff0c;使用传统的继承方式往往会导致类爆炸和耦合度增加的问题。为了解决这一问题&#xff0c;我们可以使用桥接模式。桥接模式是一种结构型设计模式&#…

ARM架构学习笔记2-汇编

RISC是精简指令集计算机&#xff08;RISC:Reduced Instruction Set Computing&#xff09; ARM汇编概述 一开始&#xff0c;ARM公司发布两类指令集&#xff1a; ① ARM指令集&#xff0c;这是32位的&#xff0c;每条指令占据32位&#xff0c;高效&#xff0c;但是太占空间 2…

物联网实战--入门篇之(十)安卓QT--后端开发

目录 一、项目配置 二、MQTT连接 三、数据解析 四、数据更新 五、数据发送 六、指令下发 一、项目配置 按常规新建一个Quick空项目后&#xff0c;我们需要对项目内容稍微改造、规划下。 首先根据我们的需要在.pro文件内添加必要的模块&#xff0c;其中quick就是qml了&…

燃气管网安全运行监测系统功能介绍

燃气管网&#xff0c;作为城市基础设施的重要组成部分&#xff0c;其安全运行直接关系到居民的生命财产安全和城市的稳定发展。然而&#xff0c;随着城市规模的不断扩大和燃气使用量的增加&#xff0c;燃气管网的安全运行面临着越来越大的挑战。为了应对这些挑战&#xff0c;燃…