【手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理】

news2024/11/19 20:28:51

手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理

  • RPC框架简介
  • 了解Dubbo的实现原理
    • 服务暴露
    • 服务引入
    • 服务调用
  • 手写实现一个简单版的Dubbo
    • 服务暴露
      • ServiceBean
      • ProxyFactory#getInvoker
      • Protocol#export
      • RegistryProtocol#export
    • 服务引入
      • RegistryProto#refer
      • DubboProtocol#refer
    • 服务调用
      • 服务消费者
      • 服务提供者
    • 与Spring对接
  • 代码

我越来越觉得学习一个开源框架或者中间件的底层原理,最好的方法不是看源码,而是自己尝试去写一个。之前看过许多开源框架和中间件的源码,但是随着时间的推移,多多少少都有点遗忘了,遗忘之后又花时间去把它捡一捡,于是就在这捡起又遗忘,遗忘又捡起这样来来回回的折腾中浪费了许多时间。

Dubbo的源码我很早以前就看过了,而且看了不止一遍,现在对于Dubbo的底层原理只记得个大概,至于源码中的细节,已经忘得差不多了。最近想起之前学习过Dubbo源码,为了加深印象,于是乎自己动手写了一个。这自己动手写跟只用眼睛看,效果还真不一样,因此写成文章分享出来。

RPC框架简介

在以前都是单体应用的年代,是不需要RPC框架的,那时候还不知道RPC是什么,所有的数据都是来自于本地的数据库,然后全都是本地的方法调用。

在这里插入图片描述

但是随着业务的发展,访问量的不断增加,原先的单体应用已经不能满足需求,于是要做应用拆分,原先的单体应用被拆成了多个服务,然后就出现了服务间调用的问题。

在这里插入图片描述

这样复杂度就一下子上来了,如何解决服务间调用的问题呢?我们可以自己写代码,通过http的方式去访问对方,或者通过Socket连上对方的系统进行通信,发送调用的方法名和调用参数等信息。但是这种方式有点笨,因为这种网络通信的代码一般都是重复的,而且也容易出错。

那么有没有什么办法可以让远程调用变成像之前单体应用那样就调一个本地方法,就能获取到返回结果呢?那就是引入一个RPC框架,RPC框架封装了远程调用的底层逻辑,屏蔽掉了网络通信的过程,让API调用工程师们简单调一下方法,就可以获取到远程系统返回的方法调用结果,于是这些API调用工程师们就觉得自己很牛B了。

在这里插入图片描述

我们当然不能当这种API调用工程师,要不然35岁之后就要强势加入骑士军团了。

在这里插入图片描述

其实RPC框架的原理并没有多复杂。

比如现在有一个 UserService接口,在以前还是单体应用的时候,Spring给我们注入的是一个实现类,我们调用这个接口的方法时就可以调用UserServiceImpl实现类。

在这里插入图片描述

而现在这个实现类在别的系统上,我们拿不到,于是RPC框架给我们注入的是一个代理对象,这个代理对象也实现了这个UserService接口,通过这个代理对象可以访问到对方系统的UserServiceImpl。然后RPC框架通过一个stub存根封装了网络通信的逻辑,当我们调用UserService接口的某个方法时,实际上是通过代理对象调用到了底层的存根类,stub存根再把我们调用的方法和参数封装为网络协议包,发送到对端系统。对端系统也有一个stub存根,接收到发送过来的协议包后,解析出要调用的方法和参数,交给UserServiceImpl去处理,然后再把处理结果通过stub存根返回给我们本地的存根,我们本地的存根再把结果返回给我们。

在这里插入图片描述

但是A系统怎么知道B系统的ip地址和端口好呢?如果是在A系统上配死那就太low了,一旦B系统改了ip地址或端口,A系统就要改配置重启。因此,还需要一个注册中心存储B系统发布的ip地址和端口,A系统从注册中心拉取B系统的ip地址和端口。此时B系统就是服务提供者,把ip地址和端口注册到注册中心这个动作就是服务暴露,而A系统就是服务消费者,从注册中心拉取服务提供者的ip地址端口等信息这个动作就是服务引入,A系统调用B系统这个动作就叫服务调用

