主机组件标识了因特网上能够访问资源的宿主机器,比如 www.xxx.com192.168.1.66

4. 端口号

端口组件标识了服务器正在监听的网络端口,对下层使用了 TCP 协议的 HTTP 来说,默认端口为 80

5. 查询路径

服务器上资源的本地名,由斜杠( / )将其与前面的 URL 组件分隔开来,路径组件的语法与服务器的方案有关。

路径组件说明了资源位于服务器的什么地方,类似于分级的文件系统路径,比如 /goods/details

6. 查询参数

比如数据库服务是可以通过提供查询参数缩小请求资源范围的,传入页码和页大小查询列表 http://www.xxx.com/?page=1&pageNum=20

7. 片段标识符

片段(fragment)表示一部分资源的名字,该字段不会发送给服务器,是在客户端内部使用的,通过井号(#)将其与 URL 其余部分分割开来。

1.1.2 首部字段 Headers

Header 用于存放 HTTP 首部,Headers 中只有一个 namesAndValues 字段,类型为 Array ,比如 addHeader(a, 1) 对应的 namesAndValues 为 [a, 1]

HTTP 协议的请求和响应报文中必定包含 HTTP 首部,首部内容为客户端和服务器分别处理请求和响应提供所需要的信息,HTTP 报文由方法、URI、HTTP 版本、HTTP 首部字段等部分构成。

1.1.3 请求体 RequestBody

RequestBody 是一个抽象类,有下面 3 个方法。

  1. 内容类型 contentType()

比如 application/x-www-form-urlencoded

  1. 内容长度 contentLength()

  2. 写入内容 writeTo()

把请求的内容写入到 okio 提供的 Sink 中;

RequestBody 中还有 4 个用于创建 RequestBody 的扩展方法 xxx.toRequestBody() ,比如 Map.toString().toRequestBody()。

1.1.4 标签

我们可以用 tag() 方法给请求加上标签,然后在拦截器中根据不同的标签栏做不同的操作。

val request = Request.Builder()
.url(…)
.tag(“666”)
.build()

在 Retrofit 中用的则是 @Tag 注解,比如下面这样。

@POST(“app/login”)
suspend fun login(
@Query(“account”) phone: String,
@Query(“password”) password: String,
@Tag tag: String
) : BaseResponse

然后在自定义拦截器中,就能根据 tag 的类型来获取标签。

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val tag = request.tag(String::class.java)
Log.e(“intercept”, “tag: ${tag}”)
return chain.proceed(request)
}

2. OkHttp 请求分发机制

2.1 请求操作 Call

在我们创建请求 Request 后,要用 OkHttpClientnewCall() 方法创建一个 RealCall 对象,然后调用 execute() 发起同步请求或调用 enqueue() 发起异步请求。

RealCall 实现了 Call 接口,也是这个接口唯一的实现类,按注释来说,RealCall 是一个 OkHttp 应用与网络层之间的桥梁,该类暴露了高级应用层的原语(primitives):连接、请求、响应与流,你也可以把 RealCall 理解为同步请求操作,而 RealCall 的内部类 AsyncCall 则是异步请求操作

下面但是 RealCall 中比较中要的两个方法的实现:execute()enqueue()

1. 发起同步请求 execute()

当我们调用 RealCall 的 execute() 方法发起同步请求时,如果该请求已执行,那么会抛出非法状态异常,所以发起同步请求时要注意捕获异常

如果请求没有被执行的话,execute() 方法则会调用 AsyncTimeout 的 enter() 方法让 AsyncTimeout 做请求超时判断,AsyncTimeout 中有一个继承了 Thread 的内部类 WatchDog,而 AsyncTimeout 会用 Object.wait()/notify() 阻塞和唤醒 Watchdog 线程。

当请求超时时,AsyncTimeout 会调用 RealCall 中实现的 timeOut() 方法关闭连接。

RealCall 的 execute() 方法调用完 enter() 方法后,会调用 Dispatcher 的 executed() 把请求加入同步请求队列,然后调用 getResponseWithInterceptorChain() 方法获取响应,获取到响应后就会让 Dispatcher 把请求从同步请求队列中移除。

2. 发起异步请求 enqueue()

RealCall 的 execute() 方法会创建一个异步请求操作 AsyncCall,并把它交给 Dispatcher 处理。

AsyncCall 实现了 Runnable 接口,Dispatcher 接收到 AsyncCall 后,会把 AsyncCall 添加到待执行异步请求队列 readyAsyncCalls 中,然后调用自己的 promoteAndExecute() 方法。

把 AsyncCall 加入到异步请求队列后,Dispatcher 会看情况决定什么时候执行该异步请求,要执行的时候就会把请求任务提交到线程池 ExecutorService 中。

和同步请求一样,在 AsyncCall 的 run() 方法中做的第一件事情就是让 AsyncTimeout 进入超时判断逻辑,然后用拦截器链获取响应。

当请求的过程中没有遇到异常时,AsyncCall 的 run() 方法就会调用我们设定的 Callback 的 onResposne() 回调,如果遇到了异常,则会调用 onFailure() 方法。

不论异步请求是成功还是失败,RealCall 最后都会调用 Dispatcher 的 finished() 方法把请求从已运行异步请求队列 runningAsyncCalls 中移除。

