攔截器
ConnectInterceptor
打開與目標服務器的連接,並執行下一個攔截器。
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
StreamAllocation這個對象是在第一個攔截器:重定向攔截器(RetryAndFollowUpInterceptor)創建的,但是真正使用的地方卻在這裏。
當一個請求發出,需要建立連接,連接建立後需要使用流用來讀寫數據。
這個StreamAllocation就是協調請求、連接與數據流三者之間的關係,它負責爲一次請求尋找連接,然後獲得流來實現網絡通信。
StreamAllocation中簡單來說就是維護連接:RealConnection——封裝了Socket與一個Socket連接池。可複用的RealConnection需要:
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
//todo 找到一個健康的連接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled,
doExtensiveHealthChecks);
//todo 利用連接實例化流HttpCodec對象,如果是HTTP/2返回Http2Codec,否則返回Http1Codec
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
這裏使用的newStream方法實際上就是去查找或者建立一個與請求主機有效的連接,返回的HttpCodec中包含了輸入輸出流,並且封裝了對HTTP請求報文的編碼與解碼,直接使用它就能夠與請求主機完成HTTP通信。
/**
* Returns true if this connection can carry a stream allocation to {@code address}. If non-null
* {@code route} is the resolved route for a connection.
*/
public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// At this point we don't have a hostname match. But we still be able to carry the
// request if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;
// 2. The routes must share an IP address. This requires us to have a DNS address for both
// hosts, which only happens after route planning. We can't coalesce connections that use a
// proxy, since proxies don't tell us the origin server's IP address.
if (route == null) return false;
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
// 3. This connection's server certificate's must cover the new host.
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
if (allocations.size() >= allocationLimit || noNewStreams) return false;
連接到達最大併發流或者連接不允許建立新的流;如http1.x正在使用的連接不能給其他人用(最大併發流爲:1)或者連接被關閉;那就不允許複用;
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
DNS、代理、SSL證書、服務器域名、端口完全相同則可複用;
總結
這個攔截器中的所有實現都是爲了獲得一份與目標服務器的連接,在這個連接上進行HTTP數據的收發。
CallServerInterceptor
請求服務器攔截器,
@Override
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
//todo 利用HttpCodec發出請求到服務器並且解析生成Response
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
//todo 將請求頭寫入到緩存中(直到調用flushRequest()才真正發送給服務器)
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
//todo 是否會攜帶請求體的方式(POST),如果命中if,則會先給服務器發起一次查詢是否願意接收請求體
//這時候如果服務器願意會響應100(沒有響應體,responseBuilder 即爲nul)。
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
//todo 這個請求頭代表了在發送請求體之前需要和服務器確定是否願意接受客戶端發送的請求體
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1
// connection
// from being reused. Otherwise we're still obligated to transmit the request
// body to
// leave the connection in a consistent state.
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
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();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
1、POST方式請求,請求頭中包含Expect,服務器允許接受請求體,並且已經發出了請求體,responseBuilder爲null;
2、POST方式請求,請求頭中包含Expect,服務器不允許接受請求體,responseBuilder不爲null
3、POST方式請求,未包含Expect,直接發出請求體,responseBuilder爲null;
4、POST方式請求,沒有請求體,responseBuilder爲null;
5、GET方式請求,responseBuilder爲null;
對應上面的5種情況,讀取響應頭並且組成響應Response,注意:此Response沒有響應體。同時需要注意的是,如果服務器接受 Expect: 100-continue這是不是意味着我們發起了兩次Request?那此時的響應頭是第一次查詢服務器是否支持接受請求體的,而不是真正的請求對應的結果響應。
如果響應是100,這代表了是請求Expect: 100-continue成功的響應,需要馬上再次讀取一份響應頭,這纔是真正的請求對應結果響應頭。
forWebSocket代表websocket的請求,我們直接進入else,這裏就是讀取響應體數據。然後判斷請求和服務器是不是都希望長連接,一旦有一方指明close,那麼就需要關閉socket。而如果服務器返回204/205,一般情況而言不會存在這些返回碼,但是一旦出現這意味着沒有響應體,但是解析到的響應頭中包含Content-Lenght且不爲0,這表響應體的數據字節長度。此時出現了衝突,直接拋出協議異常!
總結
在這個攔截器中就是完成HTTP協議報文的封裝與解析。
整體總結
整個OkHttp功能的實現就在這五個默認的攔截器中,所以先理解攔截器模式的工作機制是先決條件。這五個攔截器分別爲: 重試攔截器、橋接攔截器、緩存攔截器、連接攔截器、請求服務攔截器。每一個攔截器負責的工作不一樣,就好像工廠流水線,最終經過這五道工序,就完成了最終的產品。
但是與流水線不同的是,OkHttp中的攔截器每次發起請求都會在交給下一個攔截器之前幹一些事情,在獲得了結果之後又幹一些事情。整個過程在請求向是順序的,而響應向則是逆序。
當用戶發起一個請求後,會由任務分發起Dispatcher將請求包裝並交給重試攔截器處理。
1、重試攔截器在交出(交給下一個攔截器)之前,負責判斷用戶是否取消了請求;在獲得了結果之後,會根據響應碼判斷是否需要重定向,如果滿足條件那麼就會重啓執行所有攔截器。
2、橋接攔截器在交出之前,負責將HTTP協議必備的請求頭加入其中(如:Host)並添加一些默認的行爲(如:GZIP壓縮);在獲得了結果後,調用保存cookie接口並解析GZIP數據。
3、緩存攔截器顧名思義,交出之前讀取並判斷是否使用緩存;獲得結果後判斷是否緩存。
4、連接攔截器在交出之前,負責找到或者新建一個連接,並獲得對應的socket流;在獲得結果後不進行額外的處理。
5、請求服務器攔截器進行真正的與服務器的通信,向服務器發送數據,解析讀取的響應數據。