OkHttp(四) - 核心攔截器
前面分析了okhtt底層請求程式碼,瞭解到請求的處理是通過攔截器鏈來進行的。框架總共提供了5個核心的攔截器,每個攔截器都有其特定的功能,後面將會逐個分析。除此之外我們還可以在系統攔截器之前或之後擴充套件自己的攔截器,,下圖所示為攔截器工作鏈條:
使用者自定義攔截器,只需實現intercept方法,並在其中呼叫chain的proceed方法即可。下面將按照請求順序重點介紹框架提供的攔截器1. RetryAndFollowUpInterceptor
此攔截器提供兩個功能:
- 錯誤恢復
- 重定向
這兩個功能是在intercept方法中得以體現的,該方法中有一個while(true)的迴圈。當後續的請求發生異常會呼叫類中的recover方法,recover中依據一定規則,如是否是fatal異常,是否配置了失敗重試等來決定是否重試;當返回的響應是重定向,則會呼叫followUpRequest方法,生成重定向請求,再重新發起請求。無論錯誤恢復還是重定向實際都是通過迴圈來實現的。下面貼出intercept方法中的關鍵程式碼:
while (true) {
try {
//執行請求
response = ((RealInterceptorChain) chain).proceed(request,streamAllocation,null,null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
//如果能夠恢復則重試,否則丟擲異常
if (!recover(e.getLastConnectException(),false,request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
//如果能夠恢復則重試,否則丟擲異常
if (!recover(e,requestSendStarted,request)) throw e;
releaseConnection = false;
continue;
} finally {
......
}
......
//判斷響應是否有重定向,如果有則重定向
Request followUp = followUpRequest(response);
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
......
request = followUp;
priorResponse = response;
}
複製程式碼
2. BridgeInterceptor
此攔截器主要的主要有兩個:
- 請求頭處理(新增一些預設的請求等)
- 響應及響應頭處理(新增預設的響應頭及響應體處理)
此攔截器中的intercept方法比較簡單,就是設定一些預設的請求頭或響應頭等,就不單獨說了,看程式碼便可一目瞭然。
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
//設定相關請求頭資訊
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type",contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length",Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding","chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host",hostHeader(userRequest.url(),false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection","Keep-Alive");
}
//設定cookie
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding","gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie",cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent",Version.userAgent());
}
//將請求傳遞到下個攔截器處理
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar,userRequest.url(),networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
//解析gzip壓縮
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
responseBuilder.body(new RealResponseBody(strippedHeaders,Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
複製程式碼
3. CacheInterceptor
從名字中就可以看出此攔截器是提供快取相關功能的。okhttp底層的快取採用的是lru演演算法,具體的實現類是okhttp3.internal.cache.DiskLruCache,這裡就不具體展開說了。攔截器根據請求頭中或響應頭中的有關快取的設定來決定快取策略。具體的實現稍顯繁瑣,但理解起來並不複雜,具體參考其程式碼。
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//獲取快取策略
CacheStrategy strategy = new CacheStrategy.Factory(now,chain.request(),cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate was not applicable. Close it.
}
// 如果不需要網路請求,而快取又不存在,則返回一個504的失敗響應
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 如果不需要網路請求,返回快取的響應
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//不走快取,執行網路請求
Response networkResponse = null;
try {
//將請求傳到到下一個攔截器進行處理
networkResponse = chain.proceed(networkRequest);
} finally {
// If we are crashing on I/O or otherwise,do not leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
if (cacheResponse != null) {
//如果返回的響應程式碼為HTTP_NOT_MODIFIED,則從快取中提取內容返回
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(),networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
//更新快取相關內容
cache.trackConditionalCacheHit();
cache.update(cacheResponse,response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
//更新快取
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response,networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest,response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
//快取失效
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
複製程式碼
4. ConnectInterceptor
此攔截器負責與遠端服務之間建立連線,來看看具體的程式碼,雖然攔截方法程式碼不多,但是方法的呼叫鏈卻非常深:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
//返回的streamAllocation物件是在RetryAndFollowUpInterceptor攔截器中建立的
//streamAllocation = new StreamAllocation(client.connectionPool(),createAddress(request.url()),callStackTrace);
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//在newStream建立socket連線,返回HttpCodec物件,此物件用於對流的解析
HttpCodec httpCodec = streamAllocation.newStream(client,doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request,httpCodec,connection);
}
複製程式碼
關鍵註釋都已在程式碼中給出,這裡就不具體解釋了。網路請求可以抽象為底層的連線即:connection;在connection之上進行資料通訊,可以把交換的資料抽象為流,即stream;在上層,將我們的發起的請求呼叫抽象為call。StreamAllocation物件就是負責協調管理這三者之間的關係。程式碼中還出現了一個物件:HttpCodec,此物件用於對流進行解析。streamAllocation建立了一條流就等效於與遠端服務建立了一條通訊鏈路,我們可以在這條鏈路上進行資料通訊。深入newStram()方法:
public HttpCodec newStream(OkHttpClient client,boolean doExtensiveHealthChecks) {
int connectTimeout = client.connectTimeoutMillis();
int readTimeout = client.readTimeoutMillis();
int writeTimeout = client.writeTimeoutMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
//與遠端請求地址通過socket建立連線
RealConnection resultConnection = findHealthyConnection(connectTimeout,readTimeout,writeTimeout,connectionRetryEnabled,doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client,this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
複製程式碼
具體的連線建立過程就是在此方法中進行的,此過程比較簡單就不深入跟蹤,這裡給出時序圖,如下圖所示。總之最後還是通過socket與遠端的請求建立連線。
5. CallServerInterceptor
連線建立好後,就要進行資料通訊了,此攔截器的作用就是傳送請求資料並從服務端獲取響應資料。幾行程式碼勝過千言萬語:
@Override public Response intercept(Chain chain) throws IOException {
......
Request request = realChain.request();
//傳送請求頭
httpCodec.writeRequestHeaders(request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
......
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
//建立請求體sink,也就是將請求體寫入到一個緩衝buffer中
Sink requestBodyOut = httpCodec.createRequestBody(request,request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
//將請求體中的內容寫入BufferedSink
request.body().writeTo(bufferedRequestBody);
//將內容寫到遠端請求端
bufferedRequestBody.close();
}
......
}
//完成請求傳送
httpCodec.finishRequest();
//獲取響應頭
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
......
int code = response.code();
if (forWebSocket && code == 101) {
// Connection is upgrading,but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
//解析請求體
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
......
return response;
}
複製程式碼
上面的程式碼將資料通訊的核心程式碼提煉了出來,可以發現通訊過程為:傳送請求頭-->傳送請求體(如果有) --> 獲取響應頭 --> 獲取響應體。
總結
至此我們就將okhttp中傳送請求到獲取響應的流程分析完了。各個攔截器各司其職,每個都有自己獨立需要完成的功能,通過呼叫鏈模式組合在一起,降低了耦合性並具有很好的擴充套件性,此設計值得學習和借鑑。