2.2 请求分发器 Dispatcher

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPFS1yfl-1649672141943)(https://s3.jpg.cm/2021/06/09/ILfs5O.png)]

请求分发器 Dispatcher 做的事情并不多,只是维护了三个队列和一个线程池,这三个队列分别是待执行异步请求队列运行中异步请求队列以及运行中同步请求队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OIClUhGr-1649672141944)(https://s3.jpg.cm/2021/06/09/ILf9VD.png)]

Dispatcher 的 enqueue() 方法首先会把 AsyncCall 加入到待执行请求队列,然后从待运行和已运行请求队列中找出与当前请求的主机地址相同的其他请求,找到的话就找到的请求的重用 AsyncCall 的 callsPerHost 字段,callsPerHost 表示当前请求的主机地址的已执行请求数量,每执行一个相同主机地址的请求时, callsPerHost 的值就会加 1 ,如果我们的应用中经常会发起多个请求,并且不会请求多个不同的主机地址的话,我们就可以修改 Dispatcher 中的 maxRequestsPerHost 的值,maxRequetsPerHost 表示单个主机地址在某一个时刻的并发请求的最大值,修改方式如下。

okHttpClient.dispatcher.maxRequestsPerHost = 10

maxRequestsPerHost 默认为 5 ,如果对应主机地址的请求数量没有超过最大值的话,Dispatcher 就会遍历待运行异步请求队列,在遍历时,Dispatcher 会判断已运行的异步请求数量是否超出了允许的并发请求的最大值 maxRequests ,这个值默认为 64 ,也是可以被修改的,当异步请求数量不超过最大值,并且对应主机地址的请求数量不超过最大值时,就会把待运行请求提交到线程池中执行

当同步请求或异步请求执行时,RealCall 就会调用getResponseWithInterceptorChain() 方法发起请求,在 getResponseWithInterceptorChain() 方法中,首先会创建一个 interceptors 列表,然后按下面的顺序添加拦截器。

  1. 自定义拦截器
  2. 重试拦截器(RetryAndFollowUpInterceptor)
  3. 网络请求构建拦截器(BridgeInterceptor)
  4. 缓存拦截器(CacheInterceptor)
  5. 连接拦截器(ConnectInterceptor)
  6. 自定义网络拦截器
  7. 数据传输拦截器(CallServerInterceptor)

添加完这些拦截器后,就会用 interceptors 创建一个拦截器链 RealInterceptorChain() ,然后调用拦截器链的 proceed() 方法,最后返回响应,其中自定义网络拦截器和自定义拦截器的区别,就是自定义网络拦截器在不会处理 WebSocket 连接。

3. OkHttp 重试与重定向机制

3.1 重试机制

重试与重定向拦截器负责在请求失败时重试和重定向,在重试拦截器的 intercept() 方法中的代码是放在 while 中执行的,只有当重试的条件不成立时,请求才会被中断,而且这个拦截器没有设定重试次数的上限,最大重定向次数是写死的 20 次,如果有特殊需求的话,则要自定义一个重试拦截器和重定向拦截器。

在重试与重定向拦截器的 intercept() 方法中,当请求在后续的拦截器中处理时遇到路线异常(RouteException)或 IO 异常(IOException 时)才会调用 recover() 方法判断是否要重试,不重试则抛出异常。

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
try {
response = realChain.proceed(request)
// …
} catch (e: RouteException) {
// 通过某个路线连接后失败,请求不会被发送
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
throw e.firstConnectException.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e.firstConnectException
}
// …
continue
} catch (e: IOException) {
// 与服务器通信失败,请求可能已经发送
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
// …
continue
}
}

private fun recover(): Boolean {
// 应用层禁止重试
if (!client.retryOnConnectionFailure) return false

// 不能再次发送请求体
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

// 致命异常
if (!isRecoverable(e, requestSendStarted)) return false

// 没有更多路线可重试
if (!call.retryAfterFailure()) return false

// 使用新的连接和同一个路线选择器进行重试
return true
}

}

当下面 4 个条件之一满足时,则不进行重试。

  • OkHttpClient 的 retryOnConnectionFailure 的值为 false

  • 不能再次发送请求体

满足下面两个条件时表示不能再次发送请求体。

  • 请求执行过程中遇到 IO 异常(不包括 Http2Connection 抛出的 ConnectionShutdownException)

  • requestIsOneShot() 返回 true,这个方法默认为 false ,除非我们自己重写了这个方法)

  • 致命异常

  • 协议异常 ProtocalException

  • Socket 超时异常 SocketTimeoutException

  • 证书验证异常 CertificateExeption

  • SSL 对端验证异常 SSLPeerUnverifiedException

  • 没有更多路线可重试

只有下面两种情况发生时,才有可能有更多路线可重试

  • 给 OkHttpClient 设置了代理

  • DNS 服务器返回多个 IP 地址

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
while (true) {
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
// …
try {
response = realChain.proceed(request);
} catch (e: IOException) {
// …
} catch (e: IOException) {

}
}
}

}

RealCall 的 enterNetworkInterceptorExchange() 方法用于初始化一个 ExchangeFinder,ExchangeFinder 的作用是查找可重用的连接,关于 ExchangeFinder 的实现后面会讲。

初始化 ExchangeFinder 后会把 Request 给其他拦截器处理,如果在这个过程中遇到了 IO 异常或路线异常,则会调用 rocover() 方法判断是否恢复请求,不恢复的话则抛出异常。

3.2 重定向机制