在这里插入图片描述
这样,一个RPC调用框架就形成了。

了解Dubbo的实现原理

但是,理解到这一层,还远远不够,毕竟别人可以面试前把这些背一背,面试的时候忽悠面试官。于是,我们要阅读源码,对开源框架有更深一层的理解,这样才能让面试官从一众的渣渣中把我们区分出来。

Dubbo是众多的RPC框架中用的比较多的一个,我们来参考参考Dubbo的原理,也是分为服务暴露、服务引入、服务调用三块。

服务暴露

首先我们写的类是这样子的:
在这里插入图片描述

UserServiceImpl上声明了@Service注解(这个是Dubbo自己定义的@Service注解,不是Spring的那个),就代表我们要把这个UserServiceImpl暴露出来供服务消费者调用。

Dubbo要扫描它,然后把它封装成ServiceBean,ServiceBean是Dubbo封装的一个用来做服务暴露的实现类,它实现了Spring的ApplicationListener<ContextRefreshedEvent>接口,会监听ContextRefreshedEvent容器刷新完成事件触发服务暴露,而ServiceBean的export方法就是服务暴露的入口。

在这里插入图片描述

Dubbo在服务暴露中,一共做了三件事:

  1. 把我们的UserServiceImpl对象包装成Invoker,再把Invoker包装成Exporter,再把Exporter放入一个map中,key是根据实现类计算出来的一个唯一的serviceKey。这一步相当于是本地注册,方便后面接收到远程调用请求时,根据请求信息计算出serviceKey,找到目标实现类。
  2. 开启Netty服务端,用于接收远程调用请求。
  3. ip地址、端口、协议等服务信息注册到注册中心,这些信息注册到注册中心后,消费方就可以从注册中心中获取到服务提供者的信息了。

在这里插入图片描述

服务引入

至于服务引入,我们写的消费者类一般是这样:
在这里插入图片描述

属性上声明了这个@Reference注解,表示这个属性需要通过Dubbo的服务引入来进行属性注入。

那么,Dubbo就是扫描所有被@Reference注解修饰的属性,进行属性注入。Dubbo这里处理属性注入的逻辑与Spring处理@Autowired的逻辑大体是相同的,这里先不用管(后面会讲解)。扫描到的属性,通过ReferenceBean的get()方法触发服务引入,返回一个代理对象,注入到该属性中。

在这里插入图片描述

而服务引入里面做了些啥呢?其实就是从注册中心中获取服务提供者的ip端口等信息,开启NettyClient连接上服务提供者,然后封装到一个Invoker中,这个Invoker就等同于上面说的存根类。然后通过动态代理返回一个代理对象,这个代理对象会调用Invoker进行远程方法调用。

在这里插入图片描述

服务调用

服务调用的逻辑自然是被隐藏在了Invoker里面。服务间通过Netty进行网络通信,服务消费者把调用的方法名,接口的类全限定名,方法参数等封装成一个Invocation对象,然后序列化成二进制,通过Netty发送给服务提供者,服务提供者的Netty接收到服务消费者发来的数据时,会反序列化成Invocation对象,根据服务消费者提供的信息,找到具体的实现类进行处理,处理结果再由Netty返回给服务消费者。

在这里插入图片描述

最终整体流程就是这样:
在这里插入图片描述

没有看过源码的,也可以记一下这张图,然后面试的时候喷给面试官,也能拿个60分。

按照这个思路,我们就可以动手写代码了。

手写实现一个简单版的Dubbo

我们写代码的顺序还是按照服务暴露、服务引入、服务调用的顺序来,但是我们实现的迷你版Dubbo是要与Spring对接的,所以最后还要实现与Spring对接的逻辑。

