1 介紹
在我們所處的互聯(lián)網(wǎng)世界中遇革,HTTP協(xié)議算得上是使用最廣泛的網(wǎng)絡(luò)協(xié)議铣除。OKHttp是一款高效的HTTP客戶端,支持同一地址的鏈接共享同一個socket横殴,通過連接池來減小響應(yīng)延遲,還有透明的GZIP壓縮卿拴,請求緩存等優(yōu)勢衫仑。如果您的服務(wù)器配置了多個IP地址,當(dāng)?shù)谝粋€IP連接失敗的時候堕花,OkHttp會自動嘗試下一個IP文狱。OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題。
值得一提的是:Android4.4原生的HttpUrlConnection底層已經(jīng)替換成了okhttp實現(xiàn)了缘挽。
public final class URL implemen ts Serializable { public URLConnection openC onnection() throws IOException { return This.handler.openConnection(this); }}
這個handler瞄崇,在源碼中判斷到如果是HTTP協(xié)議,就會創(chuàng)HtppHandler:
public final class HttpHandler extends URLStreamHandler {
@Override protected URLConnection openConnection(URL url) throws IOException {
// 調(diào)用了OKHttpClient()的方法
return new OkHttpClient().open(url); }
@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
return new OkHttpClient().setProxy(proxy).open(url); }
@Override protected int getDefaultPort() {
return 80;
}
}
2 基本使用方式
在OKHttp壕曼,每次網(wǎng)絡(luò)請求就是一個Request
苏研,我們在Request里填寫我們需要的url,header等其他參數(shù)腮郊,再通過Request構(gòu)造出Call
摹蘑,Call內(nèi)部去請求服務(wù)器,得到回復(fù)轧飞,并將結(jié)果告訴調(diào)用者衅鹿。同時okhttp提供了同步和異步兩種方式進行網(wǎng)絡(luò)操作。
2.1 同步
OkHttpClient client = new OkHttpClient();String run(String url) throws IOException {
Request request = new Request.Builder() .url(url) .build();
Response response = client.newCall(request).execute();
return response.body().string();
}
直接execute執(zhí)行得到Response过咬,通過Response可以得到code,message等信息恢着。android本身是不允許在UI線程做網(wǎng)絡(luò)請求操作源哩,需要在子線程中執(zhí)行。
2.2 異步
Request request = new Request.Builder() .url("http://www.baidu.com") .build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, IOException e) {}
@Override public void onResponse(Response response) throws IOException {
//NOT UI Thread
if(response.isSuccessful()){
System.out.println(response.code());
System.out.println(response.body().string());
}
}
});
在同步的基礎(chǔ)上講execute改成enqueue,并且傳入回調(diào)接口磅崭,但接口回調(diào)回來的代碼是在非UI線程的,因此如果有更新UI的操作必須切到主線程合呐。
3 整體結(jié)構(gòu)
3.1 處理網(wǎng)絡(luò)響應(yīng)的攔截器機制
無論是同步的call.execute()
還是異步的call.enqueue()
板乙,最后都是殊途同歸地走到
call.getResponseWithInterceptorChain(boolean forWebSocket)方法。
private Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!retryAndFollowUpInterceptor.isForWebSocket()) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor( retryAndFollowUpInterceptor.isForWebSocket()));
Interceptor.Chain chain = new RealInterceptorChain( interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest); }
可以發(fā)現(xiàn)okhttp在處理網(wǎng)絡(luò)響應(yīng)時采用的是攔截器機制具篇。okhttp用ArrayList對interceptors進行管理纬霞,interceptors將依次被調(diào)用。
如上圖:
橙色框內(nèi)是okhttp自帶的Interceptors的實現(xiàn)類驱显,它們都是在call.getResponseWithInterceptorChain()
中被添加入 InterceptorChain中诗芜,實際上這幾個Interceptor都是在okhttp3后才被引入瞳抓,它們非常重要,負責(zé)了重連伏恐、組裝請求頭部孩哑、讀/寫緩存、建立socket連接翠桦、向服務(wù)器發(fā)送請求/接收響應(yīng)的全部過程横蜒。
在okhttp3之前,這些行為都封裝在HttpEngine類中销凑。okhttp3之后丛晌,HttpEngine已經(jīng)被刪去,取而代之的是這5個Interceptor斗幼,可以說一次網(wǎng)絡(luò)請求中的細節(jié)被解耦放在不同的Interceptor中澎蛛,不同Interceptor只負責(zé)自己的那一環(huán)節(jié)工作(對Request或者Response進行獲取/處理),使得攔截器模式完全貫穿整個網(wǎng)絡(luò)請求蜕窿。
用戶可以添加自定義的Interceptor瓶竭,okhttp把攔截器分為應(yīng)用攔截器和網(wǎng)絡(luò)攔截器:
public class OkHttpClient implements Cloneable, Call.Factory { final List<Interceptor> interceptors; final List<Interceptor> networkInterceptors; ...... }
調(diào)用OkHttpClient.Builder的addInterceptor()
可以添加應(yīng)用攔截器,只會被調(diào)用一次渠羞,可以處理網(wǎng)絡(luò)請求回來的最終Response
調(diào)用addNetworkInterceptor()
可以添加network攔截器斤贰,處理所有的網(wǎng)絡(luò)響應(yīng)(一次請求如果發(fā)生了redirect ,那么這個攔截器的邏輯可能會被調(diào)用兩次)
Interceptor解析
由上面的分析可以知道次询,okhttp框架內(nèi)自帶了5個Interceptor的實現(xiàn):
RetryAndFollowUpInterceptor荧恍,重試那些失敗或者redirect的請求。
BridgeInterceptor屯吊,請求之前對響應(yīng)頭做了一些檢查送巡,并添加一些頭,然后在請求之后對響應(yīng)做一些處理(gzip解壓or設(shè)置cookie)盒卸。
CacheInterceptor骗爆,根據(jù)用戶是否有設(shè)置cache,如果有的話蔽介,則從用戶的cache中獲取當(dāng)前請求的緩存摘投。
ConnectInterceptor,復(fù)用連接池中的連接虹蓄,如果沒有就與服務(wù)器建立新的socket連接犀呼。
CallServerInterceptor,負責(zé)發(fā)送請求和獲取響應(yīng)薇组。
下圖是在Interceptor Chain中的數(shù)據(jù)流:
官方文檔關(guān)于Interceptor的解釋是:
Observes, modifies, and potentially short-circuits requests going out and the corresponding responses coming back in. Typically interceptors add, remove, or transform headers on the request or response.通過Interceptors
可以 觀察外臂,修改或者攔截請求/響應(yīng)。一般攔截器添加律胀,刪除或修改 請求/響應(yīng)的header宋光。
Interceptor是一個接口貌矿,里面只有一個方法:
public interface Interceptor { Response intercept(Chain chain) throws IOException;}
實現(xiàn)Interceptor需要注意兩點(包括源碼內(nèi)置的Interceptor也是嚴(yán)格遵循以下兩點):
- 通過intercept()方法里的Chain
參數(shù)可以拿到request,這樣子就可以對request進行統(tǒng)一的修改(例如BridgeInterceptor對所有request的頭部進行了設(shè)置)罪佳,或者根據(jù)request去做一些事情逛漫。 - 在intercept()方法中通過chain.proceed(request)
得到Response,從而攔截了網(wǎng)絡(luò)響應(yīng)進行修改菇民,或者根據(jù)response去做一些事情。
** 關(guān)鍵代碼 **
以下是HTTP客戶端向服務(wù)器發(fā)送報文的過程:
- 從URL中解析出服務(wù)器的IP地址和端口號
- 在客戶端和服務(wù)器之間建立一條TCP/IP連接
- 開始傳輸HTTP報文
HTTP是個應(yīng)用層協(xié)議投储。HTTP無需操心網(wǎng)絡(luò)通信的具體細節(jié)第练;它把聯(lián)網(wǎng)的細節(jié)都交給了通用、可靠的因特網(wǎng)傳輸協(xié)議TCP/IP玛荞。TCP/IP隱藏了各種網(wǎng)絡(luò)和硬件的特點及弱點娇掏,使各種類型的計算機和網(wǎng)絡(luò)都能夠進行可靠的通信。簡單來說勋眯,HTTP協(xié)議位于TCP的上層婴梧。HTTP使用TCP來傳輸其報文數(shù)據(jù)。
如果你使用okhttp請求一個URL客蹋,具體的工作如下:
框架使用URL和配置好的OkHttpClient創(chuàng)建一個address塞蹭。此地址指定我們將如何連接到網(wǎng)絡(luò)服務(wù)器。
框架通過address從連接池中取回一個連接讶坯。
如果沒有在池中找到連接番电,ok會選擇一個route嘗試連接。這通常意味著使用一個DNS請求辆琅, 以獲取服務(wù)器的IP地址漱办。如果需要,ok還會選擇一個TLS版本和代理服務(wù)器婉烟。
如果獲取到一個新的route娩井,它會與服務(wù)器建立一個直接的socket連接、使用TLS安全通道(基于HTTP代理的HTTPS)似袁,或直接TLS連接洞辣。它的TLS握手是必要的。
開始發(fā)送HTTP請求并讀取響應(yīng)昙衅。
如果有連接出現(xiàn)問題屋彪,OkHttp將選擇另一條route,然后再試一次绒尊。這樣的好處是當(dāng)服務(wù)器地址的一個子集不可達時畜挥,OkHttp能夠自動恢復(fù)。而且當(dāng)連接池過期或者TLS版本不受支持時婴谱,這種方式非常有用蟹但。一旦響應(yīng)已經(jīng)被接收到躯泰,該連接將被返回到池中,以便它可以在將來的請求中被重用华糖。連接在池中閑置一段時間后麦向,它會被趕出。
下面就說說這五個步驟的關(guān)鍵代碼:
4.1 建立連接 —— ConnectInterceptor
上面所述前四個步驟都在ConnectInterceptor中客叉。HTTP是建立在TCP協(xié)議之上诵竭,HTTP協(xié)議的瓶頸及其優(yōu)化技巧都是基于TCP協(xié)議本身的特性。比如TCP建立連接時也要在第三次握手時才能捎帶 HTTP 請求報文兼搏,達到真正的建立連接卵慰,但是這些連接無法復(fù)用會導(dǎo)致每次請求都經(jīng)歷三次握手和慢啟動。正是由于TCP在建立連接的初期有慢啟動(slow start)的特性佛呻,所以連接的重用總是比新建連接性能要好裳朋。
而okhttp的一大特點就是通過連接池來減小響應(yīng)延遲。如果連接池中沒有可用的連接吓著,則會與服務(wù)器建立連接鲤嫡,并將socket的io封裝到HttpStream(發(fā)送請求和接收response)中,這些都在ConnectInterceptor中完成绑莺。具體在StreamAllocation.findConnection()
方法中暖眼,下面是具體邏輯:
/**
* Returns a connection to host a new stream. This prefers the existing connection if it exists, * then the pool, finally building a new connection. */
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
Route selectedRoute; synchronized (connectionPool) {
...... // Attempt to get a connection from the pool.
RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);// 1 ......
if (selectedRoute == null) {
selectedRoute = routeSelector.next();//2 ......
}
RealConnection newConnection = new RealConnection(selectedRoute);//3 ......
synchronized (connectionPool) {//4
Internal.instance.put(connectionPool, newConnection); this.connection = newConnection;
if (canceled) throw new IOException("Canceled");
}
newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(), connectionRetryEnabled);//5
return newConnection;
}
下面具體說說每一步做了什么:
線程池中取得連接
RealConnection pooledConnection = pool.get(address, streamAllocation)
//StreamAllocation.java
RealConnection get(Address address, StreamAllocation streamAllocation) {
for (RealConnection connection : connections) {
if (connection.allocations.size() < connection.allocationLimit && address.equals(connection.route().address)//根據(jù)url來命中connection && !connection.noNewStreams) {
streamAllocation.acquire(connection);//將可用的連接放入 return connection; } } return null; }
如果selectedRoute為空,則選擇下一條路由RouteselectedRoute = routeSelector.next();
//RouteSelector.java public final class RouteSelector { public Route next() throws IOException { // Compute the next route to attempt. if (!hasNextInetSocketAddress()) { if (!hasNextProxy()) { if (!hasNextPostponed()) { throw new NoSuchElementException(); } return nextPostponed(); } lastProxy = nextProxy(); } lastInetSocketAddress = nextInetSocketAddress(); // Route route = new Route(address, lastProxy, lastInetSocketAddress); if (routeDatabase.shouldPostpone(route)) { postponedRoutes.add(route); // We will only recurse in order to skip previously failed routes. They will be tried last. return next(); } return route; } private Proxy nextProxy() throws IOException { if (!hasNextProxy()) { throw new SocketException("No route to " + address.url().host() + "; exhausted proxy configurations: " + proxies); } Proxy result = proxies.get(nextProxyIndex++); resetNextInetSocketAddress(result); return result; } private void resetNextInetSocketAddress(Proxy proxy) throws IOException { ...... List<InetAddress> addresses = address.dns().lookup(socketHost); //調(diào)用dns查詢域名對應(yīng)的ip ... } }`
瀏覽器需要知道目標(biāo)服務(wù)器的 IP地址和端口號 才能建立連接纺裁。將域名解析為 IP地址 的這個系統(tǒng)就是 DNS罢荡。
以前面創(chuàng)建的route為參數(shù)新建一個RealConnectionRealConnection newConnection = new RealConnection(selectedRoute);
public RealConnection(Route route) { this.route = route; }
添加到連接池
`public final class ConnectionPool { void put(RealConnection connection) { assert (Thread.holdsLock(this)); if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); //這里很重要,把閑置超過keepAliveDurationNs時間的connection從連接池中移除对扶。 //具體細節(jié)看ConnectionPool 的cleanupRunnable里的run()邏輯 } connections.add(connection); } }`
調(diào)用RealConnection的connect()方法区赵,實際上是buildConnection()
構(gòu)建連接。
`//RealConnection.javaprivate void buildConnection(int connectTimeout, int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException { connectSocket(connectTimeout, readTimeout); //建立socket連接establishProtocol(readTimeout, writeTimeout, connectionSpecSelector); }`
調(diào)用connectSocket連接socket浪南。調(diào)用establishProtocol根據(jù)HTTP協(xié)議版本做一些不同的事情:SSL握手等等笼才。
重點來了!connectSocket(connectTimeout, readTimeout);
里的邏輯實際上是:
`public final class RealConnection extends FramedConnection.Listener implements Connection { public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout) throws IOException { socket.connect(address, connectTimeout); //Http是基于TCP的络凿,自然底層也是建立了socket連接 ... source = Okio.buffer(Okio.source(rawSocket)); sink = Okio.buffer(Okio.sink(rawSocket)); //用Okio封裝了socket的輸入和輸出流 }``
public final class Okio { public static Source source(Socket socket) throws IOException { if(socket == null) { throw new IllegalArgumentException("socket == null"); } else { AsyncTimeout timeout = timeout(socket); Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout); return timeout.source(source); } } public static Sink sink(Socket socket) throws IOException { if(socket == null) { throw new IllegalArgumentException("socket == null"); } else { AsyncTimeout timeout = timeout(socket); Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout); return timeout.sink(sink); } } }`
`構(gòu)建HttpStream
resultConnection.socket().setSoTimeout(readTimeout); resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS); resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS); resultStream = new Http1xStream( client, this, resultConnection.source, resultConnection.sink);`
至此骡送,HttpStream就構(gòu)建好了,通過它可以發(fā)送請求和接收response絮记。
4.2 發(fā)送request/接收Response —— CallServerInterceptor
CallServerInterceptor的intercept()
方法里 負責(zé)發(fā)送請求和獲取響應(yīng)摔踱,實際上都是由HttpStream
類去完成具體的工作。
Http1XStream
一個socket連接用來發(fā)送HTTP/1.1消息怨愤,這個類嚴(yán)格按照以下生命周期:
writeRequestHeaders()
發(fā)送request header
打開一個sink
來寫request body派敷,然后關(guān)閉sink
readResponseHeaders()讀取response頭部
打開一個source
來讀取response body,然后關(guān)閉source
4.2.1 writeRequest
HTTP報文是由一行一行的簡單字符串組成的,都是純文本篮愉,不是二進制代碼腐芍,可以很方便地進行讀寫。
public final class Http1xStream implements HttpStream { /** Returns bytes of a request header for sending on an HTTP transport. */ public void writeRequest(Headers headers, String requestLine) throws IOException { if (state != STATE_IDLE) throw new IllegalStateException("state: " + state); sink.writeUtf8(requestLine).writeUtf8("\r\n"); for (int i = 0, size = headers.size(); i < size; i++) { sink.writeUtf8(headers.name(i)) .writeUtf8(": ") .writeUtf8(headers.value(i)) .writeUtf8("\r\n"); } sink.writeUtf8("\r\n"); state = STATE_OPEN_REQUEST_BODY; }}public final class Headers { private final String[] namesAndValues; /** Returns the field at {@code position}. */ public String name(int index) { return namesAndValues[index * 2]; } /** Returns the value at {@code index}. */ public String value(int index) { return namesAndValues[index * 2 + 1]; }}`
![](http://upload-images.jianshu.io/upload_images/5827906-1a1ffde2e913d01a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)debug_write_request.png
`4.2.2 readResponse
public final class Http1xStream implements HttpStream {//讀取Response Header public Response.Builder readResponse() throws IOException { ...... while (true) { StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());//1 從InputStream上讀入一行數(shù)據(jù) Response.Builder responseBuilder = new Response.Builder() .protocol(statusLine.protocol) .code(statusLine.code) .message(statusLine.message) .headers(readHeaders()); if (statusLine.code != HTTP_CONTINUE) { state = STATE_OPEN_RESPONSE_BODY; return responseBuilder; } } }//讀取Response Body试躏,獲得 @Override public ResponseBody openResponseBody(Response response) throws IOException { Source source = getTransferStream(response); return new RealResponseBody(response.headers(), Okio.buffer(source)); }}`
解析HTTP報文猪勇,得到HTTP協(xié)議版本。
`public final class StatusLine { public static StatusLine parse(String statusLine/*HTTP/1.1 200 OK*/) throws IOException { // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 // Parse protocol like "HTTP/1.1" followed by a space. int codeStart; Protocol protocol; if (statusLine.startsWith("HTTP/1.")) { .......`
讀取ResponseHeader
`/** Reads headers or trailers. */ public Headers readHeaders() throws IOException { Headers.Builder headers = new Headers.Builder(); // parse the result headers until the first blank line for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) { Internal.instance.addLenient(headers, line); } return headers.build(); }`
讀取ResponseBody颠蕴,讀取InputStream獲得byte數(shù)組泣刹,至此就完全得到了客戶端請求服務(wù)端接口 的響應(yīng)內(nèi)容。
`public abstract class ResponseBody implements Closeable { public final byte[] bytes() throws IOException { ...... try { bytes = source.readByteArray(); } finally { Util.closeQuietly(source); } ...... return bytes; } /** * Returns the response as a string decoded with the charset of the Content-Type header. If that * header is either absent or lacks a charset, this will attempt to decode the response body as * UTF-8. */ public final String string() throws IOException { return new String(bytes(), charset().name()); }`
5 總結(jié)
從上面關(guān)于okhttp發(fā)送網(wǎng)絡(luò)請求及接受網(wǎng)絡(luò)響應(yīng)的過程的分析犀被,可以發(fā)現(xiàn) okhttp并不是Volley和Retrofit這種二次封裝的網(wǎng)絡(luò)框架椅您,而是基于最原始的java socket連接自己去實現(xiàn)了HTTP協(xié)議,就連Android源碼也將其收錄在內(nèi)弱判,堪稱網(wǎng)絡(luò)編程的典范襟沮。結(jié)合HTTP協(xié)議相關(guān)書籍與okhttp的源碼實踐相結(jié)合進行學(xué)習(xí)锥惋,相信可以對HTTP協(xié)議有具體且深入的掌握昌腰。
[轉(zhuǎn)載至]:http://www.reibang.com/p/57c0b069452b ##