如果其他拦截器处理当前请求时没有抛出异常的话,那么 RetryAndFollowUpInterceptor 的 intercept() 方法就会判断上一个响应(priorResponse)是否为空,如果不为空的话,则用上一个响应的信息创建一个新的响应(Response),创建完新响应后,就会调用 followUpRequest() 方法 获取重定向请求。

followUpRequest() 方法会根据不同的状态码构建重定向请求,当状态码为 407 ,并且协议为 HTTP ,则返回一个包含认证挑战的请求,而获取这个请求用的是 Authenticator

Authenticator 有一个 authenticate() 方法,默认的是一个空实现 NONE,如果我们想替换的话,可以在创建 OkHttpClient 的时候调用 authenticator() 方法替换默认的空实现。

除了 NONE 以外,Authenticator 中还提供了另一个实现 JavaNetAutheitcator,对应的静态变量为 Authenticator.JAVA_NET_AUTHENTICATOR

在 JavaNetAuthenticator 的 authenticate() 方法中,会获取响应中的 Challenge(质询)列表,Challenge 列表就是对 WWW-AuthenticateProxy-Authenticate 响应头解析后生成的。

3.3 基本认证

HTTP 通过一组可定制的控制首部,为不同的认证协议提供了一个可扩展框架,下面列出的首部格式和内容会随认证协议的不同而发生变化,认证协议也是在 HTTP 认证首部中指定的。

基本(BASIC)认证是 HTTP 定义的官方认证协议之一,基本认证相关的首部如下。

  • WWW-Authenticate

服务器上可以会分为不同的区域,每个区域都有自己的密码,所以服务器会在 WWW-Authenticate 首部对保护区域进行描述。

  • Authorization

客户端收到 401 状态码后,重新发出请求,这次会附加一个 Authorization 首部,用于说明认证算法以及用户名和密码;

  • Authentication-Info

如果授权书是正确的,服务器就会返回指定资源。有的授权算法会在可选的 Authentication-Info 首部返回一些与授权会话相关的附加信息;

3.4 处理 3XX 重定向状态码

当响应的状态码为 300、301、302、303、307、308 时, followUpRequest() 方法就会调用 buildRedirectRequest() 构建重定向请求,3xx 重定向状态码要么告诉客户端使用替代位置访问客户端感兴趣的资源,要么提供一个替代的响应而不是资源的内容。

当资源被移动后,服务器可发送一个重定向状态码和一个可选的 Location 首部告诉客户端资源已被移走,以及现在哪里可以找到该资源,这样客户端就可以在不打扰使用者的情况在新的位置获取资源了。

4.OkHttp 首部构建机制

重试与重定向拦截器只有在请求的过程中遇到异常或需要重定向的时候才有活干,在它收到请求后会把请求直接通过拦截器链交给下一个拦截器,也就是 BridgeInterceptor 处理。

之所以把 BridgeInterceptor 叫首部构建拦截器,是因为我们给 Request 设置的信息缺少了部分首部信息,这时就要 BridgeInterceptor 把缺失的首部放到 Request 中,下面是 BridgeInterceptor 为请求添加的首部字段。

  • Content-Type:实体主体的媒体类型
  • Content-Length:实体主体的大小(字节)
  • Transfer-Encoding:指定报文主体的传输方式
  • Host:请求资源所在的服务器
  • Connection:逐跳首部、连接的管理
  • Accept-Encoding:优先的内容编码
  • Cookie:本地缓存
  • User-Agent:HTTP 客户端程序的信息

下面我们来看下这些首部的作用。

1. Content-Type:实体主体的媒体类型

Content-Type: text/html; charset-UTF-8

首部字段 Content-Type 说明了实体主体内对象的媒体类型,字段值用 type/subtype 形式赋值,比如 image/jpeg 。

2. Content-Length:实体主体的大小

首部字段 Content-Length 表明了实体主体部分的大小(单位是字节),对实体主体进行内容编码传输时,不能再使用 Content-Length 首部字段。

3. Transfer-Encoding:指定报文主体的传输方式

Transfer-Encoding: chunked

首部字段 Transfer-Encoding 规定了传输报文主体时采用的编码方式,HTTP/1.1 的传输编码方式仅对分块传输编码有效。

4. Host:请求资源所在的服务器

Host: www.xxx.com

首部字段 Host 告诉服务器请求的资源所处的互联网主机名和端口号,Host 首部字段在 HTTP/1.1 规范中是一个必须被包含在请求内的首部字段。

5. Connection

HTTP 允许在客户端和最终的源服务器之间存在一串 HTTP 的中间实体(代理、高速缓存等),可以从客户端开始,逐跳地将 HTTP 报文经过这些中间设备转发到源服务器上。

在某些情况下,两个相邻的 HTTP 应用程序会为它们共享的连接应用一组选项,而 Connection 首部字段中有一个由逗号分隔的链接标签列表,这些标签为此连接指定了一些不会被传播到其他连接中的选项,比如用 Connection:close 说明发送完下一条报文后必须关闭的连接。

Connection 首部可以承载 3 种不同类型的标签。

  • HTTP 首部字段名,列出了只与此连接有关的首部;
  • 任意标签值,用于描述此连接的非标准选项;
  • close,说明操作完成后要关闭这条持久连接;

