OkHttp完全解读

一,概述

OkHttp作为android非常流行的网络框架,笔者认为有必要剖析此框架实现原理,抽取并理解此框架优秀的设计模式。OkHttp有几个重要的作用,如桥接、缓存、连接复用等,本文笔者将从使用出发,解读源码,剖析此功能的实现原理。最后阅读完源码后总结出如下结论,OkHttp是一款优秀的网络请求框架,内部采用优雅的责任链模式、构造模式、桥接模式、享元模式、门面模式等设计模式,符合依赖导致原则、里氏替换原则等面向对象原则,将复杂的网络请求封装从简单的调用,属实优雅。笔者推荐感兴趣的读者从笔者粗陋的源码解读思路去思考更多的源码设计与实现,彻底完全了解OkHttp的设计思路,并抽象出时序图和类图。

以下是笔者粗陋的时序图总结,供读者参考。

二,主要成员解读

1,OkHpptClient

如上图,我们从OkHttp提供的API出发。

OkHttpClient,从名字可知,网络请求的客户端,该客户端主要的作用是什么呢?不妨通过其Builder的重要成员进行分析,

dispatch分发器,主要负责网络请求队列的调度,稍后再谈。

connectionPool,连接池。

interceptors,拦截器-责任链,OkHttp内置了几个重要的拦截器,有失败-重定向、桥接、缓存、连接、访问服务等。

剩下的成员读者可自行分析。那么OkHttpCient笔者认为是一系列网络请求的基础配置。

2,Call

Call是一个接口,继承了克隆接口,我们看下定义的方法,

request,返回Request,

execute,同步执行此处请求,

enqueue,异步执行此次请求,

cancel,取消此次请求,

isExecuted,是否执行完毕,

isCanceld,是否成功取消,

timeout,返回超时相关

因此,Call可以理解为一次Request,通过OkHttpClient.newCall创建,其唯一实现是RealCall,

构造函数中需传入OkHttpClient,原始请求Request,是否WebSocket属性。

3,Request

Request不用笔者多说,主要封装request,包裹了url、method、headers、body等http基础request字段。

4,Response

封装了message,code,hearer,body等。

以上Request和Response是网络请求的主要实体类。

5,Dispatch

笔者认为这是异步请求时主要的调度器,调度对象是Call,内部定义有如下三种队列,

raedyAsyncCalls是通过Call#enqueue添加到此的Call队列,

runningAsyncCalls,从readyAsyncCalls中转移到此队列,表现正在运行的异步Call,

runningSyncCalls,同步运行队列,

下面我们看下调度方法promoteAndExecute

主要在enqueue、finished时调用,作用是将符合条件的raedyAsyncCalls转移至runningAsyncCalls中。

在从readyAsyncCalls转移到runningAsyncCalls时,有默认最大运行数maxRequests64。将真正执行运行的Call放进executeableCalls,然后碟调调用其executeOn方法,传入的executorService是一个线程池,

核心线程数定义0,最大容量MAX_VALUE,因maxRequests的存在,可以忽略此参数。

6,Interceptor&Chin

Interceptor只定义了一个方法Intercept,参数为Chain,每个拦截器负责处理自己的逻辑,如果处理完毕并且需要下一个拦截器处理,需要显式调用Chain#proceed方法。

我们看下Chain接口的唯一实现RealInterceptorChain,

传入此次请求call,拦截器,拦截器执行下标index0,原始请求request等,主要供多个拦截器从Chain中获取信息,我们看下核心方法process,

Interceptor每调用一次proceed方法,会触发Chain被负责,传入index+1(这使得同一个拦截器可以多次调用proceed方法,从该节点重试),进一步获取到下一个拦截器,再调用其拦截器intercept方法。这样就实现了链式请求。

以上,我们介绍了OkHttp框架的主要角色,下面介绍下一次请求的主要流程,以及各个重要拦截器所做的工作。

三,一次请求解读

1,伊始

笔者还得从创建了一个Call对象开始,

我们跟进enqueue方法

原子式设置executed为true,否则抛出异常,合理,一个call只能调用enqueue一次。

client是通过RealCall构造方法传入,我们进入Dispatcher#enqueue查看。笔者注意,这里的RealCall在进入dispatcher时,被转换成了AsyncCall,这个稍后再谈。

所做的事情,是将异步call放入readAsyncCalls中,如果不是webSocket需做点什么,这个笔者不展开说,主要调用到调度方法promoteAndExecute。

这个方法如在Dispatch中解读,加入runningAsyncCalls,并且加入到executableCalls列表,调用AsyncCall#executeOn方法,我们跟进。

AsyncCall实现了Running接口,并且封装了RealCall,我们直接看run方法的实现,

通过TimeOut#enter开启请求超时调度(如果设置的话),然后最重要的调用了getResponseWithInterceptorChain方法,直接返回请求到的Response。当请求完毕后,最终调用到dispatcher.finished方法,我们暂且先查看finished方法,

