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

开源库源码分析:OkHttp源码分析(二)

开源库源码分析:OkHttp源码分析(二)

在这里插入图片描述

导言

上一篇文章中我们已经分析到了OkHttp对于网络请求采取了责任链模式,所谓责任链模式就是有多个对象都有机会处理请求,从而避免请求发送者和接收者之间的紧密耦合关系。这篇文章我们将着重分析OkHttp中这个责任链的行为逻辑。

责任链中拦截器的位置

既然是责任链,那么每个拦截器自然有其位置,决定它是先处理请求还是后处理请求。这个请求是在构造责任链的时候确定的,更具体来说是在构造拦截器集合的时候确定的:

val interceptors = mutableListOf<Interceptor>()interceptors += client.interceptorsinterceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptorif (!forWebSocket) {interceptors += client.networkInterceptors}interceptors += CallServerInterceptor(forWebSocket)

因为我们知道责任链中访问拦截器的顺序就是每次将索引+1访问下一个拦截器,所以构造拦截器集合的时候确定了各个拦截器的上下级关系。从代码中我们可以清楚了解每一个拦截器的位置:

用户应用拦截器->重试拦截器->桥接拦截器->缓存拦截器->连接拦截器->网络拦截器->请求服务拦截器

还记得这每个拦截器的作用吗:

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

请求传递过去时的拦截器行为

重试拦截器