这里要注意,下面还是以一边讲解一边画图的方式进行,不会贴代码。因为如果贴代码的话,这文章没个几万字是结束不了的。想看代码的可以到代码仓去下载,文末会附上代码仓的地址。

服务暴露

ServiceBean

服务暴露的逻辑从ServiceBean开始,我们就先从ServiceBean开始实现我们的迷你版Dubbo。

我们的ServiceBean也是实现ApplicationListener<ContextRefreshedEvent>接口,在onApplicationEvent(ContextRefreshedEvent event)方法中调用export()方法触发服务暴露。

在这里插入图片描述

export()方法首先要读取注册中心的配置,Dubbo是通过RegistryConfig类型封装注册中心的配置的,我们也定义一个RegistryConfig类封装注册中心的配置,这里通过RegistryConfig获取到注册中心的ip地址和端口等信息。

Dubbo是通过URL对象去封装服务暴露的信息,比如协议、ip地址、端口、访问路径(调哪个类的哪个方法)、方法参数等信息,然后注册到注册中心的也是这个URL对象转成的url格式的字符串。我们这里也定义一个URL对象去存这些信息,我们组装我们的URL对象。

URL中包含的协议、端口等信息,可以通过ProtocolConfig进行配置,然后从ProtocolConfig中取出放入到URL对象中。

封装好URL对象后,接下来就是通过ProxyFactory(代理工厂)生成服务提供方的Invoker,这个Invoker保存了真正的服务实现类。

最后就是通过Protocol对象的export方法进行服务暴露,这里也是参考Dubbo的,Dubbo真正的服务暴露逻辑是封装在Protocol的export方法里面的,本地注册、开启Netty、注册相关信息到注册中心等动作都是在这里面进行。我们也定义一个Protocol接口。

这样看来,ServiceBean需要RegistryConfig和ProtocolConfig这两个对象,我们可以通过ApplicationContext去获取,因此ServiceBean需要实现ApplicationContextAware这个接口,通过这个接口获取到ApplicationContext,然后ServiceBean还要实现InitializingBean接口,在InitializingBean接口的afterPropertiesSet()方法里面通过ApplicationContext获取RegistryConfig和ProtocolConfig这两个对象。

这里的ApplicationContextAware、InitializingBean、ApplicationListener等接口都是Spring的接口,不熟悉的可以去补一下Spring的知识。

在这里插入图片描述

ProxyFactory#getInvoker

ProxyFactory.getInvoker(ref, this.interfaceClass, url) 这一步是封装具体实现类ref为Invoker的。这个Invoker的invoke方法会从Invocation中获取方法名、方法参数类型和方法参数,然后通过反射调用具体实现类ref。

在这里插入图片描述

Protocol#export

protocol.export(registryURL, invoker);这一步是真正进行服务暴露的,但是这里的protocol其实是一个代理类,Dubbo通过他自己实现的SPI机制,会调用到具体的Protocol实现类。我们这里也是一个代理类,使用的时JDK的动态代理,但是我们就不实现自己的SPI机制了,我们就使用Java的SPI机制。在InvocationHandler的invoke方法中通过Java的SPI机制加载所有的实现类,循环遍历进行匹配,匹配逻辑是看URL中的protocol属性,也就是url中的协议,然后反射调用匹配到的具体实现类。

在这里插入图片描述

现在的整体流程就走到这里:
在这里插入图片描述

RegistryProtocol#export

这里的URL的protocol属性是registry,因此这里匹配到的是RegistryProtocol类型的Protocol,于是会调用到RegistryProtocol的export方法。

RegistryProtocol的export方法又会再次调用protocol的export方法,这里的protocol依然是代理对象,但是这次的URL的protocol属性是dubbo,因此会调用到DubboProtocol的export方法。

DubboProtocol的export方法会把Invoker包装成Exporter,用接口类全限定名作为key,Exporter作为value,放入到一个map,这样当接收到远程调用请求时,就可以通过接口名找到对应的Invoker,进而调用里面的实现类。

DubboProtocol的export方法接下来会开启Netty服务端,用于接收远程调用请求。

