嗨,我是哈利迪~《看完不忘系列》之okhttp(樹干篇)一文對okhttp的請求流程做了初步介紹,本文將對他的一些實(shí)現(xiàn)細(xì)節(jié)和相關(guān)網(wǎng)絡(luò)知識進(jìn)行補(bǔ)充。
本文約2000字,閱讀大約5分鐘酝掩。
源碼基于3.14.9,即java版本的最新版
推薦閱讀「查缺補(bǔ)漏」鞏固你的HTTP知識體系眷柔,常用的概念都在了期虾,由于目前用的比較多的還是http 1.1原朝,所以下面分析會跳過http2,以http 1.1為主镶苞。
cache
強(qiáng)緩存:Cache-Control(maxAge過期時(shí)長)喳坠、Expires(過期時(shí)間);
協(xié)商緩存:etag(唯一標(biāo)識)宾尚、lastModified(最后修改時(shí)間)丙笋。
緩存優(yōu)先級:Cache-Control > Expires > etag > lastModified,從樹干篇中可知煌贴,在CacheInterceptor
攔截器中會從磁盤取出緩存的Response(如果有)御板,然后在CacheStrategy.Factory
中,解析緩存的Response來得到緩存策略CacheStrategy
牛郑,
//CacheStrategy.Factory.java
CacheStrategy getCandidate() {
//1.強(qiáng)緩存
//計(jì)算Age
long ageMillis = cacheResponseAge();
//根據(jù)Response的Date和Age怠肋,計(jì)算新鮮度
long freshMillis = computeFreshnessLifetime();
//新鮮度符合要求,返回策略淹朋,走強(qiáng)緩存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
return new CacheStrategy(null, builder.build());
}
//2.協(xié)商緩存
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
//etag唯一標(biāo)識
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
//最后修改時(shí)間
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
//特殊處理:把Response接收時(shí)間設(shè)置為最后修改時(shí)間
conditionValue = servedDateString;
} else {
//啥參數(shù)都沒有笙各,返回策略,cacheResponse為null
return new CacheStrategy(request, null);
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
//header添加行
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
//Request設(shè)置該header
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
強(qiáng)緩存內(nèi)部細(xì)節(jié)础芍,
//CacheStrategy.Factory.java
//強(qiáng)緩存
long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {
//返回CacheControl的maxAge杈抢,即過期時(shí)長
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {
//返回過期時(shí)間expires減接收時(shí)間served的差值
long servedMillis = servedDate != null
? servedDate.getTime()
: receivedResponseMillis;
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null
&& cacheResponse.request().url().query() == null) {
//特殊處理:RFC建議:文檔的最長期限應(yīng)默認(rèn)為提供文檔時(shí)的期限的10%
long servedMillis = servedDate != null
? servedDate.getTime()
: sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
本地磁盤緩存了Response的頭信息文件和data文件,頭信息如下(借玩安卓API一用~)仑性,
看看抓包數(shù)據(jù)惶楼,請求可見okhttp自動幫我們加上了gzip壓縮(具體支不支持還得看后端接口),
響應(yīng)可見Cache-Control是private(不是max-age=xxx)诊杆,Expires是1970年(沒做支持)歼捐,所以這個(gè)get請求不走強(qiáng)緩存;
然后etag和lastModified也沒有晨汹,getCandidate方法會嘗試把Response接收時(shí)間設(shè)置為最后修改時(shí)間
即If-Modified-Since=servedDateString豹储,再抓一次可見時(shí)間被帶上了,
不過由于這個(gè)接口沒做支持淘这,帶上If-Modified-Since也沒用剥扣,接口直接返回200(整個(gè)Response)而不是304(緩存可用),所以協(xié)商緩存也沒走铝穷,即其實(shí)每次請求都會返回完整的Response朦乏,磁盤緩存Response的data并沒有被用上。
要是在面試官前吹:“我做的玩安卓App,用了okhttp吃引,他強(qiáng)大的緩存機(jī)制可以為用戶提速筹陵、節(jié)省流量”刽锤,是會被吊打的!
緩存體系需要客戶端和后端共建朦佩,不然okhttp也有心無力并思。(當(dāng)然,客戶端也可以在okhttp外自行實(shí)現(xiàn)一層緩存语稠,那就另說了)
connection
ConnectInterceptor
攔截器中會獲取和建立連接宋彼,
- 發(fā)射器創(chuàng)建交換器:transmitter.newExchange、
- 交換尋找器find連接:exchangeFinder.find仙畦、findHealthyConnection输涕、findConnection、
- 有分配好的連接可用慨畸,return
- 從連接池里找到池化的連接莱坎,return
- 創(chuàng)建連接,進(jìn)行socket連接
一個(gè)連接池有多個(gè)連接寸士,一個(gè)連接可以同時(shí)處理多個(gè)發(fā)射器檐什,下面看建立連接,
//RealConnection.java
void connect(...) {
if (route.requiresTunnel()) {
//如果此路由通過HTTP代理隧道HTTPS弱卡,忽略
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
break;
}
} else {
//默認(rèn)沒代理乃正,走這里
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
//建立協(xié)議
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
}
void connectSocket(...) throws IOException {
//判斷android平臺或java平臺,進(jìn)行連接婶博,最終調(diào)了socket.connect
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
}
void establishProtocol(...){
//...忽略了一些http2相關(guān)內(nèi)容
//創(chuàng)建SSLSocket瓮具、進(jìn)行tls握手
connectTls(connectionSpecSelector);
}
socket連上后,會創(chuàng)建SSLSocket進(jìn)行tls握手凡蜻,
//RealConnection.java
void connectTls(...){
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
SSLSocket sslSocket = null;
//創(chuàng)建SSLSocket
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true);
//進(jìn)行tls握手
sslSocket.startHandshake();
socket = sslSocket;
}
route和dns
在ConnectInterceptor
創(chuàng)建連接時(shí)搭综,會用RouteSelector
來選擇路線,
連接池維護(hù)了一個(gè)RouteDatabase
來記錄ip黑名單划栓,可以記錄最近連接失敗過的ip地址兑巾,在RouteSelector
中則會優(yōu)先選擇不在黑名單中的ip,
//RouteSelector.java
Selection next() throws IOException {
List<Route> routes = new ArrayList<>();
//遍歷代理忠荞,默認(rèn)有一個(gè)代理是DIRECT蒋歌,即不代理
while (hasNextProxy()) {
Proxy proxy = nextProxy();
//遍歷ip
for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
Route route = new Route(address, proxy, inetSocketAddresses.get(i));
if (routeDatabase.shouldPostpone(route)) {
//如果該ip在黑名單中,放進(jìn)推遲使用的列表
postponedRoutes.add(route);
} else {
//不在黑名單的ip
routes.add(route);
}
}
if (!routes.isEmpty()) {
//找到可用的ip就跳出
break;
}
}
if (routes.isEmpty()) {
//沒找到可用ip委煤,才把黑名單的ip拿來用
routes.addAll(postponedRoutes);
postponedRoutes.clear();
}
return new Selection(routes);
}
可見堂油,如果一個(gè)域名配了多個(gè)ip,當(dāng)某個(gè)ip不穩(wěn)定時(shí)(連接失敗過)碧绞,之后就會跳過而優(yōu)先使用更穩(wěn)定的ip府框。(不過RouteDatabase
只是簡單地基于內(nèi)存實(shí)現(xiàn),用Set記錄讥邻,App重啟黑名單就沒了)
nextProxy中迫靖,dns把域名解析成對應(yīng)ip院峡,默認(rèn)實(shí)現(xiàn)走的是InetAddress.getAllByName(hostname)
,
interface Dns {
Dns SYSTEM = hostname -> {
if (hostname == null) throw new UnknownHostException("hostname == null");
//默認(rèn)實(shí)現(xiàn)
return Arrays.asList(InetAddress.getAllByName(hostname));
};
List<InetAddress> lookup(String hostname) throws UnknownHostException;
}
有時(shí)有些數(shù)據(jù)對安全性要求不高(不需要https)系宜,或者我們要在內(nèi)網(wǎng)調(diào)試照激,可以直接換成ip訪問來省去域名解析的時(shí)間,
builder.dns(new MyDns());
class MyDns implements Dns {
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
if (hostname == null) throw new UnknownHostException("hostname == null");
if (mUseDebugIp) {//使用內(nèi)網(wǎng)ip進(jìn)行調(diào)試
return getDebugIp();
}
if (useConfigIp(hostname)) {//使用服務(wù)端下發(fā)的ip表盹牧,跳過域名解析
return getIpByConfig(hostname);
}
//走默認(rèn)實(shí)現(xiàn)俩垃,老老實(shí)實(shí)的進(jìn)行域名解析
return Dns.SYSTEM.lookup(hostname);
}
}
cookie
在BridgeInterceptor
攔截器中會自動從CookieJar
里存取Cookie
、默認(rèn)的CookieJar
是空實(shí)現(xiàn)汰寓,需要用OkHttpClient自行配置口柳,
builder.cookieJar(new MyCookieJar());
//基于內(nèi)存實(shí)現(xiàn)的cookieJar(通常是基于磁盤)
class MyCookieJar implements CookieJar {
private Map<String, List<Cookie>> mCookieMap = new HashMap<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
mCookieMap.put(url.host(), cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = mCookieMap.get(url.host());
return null == cookies ? Collections.emptyList() : cookies;
}
}
tls
默認(rèn)支持不加密、tls 1.2踩寇、tls 1.3啄清,
//OkHttpClient.java
final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);//tls、不加密
//ConnectionSpec.java
final ConnectionSpec MODERN_TLS = new Builder(true)
.cipherSuites(APPROVED_CIPHER_SUITES)
//1.2和1.3
.tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
.supportsTlsExtensions(true)
.build();
eventListener
在樹干篇提到俺孙,EventListener
是航班狀態(tài)監(jiān)聽辣卒,因?yàn)樗櫫苏麄€(gè)請求流程,通過他可以看到每個(gè)環(huán)節(jié)的數(shù)據(jù)和耗時(shí)睛榄,引用官方圖片荣茫,
打印日志,
class PrintingEventListener extends EventListener {
private long callStartNanos;
private static final String TAG = "PrintingEventListener";
private void printEvent(String name) {
long nowNanos = System.nanoTime();
if (name.contains("callStart")) {
callStartNanos = nowNanos;
}
long elapsedNanos = nowNanos - callStartNanos;
Log.e(TAG, String.format("%.3f %s%n", elapsedNanos / 1000000000d, name));
}
public void callStart(Call call) {
printEvent("callStart url = " + call.request().url());
}
public void callEnd(Call call) {
printEvent("callEnd");
}
public void dnsStart(Call call, String domainName) {
printEvent("dnsStart domainName = " + domainName);
}
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
printEvent("dnsEnd");
}
//...
}
可見第二次請求省去了域名解析场靴、建立連接啡莉、tls握手的環(huán)節(jié),