将执行完毕的Call从runingAsyncCalls中移除,然后在调用一次promoteAndExecute方法,将准备队列的Call执行,如果没有Call执行了,就调用闲置回调idleCallback。这样就实现了队列的简单调度。那么,我们将注意力重新回到核心方法getResponseWithInterceptorChain。

创建一个拦截器list,先放入OkHttpClient中用户自定义的拦截器,随后放入几个核心拦截器,

RetryAndFollowUpInterceptor、负责重试重定向的拦截器。

BridgeInterceptor、桥接拦截器,负责自动设置一些heads、cook等。

CacheInterceptor、缓存的核心实现拦截器。

ConnectInterceptor、连接拦截器,维护了一个连接池,复用连接核心逻辑拦截器。

CallServerInterceptor、与服务器正式请求的拦截器,

这些全部封装进RealInterceptorChain方法中,然后调用proceed方法,参数是request,

通过上文我们了解proceed是顺序调用下一个拦截器逻辑,因此,笔者这里暂忽略用户自定义拦截器,直接顺序解读核心拦截器实现逻辑。

2,RetryAndFollowUpInterceptor

一个无限循环,直接调用chain.process让下一个拦截器处理,然后解析Response,

通过followUpRequest解析Response,如果返回空,代表无需重试或重定向,直接返回Response。否则,重复调用chain#proceed(注意,chain在realChanin中通过copy方法实现原型模式,因此后面的index+1对此处无影响,chain#index仍为原始值)

我们看下followUpRequest方法,

对各种Response#code作解析,新创建Request,当Response正常返回,此方法返回null,笔者在此处不展开说,有兴趣的读者可自行研究。接下来,我们看下一个拦截器。

3,BridgeInterceptor

此处,将OkHttpClient的cookieJar保存,继续跟进拦截逻辑,

(1)如果存在body,body中存在contentType,自动设置进Request的Hearer中,

(2)如果存在body,且内容长度不等于-1,自动设置Content-Length头,移除Transfer-Encoding头。否则,移除Content-Length头,添加Transfer-Encoding头。感兴趣的读者可以主动去了解下这些请求头的意思。

(3)如果Request的Hearer中Host为空,则从请求url中设置host。

(4)如果没有设置Connection字段,自动设置“Keep-Alive”,意保持连接。

(5)继续添加请求头,Cookie、User-Aagent,

笔者在这里解释,桥接拦截器的作用就是自动设置一些请求头,减少客户端操作复杂度。

接下来,就是对Response作解析,如cookieJar解析、Response#解码相关,感兴趣的读者自行了解,笔者在此不展开讲。

4,CacheInterceptor

实现缓存相关,核心逻辑如下,

根据Request从cache中获取Response,随后获取Request的缓存策略

如果从缓存中获取到Response,但是cacheResponse为null,代表此次请求不适用缓存,调用closeQuitely关闭缓存。

如果不能使用网络,且无缓存,返回失败Response。

如果不访问网络请求,那就直接从缓存中获取并返回,就不走接下来的拦截器了。

如果可以访问网络,但策略是访问网络,调用listenr#cacheConditionlHit回调,通知观察者缓存命中或缓存没命中。

于是,请求到下一个拦截器,当返回Response时,

Response code 返货 not modified,调用cache方法更新缓存

如果Response有效,添加到缓存中,另外笔者注意到,如果method不支持缓存,则移除,我们看下哪些不支持呢?

POST/DELETE/PATCH/PUT/MOVE是不支持缓存的,而GET/HEAD...才支持缓存。

5,ConnectInterceptor

通过获取到exchange,调用realChain#copy方法将exchange传入,作为一次连接复用。我们跟进看看initExchange方法,

通过exchangeFinder#find方法复用ExchangeCodec,笔者猜测这是实现连接的主要核心类,我们现看下ExchangeCodec是什么。ExchangeCodec是一个接口,方法定义如下,

从方法中,笔者猜测到这代表了连接实体,通过flushRequest发送给服务器请求,我们看看这个接口的实现类

笔者这里看下Http1ExchangeCodec,有兴趣的读者可以自己去看Http2ExchangeCodec,

从成员中发现RealConnection,因此这封装了一次连接;

BufferedSink,从命名知这是连接打开的缓冲区,通过flushReques将缓存区的数据发送给服务端。具体如何发送给服务器笔者暂不跟进。

先回到复用逻辑,exchangeFinder#find方法,

(1),首先从连接池中获取连接,如果无法获取,则新建连接,

调用RealCall#acquireConnectionNoEvents方法,将复用逻辑设置进connect成员中。

(2)如果没有复用连接,则新创建连接,

我们跟进connect,笔者直接快进到connectSockt,

当连接成功后,获取到source和sink,前者作为接受Response的缓存,后者就是发送缓存,

关于如何发送,上文已经介绍。那么如何接收呢?如下,在Http1ExchangeCodec#AbstractSource中。

