1、概述
OKHttp 是 Square 開源的一款高效的處理網絡請求的工具。不僅限于處理 Http 請求。
功能特點:
- 鏈接復用
- Response 緩存和 Cookie
- 默認 GZIP
- 請求失敗自動重連
- DNS 擴展
- Http2/SPDY/WebSocket 協議支持
OKHttp 類似于 HttpUrlConnection, 是基于傳輸層實現應用層協議的網絡框架。 而不止是一個 Http 請求應用的庫。
2、請求處理流程
- execute()/enqueue() 提交請求開始執行
- RealCall.getResponse()
- HttpEngine 處理 sendRequest, readResponse
- sendRequest 之前,會先從 Cache 中判斷當前請求是否可以從緩存中返回
- connect 發起連接, 先從 ConnectPool 中找到緩存的連接,如果沒有會建立一個新的 RealConnection
- RealConnection 本身是基于 Socket 的, 在 Socket 之上建立各種協議. buildConnection(), establishProtocol()
- Platform 處理真實的 socket 連接。 通過反射適配 Android 與 Java, 以及 ALPN。
3、特性分析
緩存處理
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
if (httpStream != null) throw new IllegalStateException();
Request request = networkRequest(userRequest);
InternalCache responseCache =
Internal.instance.internalCache(client);
Response cacheCandidate = responseCache != null ? responseCache.get(request) : null;
long now = System.currentTimeMillis();
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
...
}
CacheStrategy 中會根據 Http Headers 從 Cache 中加載緩存。
// If we don't need the network, we're done.
if (networkRequest == null) {
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}
緩存判斷通過, 直接返回 Response。 不會再發起請求。
鏈路復用
StreamAllocation 負責創建 Stream 和 Connection.
Stream 有各種協議版本的實現, 負責網絡讀寫數據
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException, RouteException {
Route selectedRoute;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (stream != null) throw new IllegalStateException("stream != null");
if (canceled) throw new IOException("Canceled");
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
// Attempt to get a connection from the pool.
//先從 ConnectPool 中取出可用的 Connection
RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
if (pooledConnection != null) {
this.connection = pooledConnection;
return pooledConnection;
}
selectedRoute = route;
}
if (selectedRoute == null) {
selectedRoute = routeSelector.next();
synchronized (connectionPool) {
route = selectedRoute;
refusedStreamCount = 0;
}
}
RealConnection newConnection = new RealConnection(selectedRoute);
acquire(newConnection);
// 創建完后, 將 Connection 存入 ConnectionPool
synchronized (connectionPool) {
Internal.instance.put(connectionPool, newConnection);
this.connection = newConnection;
if (canceled) throw new IOException("Canceled");
}
// 發起連接
newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
connectionRetryEnabled);
routeDatabase().connected(newConnection.route());
return newConnection;
}
鏈接何時釋放
ConnectionPool 中有一個 Runnable, 負責清除不再使用的鏈接
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
能夠被清除,前提是這個鏈接已經被標記為 Idle 了。處理代碼在 **RealCall getResponse() ** 請求結束之后。
失敗重連
RealCall getResponse 時, 如果中間出現異常或者需要重定向請求, 會再 new HttpEngine 繼續發起請求。
*/
Response getResponse(Request request, boolean forWebSocket) throws IOException {
// Copy body metadata to the appropriate request headers.
…
int followUpCount = 0;
while (true) {
if (canceled) {
engine.releaseStreamAllocation();
throw new IOException("Canceled");
}
boolean releaseConnection = true;
try {
engine.sendRequest();
engine.readResponse();
releaseConnection = false;
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = engine.recover(e.getLastConnectException(), true, null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e.getLastConnectException();
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = engine.recover(e, false, null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
StreamAllocation streamAllocation = engine.close();
streamAllocation.release();
}
}
Response response = engine.getResponse();
Request followUp = engine.followUpRequest();
if (followUp == null) {
if (!forWebSocket) {
engine.releaseStreamAllocation();
}
return response;
}
StreamAllocation streamAllocation = engine.close();
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (!engine.sameConnection(followUp.url())) {
streamAllocation.release();
streamAllocation = null;
} else if (streamAllocation.stream() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
response);
}
}
}
DNS 擴展
在 findConnection() 的過程中,會通過 RouterSelector 組件找到需要鏈接的地址。 在如果未找到, 會通過 DNS 接口根據 hostname 查詢對應的 IP 地址列表,并且在內存緩存這個列表。
/** Prepares the socket addresses to attempt for the current proxy or host. */
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
// Clear the addresses. Necessary if getAllByName() below throws!
inetSocketAddresses = new ArrayList<>();
String socketHost;
int socketPort;
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
socketHost = address.url().host();
socketPort = address.url().port();
} else {
SocketAddress proxyAddress = proxy.address();
if (!(proxyAddress instanceof InetSocketAddress)) {
throw new IllegalArgumentException(
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
socketHost = getHostString(proxySocketAddress);
socketPort = proxySocketAddress.getPort();
}
if (socketPort < 1 || socketPort > 65535) {
throw new SocketException("No route to " + socketHost + ":" + socketPort
+ "; port is out of range");
}
if (proxy.type() == Proxy.Type.SOCKS) {
inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else {
// Try each address for best behavior in mixed IPv4/IPv6 environments.
List<InetAddress> addresses = address.dns().lookup(socketHost);
for (int i = 0, size = addresses.size(); i < size; i++) {
InetAddress inetAddress = addresses.get(i);
inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
}
}
nextInetSocketAddressIndex = 0;
}
在 Dns 類中, 有一個默認的 SYSTEM 實現,如果沒單獨配置 Dns, 會使用默認的實現。
public interface Dns {
/**
* A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
* lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
*/
Dns SYSTEM = new Dns() {
@Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
if (hostname == null) throw new UnknownHostException("hostname == null");
return Arrays.asList(InetAddress.getAllByName(hostname));
}
};
/**
* Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp. If
* a connection to an address fails, OkHttp will retry the connection with the next address until
* either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded.
*/
List<InetAddress> lookup(String hostname) throws UnknownHostException;
}
Http2/SPDY 協議支持
使用 Http2/SPDY 協議需要用到 ALPN 組件,并且服務器支持。 在 Android 5.0 以后系統包含了 ALPN。
協議兼容代碼在 AndroidPlatform 類中
public static Platform buildIfSupported() {
// Attempt to find Android 2.3+ APIs.
try {
Class<?> sslParametersClass;
try {
sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
} catch (ClassNotFoundException e) {
// Older platform before being unbundled.
sslParametersClass = Class.forName(
"org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
}
OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(
null, "setUseSessionTickets", boolean.class);
OptionalMethod<Socket> setHostname = new OptionalMethod<>(
null, "setHostname", String.class);
OptionalMethod<Socket> getAlpnSelectedProtocol = null;
OptionalMethod<Socket> setAlpnProtocols = null;
// Attempt to find Android 5.0+ APIs.
try {
Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
} catch (ClassNotFoundException ignored) {
}
return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname,
getAlpnSelectedProtocol, setAlpnProtocols);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
}
return null;
}
WebSocket 協議支持
加入 okhttp-ws 擴展包,可以使用 WebSocket 協議連接服務器。
compile 'com.squareup.okhttp3:okhttp-ws:3.3.1'
最佳實踐
OKHttp 是一個更傾向于協議的網絡框架。 通常在使用上,我們會再包裝一層,以簡化調用。
封裝的 BJNetwork 庫主要實現了:
- 緩存的自動配置(cache + cookie)
- 請求日志開關
- 基于騰訊的 DnsPod 實現的 DNS 擴展(可選)
- Get & Post 請求參數的簡化
- 上傳&下載進度
- 根據 tag 關閉請求; 或者 tag 被銷毀后,自動關閉請求
- Http2/SPDY 配置
- 支持 RxJava 調用
- WebSocket 的調用封裝以及自動重連處理
示例代碼
集成
compile 'io.github.yangxlei:bjnetwork:1.5.1'
創建 BJNetworkClient :
BJNetworkClient client = new BJNetworkClient.Builder()
// 設置緩存文件存儲路徑. 會自動將請求的緩存和 cookie 存儲在該路徑下
.setCacheDir(context.getCacheDir())
// 開啟開關后, 會在支持協議列表內加入 Http2 & SPDY. 如果服務器不支持該協議, 建議關閉
.setEnableHttp2x(false)
// 開啟日志. 建議 Debug 環境開啟, Release 環境關閉.
.setEnableLog(true)
.setConnectTimeoutAtSeconds(30)
.setReadTimeoutAtSeconds(30)
.setWriteTimeoutAtSeconds(30)
// DnsPod 的實現. 不設置默認使用 SYSTEM
.setDns(new DnsPodImpl(context.getCacheDir()))
.build();
請求管理類
// 建議由調用者維護一份實例
mNetRequestManager = new BJNetRequestManager(client);
發起請求
//創建請求
BJNetCall call = mNetRequestManager.newGetCall("http://xxx.com");
// call = mNetRequestManager.newPostCall("http://xxx.com/xx", requestBody);
Object tag = new Object();
//同步請求
try {
BJResponse response = call.executeSync(tag);
} catch (IOException e) {
e.printStackTrace();
}
//異步請求
call.executeAsync(tag, new BJNetCallback() {
@Override
public void onFailure(Exception e) {
}
@Override
public void onResponse(BJResponse bjResponse) {
}
});
// tag 會被 JVM 回收. 響應的請求也會被自動關閉
tag = null;
執行請求后, RequestManager 內會建立一個 tag 和對應的 calls 的虛引用 list。 當 tag 被回收后, 自動關閉 list 中的 call。
需要注意的是, 如果這個 tag 傳入的是 this, 因為 callback 會一直被持有直到請求結束, callback 中會有一個 this 的引用。 所以 this 不會銷毀也無法自動關閉請求。可以調用 cancelCalls 主動關閉
mNetRequestManager.cancelCalls(tag);
下載進度
BJNetCall call = requestManager.newDownloadCall("http://d.gsxservice.com/app/genshuixue.apk", getCacheDir());
call.executeAsync(tag, new BJDownloadCallback() {
@Override
public void onProgress(long progress, long total) {
System.out.println("download onProgress " + progress +"," + total +" " + Thread.currentThread());
}
@Override
public void onDownloadFinish(BJResponse response, File file) {
System.out.println("download onDownloadFinish " + Thread.currentThread());
}
@Override
public void onFailure(Exception e) {
e.printStackTrace();
System.out.println("download onFailure " + Thread.currentThread());
}
});
上傳進度
BJRequestBody requestBody = BJRequestBody.createWithMultiForm(params, "Filedata",
new File("aaaa.jpg"),BJRequestBody.MEDIA_TYPE_IMAGE);
requestManager.newPostCall("http://xxx.com/doc/upload",requestBody)
.executeAsync(this, new BJProgressCallback(){
@Override
public void onFailure(Exception e) {
System.out.println("1upload onFailure " + Thread.currentThread());
}
@Override
public void onResponse(BJResponse response) {
System.out.println("1upload onResponse "+ " "+ Thread.currentThread());
}
@Override
public void onProgress(long progress, long total) {
System.out.println("1upload onProgress " + progress*100/total+" " + Thread.currentThread());
}
});
提供了 BJProgressCallback 和 BJDownloadCallback 兩種回調實現。 在異步執行時傳入 ProgressCallback 會自動提供進度的回調。
RxJava 擴展
compile 'io.github.yangxlei:rx-bjnetwork:1.5.1'
示例:
mRxNetRequestManager = new BJRxNetRequestManager(client);
mRxNetRequestManager.rx_newGetCall("http://xxx.com")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<BJResponse>() {
@Override
public void call(BJResponse response) {
try {
String result = response.getResponseString();
} catch (IOException e) {
e.printStackTrace();
throw new HttpException(e);
}
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
HttpException exception = (HttpException) throwable;
}
});
WebSocket 擴展
compile 'io.github.yangxlei:bjnetwork-ws:1.5.1'
示例:
mWebsocketClient = new BJWebsocketClient("TestWS");
mWebsocketClient.setAddress("ws://xxx.com");
// LogLevel.Body 會打印出收發的數據以及行為. Info 只打印行為不打印詳細數據
mWebsocketClient.setLogLevel(BJWebsocketClient.LogLevel.Body);
mWebsocketClient.setListener(new BJWebsocketListener() {
@Override
public void onReconnect(BJWebsocketClient client) {
}
@Override
public void onClose(BJWebsocketClient client) {
}
@Override
public void onSentMessageFailure(BJWebsocketClient client, BJMessageBody messageBody) {
}
@Override
public void onMessage(BJWebsocketClient client, String message) {
System.out.println("WS receive:" + message);
}
@Override
public void onMessage(BJWebsocketClient client, InputStream inputStream) {
}
@Override
public void onStateChanged(BJWebsocketClient client, BJWebsocketClient.State state) {
}
});
// 啟動鏈接
mWebsocketClient.connect();
// 消息會進入發送隊列. 鏈接建立成功后, 逐個發送
mWebsocketClient.sendMessage("message");
// 關閉鏈接
mWebsocketClient.disconnect();
1.WebSocketClient 內維護一個消息發送線程和消息隊列。 鏈接成功后自動發送消息。 不排除消息可能會在本地發送失敗, 內部處理默認情況會自動重試五次, 如果都失敗會將 message 通過 onSentMessageFailure 返回上層。
2.WebSocketClient 維護三種狀態: Offline, Connecting, Connected.
3.自動重連,無法避免某些情況下鏈接會被斷開。 內部處理如果非主動斷開鏈接, 都會自動重新建立鏈接。
踩過的幾個坑
1、gzip 問題
OKHttp 響應會自動處理 gzip 解壓縮. 在 HttpEngine 類中收到響應后會調用:
private Response unzip(final Response response) throws IOException {
if (!transparentGzip || !"gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) {
return response;
}
if (response.body() == null) {
return response;
}
GzipSource responseBody = new GzipSource(response.body().source());
Headers strippedHeaders = response.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
return response.newBuilder()
.headers(strippedHeaders)
.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)))
.build();
}
發現里面有個 transparentGzip 的判斷,再看來這個值是怎么回事:
private Request networkRequest(Request request) throws IOException {
Request.Builder result = request.newBuilder();
if (request.header("Host") == null) {
result.header("Host", hostHeader(request.url(), false));
}
if (request.header("Connection") == null) {
result.header("Connection", "Keep-Alive");
}
if (request.header("Accept-Encoding") == null) {
transparentGzip = true;
result.header("Accept-Encoding", "gzip");
}
List<Cookie> cookies = client.cookieJar().loadForRequest(request.url());
if (!cookies.isEmpty()) {
result.header("Cookie", cookieHeader(cookies));
}
if (request.header("User-Agent") == null) {
result.header("User-Agent", Version.userAgent());
}
return result.build();
}
sendRequest 的時候會先補全常用的 Http Headers。 如果不存在 “Accept-Encoding” 頭會 transparentGzip=true。
也就是說,OKHttp 的處理規則是:如果你的請求自己主動加了 gzip 支持, 那么響應也自己處理 gzip 解壓縮; 否則我給你做。
2、WebSocket SocketTimeOutException
WebSocket 在鏈接成功之后,可能過一會就會拋個 timeout 異常然后斷開鏈接。出問題的地方是:
private void createWebSocket(Response response, WebSocketListener listener) throws IOException {
if (response.code() != 101) {
throw new ProtocolException("Expected HTTP 101 response but was '"
+ response.code()
+ " "
+ response.message()
+ "'");
}
String headerConnection = response.header("Connection");
if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
throw new ProtocolException(
"Expected 'Connection' header value 'Upgrade' but was '" + headerConnection + "'");
}
String headerUpgrade = response.header("Upgrade");
if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
throw new ProtocolException(
"Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
}
String headerAccept = response.header("Sec-WebSocket-Accept");
String acceptExpected = Util.shaBase64(key + WebSocketProtocol.ACCEPT_MAGIC);
if (!acceptExpected.equals(headerAccept)) {
throw new ProtocolException("Expected 'Sec-WebSocket-Accept' header value '"
+ acceptExpected
+ "' but was '"
+ headerAccept
+ "'");
}
StreamAllocation streamAllocation = Internal.instance.callEngineGetStreamAllocation(call);
RealWebSocket webSocket = StreamWebSocket.create(
streamAllocation, response, random, listener);
listener.onOpen(webSocket, response);
while (webSocket.readMessage()) {
}
}
最后一行會進入循環不斷的讀取數據。 默認 OkHttpClient 的 readTimeOut 時間是 10s。所以如果服務器 10s 內沒有數據返回,客戶端就會自動斷開。 所以配置的時候可以把 readTimeout 值設置長一點。 BJWebsocketClient 默認是 10 分鐘。