OkHttp 源码分析

流程分析

我们从一个简单的 HTTP 请求开始:

client = new OkHttpClient();
Request request = new Request.Builder().url("your url").build();
//同步发起请求
Response syncResponse = client.newCall(request).execute();
//异步发起请求
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 { }
});

上面的代码将会发起两个简单的 HTTP 请求,请求流程如下图所示。

上面的流程图只画到了责任链的部分,前面的介绍完后会单独介绍责任链及每个 interceptor 的原理。

OkHttpClient

我们使用 new OkHttpClient() 创建一个默认的 OkHttpClient,同样也可以使用 OkHttpClient.Builder 来通过自定义的参数来构造一个 client。
后面我们使用网络请求时都将通过这个 client 来进行,可以将它理解为整个 OkHttp 的核心类,其对整体 OkHttp 进行了封装,对外提供了请求的发起以及一些参数配置的接口,我们可以通过 OkHttpClient.Builder 来设置,对内负责协调各个类的运作,本身其实并没有包含过多的代码。

Request

Request 很好理解,负责组装请求。

Call(RealCall)

然后调用 client.newCall(request) 方法,该方法是指创建一个新的将要被执行的请求,通过 newCall 方法获取一个 Call 对象(实现为 RealCall),这时我们使用 Call 的 execute/enqueue 将发起一个同步/异步请求。

所以每一个 Request 最终将会被封装成一个 RealCall 对象,RealCall 与 Request 是一一对应的关系,Call 用来描述一个可被执行、中断的请求,我们每发起一个请求时就会创建一个 RealCall 对象,最终调用 RealCall#getResponseWithInterceptorChain() 发起请求,该方法将返回一个响应结果 Response。

Dispatcher

Dispatcher 用于管理其对应 OkHttpClient 的所有请求,通过上面的流程图可以看到,使用异步请求时会将请求委托给 Dispatcer 对象来处理,Dispatcher 对象随 OkHttpClient 创建而创建

实际上,Dispatcher 不仅用于管理异步请求,也负责管理同步请求,当我们发起一个请求时,无论是异步还是同步都会被 Dispatcher 记录下来。我们可以通过 OkHtpClient#dispatcher() 获取 Dispatcher 对象对请求进行统一的控制,例如结束所有请求、获取线程池等等。
Dispatcher 中包含三个队列:

  • readyAsyncCalls:一个新的异步请求首先会被加入该队列中
  • runningAsyncCalls:当前正在运行中的异步请求
  • runningSyncCalls:当前正在运行的同步请求

Dispatcher 中包含一个默认的线程池用于执行所有的异步请求,也可以通过构造器指定一个线程池,所有的异步请求都将会通过这个线程池来执行。异步请求与同步请求一样,最终也会调用 RealCall#getResponseWithInterceptorChain() 发起请求,只不过一个是直接调用,一个是在线程池中调用。

通过上面的介绍已经发现了关键之处就在于那个名字超长的方法,只要调用了它就能返回一个 Response,这个方法就开始涉及到广为人知的 OkHttp 的责任链模式了。

OkHttp 责任链

讲真的,网上现在随便搜搜 OkHttp 源码的都是在将责任链,搞的我都不想讲了,但是作为一个 OkHttp 源码分析的文章不讲又感觉过不去,那还是说一下吧(这里点名批评一些为知笔记的 MarkDown 编辑器,写了一下午的东西说没就没,丝毫没有给我反应的余地)。

这一切都还要从那个名字超长的方法开始说起,我们知道,无论如何都会调用 RealCall#getResponseWithInterceptorChain() 发起请求并获取最终的 Response。

这个方法会根据用户设置的 Interceptor 以及默认的几个 Interceptor 组装 Interceptor 列表,然后创建责任链。责任链创建好后会调用其 process 方法获取 Response 并返回,其中涉及两个概念:Interceptor、Chain

Interceptor

Interceptor 接口作为一个拦截器的抽象概念,被设计为责任链上的单位节点,用于观察、拦截、处理请求等,例如添加 Header、重定向、数据处理等等。
Interceptor 之间互相独立,每个 Interceptor 只负责自己关注的任务,不与其他 Interceptor 接触。
Interceptor 接口中只包含一个方法(OkHttp 现在已经用 Kotlin 重写了):

interface Interceptor {
  @Throws(IOException::class)
  fun intercept(chain: Chain): Response
}