在 BridgeInterceptor 中,当我们没有设置 Connection 首部时,BridgeInterceptor 会传一个值为 Keep-Alive 的 Connection 首部用于开启持久连接,关于持久连接后面会讲到。

6. Cookie

两个与 Cookie 有关的首部字段。

  • 响应首部字段 Set-Cookie:开始状态管理所使用的 Cookie 信息
  • 请求首部字段 Cookie:服务器接收到的 Cookie 信息

Cookie: status=enable

首部字段 Cookie 会告诉服务器,当客户端想获得 HTTP 状态管理支持时,就会在请求中包含从服务器接收到的 Cookie,接收到多个 Cookie 时,同样可以以多个 Cookie 形式发送。

在 BridgeInterceptor 中,与 Cookie 相关的实现为 CookieJar 接口,默认是一个空实现类,如果我们想传 Cookie 给服务器端的话,可以在创建 OkHttpClient 时调用 cookieJar() 传入我们自己的实现。

7. User-Agent:HTTP 客户端程序的信息

首部字段 User-Agent 会将创建请求的浏览器和用户代理名称等信息传达给服务器,由网络爬虫发起请求时,有可能会在字段内添加爬虫作者的电子邮件地址,如果请求经过代理,中间也有可能被添加上代理服务器的名称。

在 BridgeInterceptor 中,当我们没有设置 User-Agent 时,默认的 UserAgent 为 okhttp:版本号,也就是User-Agent: okhttp:4.9.0

5. OkHttp 缓存机制

当 BridgeInterceptor 把要传给服务器端的首部放到 Request 中后,就会把请求交给缓存拦截器 CacheInterceptor 处理,为了更好地了解 CacheInterceptor 的实现,我们先来看下 HTTP 缓存机制以及相关的缓存控制首部。

5.1 HTTP 缓存机制

Web 缓存是可以自动保存常见文档副本的 HTTP 设备,当 Web 请求抵达缓存时,如果本地有已缓存的副本,就可以从本地存储设备中读取文档,不需要去源服务器提取,使用缓存有下面几个好处。

  • 减少冗余的数据传输,节省用户的流量;
  • 缓解网络瓶颈,不需要更多的贷款就能更快地加载页面;
  • 降低对源服务器的要求,服务器可以更快地响应,避免过载;
  • 降低了距离时延,因为从较远的地方加载页面会慢一些;

5.1.1 冗余的数据传输

有很多客户端访问一个流行的原始服务器页面时,服务器会多次传输同一份文档,每次传送给一个客户端,一些相同的字节会在网络中一遍遍地传输,冗余的数据传输会对导致的网络带宽费用增加、降低传输速度,加重 Web 服务器的负载。

如果有缓存,就可以保留第一条服务器响应的副本,后续请求就可以由缓存的副本来应对了,这样可以降低流量的消耗。

1. 带宽瓶颈

缓存还可以缓解网络的瓶颈问题,很多网路欧威本地服务器客户端提供的带宽比为远程服务器提供的带宽要宽,客户端会以路径上最慢的网速访问服务器,如果客户端从一个快速局域网的缓存中得到了一份副本,那么缓存就可以提高性能,尤其是传输大文件时。

2. 瞬间拥塞

缓存在破坏瞬间拥塞(Flash Crowds)时显得非常重要,突发事件(比如爆炸性新闻)会让很多人同时去访问同一个资源,这时机会出现拥塞,由此造成的流量峰值可能会导致 Web 服务器产生灾难性的崩溃。

3. 距离时延

即使带宽不是问题,距离也可能成为问题,每台网络路由器都会增加因特网流量的时延,即使客户端和服务器之间没有太多路由器,光速自身也会造成显著的时延。

5.1.2 缓存的处理步骤

对一条 HTTP GET 报文的基本缓存处理包括下面 7 个步骤。

  1. 接收:缓存从网络中读取抵达的请求报文;
  2. 解析:缓存对报文进行解析,提取出 URL 和各种首部;
  3. 查询:缓存查看是否有本地副本可用,如果没有就获取一份副本并将其保存在本地;
  4. 新鲜度监测:缓存查看已缓存副本是否足够新鲜,如果不是就询问服务器是否有新的资源;
  5. 创建响应:缓存会用新的首部和已缓存的主题来构建一条响应报文;
  6. 发送:缓存通过网络把响应发挥给客户端;
  7. 日志:缓存可选地创建一个日志文件条目描述这个事务;

CacheInterceptor 大致上也是按这个流程来处理缓存的,只是在这个而基础上进行了一些细化。

5.2 缓存控制首部 CacheControl

由于通用请求首部 Cache-Control 在 OkHttp 的缓存机制中发挥着主要作用,所以下面先来看下 CacheControl 中各个字段对应的指令的作用。

通过指定通用首部字段 Cache-Control 的指令,就能操作缓存的工作机制,该指令的参数是可选的,多个指令之间通过“,”分隔。

Cache-Control: private, max-age=0, no-cache

5.3 获取缓存

RealCall 在创建 CacheInterceptor 时,会把 OkHttpClient 中的 cache 字段赋值给 CacheInterceptor ,默认是空,如果我们想使用缓存的话,要在创建 OkHttpClient 的使用使用 cache() 方法设置缓存,比如下面这样。

/**

  • 网络缓存数据的最大值(字节)
    */
    const val MAX_SIZE_NETWORK_CACHE = 50 * 1024 * 1024L

