OKHttp 请求的整体流程是怎样的?
val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url("https://www.google.com/")
.build()
okHttpClient.newCall(request).enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
}
})
所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。
分发器 -> 内部维护队列与线程池,完成请求调配;拦截器 -> 五大默认拦截器完成整个请求过程。
整个网络请求过程大致如上所示:
1,通过建造者模式构建 OKHttpClient
与 Request
2,OKHttpClient
通过 newCall
发起一个新的请求
3,通过分发器维护请求队列与线程池,完成请求调配
4,通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作
5,得到网络请求结果
OKHttp 分发器是怎样工作的?
分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为 正在请求中的列表
和 正在等待的列表
, 待正在请求中的列表的所有请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求。
同步请求:
同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可。
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
异步请求:
synchronized void enqueue(AsyncCall call) {
// 当正在执行的任务未超过最大限制64,并且同一Host的请求不超过5个,则会添加到正在执行队列
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call); // 提交给线程池
} else {
// 加入等待队列
readyAsyncCalls.add(call);
}
}
每个任务完成后,最后都会调用分发器的 dispatcher.finished(this)
方法,这里面会取出等待队列中的任务继续执行。
OKHttp 拦截器是怎样工作的?
# RealCall
override fun execute(): Response {
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}
// 构建了一个 OkHttp 拦截器的责任链
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
...
try {
// 将请求移交给下一个拦截器
val response = chain.proceed(originalRequest)
...
return response
}
...
}
责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。
如上所示责任链添加的顺序及作用如下表所示:
我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptor
的 intercept
方法,此方法会将网络响应的结果封装成一个 Response
对象并`return。
RetryAndFollowUpInterceptor
出错和重定向的判断标准也简单说一下:
-
判断出错的标准: 利用
try-catch
块对请求进行异常捕获,这里会捕获RouteException
和IOException
,并且在出错后都会先判断当前请求是否能够进行重试的操作。 -
重定向标准:对
Response
的状态码Code
进行审查,当状态码为3xx
时,则表示需要重定向,而后创建一个新的Request
,进行重试操作。
BridgeInterceptor
帮用户处理网络请求,它会帮助用户填写服务器请求所需要的配置信息(例如:请求头),如上面所展示的 User-Agent
、Connection
、Host
、Accept-Encoding
等。同时也会对请求的结果进行相应处理。
GET /wxarticle/chapters/json HTTP/1.1
Host: wanandroid.com
Accept: application/json, text/plain, /
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
User-Agent: Mozilla/5.0 xxx
BridgeInterceptor的内部实现主要分为以下三步:
1,为用户网络请求设置 Content-Type
、Content-Length
、Host
、Connection
、Cookie
等参数,也就是将一般请求转换为适合服务器解析的格式,以适应服务器端;
2,通过 chain.proceed
(requestBuilder.build())方法,将转换后的请求移交给下一个拦截器 CacheInterceptor
,并接收返回的结果 Response
;
3,对结果 Response
进行 gzip、Content-Type 等转换,以适应应用程序端。
所以说 BridgeInterceptor
是应用程序和服务器端的一个桥梁。
CacheInterceptor
CacheInterceptor
是一个处理网络请求缓存的拦截器。它的内部处理和一些图片缓存的逻辑相似,首先会判断是否存在可用的缓存,如果存在,则直接返回缓存,反之,调用chain.proceed(networkRequest)
方法将请求移交给下一个拦截器,有了结果后,将结果 put 到 cache 中。
** ConnectInterceptor**
internal fun initExchange(chain: RealInterceptorChain): Exchange {
...
val exchangeFinder = this.exchangeFinder!!
val codec = exchangeFinder.find(client, chain)
val result = Exchange(this, eventListener, exchangeFinder, codec)
this.interceptorScopedExchange = result
this.exchange = result
...
if (canceled) throw IOException("Canceled")
return result
}
它首先会通过 ExchangeFinder
查询到 codec
,这个 ExchangeFinder
是不是很熟悉?在上面 RetryAndFollowUpInterceptor
分析中,每次循环都会先做创建 ExchangeFinder
的准备工作。而这个 codec
是什么?它是一个编码解码器,来确定是用 Http1 的方式还是以 Http2 的方式进行请求。在找到合适的codec
后,作为参数创建 Exchange
。Exchange
内部涉及了很多网络连接的实现,这个后面再详细说,我们先看看是如何找到合适的 codec
?
利用编解码器 codec
创建了一个 Exchange
,而这个 Exchange
的内部其实是利用 Http1 解码器或者 Http2 解码器,分别进行请求头的编写writeRequestHeaders
,或者创建 Request Body
,发送给服务器。
找到合适的 codec
,就必须先找到一个可用的网络连接,再利用这个可用的连接创建一个新的 codec
。 为了找到可用的连接,内部使用了大概5种方式进行筛选。
1,从连接池中查找
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
val result = call.connection!!
return result
}
尝试在连接池中查找可用的连接,在遍历连接池中的连接时,就会判断每个连接是否可用,而判断连接是否可用的条件如下:请求数要小于该连接最大能承受的请求数,Http2 以下,最大请求数为1个,并且此连接上可创建新的交换;
该连接的主机和请求的主机一致;如果从连接池中拿到了合格的 connection,则直接返回。如果没有拿到,那就进行第二种拿可用连接的方式。
2,传入Route,从连接池中查找
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
return result
}
依然是从连接池中拿,但是这次不同的是,参数里传入了
routes
,这个routes
是包含路由 Route 的一个 List 集合,而 Route 其实指的是连接的 IP 地址、TCP 端口以及代理模式。而这次从连接池中拿,主要是针对 Http2,路由必须共用一个 IP 地址,此连接的服务器证书必须包含新主机且证书必须与主机匹配。
3,自己创建连接
如果前两次从连接池里都没有拿到可用连接,那么就自己创建连接。
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
}
4,多路复用置为 true,依然从连接池中查找
if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
val result = call.connection!!
newConnection.socket().closeQuietly()
return result
}
这次从连接池中查找,
requireMultiplexed
置为了true,只查找支持多路复用的连接。并且在建立连接后,将新的连接保存到连接池中。
建立连接,找到合适的 codec
后,又将请求移交给了下一个拦截器CallServerInterceptor
。
CallServerInterceptor
CallServerInterceptor
是链中最后一个拦截器,主要用于向服务器发送内容,主要传输 http 的头部和 body 信息。其内部利用上面创建的Exchange
进行请求头编写,创建 Request body,发送请求,得到结果后,对结果进行解析并回传。
应用拦截器
和 网络拦截器
有什么区别?
从整个责任链路来看,应用拦截器
是最先执行的拦截器,也就是用户自己设置 request 属性后的原始请求,而网络拦截器位于 ConnectInterceptor
和CallServerInterceptor
之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别:
1,应用拦截器永远只会触发一次
2,每个拦截器都应该至少调用一次 realChain.proceed
方法。
OKHttp
如何复用 TCP 连接?
ConnectInterceptor
的主要工作就是负责建立 TCP 连接,建立 TCP 连接需要经历三次握手四次挥手等操作,如果每个 HTTP 请求都要新建一个 TCP,那么消耗资源比较多。而 Http1.1 已经支持 keep-alive
,即多个 Http 请求复用一个 TCP 连接,OKHttp 也做了相应的优化,下面我们来看下 OKHttp 是怎么复用TCP 连接的:
ConnectInterceptor
中查找连接的代码会最终会调用到ExchangeFinder.findConnection
方法,具体如下:
# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
// 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
releasedConnection = transmitter.connection;
result = transmitter.connection;
if (result == null) {
// 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
result = transmitter.connection;
}
}
}
synchronized (connectionPool) {
if (newRouteSelection) {
//3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
// 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
synchronized (connectionPool) {
// 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
//意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果获取到,就关闭我们创建里的连接,返回获取的连接
result = transmitter.connection;
} else {
//最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
connectionPool.put(result);
}
}
return result;
}
上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接:
1,首先会尝试使用已分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)
2,若没有已分配的可用连接,就尝试从连接池中匹配获取。因为此时没有路由信息,所以匹配条件:address一致——host、port、代理等一致,且匹配的连接可以接受新的请求。
3,若从连接池没有获取到,则传入 routes 再次尝试获取,这主要是针对 Http2.0 的一个操作,Http2.0 可以复用 square.com 与 square.ca 的连接
若第二次也没有获取到,就创建 RealConnection
实例,进行 TCP + TLS 握手,与服务端建立连接。
4,此时为了确保 Http2.0 连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。
5,第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。
如何建立TCP/TLS连接?
TCP 连接:
fun connect(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean,
call: Call,
eventListener: EventListener
) {
...
while (true) {
try {
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener)
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
break
} catch (e: IOException) {
...
}
route.requiresTunnel()
这个方法表示该请求是否使用了 Proxy.Type.HTTP
代理且目标是 Https
连接;如果是,则创建一个代理隧道连接 Tunnel(connectTunnel)。创建这个隧道的目的在于利用 Http 来代理请求 Https;如果不是,则直接建立一个 TCP 连接(connectSocket)
代理隧道是如何创建的?
它的内部会先通过 Http 代理创建一个 TLS 的请求,也就是在地址 url 上增加Host、Proxy-Connection、User-Agent 首部。接着最多21次的尝试,利用connectSocket 开启 TCP 连接且利用 TLS 请求创建一个代理隧道。
从这里可以看见,不管是否需要代理隧道,都会开始建立一个 TCP 连接(connectSocket),那又是如何建立 TCP 连接的?
private fun connectSocket(
connectTimeout: Int,
readTimeout: Int,
call: Call,
eventListener: EventListener
) {
val proxy = route.proxy
val address = route.address
val rawSocket = when (proxy.type()) {
Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
else -> Socket(proxy)
}
this.rawSocket = rawSocket // 这个就代表着TCP连接
eventListener.connectStart(call, route.socketAddress, proxy)
rawSocket.soTimeout = readTimeout
try {
// 调用socket的connect方法来打开一个TCP连接。
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
} catch (e: ConnectException) {
throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
initCause(e)
}
}
...
}
TLS 连接
在建立 TCP 连接或者创建 Http 代理隧道后,就会开始建立连接协议(establishProtocol)。
private fun establishProtocol(
connectionSpecSelector: ConnectionSpecSelector,
pingIntervalMillis: Int,
call: Call,
eventListener: EventListener
) {
// 判断当前地址是否是HTTPS;
if (route.address.sslSocketFactory == null) {
// 如果不是HTTPS,判断当前协议是否是明文HTTP2
if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
socket = rawSocket
protocol = Protocol.H2_PRIOR_KNOWLEDGE
// 开始Http2的握手动作
startHttp2(pingIntervalMillis)
return
}
// 如果是Http/1.1,则直接return返回;
socket = rawSocket
protocol = Protocol.HTTP_1_1
return
}
// 如果是HTTPS,就开始建立TLS安全协议连接了(connectTls);
eventListener.secureConnectStart(call)
connectTls(connectionSpecSelector)
eventListener.secureConnectEnd(call, handshake)
if (protocol === Protocol.HTTP_2) {
// 如果是 HTTPS 且为 HTTP2,除了建立TLS连接外,还会调用startHttp2,开始Http2的握手动作。
startHttp2(pingIntervalMillis)
}
}
上述提到了 TLS 的连接(connectTls),那我们就来看一下它的内部实现:
private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
val address = route.address
val sslSocketFactory = address.sslSocketFactory
var success = false
var sslSocket: SSLSocket? = null
try {
// Create the wrapper over the connected socket.
sslSocket = sslSocketFactory!!.createSocket(
rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket
// Configure the socket's ciphers, TLS versions, and extensions.
val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)
if (connectionSpec.supportsTlsExtensions) {
Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)
}
// Force handshake. This can throw!
sslSocket.startHandshake()
// block for session establishment
val sslSocketSession = sslSocket.session
val unverifiedHandshake = sslSocketSession.handshake()
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {
val peerCertificates = unverifiedHandshake.peerCertificates
if (peerCertificates.isNotEmpty()) {
val cert = peerCertificates[0] as X509Certificate
throw SSLPeerUnverifiedException("""
|Hostname ${address.url.host} not verified:
| certificate: ${CertificatePinner.pin(cert)}
| DN: ${cert.subjectDN.name}
| subjectAltNames: ${OkHostnameVerifier.allSubjectAltNames(cert)}
""".trimMargin())
} else {
throw SSLPeerUnverifiedException(
"Hostname ${address.url.host} not verified (no certificates)")
}
}
val certificatePinner = address.certificatePinner!!
handshake = Handshake(unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite,
unverifiedHandshake.localCertificates) {
certificatePinner.certificateChainCleaner!!.clean(unverifiedHandshake.peerCertificates,
address.url.host)
}
// Check that the certificate pinner is satisfied by the certificates presented.
certificatePinner.check(address.url.host) {
handshake!!.peerCertificates.map { it as X509Certificate }
}
// Success! Save the handshake and the ALPN protocol.
val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
Platform.get().getSelectedProtocol(sslSocket)
} else {
null
}
socket = sslSocket
source = sslSocket.source().buffer()
sink = sslSocket.sink().buffer()
protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1
success = true
} finally {
...
}
}
这段代码很长,具体逻辑总结了以下几点:
1,利用请求地址host,端口以及TCP socket共同创建sslSocket;
2,为Socket 配置加密算法,TLS版本等;
3,调用startHandshake()进行强制握手;
4,验证服务器证书的合法性;
5,利用握手记录进行证书锁定校验(Pinner);
6,连接成功则保存握手记录和ALPN协议。
OKHttp 空闲连接如何清除?
上面说到我们会建立一个 TCP 连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp 是如何做到的呢?
# RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}
long cleanup(long now) {
int inUseConnectionCount = 0;//正在使用的连接数
int idleConnectionCount = 0;//空闲连接数
RealConnection longestIdleConnection = null;//空闲时间最长的连接
long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间
//遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//若连接正在使用,continue,正在使用连接数+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空闲连接数+1
idleConnectionCount++;
// 赋值最长的空闲时间和对应连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//连接没有空闲的,就5分钟后再尝试清理.
return keepAliveDurationNs;
} else {
// 没有连接,不清理
cleanupRunning = false;
return -1;
}
}
//关闭移除的连接
closeQuietly(longestIdleConnection.socket());
//关闭移除后 立刻 进行下一次的 尝试清理
return 0;
}
思路还是很清晰的:
1,在将连接加入连接池时就会启动定时任务
2,有空闲连接的话,如果最长的空闲时间大于5分钟或空闲数大于5,就移除关闭这个最长空闲连接;
3,如果空闲数不大于5 且最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。
4,没有空闲连接就等5分钟后再尝试清理。
5,没有连接不清理。
OKHttp 有哪些优点?
1,使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端 OkHttpClient 统一暴露出来。
2,扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求。
3,功能强大,支持 Spdy、Http1.X、Http2、以及 WebSocket 等多种协议。
4,通过连接池复用底层TCP(Socket),减少请求延时。
5,无缝的支持 GZIP 减少数据流量。
6,支持数据缓存,减少重复的网络请求。
7,支持请求失败自动重试主机的其他 ip,自动重定向。
OKHttp 框架中用到了哪些设计模式?
1,构建者模式:OkHttpClient
与 Request
的构建都用到了构建者模式。
2,外观模式: OkHttp
使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端 OkHttpClient
统一暴露出来。
3,责任链模式: OKHttp
的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置。
4,享元模式:享元模式的核心即池中复用,OKHttp
复用 TCP 连接时用到了连接池,同时在异步请求中也用到了线程池。
Ref:
网友评论