intercept 方法接收一个 Chain 作为参数,并返回一个 Response,该方法的一般处理逻辑如下:

上面的流程中的步骤并不是必选的,也不是一定要按照这个步骤来,你完全可以按照自己的想法进行你喜欢的各种骚操作。
在 RealCall 中会按照顺序添加如下几个默认的 Interceptor 到责任链中用来完成基本功能:

  • 用户设置的 Interceptor
  • RetryAndFollowUpInterceptor:失败重试及重定向
  • BridgeInterceptor:处理网络 Header、Cookie、gzip 等
  • CacheInterceptor:管理缓存
  • ConnectInterceptor:连接服务器
  • 如果是 WebSocket 请求则添加对应的 Interceptors
  • CallServerInterceptor:数据发送/接收

后面再详细介绍这几个 Interceptor 的具体含义及原理。
责任链将会按照添加的顺序依次执行这些 Interceptor,所以,顺序是很重要的,通过这些 Interceptor 的处理,最终会返回一个完美的 Response 给到 RealCall 里面的那个名字超长的方法,然后在返回到下游用户。至此,一个完整的请求就落下帷幕了。

Chain

Chain 被用来描述责任链,通过其中的 process 方法开始依次执行链上的每个节点,并返回处理后的 Response。
Chain 的唯一实现为 RealInterceptorChain(下文简称 RIC),RIC 可以称之为拦截器责任链,其中的节点由 RealCall 中添加进来的 Interceptor 们组成。由于 Interceptor 的互相独立性,RIC 中还会包含一些公共参数及共享的对象。

Interceptor 与 Chain 彼此互相依赖,互相调用,共同发展,形成了一个完美的调用链,下面来看下他们的调用关系图:

通过上图可以明确的看到,当我们在某个 Interceptor 中调用 Chain#process 方法获取 Response 时,将会依调用当前位置之后的 Interceptor 来处理这个请求,处理完成后把 Response 返回到当前 Interceptor,然后处理完再向上级返回,直到遍历结束。

网络连接与数据收发

上面已经介绍了 OkHttp 的基本概念、基础配置、线程控制、责任链,下面再说说一个网络框架的灵魂:网络请求的建立与数据收发。
RealCall 中添加的几个不同的 Interceptor 就互相协作完成了这些功能,只要明白了这几个基础的 interceptor 就明白了 OkHttp 的灵魂。
其实我不太建议阅读源码时太过关心实现细节,只要明白设计思路,大体上的实现就差不多了,不然容易被负责的细节绕晕。
那么在介绍这几个 interceptor 之前先介绍一些 OkHttp 中的基本概念。

连接如何建立

我们之前看的 Volley 啊等等很多网络请求框架很多底层都是通过 HTTPURLConnection 来与服务端建立连接的,而 OkHttp 就比较优秀了。因为 HTTP 协议是建立在 TCP/IP 协议基础之上的,底层还是走的 Socket,所以 OkHttp 直接使用 Socket 来完成 HTTP 请求。

Route

route 为用于连接到服务器的具体路由。其中包含了 IP 地址、端口、代理等参数。
由于存在代理或者 DNS 可能返回多个 IP 地址的情况,所以同一个接口地址可能会对应多个 route。
在创建 Connection 时将会使用 Route 而不是直接用 IP 地址。

RouteSelector

Route 选择器,其中存储了所有可用的 route,在准备连接时时会通过 RouteSelector#next 方法获取下一个 Route。
值得注意的是,RouteSelector 中包含了一个 routeDatabase 对象,其中存放着连接失败的 Route,RouteSelector 会将其中存储的上次连接失败的 route 放在最后,以此提高连接速度。

RealConnection

RealConnection 实现了 Connection 接口,其中使用 Socket 建立 HTTP/HTTPS 连接,并且获取 I/O 流,同一个 Connection 可能会承载多个 HTTP 的请求与响应
其实可以大概的理解为是对 Socket 、I/O 流以及一些协议的封装,这个里面涉及到的计算机网络相关的知识较多,例如 TLS 握手,HTTPS 验证等等。

RealConnectionPool