private fun initOkHttpClient() {
val networkCacheDirectory = File(cacheDir?.absolutePath + “networkCache”)

if (!networkCacheDirectory.exists()) {
networkCacheDirectory.mkdir()
}

val cache = Cache(networkCacheDirectory, MAX_SIZE_NETWORK_CACHE)

okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
}

这里要注意的是,CacheInterceptor 只会缓存 GETHEAD 等获取资源的方法的请求,而对于 POSTPUT 等修改资源的请求和响应数据是不会进行缓存的。

在 CacheInterceptor 的 intercept() 方法中,首先会通过 Cache.get() 获取候选缓存,而在 Cache.get() 方法中,首先会根据请求地址获取 key ,缓存快照的 key 就是 URL 经过 md5 处理后的值,而缓存快照 Snapshot 就是 Cache 中的磁盘缓存 DiskLruCache 缓存的值,并且快照中有对应缓存文件的输入流。

当 get() 方法获取到快照后,就会用快照的输入流创建 Entry ,在 Entry 的构造方法中,会从输入流读取缓存的请求和响应的相关信息,读取完后就会完毕输入流。

创建完 Entry 后,Cache.get() 就会判断缓存中的请求地址和请求方法与当前请求是否匹配,匹配的话则返回响应,不匹配的话则关闭响应体并返回 null ,这里说的关闭响应体指的是关闭要用来写入响应体的文件输入流。

5.4 缓存策略 CacheStrategy

获取完候选缓存响应后,CacheInterceptor 就会用缓存策略工厂的 compute() 方法生产一个缓存策略 CacheStrategy ,CacheStrategy 中比较重要的方法就是用来判断是否对当前请求和响应进行缓存的 isCacheable() 。

1. 可缓存响应的状态码

在 CacheStrategy 的 isCacheable() 方法中,首先会判断响应的状态码是否为“可缓存的状态码”。

为了简化 isCacheable() 的活动图,我把下面的状态码称为“可缓存的状态码”;

  • 200 OK
  • 203 Not Authoritative Information
  • 204 No Content
  • 300 Multiple Choices
  • 301 Moved Permanently
  • 308 Permanent Redirect
  • 404 Not Found
  • 405 Method Not Allowed
  • 410 Gone
  • 414 Request-URI Too Large
  • 501 Not Implemented
2. 临时重定向状态码的缓存判断

当响应的状态码为 302 或 307 时,isCacheable() 方法就会根据响应的 Expires 首部和 Cache-Control 首部判断是否返回 false(不缓存)。

Expires 首部的作用是服务器端可以指定一个绝对的日期,如果已经过了这个日期,就说明文档不“新鲜”了。

5.5 获取响应

在 CacheInterceptor 调用 compute() 方法创建 CacheStrategy 时,如果 CacheControl 中有 onlyIfCached(不重新加载响应)指令,那么 CacheStrategy 的 cacheResponse 字段也为空。

当 CacheControl 中有 onlyIfCached 指令时,表明不再用其他拦截器获取响应,这时 CacheInterceptor 就会直接返回一个内容为空的响应。

当请求还是新鲜的(存在时间 age 小于新鲜时间 fresh ),那么 CacheStrategy 的 networkRequest 字段就为空,这时 CacheInterceptor 就会返回缓存中的响应。

当请求已经不新鲜时,CacheInterceptor 就会通过 ConnectInterceptor 和 CallServerInterceptor 获取响应。

5.6 保存响应

在获取到响应后,CacheInterceptor 会判断缓存响应的是否为空,如果不为空,并且状态码为 304(未修改)的话,则用新的响应替换 LruCache 中的缓存。

如果缓存响应为空,就把响应通过 Cache.put() 方法保存到磁盘中,保存后,如果请求方法为 PATCH、PUT、DELETE 会 MOVE 等修改资源的方法,那就把响应从缓存中删除。

6. OkHttp 连接建立机制

看完了缓存处理机制后,下面我们来看下 OkHttp 中负责建立连接的 ConnectInterceptor。

ConnectInterceptor 的 intercept() 方法没做什么事情,主要就是调用了 RealCall 的 initExchange() 方法建立连接。

在 RealCall 的 initExchange() 方法中,会用 ExchangeFinder.find() 查找可重用的连接或创建新连接,ExchangeFinder.find() 方法会返回一个 ExchangeCodec。

ExchangeCodec 是数据编译码器,负责编码 HTTP 请求进行以及解码 HTTP 响应,Codec 为 Coder-Decoder 的缩写。

RealCall 获取到 ExchangeCodec 后,就会用 ExchangeCodec 创建一个数据交换器 Exchange ,而下一个拦截器 CallServerInterceptor 就会用 Exchange 来写入请求报文和获取响应报文。

ExchangeFinder 的 find() 方法会辗转调用到它最核心的 findConnection() 方法,在看 findConnection() 方法的实现前,我们先来了解一些 HTTP 连接相关的知识。

6.1 HTTP 连接管理

HTTP 规范对 HTTP 报文解释得很清楚,但对 HTTP 连接介绍的并不多,HTTP 连接是 HTTP 报文传输的文件通道,为了更好地理解网络编程中可能遇到的问题,HTTP 应用程序的开发者需要理解 HTTP 连接的来龙去脉以及如何使用这些连接。

世界上几乎所有的 HTTP 通信都是由 TCP/IP 承载的,TCP/IP 是全球计算机及网络设备都在使用的一种常用的分组交换网络分层鞋以及。

