当前位置: 首页 > news >正文

开源库源码分析:Okhttp源码分析(一)

开源库源码分析:OkHttp源码分析

在这里插入图片描述

导言

接下来就要开始分析一些常用开源库的源码了,作为最常用的网络请求库,OkHttp以其强大的功能深受Android开发者的喜爱(比如说我),还有对该库进行二次封装而成的热门库,比如说Retrofit。本文我们将从源码入手看看OkHttp是如何运作的。注意本文解析的是OkHttp3库,该库是用Kotlin写的,需要大家有一些Kotlin基础。

OkHttp的最佳使用

这个问题是在OkHttp3的OkHttpClient中的注释中发现的:
在这里插入图片描述
这段注释中提到了OkHttp最好是用单例的OkHttpClient来实现请求,我们可以对该单例进行复用。这是因为每一个OkHttpClient都会持有一个连接池和线程池,都称之为池了,那么其作用肯定就是为了复用。通过复用这些连接和线程我们可以显著地降低延迟和节约内存使用。

实际上这段注释中已经提到了OkHttp中的一些复用机制了,连接复用和线程复用。

调度类Dispatcher

调度类的线程池

OkHttp中核心类之一就是这个调度类Dispatcher,所有的网络请求类Call都需要通过这个调度类来执行任务。这个调度类是OkHttpClient类的成员变量,我们是在调度类中执行请求的,那我们就顺便再来介绍一下客户端类中的成员变量:

//调度类
@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher
//连接池
@get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool
//应用拦截器
@get:JvmName("interceptors") val interceptors: List<Interceptor> = builder.interceptors.toImmutableList()
//网络拦截器
@get:JvmName("networkInterceptors") val networkInterceptors: List<Interceptor> =builder.networkInterceptors.toImmutableList()
//事件监听工厂
@get:JvmName("eventListenerFactory") val eventListenerFactory: EventListener.Factory =builder.eventListenerFactory

这几个应当是最核心的几个成员变量,我们将在后面再接触它们。回归正题,我们继续看调度类,既然要执行Call,那么这个调度器类就一定会有线程池,这个线程池有两个手段可以设置:

  private var executorServiceOrNull: ExecutorService? = null//线程池@get:Synchronized //在调用get时设置线程池为一个默认线程池,类似于一个CachedPool@get:JvmName("executorService") val executorService: ExecutorServiceget() {if (executorServiceOrNull == null) {executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))}return executorServiceOrNull!!}constructor(executorService: ExecutorService) : this() { //通过构造方法设置线程池this.executorServiceOrNull = executorService}

可以看到,方法一是调用构造函数来设置线程池,第二个方法就是在调用get方法时会自动设置一个效果类似于CachedPool的线程池。

调度类执行请求

我们使用OkHttp时一般是两种方式,同步请求execute异步请求enqueue,我们以异步请求为例来分析调度类是如何执行网络请求的。一般我们发送异步请求是这样的形式:

    public static void main(String[] args) {OkHttpClient client = new OkHttpClient();Request r = new Request.Builder().build();Call call = client.newCall(r);call.enqueue(new Callback() {@Overridepublic void onFailure(@NonNull Call call, @NonNull IOException e) {}@Overridepublic void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {}});}

首先通过RequestBuilder类来构建了一个请求类,然后将该请求传入newCall方法中新创建出了一个Call

override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