跟进到底层有如下代码,笔者在此不再跟进,后面是基本的一些读取相关。有兴趣读者自行了解。

在这个拦截器中,笔者注意到创建或者复用了Connect,接下来,就是通过连接访问服务器了。

6,CallServerInterceptor

通过exchange写入request中的Hearer到缓存区中,

写入请求体什么的,到缓存区中。

最后,调用exchange,finishRequest,通过sink,flush,发送数据并清空缓存区。

随后,通过readResponseHearer,获取Response请求头,

获取请求头完毕后,接下来去读取请求body,

随后赋值给Response,即完成了一次请求。通过将Response链式返回给Chain,最后在AsyncCall中调用onResponse方法,即可通知客户端请求完成。

于是乎,一次请求的过程解读完毕,感谢读者的耐心观看。

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

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

相关文章

在WebSocket中使用Redis出现空指针异常解决方案

文章目录 在WebSocket中使用Redis1.问题描述2.原因3.解决步骤1.新建一个SpringUtil.java类,通过getBean的方法主动获取实例2.在WebSocketSingleServer.java中导入 在WebSocket中使用Redis 1.问题描述 在controller 和 service中都可以正常使用Redis,在…

金融行业现场故障处理实录

KL银行现场服务记录—HA故障 服务时间 2019年9月10日星期二 14:40 到2019年9月11日星期三 0:30 服务内容 排查redhat RHEL 6.4 一个节点cman启动故障。 (1)、查看系统日志; (2)、查看ha日志…

.ui文件相关

目录 ui类生成过程: 提问: 等以后自己熟练了用代码写这些样式内容,尽量用代码写,原因很简单: 用代码写的可以直接修改代码,但是在设计界面修改的东西,电脑没有QC这玩意,还真不好改…

每日一题 力扣514自由之路

514. 自由之路 题目描述: 电子游戏“辐射4”中,任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘,并使用表盘拼写特定关键词才能开门。 给定一个字符串 ring ,表示刻在外环上的编码;给定另一…

PHP语法

#本来是在学命令执行,所以学了学,后来发现,PHP语法和命令执行的关系好像没有那么大,不如直接学php的一些命令执行函数了。# #但是还是更一下,毕竟还是很多地方都要求掌握php作为脚本语言,所以就学了前面的…

【AI视野·今日Sound 声学论文速览 第四十七期】Fri, 12 Jan 2024

AI视野今日CS.Sound 声学论文速览 Fri, 12 Jan 2024 Totally 10 papers 👉上期速览✈更多精彩请移步主页 Daily Sound Papers Contrastive Loss Based Frame-wise Feature disentanglement for Polyphonic Sound Event Detection Authors Yadong Guan, Jiqing Han,…

八种Flink任务监控告警方式

目录 一、Flink应用分析 1.1 Flink任务生命周期 1.2 Flink应用告警视角分析 二、监控告警方案说明 2.1 监控消息队中间件消费者偏移量 2.2 通过调度系统监控Flink任务运行状态 2.3 引入开源服的SDK工具实现 2.4 调用FlinkRestApi实现任务监控告警 2.5 定时去查询目标库…

Ubuntu 22.04安装Nginx负载均衡

君衍. 一、编译安装Nginx二、轮询算法实现负载均衡三、加权轮询算法实现负载均衡四、ip_hash实现负载均衡 一、编译安装Nginx 这里我们先将环境准备好,我使用的是Ubuntu22.04操作系统: 这个是我刚安装好的,所以首先我们进行保存快照防止安装…

实战教学:用Semantic Kernel框架集成腾讯混元大模型应用

导语 | 众所周知,Semantic Kernel 主要支持国外的两款大模型,但这对于开发者而言,显然是不够的,尤其是当我们希望对接国内的大模型时,我们应该怎么做呢?如何用 Semantic Kernel 通过 oneapi 来集成腾讯混元…

如何对Microsoft 365中Loop文档进行权限划分

Microsoft 365中Loop文档的权限划分为两种:第一种是共享创意页面。第二种是共享特定的Loop组件。 共享创意页面。 进入创意页面后,点击右上角的“共享”-“页面链接”。 如上图所示,页面共享分三种类型。以“你选择的人”为例。添加要共享的…

iOS 文件分割保存加密

demo只是验证想法,没有做很多异常处理 默认文件是大于1KB的,对于小于1KB的没有做异常处理demo中文件只能分割成2个,可以做成可配置的N个文件分割拼接还可以使用固定的二进制数据,拼接文件开头或结尾 不论哪种拼法,目的…

防御保护--NAT策略

目录 NAT策略 NAT类型 server-map表 P2P --- peer to peer 网络类型 ​编辑 目标NAT--服务器映射 双向NAT ​编辑 多出口NAT NAT策略 静态NAT --- 一对一 动态NAT --- 多对多 NAPT --- 一对多的NAPT --- easy ip --- 多对多NAPT 服务器映射 源NAT--基于源IP地址进行转…