客户端应用程序可以打开一条 TCP/IP 连接,连接到可能运行在世界任何地方的服务器应用程序,一旦连接建立起来了,在客户端与服务器的计算机之间交换的报文就永远不会丢失、受损或失序。

1. TCP/IP 通信传输流

用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通信,发送端从应用层往下走,接收端从链路层往上走。

以 HTTP 为例,首先作为发送端的客户端在应用层(HTTP 协议)发出一个想看某个 Web 页面的 HTTP 请求。

接着发送端在传输层把从应用层收到的 HTTP 报文进行分割,并在各个报文上打上标记序号及端口号转发给网络层,然后接收端的服务器在链路层接收到数据,按顺序往上层发送,一直到应用层。

也就是发送端在层与层之间传输数据时,每经过一层就会被打上该层所属的首部信息,接收端在层与层传输数据时,每经过一层就会把对应的首部消去,这种把数据信息包装起来的做法称为封装(encapsulate)。

2. TCP 套接字编程

操作系统提供了一些操作 TCP 连接的工具,下面是 Socket API 提供的一些主要接口,Socket API 最初是为 Unix 操作系统开发的,但现在几乎所有的操作系统和语言中都有其变体存在。

  • socket():创建一个新的、未命名、未关联的套接字;
  • bind():向 Socket 赋一个本地端口号和接口;
  • listen():标识一个本地 Socket,使其可以合法地接收连接;
  • accept():等待某人建立一条到本地端口的连接;
  • connect():创建一条连接本地 Socket 与远程主机及端口的连接;
  • read():尝试从套接字向缓冲区读取 n 个字符;
  • write():尝试从缓冲区向套接字写入 n 个字节;
  • close():完全关闭 TCP 连接;
  • shutdown():只关闭 TCP 连接的输入或输出端;

Socket API 允许用户创建 TCP 的端点和数据结构,把这些端点与远程服务器的 TCP 端点进行连接,并对数据流进行读写。

6.2 释放连接

看完了 HTTP 连接的相关知识,下面我们来看下 ExchangeFinder 的 findConnection() 方法的实现。

findConnection() 方法大致做了 3 件事,首先是释放 RealCall 已有的连接,然后是尝试从连接池中获取已有的连接以进行复用,如果没有获取到连接时,则创建一个新连接并返回给 CallServerInterceptor 使用。

在 ExchangeFinder 的 findConnection() 方法中,首先会看下是否要释放当前 RealCall 的连接。

ExchangeFInder 会判断 RealCall 的 connection 字段是否为空,如果不为空,表示该请求已经被调用过并且成功建立了连接。

这时 ExchangeFinder 就会判断 RealCall 的 connection 的 noNewExchanges 是否为 true,这个值表示不能创建新的数据交换器,默认为 false。

当请求或响应有 Connection 首部,并且 Connection 首部的值为 close 时,那么 Connection 的 noNewExchanges 的值就会被改为 true ,因为 Connection:close 表示不重用连接,如果你忘了 Connection 首部的作用,可以回到第 4 大节首部拦截器看一下。

当连接的 noNewExchanges 的值为 true 时,或当前请求地址的主机和端口号和与有连接中的主机和端口号不相同时,ExchangeFinder 就会调用 RealCall 的 releaseConnectionNoevents() 方法尝试释放连接,如果如果连接未释放,则返回该连接,否则关闭连接对应的 Socket。

RealCall 的 connection 的类型为 RealConnection,RealConnection 中维护了一个 Call 列表,每当有一个 RealCall 复用该连接时,RealConnection 就会把它添加到这个列表中。

而释放连接的操作,其实就是看下 RealConnection 的 Call 列表中有没有当前 RealCall ,有的话就把当前 RealCall 从列表中移除,这时就表示连接已释放,如果连接的 Call 列表中没有当前 Call 的话,则返回当前 Call 的连接给 CallServerInterceptor 用。

6.3 从连接池获取连接

当 RealCall 的连接释放后 ExchangeFinder 就会尝试从连接池 RealConnectionPool 获取连接,RealConnectionPool 中比较重要的两个成员是 keepAliveDuration 和 connection。

keepAliveDuration 是持久连接时间,默认为 5 分钟,也就是一条连接默认最多只能存活 5 分钟,而 connections 是连接队列,类型为 ConcurrentLinkedQueue 。

每次建立一条连接时,连接池就会启动一个清理连接任务,清理任务会交给 TaskRunner 运行,在 DiskLruCache 中,也会用 TaskRunner 来清理缓存。

当第一次从连接池获取不到连接时,ExchangeFinder 会尝试用路线选择器 RouteSelector 来选出其他可用路线,然后把这些路线(routes)传给连接池,再次尝试获取连接,获取到则返回连接。

6.4 创建新连接

当两次从尝试从连接池连接都获取不到时,ExchangeFinder 就会创建一个新的连接 RealConnection,然后调用它的 connect() 方法,并返回该连接。

6.5 连接 Socket

在 RealConnection 的 connect() 方法中,RealConnection 的 connect() 方法首先会判断当前连接是否已连接,也就是 connect() 方法被调用过没有,如果被调用过的话,则抛出非法状态异常。

如果没有连接过的话,则判断请求用的是不是 HTTPS 方案,是的话则连接隧道,不是的话则调用 connectSocket() 方法连接 Socket。