第一个用户应用拦截器是我们传入的,这里先忽略,所以照理来说第一个拦截事件的就是重试拦截器RetryAndFollowUpInterceptor了,这个拦截器主要是用来处理网络请求过程中的异常情况的,其拦截方法如下:

  override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChainvar request = chain.requestval call = realChain.callvar followUpCount = 0var priorResponse: Response? = nullvar newExchangeFinder = truevar recoveredFailures = listOf<IOException>()while (true) {//做一些潜在的准备工作call.enterNetworkInterceptorExchange(request, newExchangeFinder)var response: Responsevar closeActiveExchange = truetry {if (call.isCanceled()) {throw IOException("Canceled")}try {response = realChain.proceed(request)//传递给下一个拦截器newExchangeFinder = true} catch (e: RouteException) {........} finally {call.exitNetworkInterceptorExchange(closeActiveExchange)}}}

可以看到如果是第一次请求的话该拦截器做的事情就是调用enterNetworkInterceptorExchange设置一些参数,然后直接将其传递给下一个拦截器处理,等到后面的拦截器都处理完了再对这个返回出来的Response进行处理,所以我们到后面的拦截器行为分析完了再回过来分析后面的内容。这里实际上就是一个栈式调用,和递归一样。

在while循环的第一句话会调用enterNetworkInterceptorExchange进行一些工作的准备,在这里第一次调用的情况下的话,它会为Call设置一个Exchange:

//RealCall.ktif (newExchangeFinder) {this.exchangeFinder = ExchangeFinder(connectionPool,createAddress(request.url),this,eventListener)

看里面传入的参数就知道不简单,显然这和具体的网络请求的发起有关。

Exchange和ExchangeFinder

首先是Exchange类:
在这里插入图片描述

在OkHttp中用Exchange来描述传输一个单独的HTTP请求和响应对。它在处理实际的I/O的ExchangeCodec上添加了连接管理和事件处理的功能。ExchangeCodec负责处理底层的I/O操作,而这段代码建立在其之上,处理HTTP请求和响应的传输和管理。这一层的功能包括连接的建立、请求的发送、响应的接收以及与底层I/O的交互,以确保HTTP请求得到正确处理并获得响应。

总结一下,这个Exchange类就可以用来代表一个可以控制与所需要的服务器进行交流的类,我将它视作一个连接,在这里我们就不继续往下分析底层的ExchangeCodec的实现了。

接下来是ExchangeFinder类:
在这里插入图片描述
这段代码的主要作用是尝试查找与一个请求交互相关的连接以及可能的重试。它采用以下策略:

  • 如果当前的请求已经有一个可以满足的连接,就会使用这个连接。对于初始请求和后续的请求都使用同一个连接,可以提高连接的局部性。

  • 如果连接池中有一个可以满足请求的连接,也会使用它。需要注意的是,共享的请求可能会针对不同的主机名进行请求!有关详细信息,请参阅RealConnection.isEligible。

  • 如果没有现有的连接可用,将创建一个路由列表(可能需要阻塞的DNS查找),并尝试与这些路由建立新的连接。当发生失败时,重试会迭代可用路由列表。

  • 如果连接池在DNS、TCP或TLS工作进行中获取了一个符合条件的连接,该查找器将优先使用池中的连接。只有池中的HTTP/2连接才会用于这种去重。

  • 此外,这段代码还具有取消查找过程的能力。

需要注意的是,这个类的实例不是线程安全的,每个实例都限定在执行调用的线程中。

也就是说,这个类是用来管理与连接有关的东西的,它针对请求来查找可用的连接,按照他的上下文来看,这个链接指的应该就是ExchangeCodec,因为我们可以在RealCallinitExchange方法中找到相关的语句:

  internal fun initExchange(chain: RealInterceptorChain): Exchange {synchronized(this) {check(expectMoreExchanges) { "released" }check(!responseBodyOpen)check(!requestBodyOpen)}val exchangeFinder = this.exchangeFinder!!val codec = exchangeFinder.find(client, chain).......}

桥接拦截器

言归正传,之前的重试拦截器将请求向下传递了,接下来轮到的就是桥接拦截器,在之前的介绍中也提到过了,这个拦截器主要负责用户代码与网络代码之间的转化,更具体来说它会将用户请求转化为网络请求,其拦截方法如下:

override fun intercept(chain: Interceptor.Chain): Response {val userRequest = chain.request() //获得用于请求val requestBuilder = userRequest.newBuilder() //根据用户请求获得一个RequestBuilderval body = userRequest.body//获得请求体if (body != null) { .//若请求体不为空,处理用户的各种请求,将其转化为网络请求val contentType = body.contentType()if (contentType != null) {requestBuilder.header("Content-Type", contentType.toString())}val contentLength = body.contentLength()if (contentLength != -1L) {requestBuilder.header("Content-Length", contentLength.toString())requestBuilder.removeHeader("Transfer-Encoding")} else {requestBuilder.header("Transfer-Encoding", "chunked")requestBuilder.removeHeader("Content-Length")}}if (userRequest.header("Host") == null) { //如果Host为空,将用户的url转化为HostHead添加进去requestBuilder.header("Host", userRequest.url.toHostHeader())}if (userRequest.header("Connection") == null) { //如果Connection为空,将其设置为'Keep-Alive'requestBuilder.header("Connection", "Keep-Alive")}// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing// the transfer stream.//翻译过来就是如果需要解压缩,那么我们也要进行处理var transparentGzip = falseif (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {transparentGzip = truerequestBuilder.header("Accept-Encoding", "gzip")}val cookies = cookieJar.loadForRequest(userRequest.url) //获取Cookiesif (cookies.isNotEmpty()) {requestBuilder.header("Cookie", cookieHeader(cookies))}if (userRequest.header("User-Agent") == null) {requestBuilder.header("User-Agent", userAgent)}//将转化好的网络请求向下传递给下一个拦截器val networkResponse = chain.proceed(requestBuilder.build())cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)val responseBuilder = networkResponse.newBuilder().request(userRequest)//获得一个响应体构造器//解压缩的情况if (transparentGzip &&"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&networkResponse.promisesBody()) {val responseBody = networkResponse.bodyif (responseBody != null) {val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))}}//将响应返回return responseBuilder.build()}

上面源码中我已经将重要的部分做了注释,可以说这个网络桥接拦截器的作用就是根据我们在请求中设置的参数来生成一个真正的网络请求体然后发送给下一个拦截器。可以看到,他也是先将请求发送给下一个拦截器等待其相应,也是一个栈式的调用,后面拦截器返回之后会重新返回到这个拦截器中。

缓存拦截器

接下来就是各个网络库中绕不过去的缓存了,这是为了提高性能而必须的。缓存拦截器将从缓存中返回请求并将新的请求(响应)写入缓存中,我们来看其拦截方法:

  override fun intercept(chain: Interceptor.Chain): Response {val call = chain.call() //获得Callval cacheCandidate = cache?.get(chain.request())//根据请求在缓存中获得缓存候选val now = System.currentTimeMillis()//获得当前时间//通过缓存策略工厂构造出一个策略,然后用这个策略计算出结果val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()val networkRequest = strategy.networkRequest//结果中的网络请求部分val cacheResponse = strategy.cacheResponse//结果中的缓存响应部分cache?.trackResponse(strategy)val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE//设置事件监听器if (cacheCandidate != null && cacheResponse == null) { //如果缓存候选存在但是没有缓存响应//和换句话说就是缓存候选不可用// The cache candidate wasn't applicable. Close it.cacheCandidate.body?.closeQuietly() //将缓存候选给关闭}// If we're forbidden from using the network and the cache is insufficient, fail.if (networkRequest == null && cacheResponse == null) {//如果网络请求体为空且缓存响应也为空,这里说是网络不可用且缓存不足return Response.Builder()  //直接返回一个响应,说明响应失败.request(chain.request()).protocol(Protocol.HTTP_1_1).code(HTTP_GATEWAY_TIMEOUT).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build().also {listener.satisfactionFailure(call, it) //回调事件监听器中的方法}}// If we don't need the network, we're done.if (networkRequest == null) { //虽然网络请求为空,但是有缓存响应,说明缓存命中return cacheResponse!!.newBuilder() //返回命中的缓存响应.cacheResponse(stripBody(cacheResponse)).build().also {listener.cacheHit(call, it)//回调事件监听器中的方法}}if (cacheResponse != null) { //此时是网络请求不为空且缓存请求也不为空的情况listener.cacheConditionalHit(call, cacheResponse)//回调监听器中的方法} else if (cache != null) { //此时是网络请求不为空但是缓存响应为空的情况listener.cacheMiss(call)//说明缓存未命中,执行事件监听器中的方法}var networkResponse: Response? = null //新创建一个网络响应体,这显然是在缓存未命中的情况下才会发生的try {networkResponse = chain.proceed(networkRequest) //向下将该网络请求传递下去并获得网络响应} finally {// If we're crashing on I/O or otherwise, don't leak the cache body.if (networkResponse == null && cacheCandidate != null) {cacheCandidate.body?.closeQuietly()}}.......}return response}

这里我们先分析到缓存拦截器将网络请求发送给下一个拦截器的代码处,之后会掉的部分我们等等在分析,上面的代码处我已经将重要的部分加上了注释,这个缓存的逻辑也很简单,简单来说就是缓存命中且可用就返回缓存中的响应,若缓存未命中之后才请求网络。

网络连接拦截器

在这里插入图片描述
从介绍来看这个拦截器是用来打开一个与目标服务器进行数据交流的连接,这段代码的主要作用是打开与目标服务器的连接并继续执行下一个拦截器。这个连接可能会用于返回的响应,或者用于通过条件GET验证缓存的响应。下面是它的拦截方法:

  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对象,我们说过这个对象中的ExchangeCodec就是实现底层与网络进行I/O流的类。该方法中将Exchange对象初始化然后将其传入了RealChain副本中,最后调用了该副本的proceed方法将该请求传递给了之后的拦截器。

网络拦截器

由于网络拦截器也是由用户设置的,默认情况下并没有被设置,所以我们跳过这个先。

请求服务拦截器

注释中提到了这是责任链中的最后一环的拦截器,它是用来对服务器发起Call的,下面是他的拦截方法:

override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChainval exchange = realChain.exchange!! //获得Exchangeval request = realChain.request //获得请求val requestBody = request.body //获得请求体val sentRequestMillis = System.currentTimeMillis()//获得当前时间exchange.writeRequestHeaders(request)//通过exchange将请求中的请求头写入var invokeStartEvent = true//设置开始执行事件标志位为truevar responseBuilder: Response.Builder? = null//确保方法不是‘GET’或者‘Head’if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100// Continue" response before transmitting the request body. If we don't get that, return// what we did get (such as a 4xx response) without ever transmitting the request body.if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {exchange.flushRequest()responseBuilder = exchange.readResponseHeaders(expectContinue = true)exchange.responseHeadersStart()invokeStartEvent = false}if (responseBuilder == null) {if (requestBody.isDuplex()) { //如果请求是双工的// Prepare a duplex body so that the application can send a request body later.exchange.flushRequest()val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()requestBody.writeTo(bufferedRequestBody)} else {// Write the request body if the "Expect: 100-continue" expectation was met.val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()requestBody.writeTo(bufferedRequestBody)bufferedRequestBody.close()}} else {exchange.noRequestBody()if (!exchange.connection.isMultiplexed) {// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection// from being reused. Otherwise we're still obligated to transmit the request body to// leave the connection in a consistent state.exchange.noNewExchangesOnConnection()}}} else {exchange.noRequestBody()}if (requestBody == null || !requestBody.isDuplex()) {exchange.finishRequest()//完成请求}if (responseBuilder == null) {responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!//去读响应头if (invokeStartEvent) {exchange.responseHeadersStart()//响应头开始--具体就是执行事件监听器中的回调方法invokeStartEvent = false}}var response = responseBuilder //获得响应.request(request).handshake(exchange.connection.handshake())//握手.sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()var code = response.code//这里就是各种响应码if (code == 100) {// Server sent a 100-continue even though we did not request one. Try again to read the actual// response status.responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!if (invokeStartEvent) {exchange.responseHeadersStart()}response = responseBuilder.request(request).handshake(exchange.connection.handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()code = response.code}exchange.responseHeadersEnd(response)response = if (forWebSocket && code == 101) {// Connection is upgrading, but we need to ensure interceptors see a non-null response body.response.newBuilder().body(EMPTY_RESPONSE).build()} else {response.newBuilder().body(exchange.openResponseBody(response)).build()}if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||"close".equals(response.header("Connection"), ignoreCase = true)) {exchange.noNewExchangesOnConnection()}if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {throw ProtocolException("HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")}return response//返回最终的响应}

这里我们也没必要死扣它是如何运作的,毕竟我们只是分析一个大致的结构。这个方法中做的一句话来说就是用Exchange来获得网络响应,然后将该响应进行一些处理返回出去。

请求回调时的拦截器行为

之前提到了这整个拦截器链上的拦截方法都是栈式调用的,也就是说他们在执行完后一个拦截器的行为之后还会回调到前一个拦截器的拦截方法之中,接下来我们从最后开始看他们拦截回调时的行为。

首先最后一个拦截方法是在请求服务拦截器之中的,它会返回它的前一个拦截器的拦截方法之中,也就是网络连接拦截器之中,不过网络连接拦截器之中直接返回了:

  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)}

所以我们继续往前推,它的前一个拦截器是缓存拦截器,我们接下来看缓存拦截器的拦截方法的下半部分:

var networkResponse: Response? = nulltry {networkResponse = chain.proceed(networkRequest)} finally {// If we're crashing on I/O or otherwise, don't leak the cache body.if (networkResponse == null && cacheCandidate != null) {cacheCandidate.body?.closeQuietly()}}// If we have a cache response too, then we're doing a conditional get.if (cacheResponse != null) {if (networkResponse?.code == HTTP_NOT_MODIFIED) {val response = cacheResponse.newBuilder().headers(combine(cacheResponse.headers, networkResponse.headers)).sentRequestAtMillis(networkResponse.sentRequestAtMillis).receivedResponseAtMillis(networkResponse.receivedResponseAtMillis).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build()networkResponse.body!!.close()// Update the cache after combining headers but before stripping the// Content-Encoding header (as performed by initContentStream()).cache!!.trackConditionalCacheHit()cache.update(cacheResponse, response)return response.also {listener.cacheHit(call, it)}} else {cacheResponse.body?.closeQuietly()}}val response = networkResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build()if (cache != null) {if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {// Offer this request to the cache.val cacheRequest = cache.put(response)return cacheWritingResponse(cacheRequest, response).also {if (cacheResponse != null) {// This will log a conditional cache miss only.listener.cacheMiss(call)}}}if (HttpMethod.invalidatesCache(networkRequest.method)) {try {cache.remove(networkRequest)} catch (_: IOException) {// The cache cannot be written.}}}return response

这后半部分最重要的部分就是那个update方法,用这个方法去更新缓存中的内容,然后会检查之前缓存中的数据的有效性,如果失效了就会将其移除,最后将Response返回到之前的一个拦截器中。缓存拦截器之前的是桥接拦截器:

    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)val responseBuilder = networkResponse.newBuilder().request(userRequest)if (transparentGzip &&"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&networkResponse.promisesBody()) {val responseBody = networkResponse.bodyif (responseBody != null) {val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))}}return responseBuilder.build()

这里就是涉及到Cookies的处理和关于解压缩的工作,最后返回到最开始的重试拦截器,重试拦截器中涉及到的就是一些失败重试和重定向的处理,里面有一些关于重试最大的次数,如果超过了最大次数仍然失败的话就会抛出异常。

整个过程如图所示:
在这里插入图片描述

连接

最后我们来讲一讲连接。

OkHttp有三种方式连接服务器:URLs,Address和Route。

具体来说,当我们在OKhttp使用URL时,它是这样运作的:

  • 1.使用URL和配置好的OkHttpClient创建一个Address,这个Address说明了我们如何连接服务器。
  • 2.它首先会尝试从连接池中获取一个连接。
  • 3.如果无法在连接池中找到一条可用的连接,它会尝试选择一条route,这通常意味着将会发送一个DNS请求获得服务器的IP地址。
  • 4.如果它是一条新route,它通过构建直接套接字连接,TLS隧道,或直接TLS连接进行连接。
  • 5.发送HTTP请求并接受请求。

当连接过程中出现以外的时候,OkHttp将选择另一条route再次进行尝试,一旦响应被接受了,连接就会被回收进入连接池中以便复用。我们需要记住除了缓存之外,OkHttp还有连接池来优化性能。

相关文章:

开源库源码分析:OkHttp源码分析(二)

开源库源码分析&#xff1a;OkHttp源码分析&#xff08;二&#xff09; 导言 上一篇文章中我们已经分析到了OkHttp对于网络请求采取了责任链模式&#xff0c;所谓责任链模式就是有多个对象都有机会处理请求&#xff0c;从而避免请求发送者和接收者之间的紧密耦合关系。这篇文章…...

校园地理信息系统的设计与实现

校园地理信息系统的设计与实现 摘 要 与传统的地图相比较&#xff0c;地理信息系统有着不可比拟的优势&#xff0c;信息量大&#xff0c;切换方便&#xff0c;可扩展性强。本文阐述了研究地理信息系统的背景、目的、方法&#xff0c;介绍了一个实用的、方便可靠的校园地理信息…...

Vulnhub实战-prime1

前言 VulnHub 是一个面向信息安全爱好者和专业人士的虚拟机&#xff08;VM&#xff09;漏洞测试平台。它提供了一系列特制的漏洞测试虚拟机镜像&#xff0c;供用户通过攻击和漏洞利用的练习来提升自己的安全技能。本次&#xff0c;我们本次测试的是prime1。 一、主机发现和端…...

Scala学习笔记

Scala学习笔记 Scala笔记一、学习Scala的目的二、Scala的基本概念2.1 JDK1.8版本的新特性2.2 Scala的运行机制 三、Scala的基本语法3.1 Scala中输出语句、键盘输入、注释语法3.1.1 Scala注释三种&#xff0c;和Java一模一样的3.1.2 Scala键盘输入3.1.3 Scala输出 3.2 Scala变量…...

虹科分享 | 软件供应链攻击如何工作?如何评估软件供应链安全?

说到应用程序和软件&#xff0c;关键词是“更多”。在数字经济需求的推动下&#xff0c;从简化业务运营到创造创新的新收入机会&#xff0c;企业越来越依赖应用程序。云本地应用程序开发更是火上浇油。然而&#xff0c;情况是双向的&#xff1a;这些应用程序通常更复杂&#xf…...

gRpc入门和springboot整合

gRpc入门和springboot整合 一、简介 1、gprc概念 gRpc是有google开源的一个高性能的pc框架&#xff0c;Stubby google内部的rpc,2015年正式开源&#xff0c;云原生时代一个RPC标准。 tips:异构系统&#xff0c;就是不同编程语言的系统。 2、grpc核心设计思路 grpc核心设计…...

基于FPGA点阵显示屏设计-毕设

本设计是一1616点阵LED电子显示屏的设计。整机以EP2C5T144C8N为主控芯片,介绍了以它为控制系统的LED点阵电子显示屏的动态设计和开发过程。通过该芯片控制一个行驱动器74HC154和两个列驱动器74HC595来驱动显示屏显示。该电子显示屏可以显示各种文字或单色图像,采用4块8 x 8点…...

Rocky9.2基于http方式搭建局域网yum源

当前负责的项目有几十台Linux服务器,在安装各类软件的时候需要大量依赖包,而项目部署的环境属于内网环境,与Internet网完全隔离,无法采用配置网络yum源的方式安装rpm包,直接在每台linux服务器上配置本地yum源也比较麻烦,而采用直接下载rpm包用rpm命令安装更是费时费力。所…...

Android 串口通讯

Serial Port Android 串口通讯 arm64-v8a、armeabi-v7a、x86、x86_64 AAR 名称操作serial.jar下载arm64-v8a下载armeabi-v7a下载x86下载x86_64下载arm-zip下载x86-zip下载 Maven 1.build.grade | setting.grade repositories {...maven { url https://jitpack.io } }2./a…...

论如何在Android中还原设计稿中的阴影

每当设计稿上注明需要添加阴影时&#xff0c;Android上总是显得比较棘手&#xff0c;因为Android的阴影实现方式与Web和iOS有所区别。 一般来说阴影通常格式是有&#xff1a; X: 在X轴的偏移度 Y: 在Y轴偏移度 Blur: 阴影的模糊半径 Color: 阴影的颜色 何为阴影 但是在A…...

Hadoop生态圈中的Flume数据日志采集工具

Hadoop生态圈中的Flume数据日志采集工具 一、数据采集的问题二、数据采集一般使用的技术三、扩展&#xff1a;通过爬虫技术采集第三方网站数据四、Flume日志采集工具概述五、Flume采集数据的时候&#xff0c;核心是编写Flume的采集脚本xxx.conf六、Flume案例实操1、采集一个网络…...

FFmpeg获取媒体文件的视频信息

视频包标志位 代码 printf("index:%d\n", in_stream->index);结果 index:0视频帧率 // avg_frame_rate: 视频帧率,单位为fps&#xff0c;表示每秒出现多少帧 printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));结果 fps:29.970070fps…...

io概述及其分类

一、IO概念 • I/O 即输入Input/ 输出Output的缩写&#xff0c;其实就是计算机调度把各个存储中&#xff08;包括内存和外部存储&#xff09;的数据写入写出的过程&#xff1b; I : Input O : Output 通过IO可以完成硬盘文件的读和写。 • java中用“流&#xff08;stream&am…...

前端面试话术集锦第 14 篇:高频考点(React常考基础知识点)

这是记录前端面试的话术集锦第十四篇博文——高频考点(React常考基础知识点),我会不断更新该博文。❗❗❗ 1. 生命周期 在V16版本中引入了Fiber机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的2个API来解决问题。 在之前的版本中,如果你拥有一个很…...

UI/UX+前端架构:设计和开发高质量的用户界面和用户体验

引言 随着数字化和互联网的普及&#xff0c;越来越多的企业和组织需要高质量的用户界面和用户体验&#xff0c;以及可靠、高效的前端架构。UI/UX设计师和前端架构师可以为这些企业和组织提供所需的技术和创意支持。本文将介绍UI/UX前端架构这个方向&#xff0c;包括设计原则、…...

长尾关键词挖掘软件-免费的百度搜索关键词挖掘

嗨&#xff0c;大家好&#xff01;今天&#xff0c;我想和大家聊一聊长尾关键词挖掘工具。作为一个在网络世界里摸爬滚打多年的人&#xff0c;我对这个话题有着一些个人的感悟和见解&#xff0c;希望能与大家分享。 首先&#xff0c;让我坦白一点&#xff0c;长尾关键词挖掘工具…...

React Native 环境配置(mac)

React Native 环境配置&#xff08;mac&#xff09; 1.Homebrew2.Node.js、WatchMan3.Yarn4.Android环境配置1.安装JDK2.下载AndroidStudio1.国内配置 Http Proxy2.安装SDK1.首先配置sdk的路径2.SDK 下载 3.创建模拟器4.配置 ANDROID_HOME 环境变量 5.IOS环境1.升级ruby&#x…...

CAD for JS:VectorDraw web library 10.1004.1 Crack

VectorDraw web library经过几年的研究&#xff0c;通过互联网展示或工作的可能性并拒绝了各种项目&#xff0c;我们最终得出的结论是&#xff0c;在 javascript 的帮助下&#xff0c;我们将能够在 Microsoft IE 以外的互联网浏览器中通过网络演示矢量图形&#xff08;支持 ocx…...

代码管理工具git1

ctrl 加滚轮 放大字体 在计算机任意位置单击右键&#xff0c;选择&#xff1a;&#xff1a;Git Bash Here git version git清屏命令&#xff1a;ctrl L查看用户名和邮箱地址&#xff1a; $ git config user.name$ git config user.email修改用户名和邮箱地址&#xff1a;$ git…...

层次聚类分析

1、python语言 from scipy.cluster import hierarchy # 导入层次聚类算法 import matplotlib.pylab as plt import numpy as np# 生成示例数据 np.random.seed(0) data np.random.random((20,1))# 使用树状图找到最佳聚类数 Z hierarchy.linkage(data,methodweighted,metric…...

Jmeter性能实战之分布式压测

分布式执行原理 1、JMeter分布式测试时&#xff0c;选择其中一台作为调度机(master)&#xff0c;其它机器作为执行机(slave)。 2、执行时&#xff0c;master会把脚本发送到每台slave上&#xff0c;slave 拿到脚本后就开始执行&#xff0c;slave执行时不需要启动GUI&#xff0…...

学信息系统项目管理师第4版系列08_管理科学基础

1. 科学管理的实质 1.1. 反对凭经验、直觉、主观判断进行管理 1.2. 主张用最好的方法、最少的时间和支出&#xff0c;达到最高的工作效率和最大的效果 2. 资金的时间价值与等值计算 2.1. 资金的时间价值是指不同时间发生的等额资金在价值上的差别 2.2. 把资金存入银行&…...

从2023蓝帽杯0解题heapSpary入门堆喷

关于堆喷 堆喷射&#xff08;Heap Spraying&#xff09;是一种计算机安全攻击技术&#xff0c;它旨在在进程的堆中创建多个包含恶意负载的内存块。这种技术允许攻击者避免需要知道负载确切的内存地址&#xff0c;因为通过广泛地“喷射”堆&#xff0c;攻击者可以提高恶意负载被…...

基于SSM的学生宿舍管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…...

jvm 内存模型介绍

一、类加载子系统 1、类加载的过程&#xff1a;装载、链接、初始化&#xff0c;其中&#xff0c;链接又分为验证、准备和解析 装载&#xff1a;加载class文件 验证&#xff1a;确保字节流中包含信息符合当前虚拟机要求 准备&#xff1a;分配内存&#xff0c;设置初始值 解析&a…...

用Jmeter进行压测详解

简介&#xff1a; 1.概述 一款工具&#xff0c;功能往往是很多的&#xff0c;细枝末节的地方也很多&#xff0c;实际的测试工作中&#xff0c;绝大多数场景会用到的也就是一些核心功能&#xff0c;根本不需要我们事无巨细的去掌握工具的所有功能。所以本文将用带价最小的方式讲…...

Mysql001:(库和表)操作SQL语句

目录&#xff1a; 》SQL通用规则说明 SQL分类&#xff1a; 》DDL&#xff08;数据定义&#xff1a;用于操作数据库、表、字段&#xff09; 》DML&#xff08;数据编辑&#xff1a;用于对表中的数据进行增删改&#xff09; 》DQL&#xff08;数据查询&#xff1a;用于对表中的数…...

甲骨文全区登录地址

日本东部 东京 https://console.ap-tokyo-1.oraclecloud.com https://console.ap-tokyo-1.oraclecloud.com 日本中部 大阪 https://console.ap-osaka-1.oraclecloud.com https://console.ap-osaka-1.oraclecloud.com 韩国中部 首尔 https://console.ap-seoul-1.oraclecloud.c…...

Java面试题第八天

一、Java面试题第八天 1.如何实现对象克隆&#xff1f; 浅克隆 浅克隆就是我们可以通过实现Cloneable接口&#xff0c;重写clone,这种方式就叫浅克隆&#xff0c;浅克隆 引用类型的属性&#xff0c;是指向同一个内存地址&#xff0c;但是如果引用类型的属性也进行浅克隆就是深…...

什么是同步容器和并发容器的实现?

同步容器和并发容器都是用于在多线程环境中管理数据的容器&#xff0c;但它们在实现和用法上有很大的区别。 同步容器&#xff1a; 同步容器是使用传统的同步机制&#xff08;如synchronized关键字或锁&#xff09;来保护容器内部数据结构的线程安全容器。同步容器通常是单线…...