OkHttpClient是目前開發 android 應用使用最普遍的網絡框架,最近看了阿里的 httpdns 裏面對於 dns 的處理,咱們團隊就想調研一下在項目中有什麼利弊,而且框架中是否對 socket 的鏈接是怎麼緩存的。下面就帶着問題去分析一下這個框架:java
new OkHttpClient.Builder().build();
這個建造者的每一個傳參在源碼分析2中有詳細的講解android
發送請求的只有這一個方法,就順着點進去看下是怎麼設計的web
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
複製代碼
/** *注意這個方法是沒有聲明 public 的,只能在包內部調用,因此用戶沒法直接調用. *並在這裏eventListenerFactory爲每一個 call 設置了監聽 **/
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
//私有構造方法,只能經過上面的靜態方法調用,這樣的好處是,防止框架使用者傳參有誤致使沒法正常使用
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
複製代碼
//request 也是經過構造者生成的, 整個請求的頭,行,體都從這裏傳入,並多了一個 tag 做爲標識
public static class Builder {
HttpUrl url;
String method;
Headers.Builder headers;
RequestBody body;
Object tag;//使用者能夠設置一個惟一標識,拿到 Request 的時候分不出是哪一個請求
//默認設定請求使用了 GET 方法,並傳入空 Headers
public Builder() {
this.method = "GET";
this.headers = new Headers.Builder();
}
}
複製代碼
url,
String method
經過方法url()
傳入,使用HttpUrl對 url 封裝,方便獲取 url 的各個部分信息。能夠直接調用get(),head(), post(body),delete(),put(body)
等設置 methord 並傳入 Body,也可使用method(String method,RequestBody body)
直接輸字符串,這個方法裏進行了校驗,傳參出錯拋出異常算法
Headers.Builder中使用 addLenient,add 添加一行 header,能夠是「key:value」這樣的字符串,也能夠是兩個參數 key,value。 調用 add 檢測中文或不可見字符會拋出異常,而addLenient 不會檢測。 還有一個 set 會覆蓋已經存在全部name同樣的值數組
Headers 中用一個數組保存數據,偶數位是 key,奇數位是 value,裏面的大部分方法是處理這個數組的。 除了用 buidler 生成header,還能夠調用靜態方法of
傳入一個 map 或者[key1,value1,key2,value2...]生成緩存
String get(String name) //獲取 key=name 的 value public List<String> values(String name) //和 get 方法相似,若是有多個 key=name 的條目返回全部的 value。(http 協議的header不限制順序,不排重) public Date getDate(String name)//內部調用get(name)並轉成時間 public int size() //header 的條數,保存了key、value數組的一半 public String name(int index) //獲取指定位置的 name,index相對於 header 的條數,不是數組的 index public String value(int index)//獲取指定位置的value,index相對於 header 的條數,不是數組的 index public Set<String> names()//獲取全部的 name,因爲是 set 重複的只算一條 public long byteCount() //整個 header 要佔用的長度,包含分割的「: 」和回車換行 public Map<String, List<String>> toMultimap()//把這個 Headers 轉成 map,因爲可能有多個相同的 key,value 要用 list返回 複製代碼
框架中兩個實現類:FormBody上傳普通的參數請求,MultipartBody上傳文件。RequestBody中三個靜態 create 方法傳入MediaType和ByteString、byte[]、File 知足了大部分請求須要,而且傳給MultipartBody。安全
若是須要自定義須要至少實現contentType,writeTo,分別傳入數據類型和寫入。若是有斷點續傳的要求複寫contentLength。服務器
public abstract MediaType contentType();
public long contentLength() throws IOException {
return -1;
}
public abstract void writeTo(BufferedSink sink) throws IOException;
複製代碼
/**出來下面三個主要方法的調用還有一些獲取 name 和 value 的方法,和 builder 裏添加name 和 value的方法,容易理解不作解釋。 **/
public final class FormBody extends RequestBody {
private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");
@Override public MediaType contentType() {
return CONTENT_TYPE;//返回了x-www-form-urlencoded的類型,適用於帶參數的接口請求
}
@Override public long contentLength() {
return writeOrCountBytes(null, true);//調用了writeOrCountBytes
}
@Override public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);//調用了writeOrCountBytes
}
private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {//只須要獲取 body 的長度,contentLength
buffer = new Buffer();
} else {
buffer = sink.buffer();//寫入數據 writeTo
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {
if (i > 0) buffer.writeByte('&');//第一個請求參數前不拼'&'
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');//拼接請求體
buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {//若是是 contentLength 調用須要返回長度
byteCount = buffer.size();
buffer.clear();
}
return byteCount;
}
複製代碼
MultipartBody和FormBody計算 body 長度和寫入數據的方法相似,可是MediaType類型比較多,看服務器須要哪一種類型的,咱們公司服務器是MediaType.parse("multipart/form-data")的,在 builder 中添加了多個part,writeTo的時候用boundary分割開。cookie
public static final class Builder {
private final ByteString boundary;//每一個 part 的分割線,基本上都是一個夠長的隨機串
private MediaType type = MIXED;//默認類型MediaType.parse("multipart/mixed")
private final List<Part> parts = new ArrayList<>();//保存了全部的 part
public Builder(); //隨機生成了一個boundary,沒有特殊需求不要修改
public Builder(String boundary); //自定義一個boundary
public Builder setType(MediaType type);//自定義類型
public Builder addPart(Headers headers, RequestBody body); //添加一個 part 若是上傳文件能夠自定義 headers, FormBody.create(MultipartBody.FORM, file)
public Builder addFormDataPart(String name, String value); //添加一個字符串的 body
/**添加普通 header 的文件. *@params name 是服務器接受 file 的字段名; *@params filename 本地的文件名 *@params body 傳FormBody.create(MultipartBody.FORM, new File(filePath)) **/
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) ;
複製代碼
使用字段 executed 防止一個 RealCall 調用 enqueue 或者 execute 屢次,調用 eventListener.callStart
開始請求,兩個方法都調用到了client.dispatcher()
(okHttpClient.Builder中設置見 源碼解析2) 裏,其中enqueue使用了NamedRunnable, 最終都調用到了getResponseWithInterceptorChain();
發送請求的核心方法。網絡
public abstract class NamedRunnable implements Runnable {
protected final String name;
//在子類AsyncCall的傳參是「OkHttp%s」,redactedUrl()這個方法最終生成了一個和請求url類似的string。
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);//拼接成一個字符串,標識okhttp的請求
}
/**在執行請求時,替換了線程名,子線程的名字多是dispatcher或者用戶定義的線程池設置的。調試更方便 *@methord execute 倒了個方法名,就是run方法。okhttp框架的方法名起得都很怪異,java原生 execute 是在origin thread調用 **/
@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
複製代碼
設計思路是使用RealInterceptorChain
保存了全部參數,相比舊版使用ApplicationInterceptorChain
調用HttpEngine
完成請求,新版依次調用攔截器interceptors
的list生成請求,並把框架使用者設置的攔截器插入進來,能夠在請求過程當中拿到並修改包含request和response的全部值,提升了擴展性,而且這種鏈式依次調用應該會更容易理解。
client.interceptors()、client.networkInterceptors()
(源碼分析2) 在OkHttpClient.Builder
裏傳入,分別在 創建鏈接前 和 request 寫入服務器前 注入。 networkInterceptors 這個名字很到位,代表了是在網絡鏈接以後的注入的攔截器,最終寫入數據到socket中並獲取服務器返回的流,造成了一條數據調用鏈。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());//OkHttpClient.Builder.addInterceptor(Interceptor interceptor)
interceptors.add(retryAndFollowUpInterceptor);//錯誤處理,和其餘的攔截器相比 這個攔截器先初始化了,這樣設計我以爲是處理 cancel 用的
interceptors.add(new BridgeInterceptor(client.cookieJar()));//拼接http協議 組裝header
interceptors.add(new CacheInterceptor(client.internalCache()));//緩存處理
interceptors.add(new ConnectInterceptor(client));//使用鏈接池創建鏈接
if (!forWebSocket) {//forWebSocket 在 httpclient.newCall時是 false
interceptors.addAll(client.networkInterceptors());//添加OkHttpClient.Builder.addNetworkInterceptor(Interceptor interceptor)
}
interceptors.add(new CallServerInterceptor(forWebSocket));//想服務器發起請求
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);//開始請求
}
複製代碼
Interceptor.Chain 它只有一個子類 RealInterceptorChain(舊版有兩個子類) 實際上就是一個保存了全部參數的類,裏面只有一個有用的方法 Response proceed(Request request)
。像是 Interceptor 對應的 bean,而且每個攔截器都有一個 chain。
/**鏈式調用下一個攔截器,直到CallServerInterceptor發出請求,不在調用proceed。 *@params index 標識調用到第幾個攔截器了,固然不會超出interceptors.size(),邏輯上看是不會拋出AssetionError的 *@params calls 標識和攔截器對應的Chain調用了幾回,若是用戶定義的interceptor調用了屢次 chain.proceed會拋出異常 **/
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {//connect不支持這個請求
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {//若是用戶定義的interceptor調用了屢次 chain.proceed會拋出異常
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
Interceptor interceptor = interceptors.get(index);//arraylist的下一個攔截器
Response response = interceptor.intercept(next);//這裏的 interceptor若是命名爲next更好理解一點
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {//請求已經完成了,確保proceed必須僅僅調用一次,若是沒有調用 index+1小於size,若是調用屢次,calls會大於1
throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {//已經走完了全部的攔截器,這時候必須有response
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {//response必須有相應體。分開判斷。提示不一樣的錯誤日誌,更好調試
throw new IllegalStateException( "interceptor " + interceptor + " returned a response with no body");
}
return response;
}
}
複製代碼
若是用戶沒有設置攔截器,第一個進入了RetryAndFollowUpInterceptor
,經過 followRedirects、followSslRedirects(源碼分析2)控制302,401等響應碼邏輯。
/**使用StreamAllocation找到一個可用的 connect,而且傳給後面的攔截器繼續處理, *處理完成生成了 response,經過響應碼判斷是否還須要後續請求, *若是須要後續請求,判斷StreamAllocation這個鏈接是否能夠複用或者 new 一個新的。 *@methord createAddress 多是由於變量太多,不容易經過方法傳遞弄個Address類,沒有任何邏輯 *@field canceled 若是取消請求了,這裏拋出了IOException異常 *@constant MAX_FOLLOW_UPS最多能夠重定向20 **/
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;//3.9版本只有這一中攔截器,若是用戶定義本身的攔截器要繼承RealInterceptorChain了
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
//處理流和鏈接池的類
streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace);
int followUpCount = 0;//重定向多少次了
Response priorResponse = null;//若是有重定向,記錄上一次返回值
while (true) {//若是不 return 或拋出異常 會一直重試下去
if (canceled) {//調用了 Call.cancel,若是這個 call 在子線程的隊列裏尚未發出去就能夠 cancel 掉
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;//記錄請求結果
boolean releaseConnection = true;//執行後面的攔截器的拋出的異常沒有抓到,是鏈接不到服務器或者服務器出錯,要釋放 StreamAllocation
try {//執行後續的攔截器(下面一一介紹),當全部的攔截器執行完,就有了 response
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
/* recover() 這個方法判斷若是鏈接出現問題是否能夠重試 *先判斷 Okhttpclient.retryOnConnectionFailure(), *在判斷是否能夠經過重試解決的Exception, *若是有更多的鏈接方案返回 true */
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();//路由認證失敗拋出異常
}
releaseConnection = false;
continue;//掉用了streamAllocation.hasMoreRoutes,去找下一個可用的 connection
} catch (IOException e) {
// 和RouteException相似,重試請求仍是拋出異常
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {//若是有請求過程當中拋出了異常,connection 不可用了,要釋放掉
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
if (priorResponse != null) {//若是重試了一次或屢次,要把第一個響應的除了 body 部分返回給調用者
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder().body(null).build())
.build();
}
Request followUp = followUpRequest(response);
if (followUp == null) {//根據響應碼判斷是否須要後續請求,不須要後續處理則返回 response
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
closeQuietly(response.body());//body 不須要複用給priorResponse,則關掉
if (++followUpCount > MAX_FOLLOW_UPS) {//若是從新發起請求次數超過20次就不在重試了
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {//返回來的 requestbody 繼承了 UnrepeatableRequestBody。
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
//經過方法sameConnection判斷是否是要請求同一個 url,若是不是統一要 new 一個新的StreamAllocation
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace);
} else if (streamAllocation.codec() != null) {//當前的streamAllocation沒有設置協議類型,正常不會進入到這裏
throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?");
}
request = followUp;//繼續請求這個
priorResponse = response;//保存此次請求的結果,須要裏面的 request 和 header
}
}
複製代碼
300-399之間的請求碼是重定向,須要從新請求,拼接到方法裏了,實際這部分代碼也能夠在 builder 裏暴露出來提供默認實現。注意第一次408超時,只會再重試1次 client.proxyAuthenticator().authenticate(route, userResponse);
參見源碼分析2
client.authenticator().authenticate(route, userResponse);
參見源碼分析2
//當請求失敗的時候,經過這個方法判斷狀態碼,是否是要繼續請求,這裏只作簡單的分析
private Request followUpRequest(Response userResponse) throws IOException {
..........
switch (responseCode) {
case HTTP_PROXY_AUTH://407 設置了代理,而且須要登陸代理才能繼續下面的訪問
Proxy selectedProxy = route != null ? route.proxy() : client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
//返回一個登陸代理的請求
return client.proxyAuthenticator().authenticate(route, userResponse);
case HTTP_UNAUTHORIZED://401服務器須要受權,這裏返回一個登陸服務器的請求
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
if (!client.followRedirects()) return null;//若是設置成不支持重定向直接返回當前 response。
String location = userResponse.header("Location");
if (location == null) return null;//這裏的判空很嚴謹,按理說300的響應碼都會包含這個字段
HttpUrl url = userResponse.request().url().resolve(location);
if (url == null) return null;
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {//判斷是否是要修改 body,除了 post、get 都須要
final boolean maintainBody = HttpMethod.redirectsWithBody(method);// 若是是PROPFIND須要保留 body,其餘的都不須要
if (HttpMethod.redirectsToGet(method)) {//PROPFIND須要把方法給爲 get
requestBuilder.method("GET", null);
} else {//根據maintainBody是否保留 body
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {//body 都不要了,這些 header 裏標識 body 信息的都不要了
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
if (!sameConnection(userResponse, url)) {//認證過一次就不要再次認證了
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT://請求超時,只重試一次
if (!client.retryOnConnectionFailure()) {
return null;
}
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
if (userResponse.priorResponse() != null && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
return null;//若是上次也是請求超時,此次仍是就不在重試了
}
return userResponse.request();
default:
return null;
}
}
複製代碼
根據body 中的信息設置 header 中的一些參數,好比數據長度,數據類型。
使用 OkHttpClient.Builder 設置過來的 CookieJar(源碼分析2) 接口設置 cookie,若是須要支持設置 cookie 請調用Builder cookieJar(CookieJar cookieJar)
,默認不支持cookie, 這樣設計多是考慮爲客戶端接口使用,基本不用 cookie,少了處理 cookie 的邏輯性能上更好一些。
其中 Connection:Keep-Alive 對 socket 鏈接緩存,socket內部會每隔一段時間檢測是否和服務器連通,好比斷網了,檢測不到服務器在線,發送 RST 包斷開鏈接。
請求頭中的 Host 是爲了讓服務器定位哪一個域名下的服務的,只有不多的大公司會一個 ip 下的主機部署一套域名的服務,每每是一臺主機承載了不少個域名的服務,http 的請求頭裏只有要請求的路徑。爲了區分把請求下發給哪一個域名須要在 header 設置 Host,框架中已經自動搞定了。你們能夠試試,不帶 host 的請求有的是能夠走通的,但大部分是會報錯的
gzip 框架內部是自動支持壓縮的,這樣能夠減小傳輸的數據量。注意若是框架使用者在 Header 中設置了Accept-Encoding:gzip,框架不會自動解壓,會把原始數據在 response 直接返回。保證響應內容和請求是對應的
public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {//若是是 post 請求,會設置 body
MediaType contentType = body.contentType();
if (contentType != null) {//請求體的數據類型,form 格式的 header 如 application/x-www-form-urlencoded
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {//數據長度,服務器根據數據長度讀取 body
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) {//設置 host,一個ip會對應幾個域名的狀況,服務器判斷header 裏的 host,判斷應該去哪一個域名下查找數據
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {//創建鏈接消耗性能,全部在服務器和客戶端都有緩存,要設置這個字段
requestBuilder.header("Connection", "Keep-Alive");
}
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {//使用 gzip 傳輸數據較少很大的數據量
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {//若是實現了 cookieJar 接口,會給 header 寫入 cookie
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {//設置了默認的 user-agent:okhttp/3.9.0
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());//繼續執行後續的請求,直到服務器返回數據
//這個方法裏只是調用了cookieJar.saveFromResponse(url, cookies);
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
//這裏給碼農返回的是傳進來的 request,並非添加了不少默認值以後的 request
Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);
/**處理 gzip 的邏輯,框架使用者並無設置 gzip 的,框架自動添加了壓縮邏輯; *而且服務器的響應頭說返回了 gzip 的數據; *這裏解析了 body 中的數據並返回了沒壓縮的數據。 *清楚 header 中的Encoding和Length,由於碼農拿到的是解壓後的數據 **/
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")//已經不是 gzip 壓縮以後的數據了
.removeAll("Content-Length")//解壓後數據長度變長了
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
複製代碼
主要處理了緩存邏輯,若是須要支持 Cache-Control、Pragma 響應頭使用 cache,可使用框架中自帶的 Cache 類OkHttpClient.Builder().cache(new Cache(file,maxSize)
。默認不支持緩存
InternalCache(源碼分析2) 獲取或者保存緩存的接口,這裏只處理存取的邏輯。
判斷緩存是否可用的邏輯在CacheStrategy中,CacheStrategy裏面對header進行了一系列的計算,這些計算並無使用接口暴露出來,這樣設計由於裏面徹底遵循了http協議,只要協議裏全部的header參數都處理了不會有任何其餘實現類,則不須要抽取爲接口
Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null? cache.get(chain.request()): null;//設置了InternalCache,根據請求獲取一個緩存的 response
long now = System.currentTimeMillis();//http 的協議須要根據當前時間是否判斷使用緩存
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();//判斷 header 的緩存有效期
Request networkRequest = strategy.networkRequest;//根據 header 判斷是否須要發起請求
Response cacheResponse = strategy.cacheResponse;//根據 header 判斷是否是使用緩存的 response
if (cache != null) {//記錄使用了這個更新,在 cache 中的實現只是對使用緩存狀況計數
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}//在 cache 中找到了 request 緩存的 response,可是根據 header 中的時間策略,這個 response 已經超時失效了
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {//request 的 cacheControl只容許使用緩存 onlyIfCached 爲 true,響應投中包含only-if-cached
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()//返回了緩存的 respose
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {//若是沒有緩存或者緩存失效,後面繼續請求網絡
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());//networkResponse是空確定是拋異常了,沒有返回值
}
}
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {//若是有緩存而且網絡返回了304,仍是要使用緩存數據
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);//更新緩存,主要是 header 變了
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);//更新緩存,header/body都變了
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {//不支持緩存方法,要清除掉緩存,post 不支持
try {
cache.remove(networkRequest);//
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
複製代碼
只是根據requestHeader對 CacheStrategy 須要的參數作解析,這裏叫 Factory 並不合適,由於它不是工廠,還不如弄個靜態方法parse,全部的邏輯處理都在get()
裏(下一個分析),若是對http協議緩存處理部分有必定了解,就不用看Factory的分析了
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;//當前時間,判斷當前客戶端的時間是否是能夠直接使用緩存,若是客戶端的時間是錯的,會對緩存邏輯有影響
this.request = request;//碼農的request
this.cacheResponse = cacheResponse;//上一次的緩存
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();//緩存的請求時間 在CallServerInterceptor中給這兩個字段賦值
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();//請求的響應時間
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {//拿到緩存的response的header,找裏面和緩存時間相關的值
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {//原始服務器消息發出的時間,若是服務器沒有返回這個字段就使用框架中保存的receivedResponseMillis。 Date:Tue,17 Apr 2017 06:46:28GMT
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {//緩存數據的過時時間 Expires: Fri, 30 Oct 2018 14:19:41
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {//請求的數據的最後修改時間,下次請求使用If-Modified-Since傳給服務器,若是數據沒變化,服務器返回304。Last-modified:Tue,17 Apr 2017 06:46:28GMT
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {//數據的惟一標識,下次請求使用If-None-Match傳給服務器,若是數據沒變化,服務器返回304。Etag:"a030f020ac7c01:1e9f"
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {//從原始服務器到代理緩存造成的估算多少秒 Age: 12
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
複製代碼
Factory.get()
主要是處理header中緩存的邏輯,判斷傳進來的cacheResponse是否是能夠直接使用,又分爲兩個主要方法。
getCandidate()
方法裏主要是計算緩存過時時間,還有一小部分根據cache設置requestHeader的邏輯,是主要的分析方法。 response.cacheControl()
方法對responseHeader服務器返回的字段(Cache-Control、Pragma)解析,把字符串轉換爲Bean,CacheControl裏也有一個Budiler,這裏有點雞肋了,實際這個只是在header中parse就行了,絕對用不到new Builder出來。邏輯並不複雜,有興趣本身看一下。
private CacheStrategy getCandidate() {
if (cacheResponse == null) {//若是沒有chache直接返回request策略,繼續請求網絡
return new CacheStrategy(request, null);
}
if (request.isHttps() && cacheResponse.handshake() == null) {//若是是https請求,要有證書認證的信息,若是沒有繼續請求網絡
return new CacheStrategy(request, null);
}
if (!isCacheable(cacheResponse, request)) {//這個方法里根據響應碼判斷響應的數據是否可使用緩存
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {//若是reseponse中nocache,或者If-Modified-Since、If-None-Match字段則進行網絡請求
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {//緩存的響應頭 Cache-Control的值爲immutable,標識永久可使用的緩存
return new CacheStrategy(null, cacheResponse);
}
long ageMillis = cacheResponseAge();//根據header中date、age和當前時間,計算cacheResponse距離當前時間已通過了多久了
/*根據maxage和expires計算緩存數據還能用多久時間, *還根據上一次修改時間lastModified距返回數據時間Date的差10%時間之內,HTTP RFC 建議這段時間內數據是有效的*/
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {//在computeFreshnessLifetime中用if elseif優先去了maxAge的時間,這裏的if判斷是多餘的
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {//響應頭的min-fresh 最小刷新間隔
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {//若是不是must-revalidate過時以後必須請求新數據,max-stale容許過時多少秒以後,仍然可使用緩存
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//上面把全部控制緩存時間的數據都準備好了,判斷一下緩存數據到當前的時間 和 緩存數據最大容許的過時時間 哪一個大
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {//尚未超過最大容許過時時間,可是已經超過了數據的有效期,數據過時後能夠不及時刷新,返回一個警告說數據是陳舊的
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {//緩存的數據已經超過24小時了,也給一個警告,http協議不推薦設置超過24小時的緩存
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());//緩存還有效直接把緩存扔給了碼農
}
// 客戶端不能決定緩存是否可使用,讓服務器去決定,那就須要帶一些參數,下面這些都是根據緩存頭中服務器返回的數據標識,設置本次請求頭應該帶的參數,服務器知道客戶端如今緩存是何時返回的
String conditionValue;
if (etag != null) {//注意這裏使用的是elseif,優先級依次是 etag-lastModifity-date
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);//給請求頭添加參數
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);//有緩存,可是判斷不出來是否能夠直接使用,讓服務器去決定,若是服務器返回304,就不用返回請求體了,服務器決定使用緩存
}
複製代碼
在intercept中使用鏈接池建立連接,並根據協議獲取對應的HttpCodec的實現類,主要邏輯都在newStream、connection這兩個方法中。
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);//http 協議格式的接口,直接影響了協議版本
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
複製代碼
newStream 初始化connectTimeout、readTimeout、writeTimeout等全局變量,並調用findHealthyConnection找到一個可用的連接 RealConnection,在方法內 while (true) 從鏈接池裏找到沒有被佔用successCount=0,而且沒有close的連接,關鍵方法是findConnection。
/**找到一個connect。**/
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;//是否找到了可用的RealConnection
RealConnection result = null;//和foundPooledConnection對應,將要使用的connect
Route selectedRoute = null;//生成RealConnection用到的Address、Proxy存到這個類中
Connection releasedConnection;//將要釋放的連接
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
releasedConnection = this.connection;//默認全局保存的connection是不可用的了,須要釋放,從newStream進來是null
toClose = releaseIfNoNewStreams();//判斷connection是否須要釋放,若是已經不可用把connection置空,並返回sockte準備釋放
if (this.connection != null) {//通過上面releaseIfNoNewStreams的方法判斷,connection還可用沒有置空
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {//沒有須要釋放的 鏈接
releasedConnection = null;
}
if (result == null) {//不能直接使用connection,從緩存池裏去一個,這裏route是空,先當沒有代理處理
Internal.instance.get(connectionPool, address, this, null);//裏面調用了acquire,若是緩存池裏有可用的,就賦值給connection
if (connection != null) {//使用了緩存池中的 connect 不用從新建立。
foundPooledConnection = true;
result = connection;
} else {//緩存池中沒取到,須要從新建立鏈接,若是有代理,使用route取一個有代理的試試
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (releasedConnection != null) {//釋放掉不可用的
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {//上面已經從鏈接池緩存中找到了可用的
eventListener.connectionAcquired(call, result);
}
if (result != null) {
return result;
}
boolean newRouteSelection = false;//緩存中沒有返回一個可用的RealConnection,須要生成一個
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {//判斷剛纔的全局保存的route是否爲空,若是尚未賦值 就使用routeSelector找一個
newRouteSelection = true;
routeSelection = routeSelector.next();//routeSelector中保存了全部的代理(可能多個),從中找到一個可用的並調用 dns,找到要鏈接的 ip,準備創建 socket 鏈接
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {//Selection中返回了全部可用的route,從頭日後找,越靠前的代理連通性越高
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);//掉用了connectPool.get在方法isEligible中判斷是否有和 address、route對應的緩存 Connection,若是有調用 StreamAllocation.acquire給全局變量connection賦值
if (connection != null) {//上面的Internal.instance.get取到了緩存的 connection直接返回
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {//沒有取到緩存
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);//緩存中沒有,只能建立一個新的
acquire(result, false);
}
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);//若是緩存中取到了,調用監聽,這個方法應該在上面的 if (!foundPooledConnection) 前面就好理解了
return result;//和上面的 if 比 這裏 return 了,不會建立 socket鏈接了
}
//這裏是調用socket.connect的主要方法,並調用connectTls方法對 https進行 Handshake並調用 HostnameVerifier.verify 驗證證書, 只有新建立的纔會進入這裏,在RealConnection中會詳細分析
result.connect( connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {//把這個RealConnection緩存到鏈接池裏
reportedAcquired = true;
Internal.instance.put(connectionPool, result);
if (result.isMultiplexed()) {//是http2.0協議
socket = Internal.instance.deduplicate(connectionPool, address, this);//調用了releaseAndAcquire把connection從新賦值,並釋放舊的socket
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
複製代碼
HostnameVerifier.verify驗證tls證書 (源碼分析2)
public void connect(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();//選擇https的算法用
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {//判斷 ssl 的安全性,不支持 CLEARTEXT
throw new RouteException(new UnknownServiceException("CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException("CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
while (true) {
try {
if (route.requiresTunnel()) {//若是是https 而且含有代理,須要先建立代理通道
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);//先發出一個請求建立通道
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {//只建立一個socket
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, call, eventListener);//判斷http協議版本,連接socket,並創建 https 使用的加密通道,使用HostnameVerifier.verify驗證tls證書
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {//建立連接失敗,釋放資源
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
if (route.requiresTunnel() && rawSocket == null) {//連接太多了
ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: " + MAX_TUNNEL_ATTEMPTS);
throw new RouteException(exception);
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();//限制http2.0同一個connection的最大的鏈接數
}
}
}
複製代碼
已經在上面的步驟建立好了鏈接,下面就能夠直接寫入數據了,使用Http1Codec按照 http1.1的格式寫入數據到 socket 中,使用Http2Codec按照 http2的格式寫入數據,注意這裏返回的 Response持有的 Body是已流的形式存在的,使用完後必定要調用 close,不然鏈接池等資源不能釋放。
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();//在ConnectInterceptor中創建鏈接後賦值,
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);//把 request 中的 header按照 http 協議的各個版本拼接好寫入到 socket 中
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
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.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();//若是 get 請求數據大於1k 當前請求不能把數據所有寫入,須要分次
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());//這個 listener的位置在服務器響應以前,並非已經開始讀取 response 了
responseBuilder = httpCodec.readResponseHeaders(false);//讀取響應頭
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();//生成了請求的結果
realChain.eventListener() .responseHeadersEnd(realChain.call(), response);//在responseHeadersStart和responseHeadersEnd之間包含了讀取響應頭和服務器處理請求的時間
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))//讀取響應體到FixedLengthSource 或者ChunkedSource 中 使用完必須調用調用close,不然資源不能釋放。
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();//響應頭中包含了close須要釋放鏈接
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {//20四、205不須要返回請求體
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;//已經完成了返回了此次請求的響應
複製代碼
add by dingshaoran