DubboProtocol的export方法结束后返回到RegistryProtocol的export方法,接下来会调用注册中心的客户端把服务暴露信息注册到注册中心,比如注册中心是Zookeeper,则通过Zookeeper的客户端把服务暴露信息发布到Zookeeper注册中心。这里注册中心的具体类型还是通过动态代理加上Java的SPI机制来动态进行匹配的,而注册到注册中心的信息也是url格式的。

在这里插入图片描述

到这里,服务暴露的流程就结束了:
在这里插入图片描述

服务引入

服务引入的入口是在ReferenceBean的get()方法,我们ReferenceBean的get()的会返回一个代理对象,注入到被@Reference注解修饰的属性上。这里先不用管ReferenceBean的get()方法如何被调用到,我们先完成ReferenceBean的get()方法的服务引入逻辑。

ReferenceBean的get()方法第一步也是通过RegistryConfig获取到注册中心的ip和端口等信息。

然后调用Protocol的refer方法进行服务引入,这个方法会返回一个Invoker对象,这个Invoker对象的invoke方法会通过Netty向远端发起远程调用。

最后通过ProxyFactory的getProxy方法把Invoker封装到一个代理对象中,这里我们还是使用JDK的动态代理,InvocationHandler的invoke方法会调用Invoker的invoke方法。

在这里插入图片描述

RegistryProto#refer

Protocol的refer方法是真正进行服务引入的方法,这里还是通过动态代理,首先走到RegistryProto的refer方法

RegistryProto的refer方法返回的Invoker对象是比较复杂的,有个几层的嵌套。

考虑到服务提供者有可能是以集群的形式部署的,因此我们这里定义了一个ClusterInvoker类型的Invoker与之对应。ClusterInvoker包装了一个RegistryDirectory对象,它是一个服务目录,里面保存了一个List<Invoker>,这里的一个Invoker对应一个服务提供者。这样我们的服务就具备一定的容错能力,可以通过负载均衡选出一个Invoker,一个调不通,可以换下一个。

在这里插入图片描述

那List<Invoker>里面的Invoker是怎么来的呢?

首先,我们会监听注册中心,一旦服务提供者上下线导致注册中心中的信息有变动,我们会收到通知,收到通知后我们重新从注册中心中查询服务提供者的url。同时在创建RegistryDirectory对象时,RegistryDirectory的构造方法也会主动去注册中心查一次。

然后,我们定义了一个NotifyListener接口,我们的RegistryDirectory实现了NotifyListener接口,NotifyListener的notify方法接收从注册中心查询回来的url,循环调用protocol.refer(url, serviceType)方法。protocol.refer(url, serviceType)会调用到DubboProtocol的refer方法,DubboProtocol的refer方法就会返回与服务提供者对应的一个Invoker。

在这里插入图片描述

DubboProtocol#refer

DubboProtocol的refer方法会从参数url中解析出服务提供者的ip地址和端口号,开启Netty客户端,连接到服务提供者,然后包装成一个Invoker返回。

在这里插入图片描述

那么服务引入的整体逻辑就是这样:

在这里插入图片描述

服务调用

服务消费者

服务消费者的代理对象会通过InvocationHandler的invoke方法,调用到ClusterInvoker的invoke方法。

ClusterInvoker的invoke方法调用RegistryDirectory的list方法获取List<Invoker>,然后通过负载均衡算法选出一个进行调用,调用失败则切换下一个。

负载均衡选出一个Invoker后,调用Invoker的invoke方法就会进入到DubboInvoker的invoke方法。DubboInvoker的invoke方法把接口类全限定名、方法名、方法参数类型、方法参数等信息包装成Invocation对象,调用Netty客户端发送请求。

Netty客户端的编解码器会把Invocation对象序列化成二进制,然后发送到网络。

因为Netty是异步的,因此这里要创建一个Future对象然后绑定一个id,发送请求时把这id带上,服务提供者处理完后返回处理结果时同时返回这个id,服务消费者就能通过id拿到Future,设置返回结果到这个Future上,那么当时发送请求的线程就能通过这个Future拿到返回结果。

