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 列表,然后按下面的顺序添加拦截器。
- 自定义拦截器
- 重试拦截器(RetryAndFollowUpInterceptor)
- 网络请求构建拦截器(BridgeInterceptor)
- 缓存拦截器(CacheInterceptor)
- 连接拦截器(ConnectInterceptor)
- 自定义网络拦截器
- 数据传输拦截器(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-Authenticate
和 Proxy-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 个步骤。
- 接收:缓存从网络中读取抵达的请求报文;
- 解析:缓存对报文进行解析,提取出 URL 和各种首部;
- 查询:缓存查看是否有本地副本可用,如果没有就获取一份副本并将其保存在本地;
- 新鲜度监测:缓存查看已缓存副本是否足够新鲜,如果不是就询问服务器是否有新的资源;
- 创建响应:缓存会用新的首部和已缓存的主题来构建一条响应报文;
- 发送:缓存通过网络把响应发挥给客户端;
- 日志:缓存可选地创建一个日志文件条目描述这个事务;
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 只会缓存 GET
和 HEAD
等获取资源的方法的请求,而对于 POST
和 PUT
等修改资源的请求和响应数据是不会进行缓存的。
在 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. 握手层
客户端与服务器端交换一些信息,比如协议版本号、随机数、密码套件(密码学算法组合)等,经过协商,服务器确定本次连接使用的密码套件,该密码套件必须双方都认可。
客户端通过服务器发送的证书确认身份后,双方开始密钥协商,最终双方协商出预备主密钥、主密钥、密钥块
,有了密钥块,代表后续的应用层数据可以进行机密性和完整性保护了,接下来由加密层处理。
2. 加密层
加密层有了握手层提供的密钥块,就可以进行机密性和完整性保护了,加密层相对来说逻辑比较简单明了,而握手层在完成握手前,客户端和服务器需要经过多个来回才能握手完成,这也是 TLS/SSL 协议缓慢的原因。
下面分别是使用 RSA 密码套件和 DHE_RSA 密码套件的 TLS 协议流程图。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
上述【高清技术脑图】以及【配套的架构技术PDF】点击:Android架构视频+BAT面试专题PDF+学习笔记,或者私信回复【技能提升】即可获取!
为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!
41789)]
[外链图片转存中…(img-XXTZkbkT-1710704341789)]
[外链图片转存中…(img-nMVgYCnY-1710704341790)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-FGWfpQer-1710704341790)]
最后
现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
上述【高清技术脑图】以及【配套的架构技术PDF】点击:Android架构视频+BAT面试专题PDF+学习笔记,或者私信回复【技能提升】即可获取!
为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!
Android架构师之路很漫长,一起共勉吧!