这是用来存储 RealConnection 的池子,内部使用一个双端队列来进行存储。
在 OkHttp 中,一个连接(RealConnection)用完后不会立马被关闭并释放掉,而且是会存储到连接池(RealConnectionPool)中。
除了缓存连接外,缓存池还负责定期清理过期的连接,在 RealConnection 中会维护一个用来描述该连接空闲时间的字段,每添加一个新的连接到连接池中时都会进行一次检测,遍历所有的连接,找出当前未被使用且空闲时间最长的那个连接,如果该连接空闲时长超出阈值,或者连接池已满,将会关闭该连接。
另外 RealConnection 中还维护一个 Transmitter 的弱引用列表,用来存储当前正在使用该连接的 Transmitter。当列表为空时表示该连接已经未在使用。

ExchangeCodec

ExchangeCodec 负责对 Request 编码及解码 Response,也就是写入请求及读取响应,我们的请求及响应数据都通过它来读写。
所以 Connection 负责建立连接,ExchangeCodec 负责收发数据。
ExchangeCodec 接口的实现类有两个:Http1ExchangeCodec 及 Http2ExchangeCodec,分别对应两种协议版本。

Exchange

Exchange 功能类似 ExchangeCodec,但它是对应的是单个请求,其在 ExchangeCodec 基础上担负了一些连接管理及事件分发的作用。
具体而言,Exchange 与 Request 一一对应,新建一个请求时就会创建一个 Exchange,该 Exchange 负责将这个请求发送出去并读取到响应数据,而发送与接收数据使用的是 ExchangeCodec。

Transmitter

Transmitter 是 OkHttp 网络层的桥梁,我们上面说的这些概念最终都是通过 Transmitter 来融合在一起,并对外提供功能实现。

好了,现在基本概念介绍完毕,开始看看 interceptor 吧。

RetryAndFollowUpInterceptor

这个 interceptor 顾名思义,负责失败重试以及重定向。
可能出触发重试或重定向的条件如下:

  • 401:未授权
  • 407:代理未授权
  • 503:服务未授权
  • 3xx:请求重定向
  • 408:请求超时
  • 以及一些 I/O 异常等等连接失败的情况

下面看一下其中的逻辑:

我们上面说过,因为代理及 DNS 的原因,对于同一个 url 可能会有多个 IP 地址,连接时通过 RouteSelector 选择合适的 Route 进行连接,所以这里的失败重试并不是指对同一 IP 地址的多次重试,是逐个尝试路由表中的地址
上面连接失败之后,进行重试时虽然并没有其它操作,但实际上开始连接时会自动调用下一个 Route 进行连接。

上图流程中有两处重点这里再介绍一下,分别是 followUpRequest 及 recover 方法。
recover 方法用于判断连接是否可以恢复重试,代码如下:

  private fun recover(
    e: IOException,
    transmitter: Transmitter,
    requestSendStarted: Boolean,
    userRequest: Request
  ): Boolean {
    // 用户设置的是否重连参数
    if (!client.retryOnConnectionFailure) return false
    // 单次请求或 FileNotFoundException 异常不可恢复 
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
    // 不可恢复的异常,例如协议错误、验证错误等
    if (!isRecoverable(e, requestSendStarted)) return false
    // 没有更多的路由了
    if (!transmitter.canRetry()) return false
    return true
  }

现在再来看看 followUpRequest 方法,这个代码较多就不贴了。
当走到这个方法时意味着已经连接到服务器并接收到响应了,此时需要通过响应码来判断是否需要重定向。

如果响应码为 401 或者 407 则表示请求未认证,此时重新对请求进行认证,然后返回认证后的 Request。

响应码为 3xx 表示重定向,此时重定向地址在响应 Header 的 Location 字段中,然后通过这个新的地址以及之前的 Request 构建一个新的 Request 并返回。

响应码 503 表示服务器错误,但这个是暂时的,可能马上就会恢复,所以会直接返回之前的请求。

BridgeInterceptor

BridgeInterceptor 是用户与网络之间的桥梁,负责将用户请求转换为网络请求,也就是根据 Request 信息组建网络 Header 以及设置响应数据。
实际上,BridgeInterceptor 除了设置例如 Content-Length、Connect、Host 之类的基本请求头之外,还负责设置 Cookie 以及 gzip.
BridgeInterceptor 在开始进行网络请求之前会先通过 url 判断是否有 cookie,有的话就会把这个 cookie 带上,请求结束后同样也会判断响应头是否包含 Set-Cookie 字段,包含则会将其存下来下次使用。但是存储 Cookie 的操作会委托给 CookieJar 来实现,OkHttp 默认提供了一个空的 CookieJar 对象,也就是说默认不会做出任何操作,但可以在创建 OkHttp 时指定一个自己的 CookieJar 来使用。