在这里插入图片描述

服务提供者

服务提供者的Netty接收到请求后,反序列成Invocation对象,根据Invocation中的接口类全限定名,从map中取出Exporter。然后从Exporter中取出Invoker,调用Invoker的invoke方法,Invoker的invoke方法从Invocation取出方法名,方法参数类型、方法参数,反射调用具体实现类,具体实现类处理完成后返回的结果,带上服务消费者传过来的id,一起返回给服务提供者。

在这里插入图片描述

与Spring对接

这些都写好以后,一个RPC框架大体就形成了,但是我们还差最后一步,就是与Spring对接。

我们参考Dubbo,定义了一个@EnableDubbo注解,@EnableDubbo注解通过@Import注解Spring容器导入一个DubboComponentScanRegistrar类,DubboComponentScanRegistrar实现了Spring的ImportBeanDefinitionRegistrar接口,重写了ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,DubboComponentScanRegistrar的registerBeanDefinitions方法会被Spring回调到。DubboComponentScanRegistrar的registerBeanDefinitions方法往Spring容器中注入两个bean,一个是ServiceAnnotationBeanPostProcessor类型,实现了Spring的BeanDefinitionRegistryPostProcessor接口,用于扫描@Service注解然后注册BeanDefinition到Spring容器中;另一个是ReferenceAnnotationBeanPostProcessor类型,是一个Bean后置处理器,用于扫描@Reference注解进行服务引入的。

在这里插入图片描述

ServiceAnnotationBeanPostProcessor通过Spring的ClassPathBeanDefinitionScanner类扫描出@Service注解修饰的类的BeanDefinition,然后创建一个ServiceBean类型的BeanDefinition,配置相应的interfaceClass和ref等属性,然后注册到容器中。

ClassPathBeanDefinitionScanner是Spring提供的一个用来扫描指定包路径获取BeanDefinition的工具类。

在这里插入图片描述

ReferenceAnnotationBeanPostProcessor参考Spring处理@Autowired的逻辑,ReferenceAnnotationBeanPostProcessor继承了InstantiationAwareBeanPostProcessorAdapter,并重写了InstantiationAwareBeanPostProcessorAdapter的postProcessPropertyValues方法,ReferenceAnnotationBeanPostProcessor的postProcessPropertyValues方法会被Spring回调到,给当前bean进行属性注入,然后postProcessPropertyValues方法中收集当前bean中被@Reference注解修饰的属性,创建ReferenceBean,对ReferenceBean进行相应配置,调用ReferenceBean的get()方法返回一个代理对象,注入到该属性中。

InstantiationAwareBeanPostProcessorAdapter是Spring提供的一个Bean后置处理器,可以通过继承InstantiationAwareBeanPostProcessorAdapter并重写postProcessPropertyValues方法定制我们的属性注入逻辑。

在这里插入图片描述

代码

到这里,我们的RPC框架就大功告成了。基本上是参考Dubbo来写的,保留了Dubbo的核心逻辑,又去掉了一些细枝末节,比起原来的Dubbo代码简化了许多,但是确实能够帮助我们比较深刻的理解Dubbo的原理和逻辑。

本文章涉及的所有代码,都上传到git代码仓库了,由于篇幅有限,这些代码就不贴到本篇文章中了,有兴趣的可以到代码仓库下载下来看一下。

git代码仓库地址:https://gitcode.net/weixin_43889578/mini-dubbo

在这里插入图片描述

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

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

相关文章

3、点亮一个LED

新建工程 project—>New uVision Project LED介绍 中文名&#xff1a;发光二极管 外文名&#xff1a;Light Emitting Diode 简称&#xff1a;LED 用途&#xff1a;照明、广告灯、指引灯 电路图分析 进制的转换 生成下载文件&#xff1a; 代码 //导包 #inclu…

Keil5个性化设置及常用快捷键

