Android进阶超级全-从okhttp的源码出发,了解客户端的网络请求
Posted bug樱樱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android进阶超级全-从okhttp的源码出发,了解客户端的网络请求相关的知识,希望对你有一定的参考价值。
艳阳高照,温度高企。
然而对于知识与履历不佳的android开发来说,却仿佛坠入了寒冬。
招聘市场能看到的安卓岗位基本上来来去去都是那几家公司,大公司不敢面,小公司待遇不满足。仿佛失业就摆在面前了。
所以能怎么办呢,
只能继续学习了。
OKHttp作为Android十分流行的网络请求框架,有着精妙的设计和丰富的功能。支持了缓存能力,重试重定向能力,还自己实现了一套网络连接传输能力。完美支持客户端的各种网络需求。
居家必备,不得不看。
准备:
- Okhttp依赖
Idea 创建java项目,依赖okhttp,可以直接在main方法中执行okhttp网络请求
implementation("com.squareup.okhttp3:okhttp:4.10.0")
- Okhttp源码
直接去 github download
一,做一个同步请求,探索okhttp发起请求的过程
我们有一个get接口: mock.apifox.cn/m1/810160-0…
请求会返回一个json :
"data":"hello"
由此,我们开启http请求
发起HTTP请求
我们先来了解下这样的请求是如何在http报文中体现的
HTTP 请求报文由下面的四个部分组成
-
请求行 request line
-
请求头 header
-
空行
-
请求数据 data
在这个请求中,默认的请求头为空,因为是get请求,请求体也是空。
现在使用OKHTTP发起这样的请求:
public static void main(String... args) throws Exception
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://mock.apifox.cn/m1/810160-0-default/test")
.build();
try (Response response = client.newCall(request).execute())
ResponseBody body = response.body();
System.out.println(body.string());
client.newCall(request).execute()
代码从构建RealCall
到发起请求的调用步骤如下:
请求的发起从execute
开始。分析下RealCall
的execute
方法,
-
第一个红框对各种超时进行了处理
-
第二个红框执行了网络的拦截链,直到响应结果返回
-
第三个红框中的client.dispather,则是记录了当前进行中的请求任务
跟踪请求的发起,我们在RealCall.getResponseWithInterceptorChain
方法中看到了一系列责任链形成,并通过该链条将用户的请求一步步处理成为给用户的响应结果返回。
拦截器责任链的串联
直接看链条中的最后一个拦截器CallServerInterceptor
,它对服务器进行了网络调用。
断点CallServerInterceptor.intercept
方法,看下他的调用堆栈:
可以看到,每个拦截器通过调用RealInterceptorChain
链的process方法,进行链条的传递。
顺序和前面拦截器添加的顺序一致,对应拦截器和作用如下:
client.interceptors
用户的自定义拦截器,在所有拦截器之前,拦截的请求和响应都还是用户数据,未被封装处理。每次用户请求只会拦截一次,不参与重试的拦截。
RetryAndFollowUpInterceptor
此拦截器从故障中恢复并根据需要遵循重定向。如果调用被取消,它可能会抛出IOException 。
BridgeInterceptor
作为从应用程序代码到网络代码的桥梁,对用户的数据和网络的数据进行双向转换。首先根据用户请求构建网络请求。然后继续调用网络。最后从网络响应构建用户响应。
CacheInterceptor
处理来自缓存的请求并将响应写入缓存。
ConnectInterceptor
打开到目标服务器的连接并继续到下一个拦截器。网络可能用于返回的响应,或使用条件 GET 验证缓存的响应。
if(forWebSocket) client.networkInterceptors
WebSocket网络拦截器,除了网络请求拦截器外,该拦截器在其他所有拦截器之后。所以他会在网络连接等完成后,真正每次网络请求中进行拦截。也会在每次重试和重定向中拦截。
CallServerInterceptor
这是链中的最后一个拦截器。它对服务器进行网络调用
一个同步的任务发起,会直接开始依次处理请求。各个拦截器的细节内容繁多,后面一一分析。
二,做一个异步请求,探索okhttp的多任务请求机制
将之前的execute()
请求执行方式换成enqueue()
方法,网络请求就会以异步的形式被发起:
client.newCall(request).enqueue(new Callback()
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e)
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException
ResponseBody body = response.body();
System.out.println(body.string());
);
Dispatcher 说明
我们再次深入RealCall.enqueue()
方法的调用链,发现其中直接就创建了个AsyncCall
,并交给了Client.dispatcher
执行。
AsyncCall
是Runnable
的实现类,以便在需要的时候,将该异步请求交给线程池执行,真正的一步请求执行将会在后文提到。
我们看看dispather
变量的创建时机,是在Client.Builder创建的时候,直接实例化出来的Dispatcher
对象
//client.dispatcher
@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher
//Builder.dispatcher
internal var dispatcher: Dispatcher = Dispatcher()
Dispather
类的说明如下。
关于何时执行异步请求的策略。
每个调度程序都使用ExecutorService在内部运行调用。如果您提供自己的执行程序,它应该能够同时运行the configured maximum调用数。
很明显他是okhttp中异步的调度器,决定了okhttp异步请求的能力。
Dispatcher 的请求发起和调度
回到enqueue
方法
-
第一个红框将异步请求添加到待处理队列
-
第二个红框代码针对相同的host的请求,进行计数
-
第三个红框是调度器的关键方法,内部触发了异步请求的检查,会将符合条件的请求开始异步执行
-
readyAsyncCalls (待处理异步请求队列)
-
runningAsyncCalls (处理中的异步请求队列)
promoteAndExecute()
方法中会检查待处理请求列表,对当前同事执行的请求数量,以及单一host同事请求的数量进行阈值判断,如果满足条件就会将待处理的请求转为执行状态。并在检查完成后,调用AsyncCall.executeOn(executorService)
真正开始执行
默认的maxRequests值是64
maxRequestsPerHost值是5
如果需要自定义,可以对Dispatcher对象进行赋值
private fun promoteAndExecute(): Boolean
val executableCalls = mutableListOf<AsyncCall>()
val isRunning: Boolean
synchronized(this)
val i = readyAsyncCalls.iterator()
while (i.hasNext())
val asyncCall = i.next()
///1.数量阈值判断检查
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
///2.执行检查通过的请求
for (i in 0 until executableCalls.size)
val asyncCall = executableCalls[i]
asyncCall.executeOn(executorService)
return isRunning
到现在为止,调度树走到了红色小人所在的环节,后续异步请求的真正执行,就在于AsyncCall.run
方法中,该方法被线程池调用。
异步请求的真正执行
AsyncCall
的executeOn
方法除了一些异常捕获和断言外,主要就是将自己的对象放到线程池中执行:
所以真正开启异步网络请求的方法就在AsyncCall.run
。
代码十分的似曾相识:这不就是同步请求
RealCall.enqueue()
吗?
殊途同归。处理对当前线程命名,回调的调用外,一切都是熟悉的配方。
-
第一个红框对各种超时进行了处理
-
第二个红框执行了网络的拦截链,直到响应结果返回
-
第三个红框中的client.dispather,则是记录了当前进行中的请求任务
至此,异步请求也走完了流程
焦点又回到了拦截器链。
里面究竟怎么实现的网络请求?
三,okhttp网络请求的真正劳动力:各大拦截器
3-1 重试重定向拦截器:RetryAndFollowUpInterceptor
点击跳转github源码地址。根据源码的逻辑,我们先画出对应的流程图
可以看到逻辑很简单,其中关键是以下两点:
-
满足重试条件,就恢复请求重试。
-
响应里面的信息满足重定向条件,就重定向
这两点控制了重试和重定向的所有逻辑,我们针对源码分析下
重试恢复请求条件
恢复请求的逻辑在recover方法中。
传递的参数分别是 e: IOException
:请求错误信息,call:RealCall
:网络调用信息,userRequest:Request
:请求数据,requestSendStarted:Boolean
:是否已经发送过请求体。
总结下来的发送限制如下:
-
需要开启 **retryOnConnectionFailure **开关
-
如果发送过请求体,需要请求体支持多次发送才能重试
-
协议错误不可重试,SSL证书错误不可重试,SSL对等验证失败不可重试。io 发送中断不可重试
///报告并尝试从与服务器通信失败中恢复。如果e是可恢复的,则返回 true;如果失败是永久性的,则返回 false。只有当主体被缓冲或在请求发送之前发生故障时,才能恢复带有主体的请求。
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure) return false
// We can't send the request body again.
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false
// No more routes to attempt.
if (!call.retryAfterFailure()) return false
// For failure recovery, use the same route selector with a new connection.
return true
重定向条件
默认情况下,当原始请求因以下原因失败时,OkHttp 将尝试重新传输请求正文:
-
陈旧的连接。该请求是在重用连接上发出的,并且该重用连接已被服务器关闭。
-
客户端超时 (HTTP 408)。
-
Authenticator满足的授权质询(HTTP 401 和 407)。
-
可重试的服务器故障(带有Retry-After: 0响应标头的 HTTP 503)。
-
合并连接上的错误定向请求 (HTTP 421)。
3-2 桥接拦截器:BridgeInterceptor
该拦截器的主要功能就是将用户请求封装成网络请求,将网络响应封装成用户给用户的响应。主要逻辑如下:
-
封装用户请求,Header完善
-
Content-Type,ContentLength,Transfer-Encoding从body中完善
-
Host,Connection,Accept-Encoding,Cookie,User-Agent完善
-
网络请求
-
封装网络响应body(如果有)
-
去除 Content-Encoding ,Content-Length 响应头
-
解压gzip
-
设置contentType给body
3-3 缓存拦截器:CacheInterceptor->DiskLruCache
缓存的获取,只有在okhttpClient
配置了cache
的情况下才会生效。
OKHttp
内置了DiskLruCache
作为缓存工具类。
详解也可以参考他人的优秀文章
在CacheInterceptor
中一开头就直接尝试从DiskLruCache
中获取缓存
然后计算缓存策略,逐步执行:
-
缓存没有
-
if(策略不请求网络)->返回失败响应
-
缓存有
-
if(策略不请求网络)->返回缓存的响应
-
请求网络,并返回
-
if(状态码==HRRP_NOT_MODIFIED【304】)->返回缓存响应
-
if(响应有正文,响应可缓存)缓存新的网络请求响应,并返回网络响应
-
if(响应不可缓存) 返回网络响应,移除缓存
上文可缓存和不可缓存的部分逻辑如下,就是对各种返回状态码进行判断。
可缓存判断:
不可缓存判断(POST:PATCH:PUT:DELETE:MOVE不可缓存):
DiskLruCache
DiskLruCache
是Andorid 硬盘缓存的优秀方案。OKHttp 将其io相关的操作交由okio实现。
OKHttp中从cache中获取Response的代码分为三步
-
通过url生成的key获取
DiskLruCache.Snapshot
-
以snapshot获取metaData生成Entry
-
从entry中获取Response
journal日志
journal
是DiskLruCache
缓存的的日志文件,缓存类将会从journal
日志中读取所有的缓存操作,并生成
lruEntries
链表。
下图是典型的journal
日志样例
日志的前五行构成其标题。它们是常量字符串“libcore.io.DiskLruCache”、磁盘缓存的版本、应用程序的版本、值计数和空行。
文件中随后的每一行都是缓存条目状态的记录。每行包含空格分隔的值:一个状态、一个键(key)和可选的特定于状态的值。
-
DIRTY
行跟踪正在积极创建或更新条目。每个成功的 DIRTY 操作都应该跟随一个 CLEAN 或 REMOVE 操作。没有匹配的 CLEAN 或 REMOVE 的 DIRTY 行表示可能需要删除临时文件。 -
CLEAN
行跟踪已成功发布并可被读取的缓存条目。发布行后面是其每个值的长度。 -
READ
行跟踪 LRU 的访问。 -
REMOVE
行跟踪已删除的条目
每次缓存操作都会更新附加日志文件。日志有时可能会因为删除多余的行而被压缩。压缩期间将使用一个名为“journal.tmp”的临时文件;如果打开缓存时该文件存在,则应删除该文件。
Snapshot的获取
从DiskLruCache
获取Snapshot,会先经历初始化initalize
过程,然后再从lruEntries
从获取对应的实体及其快照。而 lruEntries
是一个LinkedHashMap
。他是一个有序的哈希链表,正式他的访问排序特性,决定了DiskLruCache
的 LRU(Least Recently Used)近期最少使用特性。
关键点在于初始化过程,将会通过journal
日志文件进行初始化。
通过查看initialize
源码。可知初始化经历了三个过程:
-
readJournal() 读日志
-
processJournal() 处理日志
-
rebuildJournal() 重建压缩日志
对于lruEntries
的填充就是在readJournal()
期间,读取每一行Journal状态记录完成的。
具体代码在readJournalLine()
方法中,简化细节如下:
@Throws(IOException::class)
private fun readJournalLine(line: String)
//...
//移除REMOVE状态的Entry
if (secondSpace == -1)
key = line.substring(keyBegin)
if (firstSpace == REMOVE.length && line.startsWith(REMOVE))
lruEntries.remove(key)
return
//将正常状态的实体,加入到lruEntries中
var entry: Entry? = lruEntries[key]
if (entry == null)
entry = Entry(key)
lruEntries[key] = entry
//对Dirty状态的可写,对READ状态的不处理,对CLEAN状态的读取状态标为正常
when (firstSpace)
CLEAN ->
val parts = line.substring(secondSpace + 1).split(' ')
entry.readable = true
entry.currentEditor = null
entry.setLengths(parts)
DIRTY -> entry.currentEditor = Editor(entry)
READ ->
就是逐行根据journal
的状态填充lruEntries
。
最后返回的Snapshot
就是对应key
的Entry.snapshot()
;
该Snapshot
将会打开所有流,以确保用户拿到已缓存完成的快照。
到此,缓存已经可以正常交由CacheInterceptor
处理了
3-4 网络连接拦截器:ConnectInterceptor
网络连接拦截器主代码非常简单:
关键点就在于Exchange
的初始化。
这一块和下面的网络传输实现非常强关联。所以和CallServerInterceptor
一起分析。
3-5 网络传输拦截器:CallServerInterceptor
100-continue
:HTTP/1.1 协议里设计 100 (Continue) HTTP 状态码的的目的是,在客户端发送 Request Message 之前,HTTP/1.1 协议允许客户端先判定服务器是否愿意接受客户端发来的消息主体(基于 Request Headers)。
即, 客户端 在 Post(较大)数据到服务端之前,允许双方“握手”,如果匹配上了,Client 才开始发送(较大)数据。
如果请求中有“Expect: 100-continue”标头,则在传输请求正文之前等待“HTTP1.1 100 Continue”响应。如果我们没有得到那个,返回我们得到的(例如 4xx 响应)而不传输请求正文。
CallServerInterceptor请求流程
CallServerInterceptor
中的网络请求全程通过exchange
实现。
-
对于有
body
的请求,检验100-continue
的响应 -
对于校验通过的请求再发送
requestBody
-
读取响应Header建立对应Builder
-
读取响应body,建立Response返回
代码中一系列操作券是通过exchange
完成,精简一下一个post请求就是这样子:
///1\\. 写入请求头
val exchange = realChain.exchange!!
exchange.writeRequestHeaders(request)
///2\\. 开始请求,并写入请求bdy
exchange.flushRequest()
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
requestBody.writeTo(bufferedRequestBody)
bufferedRequestBody.close()
exchange.finishRequest()
//3\\. 读取响应头
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
exchange.responseHeadersStart()
var response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
//3\\. 读取响应body
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
exchange.responseHeadersEnd(response)
var code = response.code
我们从Exchange
的注释中,可以得知他的主要功能:
传输单个 HTTP 请求和响应对。将在实际处理I/O 的 ExchangeCodec
上进行分层连接管理和事件。
意思也就是,他也没干啥事,所有的分层连接管理和事件一股脑交给了ExchangeCodec
。
请求中的状态码校验
继续回到之前的网络处理,其中还夹杂了一些状态码和Header
的校验,具体如下
100
:前面有提到的100的校验。如果是100 需要重新获取对应的实际响应
websocket & 101
:直接返回空的body
Connection:close
:返回对应response,标记noNewExchanges
防止在此连接上创建进一步的交换
204 | 205
:抛异常**"HTTP 状态码 拥有 非空的 Content-Length"**
Exchange 和 ExchangeCodec
在ConnectInterceptor
中,仅仅是调用了Exchange
的初始化。其中实例化了Exchange
。
将RealCall
eventLisener
exchangeFinder
和 codec
作为参数传输了进去。
-
其中
evenLisener
从client.eventListenerFactory
中获得,可在client build
的时候配置,放出事件执行的监听回调。 -
codec
作为具体网络io的工具类,由exchangeFinder.find
获取。 -
exchangeFinder
则是在RetryAndFollowUpInterceptor
拦截器中被初始化,代码如下。
通过断点调试,该方法在 RetryAndFollowUpInterceptor
每次进行请求和重试的时候被调用。根据newExchangeFinder
参数来决定是否创建。在重试的时候不重建,而在重定向或者第一次请求的时候则会重建。
ExchangeFinder
的主要功能是通过一系列的策略满足连接的复用查找工作
尝试查找交换的连接以及随后的任何重试。这使用以下策略:
-
如果当前调用已经有一个可以满足请求的连接,则使用它。对初始交换及其后续使用相同的连接可能会改善局部性。
-
如果池中有可以满足请求的连接,则使用它。请注意,共享交换可以向不同的主机名发出请求!有关详细信息,请参阅RealConnection.isEligible 。
-
如果没有现有连接,请列出路由(可能需要阻止 DNS 查找)并尝试建立新连接。当发生故障时,重试迭代可用路由列表。
如果在 DNS、TCP 或 TLS 工作正在进行时池获得了合格的连接,则此查找器将首选池连接。只有池化的 HTTP/2 连接用于此类重复数据删除。
可以取消查找过程。
此类的实例不是线程安全的。每个实例都被线程限制在执行call的线程中。
他的构造函数包含四个参数
- 第一个
connectionPool
连接池也可以在Client.Builder中配置。默认构造的代码如下,目前,此池最多可容纳 5 个空闲连接,这些连接将在 5 分钟不活动后被驱逐。
-
第二个参数
address
,提供了后续的代理Proxy,路由Route对象的生成,以及重要的寻址工作。 -
第三个
call
则是把RealCall
作为参数传递进去 -
第四个参数
eventListener
提供了事件监听回调的途径
之后通过ExchangeFinder.findConnection()
找到合适的Connection
(找到后会自动连接),通过newCodec
创建出了ExchangeCodec
实例,并且内部通过http2Connection
的判断做了http1和http2的适配。
留个问题,具体是怎么查找连接并进行三次握手的呢?
后面继续探索。
HTTP1.1 和HTTP2的适配
对于未知http2Connection
继续探索,断点他的唯一赋值处。
找到了关键点,是否调用starthttp2
还是由protocol
的值判断。
主要的原理就是。route.address.protocols
里面包含了http2
协议,就启动http2
适配。
那么问题来了,route.address.protocols
的值又是什么时候被赋予的呢?这个问题先留着,带着疑问接着往下看。
ExchangeFinder.find()找到RealConnection并开始三次握手连接
ExchangeFinder.find()
就是ExchangeFinder
的核心方法,承担了寻找合适的Connection
的任务,寻找过程中发生了代理选择,dns寻址任务,并完成了和服务器的连接工作。
可谓是网络连接中非常关键的方法。
该方法的查找逻辑如下。
源码中包含了连接池复用,路由Route
地址Address
查询细节。
FindConnection 生成RealConnection
Route是什么?
连接用于到达抽象源服务器的具体路由。创建连接时,客户端有很多选项:
-
HTTP 代理:可以为客户端显式配置代理服务器。否则使用proxy selector 。它可能会返回多个代理来尝试。
-
IP 地址:无论是直接连接到源服务器还是代理,打开套接字都需要一个 IP 地址。 DNS 服务器可能会返回多个 IP 地址进行尝试。
每条路线都是这些选项的特定选择。
问题来到了RealConnection.route
,该变量在构造函数中被赋值。再回到ExchangeFinder.findConnection()
方法,RealConnection
实例的初始化就在其中。
Route
在前面的逻辑中通过routeSelector
被初始化
Route
赋值的链条很明确:RouteSelector().next()
获取RouteSelection
。
RouteSelection.next()
获取Route
。
其中的获取细节慢慢道来。
RouteSelection
生成的时候,Route
以address
proxy
和inetSocketAddress
为参数被构造出来。
其中RouteSelector.proxies
在RouteSelector构造的时候被初始化。由
address.proxySelector.select(address.url)
取得。
所以可见关键点还是address
。
回到前文中Exchange
那一小节。其中address
就是在ExchangeFinder
构造的时候被创建的。我们回顾下RealCall.enterNetworkInterceptorExchange()
方法:
Address
中很多的参数都是直接取的client
的字段。而OkHttpClient
的字段也大多从Builder中直接取过来。
可以看到Address.protocols
的默认值就是DEFAULT_PROTOCOLS,默认协议数组里面包含了HTTP_2和HTTP_1_1。所以会默认支持http的请求。
如果没有设置代理和代理选择器。其中默认的proxySelector
则是获取的系统代理选择器。
根据源码分析图解一下,Address选择代理,生成Route路由再到构建连接的过程如下所示:
至此,ConnectInterceptor
中短短一行代码的原理已经清晰了,只有看了细节之后才知道,原来这一行代码做了那么多事情。
RealCall.initExchange()
完成连接后,会根据http协议版本生成ExchangeCodec
用来进行实际的数据传输,实际实现类型是Http2ExchangeCodec
和Http1ExchangeCodec
。
在进行数据读写的时候,则利用Http2Stream
或者Http1Stream
进行数据传输。
Http2不同于http1的一请求一回应,而是分为多个stream
流,每个stream
可以有多个请求,多个响应(Server Push)。
响应的body则是以okio
Sink
的方式放回。
Sink
相当于输出流(OutputStream),进行网络IO的输出。
所以CallServerInterceptor
中的io读写也就有了眉目。就是利用Exchange
中转,交由Http2ExhcnageCodec
进行数据传输。其中再利用Http2Stream
进行okio
数据流的读写。
完结
至此,okHttp的源码详解告一段落。也是在学习的过程中了解到不少平时所知甚少的http状态码,也学习到了网络请求代理寻址过程。
收货颇丰,浅尝辄止。
okhttp
的迷雾散去了一块。
okio
的汪洋大海还是一片迷雾。
作者:guuguo
链接:https://juejin.cn/post/7137121460358742053
更多Android学习笔记+视频资料可扫描下方CSDN官方认证二维码了解详情👇
以上是关于Android进阶超级全-从okhttp的源码出发,了解客户端的网络请求的主要内容,如果未能解决你的问题,请参考以下文章