如果 Request 请求头中没有包含 Accept-Encoding 以及 Range 字段则会给其添加一个 "Accept-Encoding: gzip" 请求头,接收到响应数据后如果响应表示使用了 gzip 则会把响应数据交给 okio 的 GzipSource 解码。

CacheInterceptor

CacheInterceptor 负责缓存响应数据。
该方法首先会通过 Cache 对象尝试获取缓存的数据,然后再通过 CacheStrategy 获取缓存策略,通过该策略的计算结果,我们可以获取到两个可空对象:networkRequest 以及 cacheResponse
其中 networkRequest 为原始 Request 但可能为空,具体是不是空的通过 CacheStrategy 控制。
cacheResponse 是通过 Cache 获取到的 Response,同上,同样也可能为空。
然后就可以通过判断两个对象的可空性来处理缓存,逻辑如下:

  • 如果两者都为空,则表示既禁止了使用网络请求,也不可以使用缓存或者未命中缓存,直接返回 504 错误。
  • 如果只有 networkRequest 为空,表示禁止了网络请求,此时直接返回从缓存中命中的 Response。
  • 如果两者都不为空,则开始发起请求并获取响应数据。
  • 如果此时 cacheResponse 不为空,且响应码为 304,直接返回 cacheResponse,并使用响应数据更新缓存。
  • 如果 cacheResponse 为空则会将响应数据存储到 Cache 中。
  • 返回响应数据。

需要注意,上面说的 Cache 对象默认为空,如果为空则与其相关的操作都不会被执行,且 cacheResponse 一定为空。
我们可以在 OkHttpClient 中设置 Cache。

ConnectInterceptor

ConnectInterceptor 用来打开一个到服务端的连接。
其中代码很简单,会通过 Transmitter#newExchange 方法创建一个 Exchange 对象,并调用 Chain#process 方法。

newExchange 方法中会先通过 ExchangeFinder 尝试去 RealConnectionPool 中寻找已存在的连接,未找到则会重新创建一个 RealConnection 并开始连接,然后将其存入 RealConnectionPool,此时已经准备好了 RealConnection 对象,然后通过请求协议创建不同的 ExchangeCodec 并返回。具体细节上面已经说过了,这里不做详细介绍。
通过上面面步骤创建好 ExchangeCodec 之后,再根据它以及其他参数创建 Exchange 对象并返回。

ConnectInterceptor 将 newExchange 方法返回的 Exchange 对象作为参数,调用 Chain#process 方法。

CallServerInterceptor

CallServerInterceptor 负责读写数据。
这是最后一个 interceptor 了,到了这里该准备的都准备好了,通过它,将会把 Request 中的数据发送到服务端,并获取到数据写入 Response。

上图为该 interceptor 整体流程图。其中操作主要都放在了 Exchange 等对象中,这里不做过多介绍。

那么 OkHttp 的源码到这里就分析的差不多啦,其实还有很多东西都没有讲到,OkHttp 是个庞大的框架,其中涉及到的东西实在太多了,而且包括了很多计算机网络的基础知识,奈何本人才疏学浅,就只讲这么多吧。

如果觉得还不错的话,欢迎关注我的个人公众号:zhangke_blog

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 那么我今天给大家简单地讲一下Okhttp这款网络框架及其原理。它是如何请求数据,如何响应数据的 有什么优点?它的应...
    卓而不群_0137阅读 311评论 0 1
  • 版本号:3.13.1 一.基本使用 Call可以理解为Request和Response之间的桥梁,Http请求过程...
    慕涵盛华阅读 1,006评论 0 8
  • 1、说明 经翻译之后的结果如下HTTP是现代应用网络的方式。这是我们交换数据和媒体的方式。高效地执行HTTP可以使...
    Kevin_Lv阅读 367评论 0 1
  • OkHttp作为时下最受欢迎的网络请求框架之一,它有着自己的优点: 使用了众多的设计模式(如:Builder模式、...
    永恒之眼V阅读 274评论 1 1
  • 功能介绍为了让大家更清楚的知道今天要学习的效,当文章浏览到某一个位置的时候点击左上角返回,然后再次点击刚刚浏览过的...
    阳光之城alt阅读 1,650评论 0 2