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方法,即可通知客户端请求完成。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

iOS 文件分割保存加密

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

牛客周赛30

思路:先把x, y除以最大公约数变成最小值,然后同时乘以倍数cnt,只记录两个数都在[l,r]间的倍数。 代码: int gcd(int a,int b){return b ? gcd(b, a % b) : a; }void solve(){int x, y, l, r;cin >> x >> y >>…

两两交换链表中的结点---链表OJ

https://leetcode.cn/problems/swap-nodes-in-pairs/description/?envType=study-plan-v2&envId=top-100-liked 1、递归 创建newhead = head->next,然后将head->next->next作为递归参数,返回值用head->next接收;递归结束条件是:没有结点或者只有一…

Python代码耗时统计

time模块 在代码执行前后各记录一个时间点,两个时间戳相减即程序运行耗时。这种方式虽然简单,但使用起来比较麻烦。 time.time() 函数返回的时间是相对于1970年1月1日的秒数 import timestart time.time() time.sleep(1) end time.time() print(f&…

洛谷 P2150 [NOI2015] 寿司晚宴

P2150 [NOI2015] 寿司晚宴 约定: n ≤ 500 n \leq 500 n≤500 题意 给定 2 → n 2 \rightarrow n 2→n 共 n − 1 n-1 n−1 个数字,现在两个人想分别取一些数字(不一定全取完),如果他们两人取的数字中存在&#xf…

05. java线程基础

05. java线程基础 01. 线程相关概念 1. 程序 ​ 是为完成特定任务、用某种语言编写的一组指令的集合。简单来说:就是我们写的代码 2. 进程 进程是指运行中的程序,比如我们使用微信,就启动了一个进程,操作系统会为该进程分配内…

【代码随想录】LC 242. 有效的字母异位词

文章目录 前言一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 前言 本专栏文章为《代码随想录》书籍的刷题题解以及读书笔记,如有侵权,立即删除。 一、题目 1、原题链接 242. 有效的字母异位词 2、题目描述 二、解题…

一文理清楚-Docker 容器如何工作

Docker 容器如何工作 集装箱什么是虚拟机?虚拟化如何运作?什么是容器?什么是 Docker?总结 五星上将麦克阿瑟曾经说过:在docker面前,虚拟机就是个弟弟 集装箱 《盒子:集装箱如何让世界变得更小&…

剑指offer——删除链表的节点

题目描述&#xff1a;给定单向链表的头指针和一个要删除的节点的值&#xff0c;定义一个函数删除该节点。返回删除后的链表的头节点。 数据范围&#xff1a; 0 <链表节点值 < 10000 0 <链表长度 < 10000 示例1&#xff1a; 输入&#xff1a;{2,5,1,9}&#xff…

1.28寒假集训

A: 解题思路&#xff1a; 移项就好v mv / (M - m) 下面是c代码&#xff1a; #include<iostream> using namespace std; int main() {int t;double M,m,v;cin >> t;while(t ! 0){cin >> M >> m >> v;printf("%.2lf\n",(m * v) / (M…

数据库之 基础概念、安装mysql、sql语句基础

数据库之 基础概念、安装mysql、sql语句基础 【一】存储数据的演变过程&#xff1a; 文件存储&#xff1a; 初始阶段随意存放数据到文件&#xff0c;格式任意。目录规范引入&#xff1a; 软件开发使用目录规范&#xff0c;限制数据位置&#xff0c;建立专门文件夹。本地数据存…

Linux报 “no route to host” 异常 ping: sendmsg: No route to host

公司有台服务器迁移机房后跟另一台服务器相互ping不通&#xff0c;但是两台服务器都能上网能ping其他机器&#xff0c;其他机器都能ping通这两台服务器。检查两台服务器没有防火墙规则拦截&#xff0c;交换机上也没检查到acl过滤。 下图是迁移机房的服务器ping截图 下图是nfs服…

分布式空间索引了解与扩展

目录 一、空间索引快速理解 &#xff08;一&#xff09;区域编码 &#xff08;二&#xff09;区域编码检索 &#xff08;三&#xff09;Geohash 编码 &#xff08;四&#xff09;RTree及其变体 二、业内方案选取 三、分布式空间索引架构 &#xff08;一&#xff09;PG数…

腾讯云幻兽帕鲁4核16G14M服务器性能测评和价格

腾讯云幻兽帕鲁服务器4核16G14M配置&#xff0c;14M公网带宽&#xff0c;限制2500GB月流量&#xff0c;系统盘为220GB SSD盘&#xff0c;优惠价格66元1个月&#xff0c;277元3个月&#xff0c;支持4到8个玩家畅玩&#xff0c;地域可选择上海/北京/成都/南京/广州&#xff0c;腾…

通讯录项目(终)

Start And Stick 上一期我们将通讯录的项目的基本功能已经实现&#xff0c;这一篇文章我们将对通讯录进行完善。 目录 Start And Stick 上期回顾&#xff1a; 上期必要代码&#xff1a; 数据打印&#xff1a; 代码讲解&#xff1a; 头部插入数据&#xff1a; 代码讲解&…

27.1K Star,优雅的JSON 数据可视化工具

Hi&#xff0c;骚年&#xff0c;我是大 G&#xff0c;公众号「GitHub指北」会推荐 GitHub 上有趣有用的项目&#xff0c;一分钟 get 一个优秀的开源项目&#xff0c;挖掘开源的价值&#xff0c;欢迎关注。 想自己之前做 APP 开发会访问后端数据&#xff0c;这个数据就是 JSON …

【网络基础】网络协议传输层UDP和TCP

UDP 解包和分用 解包&#xff08;解析数据包&#xff09; 捕获数据包&#xff1a;首先&#xff0c;接收端的网络栈捕获UDP数据包。检查目的端口&#xff1a;接收端检查数据包头部的目的端口&#xff0c;以确定哪个应用程序应该接收该数据包。验证校验和&#xff1a;接收端可能…

【排序5】基数排序:数字的组织与整理艺术

&#x1f3a1;基数排序 &#x1f38a;1、基本思想&#x1f38a;2、基本步骤&#x1f38a;3、代码示例&#x1f38a;4、特性总结 &#x1f38a;1、基本思想 基数排序&#xff08;Radix Sort&#xff09;是一种非比较排序算法&#xff0c;它根据数字的每一位来对元素进行排序。它…

2024年数学建模美赛C题(预测 Wordle)——思路、程序总结分享

1: 问题描述与要求 《纽约时报》要求您对本文件中的结果进行分析&#xff0c;以回答几个问题。 问题1&#xff1a;报告结果的数量每天都在变化。开发一个模型来解释这种变化&#xff0c;并使用您的模型为2023年3月1日报告的结果数量创建一个预测区间。这个词的任何属性是否会…

Java TemporalAdjusters 时间调节器

提供了非常多处理日期相关的函数&#xff1a; 使用示例&#xff1a; /*** JCccc* param args*/public static void main(String[] args) {DateTimeFormatter pattern DateTimeFormatter.ofPattern("yyyy-MM-dd");LocalDateTime now LocalDateTime.now();//获取当月…