关于连接隧道在后面讲 HTTPS 的时候会讲到,下面先来看下 connectSocket() 方法的实现。

在 RealConnection 的 connectSocket() 方法中,首先会判断代理方式,如果代理方式为无代理(DIRECT)或 HTTP 代理,则使用 Socket 工厂创建 Socket,否则使用 Socket(proxy) 创建 Socket。

创建完 Socket 后,RealConnection 就会调用 Platform 的 connectSocket() 方法连接 Socket ,再初始化用来与服务器交换数据的 Source 和 Sink。

Platform 的 connectSocket() 方法调用了 Socket 的 connect() 方法,后面就是 Socket API 的活了。

6.6 建立协议

创建完 Socket 后,RealConnection 的 connect() 方法就会调用 establishProtocol() 方法建立协议。

在 establishProtocol() 方法中会判断,如果使用的方案是 HTTP 的话,则判断是否基于先验启动 HTTP/2(rfc_7540_34),先验指的是预先知道,也就是客户端知道服务器端支持 HTTP/2 ,不需要不需要升级请求,如果不是基于先验启动 HTTP/2 的话,则把协议设为 HTTP/1.1 。

OkHttpClient 默认的协议有 HTTP/1.1 和 HTTP/2 ,如果我们已经知道服务器端支持明文 HTTP/2 ,我们就可以把协议改成下面这样。

val client = OkHttpClient.Builder()
.protocols(mutableListOf(Protocol.H2_PRIOR_KNOWLEDGE))
.build()

如果请求使用的方案为 HTTP 的话,establishProtocol() 方法则会调用 connectTls() 方法连接 TLS ,如果使用的 HTTP 版本为 HTTP/2.0 的话,则开始 HTTP/2.0 请求。

7. HTTPS 连接建立机制

在看 connectTls() 方法的实现前,我们先来看一些 HTTPS 相关的基础知识,如果你已经了解的话,可以跳过这一段直接从 8.2 小节看起。

7.1 HTTPS 基础知识

在 HTTP 模式下,搜索或访问请求以明文信息传输,经过代理服务器、路由器、WiFi 热点、服务运营商等中间人通路,形成了“中间人”获取数据、篡改数据的可能。

但是从 HTTP 升级到 HTTPS,并不是让 Web 服务器支持 HTTPS 协议这么简单,还要考虑 CDN、负载均衡、反向代理等服务器、考虑在哪种设备上部署证书与私钥,涉及网络架构和应用架构的变化。

7.1.1 中间人攻击

接下来我们来看下什么是中间人攻击,中间人攻击分为被动攻击主动攻击两种。

中间人就是在客户端和服务器通信之间有个无形的黑手,而对于客户端和服务器来说,根本没有意识到中间人的存在,也没有办法进行防御。

1. 被动攻击

是对着手机设备越来越流行,而移动流量的资费又很贵,很多用户会选择使用 WiFi 联网,尤其是在户外,用户想方设法使用免费的 WiFI 。

很多攻击者会提供一些免费的 WiFi,一旦连接上恶意的 WiFI 网络,用户将毫无隐私。提供 WiFI 网络的攻击者可以截获所有的 HTTP 流量,而 HTTP 流量是明文的,攻击者可以知道用户的密码、银行卡信息以及浏览习惯,不用进行任何分析就能获取用户隐私,而用户并不知道自己的信息已经泄露,这种攻击方式也叫被动攻击

2. 主动攻击

很多用户浏览某个网页时,经常会发现页面上弹出一个广告,而这个广告和访问的网页毫无关系,这种攻击主要是 ISP(互联网服务提供商,Internet Service Provider)发送的攻击,用户根本无法防护。

用户访问网站时肯定经过 ISP ,ISP 为了获取广告费等目的,在响应中插入一段 HTML 代码,就导致了该攻击的产生,这种攻击称为主动攻击,也就是攻击者知道攻击的存在。

更严重的是 ISP 或攻击者在页面插入一些恶意的 JavaScript 脚本,脚本一旦在客户端运行,可能会产生更恶劣的后果,比如 XSS 攻击(跨站脚本攻击,Cross Site Scripting)。

7.1.2 握手层与加密层

HTTPS(TLS/SSL协议)设计得很巧妙,主要由握手层和加密层两层组成,握手层在加密层的上层,提供加密所需要的信息(密钥块)。

对于一个 HTTPS 请求来说,HTTP 消息在没有完成握手前,是不会传递给加密层的,一旦握手层处理完毕,最终应用层所有的 HTTP 消息都会交给密钥层进行加密。

1. 握手层

接上恶意的 WiFI 网络,用户将毫无隐私。提供 WiFI 网络的攻击者可以截获所有的 HTTP 流量,而 HTTP 流量是明文的,攻击者可以知道用户的密码、银行卡信息以及浏览习惯,不用进行任何分析就能获取用户隐私,而用户并不知道自己的信息已经泄露,这种攻击方式也叫被动攻击

2. 主动攻击

很多用户浏览某个网页时,经常会发现页面上弹出一个广告,而这个广告和访问的网页毫无关系,这种攻击主要是 ISP(互联网服务提供商,Internet Service Provider)发送的攻击,用户根本无法防护。

用户访问网站时肯定经过 ISP ,ISP 为了获取广告费等目的,在响应中插入一段 HTML 代码,就导致了该攻击的产生,这种攻击称为主动攻击,也就是攻击者知道攻击的存在。