可以看到返回的是RealCall类,该类实现了Call接口。接下来就从Call的enqueue方法入手,这里实际上是RealCall类中:

  override fun enqueue(responseCallback: Callback) {check(executed.compareAndSet(false, true)) { "Already Executed" }callStart() //开启事件监听client.dispatcher.enqueue(AsyncCall(responseCallback)) //通过调度类入队}

这其中callStart方法会开启事件监听,我们先不管这个事件监听,看最后一句通过调度器来入队,这个AsyncCall()类是异步请求类,实现了Runnable接口,这里实际上就是将我们传入的实现了Callback的回调给包装成了AsyncCall。我们继续看Dispatcher中是如何入队的:

  internal fun enqueue(call: AsyncCall) {synchronized(this) {readyAsyncCalls.add(call)// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to// the same host.if (!call.call.forWebSocket) {val existingCall = findExistingCallWithHost(call.host)if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)}}promoteAndExecute()}

首先为了确保线程安全,调度器先将其上锁然后将之前包装成的AsyncCall加入到了readyAsyncCall就绪队列中。中间这一段判断是用于处理与主机(host)相关的异步调用复用。如果 call 不是用于 WebSocket 调用,它会尝试查找是否已经存在一个与相同主机(host)相关的异步调用任务。如果找到了已经存在的异步调用,它会尝试共享这两个异步调用任务之间的状态。这是为了复用已经建立的连接,以提高性能和减少资源消耗。

最后会调用promoteAndExecute方法执行,这个方法是执行请求的核心之一:

 private fun promoteAndExecute(): Boolean {this.assertThreadDoesntHoldLock()val executableCalls = mutableListOf<AsyncCall>()//可执行的Callsval isRunning: Booleansynchronized(this) {val i = readyAsyncCalls.iterator()//获得一个迭代器while (i.hasNext()) { //开始迭代val asyncCall = i.next()if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.i.remove()asyncCall.callsPerHost.incrementAndGet()executableCalls.add(asyncCall)runningAsyncCalls.add(asyncCall)}isRunning = runningCallsCount() > 0}for (i in 0 until executableCalls.size) {val asyncCall = executableCalls[i]asyncCall.executeOn(executorService)}return isRunning}

这段代码的核心逻辑就是while循环之中,这很显然是一个通过迭代器开始遍历就绪队列readyAsyncCall的过程,它会将就绪队列中的call移入到可执行队列executableCalls和正在执行异步队列runningAsyncCalls中。其中还有两句判断条件:

        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

一句是判断正在运行队列没有超过最大Size,一句是判断每一句的最大执行数不超过最大值,这两个值在Dispatcher中都有定义。最后会跳入到for循环中并调用AsyncCall的executeOn方法,并将线程池传入,也就是接下来就是在线程池中执行该异步请求了。我们最后来看这个executeOn方法:

fun executeOn(executorService: ExecutorService) {client.dispatcher.assertThreadDoesntHoldLock()var success = falsetry {executorService.execute(this)success = true} catch (e: RejectedExecutionException) {val ioException = InterruptedIOException("executor rejected")ioException.initCause(e)noMoreExchanges(ioException)responseCallback.onFailure(this@RealCall, ioException)} finally {if (!success) {client.dispatcher.finished(this) // This call is no longer running!}}}

这个方法真是开门见山。在try语句的第一句处就通过线程池执行了该任务,还记得这个AsyncCall是由我们传入的Call回调包装而成吗?接下来就会回调到我们的函数之中去了。众所周知,由于executorService.execute(this)这一句,线程池接下来会执行AsyncCall的run方法了:

    override fun run() {threadName("OkHttp ${redactedUrl()}") {var signalledCallback = falsetimeout.enter()try {val response = getResponseWithInterceptorChain() //1--通过拦截器链来获得响应signalledCallback = trueresponseCallback.onResponse(this@RealCall, response)//2--请求成功,回调到我们传入的onResponse方法中} catch (e: IOException) {if (signalledCallback) {// Do not signal the callback twice!Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)} else {responseCallback.onFailure(this@RealCall, e) //3--请求失败,回调到我们传入的onFailure方法之中去}} catch (t: Throwable) {cancel()if (!signalledCallback) {val canceledException = IOException("canceled due to $t")canceledException.addSuppressed(t)responseCallback.onFailure(this@RealCall, canceledException)}throw t} finally {client.dispatcher.finished(this)//结束本次请求}}}

我已经在代码之中加入一些注释了,在注释一处会通过拦截器链来获得响应,这个拦截器机制我们之后再将,这里只要知道它在这里获得了响应即可。在注释二处我们就可以看到这里显然是调用了我们传入的Call回调了,请求成功调用onResponse回调,要是请求失败的话则回调到onFailure方法之中去。最后在finally块中将本次请求结束,实际上这个finished方法还会开启下一次循环:

  internal fun finished(call: AsyncCall) {call.callsPerHost.decrementAndGet()finished(runningAsyncCalls, call)}private fun <T> finished(calls: Deque<T>, call: T) {val idleCallback: Runnable?synchronized(this) {if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")idleCallback = this.idleCallback}val isRunning = promoteAndExecute()if (!isRunning && idleCallback != null) {idleCallback.run()}}

由第一个函数会调转到第二个函数,从第二个函数来看,它首先会将本次的call从runningAsyncCalls正在运行队列中移除,然后会继续调用我们之前提到过的promoteAndExecute方法开启下一次循环,如果没有任务了的话并且空闲处理程序不为空的话,还会执行空闲处理程序。

拦截器链

之前提到网络请求的相应是通过拦截器链获得的:

val response = getResponseWithInterceptorChain()

首先,什么是拦截器呢?

在计算机编程领域,拦截器(Interceptor)是一种常见的设计模式,用于在软件系统的不同组件之间添加或修改功能。拦截器的主要目的是拦截请求或操作,允许在请求进入目标组件之前或之后执行自定义逻辑。

简单来说就是有许多个拦截器,一个请求的执行需要经过这一整条拦截器链,在一个请求到达一个拦截器的时候我们就可以判断是否要拦截这个请求并进行处理,简单来说就是类似于旅行途中的设置关口。Android中这种模式也运用,比如说广播View的事件分发和处理,这都是拦截器模式的运用。

好了,言归正传,我们继续回到上面的方法上:

internal fun getResponseWithInterceptorChain(): Response {// Build a full stack of interceptors.val interceptors = mutableListOf<Interceptor>() //创建拦截器集合interceptors += client.interceptors //添加用户设置的应用拦截器interceptors += RetryAndFollowUpInterceptor(client) //负责重试和重定向 interceptors += BridgeInterceptor(client.cookieJar) //用于桥接应用层和网络层的请求数据interceptors += CacheInterceptor(client.cache) //用于处理缓存interceptors += ConnectInterceptor //网络连接拦截器,用于获取一个链接if (!forWebSocket) {//添加用户设置的网络拦截器interceptors += client.networkInterceptors}interceptors += CallServerInterceptor(forWebSocket)//用于请求网络并获取网络响应//创建责任链val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0,exchange = null,request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)var calledNoMoreExchanges = falsetry { //启动责任链val response = chain.proceed(originalRequest)if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}return response} catch (e: IOException) {calledNoMoreExchanges = truethrow noMoreExchanges(e) as Throwable} finally {if (!calledNoMoreExchanges) {noMoreExchanges(null)}}}

具体代码的含义已经写在注释中了,首先要提醒大家上面的+=号是Kotlin的重载,就是相当于添加集合。其实把上面的方法切分一下的话主要就是做了两件事:创建和整理拦截器集合创建并启动责任链。下面是一张从官方文档中截取的图片

在这里插入图片描述
我们看右下部分就是说明网络响应是通过网络拦截器来层层向上传递的,并且拦截器是HTTP工作真正发生的地方。
下面再来介绍各个拦截器的作用:

  • interceptor:应用拦截器,通过Client设置
  • RetryAndFollowUpInterceptor:重试拦截器,负责网络请求中的重试重定向。比如网络请求过程中出现异常的时候就需要进行重试。
  • BridgeInterceptor:桥接拦截器,用于桥接应用层和网络层的数据。请求时将应用层的数据类型转化为网络层的数据类型,响应时则将网络层的数据类型转化为应用层的数据类型。
  • CacheInterceptor:缓存拦截器,负责读取和更新缓存。可以配置自定义的缓存拦截器。
  • ConnectInterceptor:网络连接拦截器,其内部会获取一个连接。
  • networkInterceptor:网络拦截器,通过Client设置。
  • CallServerInterceptor:请求服务拦截器。它是拦截器中处于末尾的拦截器,用于向服务端发送数据并获取响应

我们回到上面的方法中,在try块中的第一句就是启动责任链chain.proceed(),这个方法将会启动责任链并获取响应:

override fun proceed(request: Request): Response {check(index < interceptors.size)......// Call the next interceptor in the chain.val next = copy(index = index + 1, request = request)val interceptor = interceptors[index]@Suppress("USELESS_ELVIS")val response = interceptor.intercept(next) ?: throw NullPointerException("interceptor $interceptor returned null").......return response}

这里面的核心就是上面的这一小段,首先通过copy方法获得下一个拦截器:

internal fun copy(index: Int = this.index,exchange: Exchange? = this.exchange,request: Request = this.request,connectTimeoutMillis: Int = this.connectTimeoutMillis,readTimeoutMillis: Int = this.readTimeoutMillis,writeTimeoutMillis: Int = this.writeTimeoutMillis) = RealInterceptorChain(call, interceptors, index, exchange, request, connectTimeoutMillis,readTimeoutMillis, writeTimeoutMillis)

这个copy本质还是调用了构造函数,这这里它调用更改的唯一参数就是index参数,它将原来的index+1,显然是想要达到遍历的效果,那么这个index参数到底会影响什么呢?我们再将其与一开始的构造相比较:

     //创建责任链val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0,exchange = null,request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)

一开始这个index的值是0,通过查找整个类可以发现唯一有意义的就是我们上面提到的proceed方法:

val interceptor = interceptors[index]
val response = interceptor.intercept(next) ?: throw NullPointerException("interceptor $interceptor returned null")

就是用来从拦截器集合中获取拦截器的。之后又会用我们获取到的拦截器调用其intercept方法,并将之前copy出来的对象传进去(也就是新的责任链,只不过index向后指了一格)。这个intercept方法是一个接口方法,实现该接口的类有很多,我们以比较简单的ConnectInterceptor类为例:

  override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChainval exchange = realChain.call.initExchange(chain)val connectedChain = realChain.copy(exchange = exchange)return connectedChain.proceed(realChain.request)}

这里它做的唯一就是初始化了一个exchange参数然后将其设置进了责任链中,最后又会调用新设置好的责任链的proceed方法中。这就又回到了之前的方法中。这样看来,这个责任链的大体行为模式还是很好懂的,首先启动责任链,责任链调用proceed方法启动,proceed方法中会获得下一个拦截器并且调用下一个拦截器中的intercept拦截方法,在这个拦截方法中首先会进行该拦截器的一些拦截逻辑,拦截逻辑完成之后会再次调用proceed方法继续获得下一个拦截器,然后再调用它的拦截器方法,以此类推直到整个拦截器链上的拦截器方法都执行一遍,最后返回出一个Response响应。整个过程差不多如下所示:
在这里插入图片描述

相关文章:

开源库源码分析:Okhttp源码分析(一)

开源库源码分析&#xff1a;OkHttp源码分析 导言 接下来就要开始分析一些常用开源库的源码了&#xff0c;作为最常用的网络请求库&#xff0c;OkHttp以其强大的功能深受Android开发者的喜爱&#xff08;比如说我&#xff09;&#xff0c;还有对该库进行二次封装而成的热门库&a…...

无涯教程-JavaScript - LOOKUP函数

描述 需要查看单个行或一列并从第二行或第二列的同一位置查找值时,请使用LOOKUP函数。使用"查找"功能搜索一行或一列。 使用VLOOKUP函数可搜索一行或一列,或搜索多行和多列(如表)。它是LOOKUP的改进版本。 有两种使用LOOKUP的方法- 矢量形式 − Use this form of…...

这所院校太好考了!地处魔都!不要错过!

一、学校及专业介绍 上海电力大学&#xff08;Shanghai University of Electric Power&#xff09;&#xff0c;位于上海市&#xff0c;是中央与上海市共建、以上海市管理为主的全日制普通高等院校&#xff0c;是教育部首批“卓越工程师教育培养计划”试点院校、上海高水平地方…...

Python - PyQt6、QDesigner、pyuic5-tool 安装使用

Python 开发可视化界面可以使用原生的 tkinter&#xff0c;但是原生框架使用起来颇为不方便&#xff0c;所以最流行的还是QT UI框架&#xff0c;QT是使用C语言开发&#xff0c;Python 想使用需要对其进行封装&#xff0c;所以就出现了PyQt框架&#xff0c;这个框架使用极其方便…...

C语言——指针进阶(三)

目录 一.前言摘要 二.排序函数qsort的模拟实现 三.指针和数组笔试题解析 一.前言摘要 讲述关于strlen和sizeof对于各种数组与指针的计算规则与用法。另外还有qsort函数的模拟实现&#xff08;可以排序任意类型变量&#xff09; 二.排序函数qsort的模拟实现 目标&#xff1a;…...

三勾商城(java+vue3)微信小程序商城+SAAS+前后端源码

项目介绍 本系统功能包括&#xff1a; 前台展示后台管理SAAS管理端&#xff0c;包括最基本的用户登录注册&#xff0c;下单&#xff0c; 购物车&#xff0c;购买&#xff0c;结算&#xff0c;订单查询&#xff0c;收货地址&#xff0c;后台商品管 理&#xff0c;订单管理&…...

【洁洁送书第七期】现在学 Java 找工作还有优势吗

java 现在学 Java 找工作还有优势吗&#xff1f;活力四射的 JavaTIOBE 编程语言排行榜从零开始学会 JavaJava 语言运行过程基础知识进阶知识高级知识talk is cheap, show me the code结语 文末赠书 现在学 Java 找工作还有优势吗&#xff1f; 在某乎上可以看到大家对此问题的…...

npm发布自定义vue组件库

npm发布自定义vue组件库 创建项目 vue create test-ui自定义组件 创建自定义组件&#xff0c;组件名称根据你的需求来&#xff0c;最好一个组件一个文件夹&#xff0c;下图是我的示例。 src/components 组件和你写页面一样&#xff0c;所谓组件就是方便实用&#xff0c;不用…...

9.12 C++作业

实现一个图形类&#xff08;Shape&#xff09;&#xff0c;包含受保护成员属性&#xff1a;周长、面积&#xff0c; 公共成员函数&#xff1a;特殊成员函数书写 定义一个圆形类&#xff08;Circle&#xff09;&#xff0c;继承自图形类&#xff0c;包含私有属性&#xff1a;半…...

利用LinuxPTP进行时间同步(软/硬件时间戳) - 研一

转自&#xff1a;https://blog.csdn.net/BUPTOctopus/article/details/86246335 官方文档&#xff1a;https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/s1-using_ptp 查看网卡是否支持软硬件时间戳&#xff1a; sudo ethtoo…...

《极客时间:左耳听风——程序员练级攻略》【文章笔记个人思考】

原文链接&#xff1a;https://time.geekbang.org/column/intro/100002201 69 | 程序员练级攻略&#xff1a;开篇词70 | 程序员练级攻略&#xff1a;零基础启蒙编程入门入门语言 Python入门语言 JavaScript操作系统入门 Linux编程工具 Visual Studio CodeWeb 编程入门实践项目小…...

Springboot 实践(15)spring config 配置与运用—自动刷新

目前&#xff0c;网络上讲解spring config的自动刷新&#xff0c;都是通过git服务站的webhook功能执行“actuator/bus-refresh”服务实现的自动刷新。我们的前文讲解的配置中心&#xff0c;配置中心仓库使用的时本地地址&#xff0c;如下图所示&#xff1a; 那么&#xff0c;配…...

FirmAFL

FirmAFL使用并改进了Firmdyne模拟方式&#xff0c;并利用AFL对IoT固件实施高通量灰盒Fuzzing。 一、项目简介 FIRM-AFL 是 第一个针对物联网固件的高吞吐量灰盒模糊测试器。 支持mipsel、mipseb和armel三种CPU架构 &#xff0c;涵盖Firmadyne数据库中90.2%的固件。 FIRM-AFL 解…...

SpringMVC的整合完成CRUD(增删改查)

SpringMVC是一种基于Java的Web框架&#xff0c;它是Spring框架的一部分。SpringMVC通过使用MVC&#xff08;Model-View-Controller&#xff09;设计模式来组织和管理Web应用程序的开发。 在SpringMVC中&#xff0c;Model代表数据模型&#xff0c;View代表用户界面&#xff0c;C…...

Postman使用_Tests Script(断言测试)

断言测试可以在Collection、Folder和Request的 pre-request script 和 test script中编写&#xff0c;测试脚本可以检测请求响应的各个方面&#xff0c;包括正文、状态代码、头、cookie、响应时间等&#xff0c;只有测试符合自定义的要求后才能通过。 pm对象提供了测试相关功能…...

问道管理:华为概念股捷荣技术13天10板,监管质疑迎合热点炒作

捷荣技能&#xff08;002855.SZ&#xff09;一口气将气氛拉满。 盘面看&#xff0c;自8月29日启动到9月14日&#xff0c;捷荣技能用了13天时间&#xff0c;将累计涨幅推到了188%&#xff0c;期间有10个涨停板&#xff0c;如此冷艳之举&#xff0c;还在于其“华为概念”。 ​ …...

VR云游:让游客足不出户享受旅行的乐趣

随着人们的生活水平不断提高&#xff0c;对于旅行的需求也在日益增长&#xff0c;既要玩的舒心&#xff0c;也要享受的舒服&#xff0c;因此VR全景云游就成为了一种新型的旅行方式&#xff0c;人们足不出户就可以沉浸式游览各地自然风光与名胜古迹。 VR云游景区是一种全新的旅游…...

vue3学习源码笔记(小白入门系列)------ 重点!响应式原理 代码逐行分析

目录 备注响应式数据创建ref 和 reactive 核心 作用第一轮的 依赖收集 发生时机setup 阶段 去更改了 响应式数据 会发生依赖收集吗 派发更新派发更新是什么时候 触发的&#xff1f;扩展&#xff1a; setup阶段 响应式数据被修改 会触发组件更新吗 vue 是如何根据派发更新来触发…...

62、SpringBoot 使用RestTemplate 整合第三方 RESTful 服务

这节的要点&#xff1a; 就是弄两个项目 &#xff0c; 从 端口9090 这个项目&#xff0c;通过 restTemplate&#xff0c; 去访问 端口8080 的项目&#xff0c;并获取8080项目的数据。 ★ RESTful服务包含两方面的含义 1. 自己的应用要暴露一些功能供别人来调用。此时我们是服…...

Linux基本认识

一、Linux基本概念 Linux 内核最初只是由芬兰人林纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;在赫尔辛基大学上学时出于个人爱好而编写的。 Linux 是一套免费使用和自由传播的类 Unix 操作系统&#xff0c;是一个基于 POSIX 和 UNIX 的多用户、多任务、支持多线程和多…...

leetcode top 100 (8)无重复字符的最长子串(滑动窗口

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc"&#xff0c;所以其长度为 3。 package TOP1_10;import java.util.HashMap; import java.…...

我也惊呆了!原来软件开发根本不需要会编码

一、前言 众所周知&#xff0c;完成一个大型的企业级系统&#xff0c;公司往往需要大量的人力做支持后盾&#xff0c;例如需要需求分析师、数据库管理员、前台美工、后台程序员、测试人员等。在快速发展中的企业&#xff0c;尤其是中小企业&#xff0c;都是一个萝卜多个坑&…...

定制化图标——Element UI 组件图标替换指南

本篇博客将介绍如何在使用 Element UI 组件时对原生图标进行定制化替换&#xff0c;提供了适用于满足个性化需求的方法和技巧。 引言 Element UI 是一款基于 Vue.js 的流行 UI 组件库&#xff0c;在前端开发中得到广泛应用。然而&#xff0c;在使用 Element UI 的组件时&#…...

63、SpringBoot---定制 RestTemplate--消息转化器、拦截器

★ 定制RestTemplate 如要对RestTemplate进行自定义设置&#xff0c;Spring Boot也提供了两种主要方式&#xff1a;▲ 局部式&#xff1a;在调用RestTemplateBuilder构建RestTemplate之前&#xff0c;先调用RestTemplateBuilder的方法对其定制&#xff0c;通过这种方式设置的R…...

面试系列 - Redis持久化机制详解

目录 一、Redis 持久化机制 二、混合使用 RDB 和 AOF 三、 RDB(Redis DataBase)详解 四、AOF&#xff08;Append-Only File&#xff09;详解 Redis 是一个内存数据库&#xff0c;为了持久化数据以确保数据不会在服务器重启时丢失&#xff0c;Redis 提供了两种主要的持久化机…...

Ceph入门到精通-存储集群ceph df 用量统计算法说明

3.2.5. Ceph 如何计算数据使用量 used 值反映了使用的实际原始存储量。xxx GB / xxx GB 代表可用的存储&#xff08;其中较小的数字&#xff09;和总存储容量。总容量反映了在复制、克隆或快照前存储数据的大小。因此&#xff0c;实际存储的数据量通常会超过名义上的存储量。这…...

堡垒机的相关介绍

描述 堡垒机&#xff0c;即在一个特定的网络环境下&#xff0c;为了保障网络和数据不受来自外部和内部用户的入侵和破坏&#xff0c;而运用各种技术手段监控和记录运维人员对网络内的服务器、网络设备、安全设备、数据库等设备的操作行为&#xff0c;以便集中报警、及时处理及审…...

无涯教程-JavaScript - INDIRECT函数

描述 INDIRECT函数返回由文本字符串指定的引用。 如果您在Excel公式中键入引用B1,则Excel会理解这引用了单元格B1。但是,Excel无法将文本字符串" B1"理解为引用。因此,如果单元格引用采用文本字符串的形式,则需要使用INDIRECT函数将其转换为实际的单元格引用。 立…...

LiveNVR监控流媒体Onvif/RTSP功能-支持海康摄像头海康NVR通过EHOME协议ISUP协议接入分发视频流或是转GB28181

LiveNVR支持海康NVR摄像头通EHOME接入ISUP接入LiveNVR分发视频流或是转GB28181 1、海康 ISUP 接入配置2、海康设备接入2.1、海康EHOME接入配置示例2.2、海康ISUP接入配置示例 3、通道配置3.1、直播流接入类型 海康ISUP3.2、海康 ISUP 设备ID3.3、启用保存3.4、接入成功 4、相关…...

一年一度的中秋节马上又要到了,给你的浏览器也来点氛围感吧

说在前面 一年一度的中秋节马上又要到了&#xff0c;给你的浏览器也来点氛围感吧 &#x1f315;&#x1f315;&#x1f315; 插件设计 效果 首先我们应该要先确定一下我们想要实现的效果是怎样的&#xff0c;如上图&#xff0c;我们希望在页面上鼠标点击的时候会在点击区域随…...

池州网站建设费用/网上怎么发布广告

一、互斥锁&#xff08;同步&#xff09; 在多任务操作系统中&#xff0c;同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于&#xff0c;公司部门里&#xff0c;我在使用着打印机打印东西的同时&#xff08;还没有打印完&#xff09;&#xff0c;别人刚好也在此…...

企业网站设计风格/指数基金是什么意思

TodoList SAM SteinerTree 2018/3/26 SRM600 Div1 250 枚举哪一位是个卜。 600 枚举回文列的集合&#xff0c;然后对行进行DP&#xff0c;\(dp[i][j]\)代表最前的i行与最后的i行中&#xff0c;有j行为回文的最小耗费。我们对第i行与第n-i1行进行决策。 两行都不回文&#xff1a…...

华人汇和建设有限公司网站/电商平台怎么注册

题意&#xff1a;给定1,2,3...N个数的集合&#xff0c;现在求所有非空子集&#xff08;相同元素不同位置视为不同&#xff09;按字典序排序后的第M个集合是什么&#xff1f; 思路&#xff1a;设i个不同元素组成的非空字典序子集为kind[i],通过递推关系计算出kind[i] i * (kind…...

5分钟宣传片报价明细/sem和seo哪个工作好

一、布局管理器所属类包所属类包布局管理器名称说明Java.awtFlowLayout(流式布局)组件按照加入的先后顺序按照设置的对齐方式从左向右排列&#xff0c;一行排满到下一行开始继续排列BorderLayout(边界布局)容器划分为东、西、南、北、中五个区域&#xff0c;每个区域只能放置一…...

网站建设中模版/发布新闻稿

下面是冒泡排序的代码&#xff1a; def bubble_sort(array): for i in range(len(array)): for j in range(len(array) - i - 1): if array[j] > array[j 1]: array[j], array[j 1] array[j 1], array[j] return array...

网站排名关键词/网站seo的优化怎么做

参考文档:点击直达 1.创建请求对象 let xhr new XMLHttpRequest();请求路径&#xff1a; (1)协议&#xff1a; https (2)域名&#xff1a; restapi.amap.com 区分服务器 (3)端口号&#xff1a; 默认端口号443 (http:8080) 可以省略 区分服务器下不同的服务 (4)资源路径&…...