Keil5个性化设置及常用快捷键 1.概述 这篇文章是Keil工具介绍的第三篇文章&#xff0c;主要介绍下Keil5优化配置&#xff0c;以及工作中常用的快捷键提高开发效率。 第一篇&#xff1a;《安装嵌入式单片机开发环境Keil5MDK以及整合C51开发环境》https://blog.csdn.net/m0_380…

leetcode刷题详解——买卖股票的最佳时机含手续费

1. 题目链接&#xff1a;714. 买卖股票的最佳时机含手续费 2. 题目描述&#xff1a; 给定一个整数数组 prices&#xff0c;其中 prices[i]表示第 i 天的股票价格 &#xff1b;整数 fee 代表了交易股票的手续费用。 你可以无限次地完成交易&#xff0c;但是你每笔交易都需要付手…

PostgreSQL 分区表插入数据及报错:子表明明存在却报不存在以及column “xxx“ does not exist 解决方法

PostgreSQL 分区表插入数据及报错&#xff1a;子表明明存在却报不存在以及column “xxx“ does not exist 解决方法 问题1. 分区表需要先创建子表在插入&#xff0c;创建子表立马插入后可能会报错子表不存在&#xff1b;解决&#xff1a; 创建子表及索引后&#xff0c;sleep10毫…

动态规划经典例题leetcode思路代码详解

目录 动态规划基础篇例题 leetcode70题.爬楼梯 leetcode746题.使用最小花费爬楼梯 leetcode198题.打家劫舍 leetcode62题.不同路径 leetcode64题.最小路径和 leetcode63题.63不同路径II 动态规划基础篇例题 这一篇的例题解答是严格按照我上一篇写的动态规划三部曲做的&…

中职组网络安全-linux渗透测试-Server2203(环境+解析)

任务环境说明&#xff1a; 服务器场景&#xff1a;Server2203&#xff08;关闭链接&#xff09; 用户名&#xff1a;hacker 密码&#xff1a;123456 1.使用渗透机对服务器信息收集&#xff0c;并将服务器中SSH服务端口号作为flag提交&#xff1b; FLAG:2232 2. 使用渗透机对…

chrome 调试之 - 给微软小冰看病(无论给小冰发送什么内容都只回复“我已经开始升级啦,期待一下吧!”)

微软 Bing 搜索推出了小冰AI智能聊天模块&#xff0c;具体启用方式是用edge或chrome浏览器打开链接 cn.bing.com 后在输入框搜索任意内容&#xff0c;待搜索结果页面加载完并稍等片刻&#xff0c;页面右侧就会出现一个躲在滚动条后面的小萝莉&#xff0c;抚摸...不&#xff0c;…

音频——S/PDIF

文章目录 BMC 编码字帧(sub-frame)格式帧(frame)格式参考S/PDIF 是 SONY 和 Philips 公司共同规定的数字信号传输规范,其实就是在 AES/EBU 上进行改动的家用版本。IEC60958 的标准规范囊括了以上两个规范。spdif 采用了双相符号编码(BMC),是将时钟信号和数据信号混合在一起…

python:傅里叶分析,傅里叶变换 FFT

使用python进行傅里叶分析&#xff0c;傅里叶变换 FFT 的一些关键概念的引入&#xff1a; 1.1.离散傅里叶变换&#xff08;DFT&#xff09; 离散傅里叶变换(discrete Fourier transform) 傅里叶分析方法是信号分析的最基本方法&#xff0c;傅里叶变换是傅里叶分析的核心&…

数据库设计规范(收藏)

本文的目的是提出针对Oracle数据库的设计规范&#xff0c;使利用Oracle数据库进行设计开发的系统严格遵守本规范的相关约定&#xff0c;建立统一规范、稳定、优化的数据模型。 参照以下原则进行数据库设计&#xff1a; 方便业务功能实现、业务功能扩展&#xff1b;方便设计开发…

爪语言 之 如何处理Java异常?