更严重的是 ISP 或攻击者在页面插入一些恶意的 JavaScript 脚本,脚本一旦在客户端运行,可能会产生更恶劣的后果,比如 XSS 攻击(跨站脚本攻击,Cross Site Scripting)。

7.1.2 握手层与加密层

[外链图片转存中…(img-yKr334nS-1649672141953)]

HTTPS(TLS/SSL协议)设计得很巧妙,主要由握手层和加密层两层组成,握手层在加密层的上层,提供加密所需要的信息(密钥块)。

对于一个 HTTPS 请求来说,HTTP 消息在没有完成握手前,是不会传递给加密层的,一旦握手层处理完毕,最终应用层所有的 HTTP 消息都会交给密钥层进行加密。

1. 握手层

探索 OkHttp 原理相关推荐

  1. Android 进阶之探索 OkHttp 原理

    前言 1. OkHttp 请求处理流程概述 当我们发起同步请求时,请求会被 Dispatcher 放到同步请求队列中,然后直接执行请求. 当我们发起异步请求时,Dispatcher 会把请求放到异步请 ...

  2. Android:安卓学习笔记之OkHttp原理的简单理解和使用

    Android OkHttp使用原理的简单理解和使用 OkHttp 0.前言 1.请求与响应流程 1.1 请求的封装 1.2 请求的发送 1.3 请求的调度 1.4 请求的处理 2.拦截器 2.1 R ...

  3. OkHttp原理流程源码分析

    OkHttp已经是非常流行的android客户端的网络请求框架,我其实在项目中使用也已经好几年了,之前一直把重心放在如何快速的搞定业务上.迭代的效率上,这一点来讲,对于一个公司优秀员工是没有毛病的.但 ...

  4. 征服面试官:OkHttp 原理篇 掌握这篇面试题汇总,吊打面试官!

    前言 如今面试中高级开发工程师岗位,OKhttp 原理是必问环节,只会使用已经无法满足 Android 开发市场的需求,优秀的第三方框架源码剖析不仅能深度理解框架,也能对自己学习带来很大的帮助. 本篇 ...

  5. Android面试---OkHttp原理

    okHttp原理主要是通过5个拦截器和3个双端队列(2个异步队列,1个同步队列)工作.内部实现通过一个责任链模式完成,将网络请求的各个阶段封装到各个链条中,从而实现各层的解耦. OkHttp底层是通过 ...

  6. OkHttp原理解析(二)

    前言 上一篇我们学习了OKHttp的请求执行流程,知道了最终请求流程都会交给getResponseWithInterceptorChain方法来执行,接下来我们就详细分析执行getResponseWi ...

  7. OkHttp 原理解析

    一.前言: HTTP是现代应用常用的一种交换数据和媒体的网络方式,高效地使用HTTP能让资源加载更快,节省带宽.OkHttp是一个高效的HTTP客户端,它有以下默认特性: 支持HTTP/2,允许所有同 ...

  8. OkHttp 原理剖析

    OkHttp 原理剖析 文章目录 OkHttp 原理剖析 一.基本介绍 二.基本使用 2.1 依赖配置 2.2 基本请求 三.原理剖析 3.1 创建请求 3.2 执行同步请求 3.2.1 执行同步请求 ...

  9. OKHttp原理讲解之RetryAndFollowUpInterceptor

    一.前言: 1.1 本篇主要讲解内容 1.RetryAndFollowUpInterceptor中主要成员介绍 2.拦截器中重试机制 3.拦截器中执行流程 1.2 OKHttp项目地址: https: ...

最新文章

  1. Spring中配置DataSource数据源的几种选择
  2. Oracle 安装错误 - 无法在节点xx上执行物理内存检查 的解决
  3. 第10章* 网络 幂律分布
  4. log4j2入门(四) log4j2.xml配置文件详细实例
  5. Cpp 11 / 万能引用、引用折叠和完美转发
  6. transient-java 关键字
  7. Maven 修改默认端口启动web项目
  8. 设计模式之单件模式(Singleton Pattern)
  9. PHP中unset,array_splice删除数组中元素的区别
  10. Linux内核3.0移植并基于Initramfs根文件系统启动
  11. 每日一题:leetcode61.旋转链表
  12. 单元测试JUnit 4 (一)——keeps the bar green to keeps the code clean
  13. python创建一个csv文件_python如何写入csv
  14. JavaScriptSerializer 类
  15. POJ2074 Line of Sight
  16. mapxtreme 更改图元的位置
  17. JAVA后端开发面试小结
  18. IGS精密星历及其下载(自PureSky_Memory的博客)
  19. java text类型转换_java语言实现Text格式转换成pdf文件
  20. AUTOCAD——标注打断

热门文章

  1. JVM垃圾回收——三色标记法
  2. 交错和 (hihocoder)
  3. SQLMAP进阶:参数讲解
  4. 心电 基线漂移的处理研究论文
  5. DiskMan使用方法
  6. [BJDCTF2020]ZJCTF,不过如此
  7. Deeplearning for NLP (简介)
  8. uni-app卡片式轮播
  9. 深度学习(6)之卷积的几种方式:1D、2D和3D卷积的不同卷积原理(全网最全!)
  10. 【深度学习】各种卷积的理解笔记(2D,3D,1x1,可分离卷积)