Android網路開發(三、okhttp責任鏈)
theme: juejin highlight: a11y-dark
一、責任鏈模式概述
1.1 定義
責任鏈模式是面向物件程式設計的一種軟體設計模式。在責任鏈模式裡,把每一個物件及其對下家的引用連線起來形成一條鏈,請求在這個鏈上傳遞並返回結果。每個物件在收到請求時,如果可以處理則處理並返回結果,如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,直到某一個物件完成處理。
1.2 RealCall
在《Android網路基礎(二、okhttp&retrofit)》中分析到,okhttp執行建立連線、傳送Request、讀取Response等工作都是在RealCall類攔截器責任鏈中的各Interceptor實現的,下面是RealCall呼叫攔截鏈去獲取Response的程式碼: ```kotlin /* * okhttp業務層和網路層的橋接層 * 對建立連線、傳送請求、讀取響應、IO流進行了封裝 / class RealCall(val client: OkHttpClient, val originalRequest: Request, val forWebSocket: Boolean) : Call {
/**
* 責任鏈模式,多個攔截器組成一個攔截鏈
*/
internal fun getResponseWithInterceptorChain(): Response {
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
}
// 資料傳送&讀取攔截器CallServerInterceptor
interceptors += CallServerInterceptor(forWebSocket)
// 攔截鏈InterceptorChain
val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
// 攔截鏈InterceptorChain開始處理request物件
val response = chain.proceed(originalRequest)
return response
}
}
```
如程式碼中所示,okhttp攔截鏈有以下幾種攔截器Interceptor,按如下順序依次處理request物件:
1.3 攔截器責任鏈
攔截器責任鏈InterceptorChain呼叫proceed()方法來傳遞request物件,每個攔截器在處理request物件時如果可以處理則進行處理並返回結果,如果不能處理則繼續傳給下一個攔截器(例如如果快取攔截器CacheInterceptor可以處理則返回快取Response,否則就繼續傳遞給ConnectInterceptor執行連線建立等工作來返回Response)。
```kotlin
class RealInterceptorChain(
internal val call: RealCall,
private val interceptors: List
override fun proceed(request: Request): Response {
check(index < interceptors.size)
// 把當前攔截鏈複製一份,並把其index+1
val next = copy(index = index + 1, request = request)
// 當前指向的攔截器
val interceptor = interceptors[index]
// 當前攔截器進行處理:在每個攔截器處理request物件時,如果自身不能處理,則呼叫next.proceed()由下一個攔截器處理
val response = interceptor.intercept(next)
return response
}
}
```
攔截器責任鏈的各攔截器的主要工作總結如下所示,下面從程式碼進行分析:
二、RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor是重試和重定向等攔截器,其內部有一個while迴圈用於重試或傳送重定向請求等。 ```kotlin class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
var request = chain.request
val call = realChain.call
// 重試和重定向等攔截器內部是while迴圈,在後續攔截器都未處理時會進行對應處理並迴圈
while (true) {
var response: Response
var closeActiveExchange = true
try {
try {
// 傳給下一個攔截器執行後續處理;
response = realChain.proceed(request)
} catch (e: RouteException) {
// 路由異常時,進行相應處理程式碼並繼續迴圈,省略部分程式碼...
continue
} catch (e: IOException) {
// 讀寫異常時,進行相應處理程式碼並繼續迴圈,省略部分程式碼...
continue
}
val exchange = call.interceptorScopedExchange
// 構造重試或重定向等請求
val followUp = followUpRequest(response, exchange)
if (followUp == null) {
// 沒有重試或重定向等請求則返回
return response
}
// 重試或重定向等請求,繼續迴圈
request = followUp
} finally {
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
/**
* 構造後續的重試或重定向等請求
*/
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
val route = exchange?.connection?.route()
val responseCode = userResponse.code
val method = userResponse.request.method
when (responseCode) {
HttpURLConnection.HTTP_PROXY_AUTH -> {
return client.proxyAuthenticator.authenticate(route, userResponse)
}
HttpURLConnection.HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
StatusLine.HTTP_PERM_REDIRECT, StatusLine.HTTP_TEMP_REDIRECT, HttpURLConnection.HTTP_MULT_CHOICE,
HttpURLConnection.HTTP_MOVED_PERM, HttpURLConnection.HTTP_MOVED_TEMP, HttpURLConnection.HTTP_SEE_OTHER -> {
// 重定向
return buildRedirectRequest(userResponse, method)
}
HttpURLConnection.HTTP_CLIENT_TIMEOUT -> {
// 408錯誤重試;實踐中比較少見
if (retryAfter(userResponse, 0) > 0) {
return null
}
return userResponse.request
}
HttpURLConnection.HTTP_UNAVAILABLE -> {
// 503錯誤重試
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request
}
return null
}
StatusLine.HTTP_MISDIRECTED_REQUEST -> {
// 省略程式碼
exchange.connection.noCoalescedConnections()
return userResponse.request
}
else -> return null
}
}
} ```
三、BridgeInterceptor
BridgeInterceptor是橋接攔截器,是應用層request和網路層request的一個橋接器。BridgeInterceptor根據應用層的request物件複製出一個新的request物件,把業務層的request和後續處理的request隔離開,並在新的request物件中拼接Content-Type、Content-Length、Accept-Encoding等header。然後繼續向下傳遞,在下級攔截器返回reponse物件後,BridgeInterceptor支援對response解壓然後把response繼續傳遞給上級攔截器。 ```kotlin class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val userRequest = chain.request()
// 1.根據應用層的request物件複製出一個新的request物件,把業務層的request和後續處理的request隔離開
val requestBuilder = userRequest.newBuilder()
val body = userRequest.body
// 2.在新的request物件中拼接Content-Type、Content-Length、Accept-Encoding等header
if (body != null) {
val contentType = body.contentType()
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString())
}
val contentLength = body.contentLength()
if (contentLength != -1L) {
requestBuilder.header("Content-Length", contentLength.toString())
requestBuilder.removeHeader("Transfer-Encoding")
} else {
requestBuilder.header("Transfer-Encoding", "chunked")
requestBuilder.removeHeader("Content-Length")
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", userRequest.url.toHostHeader())
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}
var transparentGzip = false
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true
requestBuilder.header("Accept-Encoding", "gzip")
}
val cookies = cookieJar.loadForRequest(userRequest.url)
if (cookies.isNotEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies))
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", userAgent)
}
// 3.由下一個攔截器繼續處理,並返回response物件
val networkResponse = chain.proceed(requestBuilder.build())
cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
// 4.根據網路層的response物件複製出一個新的response物件
val responseBuilder = networkResponse.newBuilder()
.request(userRequest)
// 5.對返回的response支援gzip解壓
if (transparentGzip &&
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
networkResponse.promisesBody()) {
val responseBody = networkResponse.body
if (responseBody != null) {
val gzipSource = GzipSource(responseBody.source())
val strippedHeaders = networkResponse.headers.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build()
responseBuilder.headers(strippedHeaders)
val contentType = networkResponse.header("Content-Type")
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
}
}
return responseBuilder.build()
}
} ```
四、CacheInterceptor
在建立OkHttpClient的時候可以傳一個快取目錄,快取攔截器CacheInterceptor就是用來判斷request是否命中了快取。
kotlin
val cacheFile = File("")
val okHttpClient = OkHttpClient.Builder()
.connectionPool(ConnectionPool(2, 35, TimeUnit.SECONDS))
.cache(Cache(cacheFile, 1024))
.build()
如果建立OkHttpClient時設定了快取目錄,則快取攔截器CacheInterceptor會根據一定的快取策略判斷是否需要傳送請求及是否命中快取。
```kotlin
class CacheInterceptor(val cache: Cache?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
// 1.根據request的url從cache中獲取快取
val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis()
// 2.根據快取策略判斷快取是否有效
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
// 3.快取策略判定不需要傳送請求則命中快取
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(CacheInterceptor.stripBody(cacheResponse))
.build().also {
listener.cacheHit(call, it)
}
}
// 4.繼續傳遞request物件
var networkResponse = chain.proceed(networkRequest)
// 5.返回response
val response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
// 6.省略部分程式碼,根據快取策略新增到快取
cache.put(response)
}
return response
}
} ```
五、ConnectInterceptor
連線攔截器ConnectInterceptor用於建立連線,內部會判斷是否可以連線複用,否則執行TCP過程去建立一個網路連線(後面章節再分析詳細過程)。
kotlin
object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
// 初始化Exchange,並判斷是否可以連線複用,建立連線等操作
val exchange = realChain.call.initExchange(chain)
val connectedChain = realChain.copy(exchange = exchange)
// 建立連線後,繼續向下傳遞
return connectedChain.proceed(realChain.request)
}
}
六、CallServerInterceptor
在建立連線後,CallServerInterceptor用來使用Okio的Sink和Source來讀寫資料,實現把request資料傳送給後端,和從後端讀取response。 ``` class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
val exchange = realChain.exchange!!
val request = realChain.request
val requestBody = request.body
// 1.通過Okio的Sink把RequestBody傳送給後端
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
requestBody.writeTo(bufferedRequestBody)
bufferedRequestBody.close()
// 省略部分程式碼
var response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
// 2.通過Okio的Source從後端讀response
response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
return response
}
} ```
七、總結
| 攔截器 | 前處理 |後處理 | |--|--|--| | 自定義攔截器 | 可自定義,如拼接公參等 | - | 重試和重定向等攔截器 | - | 重試和重定向等 | 橋接攔截器 | 添加了Content-Type、Host、Cookie等header| gzip解壓 | 快取攔截器 | 如果快取可用,則直接返回快取 | 如果伺服器返回304則返回快取。cache不為空時對response進行快取 | 連線攔截器 | 建立連線 | | 自定義網路攔截器 |- |- | 資料傳送&讀取攔截器 | okio寫資料 | okio讀資料
The End
歡迎關注我,一起解鎖更多技能:BC的掘金主頁~💐 BC的CSDN主頁~💐💐
okhttp:https://square.github.io/okhttp/
Android網路開發專欄:https://juejin.cn/column/7198740450198749240
- Android網路開發(三、okhttp責任鏈)
- Android OpenGL基礎(一、繪製三角形四邊形)
- 元件化基礎ARouter(二、跨模組API呼叫)
- (三)MySQL之庫表設計篇:一、二、三、四、五正規化、BC正規化與反正規化詳解!
- 實戰|一次對BC網站的滲透測試
- 川娃子創始人兼CEO唐磊:從複合風味入手,做“鮮味”的標準化輸出與BC端佈局 | 追鯨者
- 中國移動首個基於 BC-Linux 尤拉版的 CRM 系統試點上線
- Android基礎:Activity&Window&ViewRootImpl的關係
- Android OpenCV基礎(二、core模組)
- Android OpenGL基礎(五、相機預覽及濾鏡)
- Android OpenGL基礎(四、圖片後處理)
- Android OpenGL基礎(三、繪製Bitmap紋理)
- 2021,32歲“佛系”程式設計師平凡的一年
- 直擊進博會|USHOPAL旗下BC極選定位高階美妝,曾獲多傢俬募投資
- Android記憶體管理基礎
- BC科技集團:數字資產及區塊鏈業務成營收主力 上半年淨虧1.58億港元
- 【IT之家評測室】螢石智慧家居攝像機 BC2 上手:小機身設計,全無線體驗
- 年交易量不足幣安日交易量,BC科技為何還能連續三年營收增長?
- java.security.NoSuchProviderException: no such provider: BC 的問題解決
- 一次BC站點滲透實錄