以小我融入大我,青春献给祖国 目录 1.异常的概念与体系 1.1异常的概念 1.2 异常的体系 1.3 异常的分类 2. 异常的处理 2.1 防御式编程 2.2异常的抛出 2.3 异常的捕获 2.3.1 异常声明throws 2.3.2 try-catch捕获并处理 2.3.3 finally 2.4 异常的处理流程总结 3. 自定…

Jmeter性能综合实战——签到及批量签到

提取性能测试的三个方面&#xff1a;核心、高频、基础功能 签 到 请 求 步 骤 1、准备工作&#xff1a; 签到线程组 n HTTP请求默认值 n HTTP cookie 管理器 n 首页访问请求 n 登录请求 n 查看结果树 n 调试取样器 l HTTP代理服务器 &#xff08;1&#xff09;创建线…

h5小游戏--2048

2048 经典2048小游戏&#xff0c;基于JS、Html5改写版 效果预览 点我下载源代码 下载代码解压后&#xff0c;双击index.html即可开始本游戏。 Game Rule 游戏规则 以下为游戏默认规则&#xff0c;若需要修改规则请修改代码。 移动箭头键来移动方块&#xff0c;当两个相同数…

一定要会用selenium的等待,三种等待方式解读

​很多人问&#xff0c;这个下拉框定位不到、那个弹出框定位不到…各种定位不到&#xff0c;其实大多数情况下就是两种问题&#xff1a; 有frame 没有加等待 殊不知&#xff0c;你的代码运行速度是什么量级的&#xff0c;而浏览器加载渲染速度又是什么量级的&#xff0c;就好…

【cf 158 c】

给你一个整数数组 a1,a2,…,an ( )。在一次操作中&#xff0c;你可以选择一个整数 x ( )&#xff0c;并用 ⌊⌋ 替换 ai ( ⌊y⌋ 表示将 y 舍入为最接近的整数)。 来替换从 1 到 n 的所有 i。请注意&#xff0c;每次操作都会影响数组中的所有元素。打印使数组中所有元素相等所…

SparkSQL之Optimized LogicalPlan生成过程

经过Analyzer的处理&#xff0c;Unresolved LogicalPlan已经解析成为Analyzed LogicalPlan。Analyzed LogicalPlan中自底向上节点分别对应Relation、Subquery、Filter和Project算子。   Analyzed LogicalPlan基本上是根据Unresolved LogicalPlan一对一转换过来的&#xff0c;…

小白也能看得懂的Jmeter性能测试中服务端资源监控技术

操作步骤&#xff1a; 1、安装插件管理器 插件管理器的作用&#xff1a;可以提供扩展插件的在线安装升级和卸载。因为我们需要在线安装监控插件&#xff0c;首先我们就要先安装插件管理器。 插件管理器的下载地址&#xff1a;https://jmeter-plugins.org/install/Install/ 如…

ESP32网络开发实例-远程Web串口监视器

远程Web串口监视器 文章目录 远程Web串口监视器1、应用介绍2、软件准备3、硬件准备4、代码实现在本文中,我们将构建一个 ESP32 网络服务器,用作远程串行监视器。 基于 Web 的串行监视器的工作方式与通常用于调试目的的 Arduino IDE 串行监视器的工作方式相同。 1、应用介绍 …

信息检索策略和技巧

指定检索策略并检索 确定检索词 检索课题&#xff1a;查找与“新型冠状病毒疫苗研制进展”有关的学术论文 检索式(2019-nCoV or 2019新型冠状病毒 or nCov-2019 or SARS-CoV-2 or COVID-19) and (疫苗 or 预防针 or 防疫针 or vaccin or vaccine) 扩展检索词的方式 同义词…

人工智能轨道交通行业周刊-第66期(2023.11.20-11.26)

本期关键词&#xff1a;智能铁鞋、TFDS、道岔密贴检查、Agent、Q*假说 1 整理涉及公众号名单 1.1 行业类 RT轨道交通人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟VSTR铁路与城市轨道交通RailMetro轨道世界铁路那些事铁路…