【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的汪洋大海还是一片迷雾。
转载自:https://juejin.cn/post/7137121460358742053