OKHttp源碼淺析與最佳實踐

1、概述

OKHttp 是 Square 開源的一款高效的處理網絡請求的工具。不僅限于處理 Http 請求。

功能特點:

  • 鏈接復用
  • Response 緩存和 Cookie
  • 默認 GZIP
  • 請求失敗自動重連
  • DNS 擴展
  • Http2/SPDY/WebSocket 協議支持

OKHttp 類似于 HttpUrlConnection, 是基于傳輸層實現應用層協議的網絡框架。 而不止是一個 Http 請求應用的庫。

2、請求處理流程

  1. execute()/enqueue() 提交請求開始執行
  2. RealCall.getResponse()
  3. HttpEngine 處理 sendRequest, readResponse
  4. sendRequest 之前,會先從 Cache 中判斷當前請求是否可以從緩存中返回
  5. connect 發起連接, 先從 ConnectPool 中找到緩存的連接,如果沒有會建立一個新的 RealConnection
  6. RealConnection 本身是基于 Socket 的, 在 Socket 之上建立各種協議. buildConnection(), establishProtocol()
  7. 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 分鐘。

Refference

HTTP DNS的實現
OkHttp3之Cookies管理及持久化
Https 詳解
Http2
WebSocket

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容