最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

探究Android客戶端網(wǎng)絡(luò)預(yù)連接優(yōu)化機(jī)制

 更新時(shí)間:2021年06月18日 09:48:57   作者:vivo互聯(lián)網(wǎng)技術(shù)  
一般情況下,我們都是用一些封裝好的網(wǎng)絡(luò)框架去請求網(wǎng)絡(luò),對底層實(shí)現(xiàn)不甚關(guān)注,而大部分情況下也不需要特別關(guān)注處理。了解底層的一些實(shí)現(xiàn),有益于我們對網(wǎng)絡(luò)加載進(jìn)行優(yōu)化。本文就是關(guān)于根據(jù)http的連接復(fù)用機(jī)制來優(yōu)化網(wǎng)絡(luò)加載速度的原理與細(xì)節(jié)

一、連接復(fù)用

對于一個(gè)普通的接口請求,通過charles抓包,查看網(wǎng)絡(luò)請求Timing欄信息,我們可以看到類似如下請求時(shí)長信息:

  • Duration 175 ms
  • DNS 6 ms
  • Connect 50 msTLS Handshake 75 ms
  • Request 1 ms
  • Response 1 ms
  • Latency 42 ms

同樣的請求,再來一次,時(shí)長信息如下所示:

  • Duration 39 ms
  • DNS -
  • Connect -
  • TLS Handshake -
  • Request 0 ms
  • Response 0 ms
  • Latency 39 ms

我們發(fā)現(xiàn),整體網(wǎng)絡(luò)請求時(shí)間從175ms降低到了39ms。其中DNS,Connect,TLS Handshake 后面是個(gè)橫線,表示沒有時(shí)長信息,于是整體請求時(shí)長極大的降低了。這就是Http(s)的連接復(fù)用的效果。那么問題來了,什么是連接復(fù)用,為什么它能降低請求時(shí)間?

在解決這個(gè)疑問之前,我們先來看看一個(gè)網(wǎng)絡(luò)請求發(fā)起,到收到返回的數(shù)據(jù),這中間發(fā)生了什么?

  • 客戶端發(fā)起網(wǎng)絡(luò)請求
  • 通過DNS服務(wù)解析域名,獲取服務(wù)器IP (基于UDP協(xié)議的DNS解析)
  • 建立TCP連接(3次握手)
  • 建立TLS連接(https才會(huì)用到)
  • 發(fā)送網(wǎng)絡(luò)請求request
  • 服務(wù)器接收request,構(gòu)造并返回response
  • TCP連接關(guān)閉(4次揮手)

上面的連接復(fù)用直接讓上面2,3,4步都不需要走了。這中間省掉的時(shí)長應(yīng)該怎么算?如果我們定義網(wǎng)絡(luò)請求一次發(fā)起與收到響應(yīng)的一個(gè)來回(一次通信來回)作為一個(gè)RTT(Round-trip delay time)。

1)DNS默認(rèn)基于UDP協(xié)議,解析最少需要1-RTT;

2)建立TCP連接,3次握手,需要2-RTT;

3)建立TLS連接,根據(jù)TLS版本不同有區(qū)別,常見的TLS1.2需要2-RTT。

Client                                               Server

ClientHello                  -------->

                                                ServerHello

                                               Certificate*

                                         ServerKeyExchange*

                                        CertificateRequest*

                             <--------      ServerHelloDone

Certificate*

ClientKeyExchange

CertificateVerify*

[ChangeCipherSpec]

Finished                     -------->

                                         [ChangeCipherSpec]

                             <--------             Finished

Application Data             <------->     Application Data

                   TLS 1.2握手流程(來自 RFC 5246)

注:TLS1.3版本相比TLS1.2,支持0-RTT數(shù)據(jù)傳輸(可選,一般是1-RTT),但目前支持率比較低,用的很少。

http1.0版本,每次http請求都需要建立一個(gè)tcp socket連接,請求完成后關(guān)閉連接。前置建立連接過程可能就會(huì)額外花費(fèi)4-RTT,性能低下。

http1.1版本開始,http連接默認(rèn)就是持久連接,可以復(fù)用,通過在報(bào)文頭部中加上Connection:Close來關(guān)閉連接 。如果并行有多個(gè)請求,可能還是需要建立多個(gè)連接,當(dāng)然我們也可以在同一個(gè)TCP連接上傳輸,這種情況下,服務(wù)端必須按照客戶端請求的先后順序依次回送結(jié)果。

注:http1.1默認(rèn)所有的連接都進(jìn)行了復(fù)用。然而空閑的持久連接也可以隨時(shí)被客戶端與服務(wù)端關(guān)閉。不發(fā)送Connection:Close不意味著服務(wù)器承諾連接永遠(yuǎn)保持打開。

http2 更進(jìn)一步,支持二進(jìn)制分幀,實(shí)現(xiàn)TCP連接的多路復(fù)用,不再需要與服務(wù)端建立多個(gè)TCP連接了,同域名的多個(gè)請求可以并行進(jìn)行。

還有個(gè)容易被忽視的是,TCP有擁塞控制,建立連接后有慢啟動(dòng)過程(根據(jù)網(wǎng)絡(luò)情況一點(diǎn)一點(diǎn)的提高發(fā)送數(shù)據(jù)包的數(shù)量,前面是指數(shù)級(jí)增長,后面變成線性),復(fù)用連接可以避免這個(gè)慢啟動(dòng)過程,快速發(fā)包。

二、預(yù)連接實(shí)現(xiàn)

客戶端常用的網(wǎng)絡(luò)請求框架如OkHttp等,都能完整支持http1.1與HTTP2的功能,也就支持連接復(fù)用。了解了這個(gè)連接復(fù)用機(jī)制優(yōu)勢,那我們就可以利用起來,比如在APP閃屏等待的時(shí)候,就預(yù)先建立首頁詳情頁等關(guān)鍵頁面多個(gè)域名的連接,這樣我們進(jìn)入相應(yīng)頁面后可以更快的獲取到網(wǎng)絡(luò)請求結(jié)果,給予用戶更好體驗(yàn)。在網(wǎng)絡(luò)環(huán)境偏差的情況下,這種預(yù)連接理論上會(huì)有更好的效果。

具體如何實(shí)現(xiàn)?

第一反應(yīng),我們可以簡單的對域名鏈接提前發(fā)起一個(gè)HEAD請求(沒有body可以省流量),這樣就能提前建立好連接,下次同域名的請求就可以直接復(fù)用,實(shí)現(xiàn)起來也是簡單方便。于是寫了個(gè)demo,試了個(gè)簡單接口,完美,粗略統(tǒng)計(jì)首次請求速度可以提升40%以上。

于是在游戲中心App啟動(dòng)Activity中加入了預(yù)連接相關(guān)邏輯,跑起來試了下,竟然沒效果...

抓包分析,發(fā)現(xiàn)連接并沒有復(fù)用,每次進(jìn)去詳情頁后都重新創(chuàng)建了連接,預(yù)連接可能只是省掉了DNS解析時(shí)間,demo上的效果無法復(fù)現(xiàn)??礃幼臃治鯫kHttp連接復(fù)用相關(guān)源碼是跑不掉了。

三、源碼分析

OKHttp通過幾個(gè)默認(rèn)的Interceptor用于處理網(wǎng)絡(luò)請求相關(guān)邏輯,建立連接在ConnectInterceptor類中;

public final class ConnectInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
​
    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
​
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

RealConnection即為后面使用的connection,connection生成相關(guān)邏輯在StreamAllocation類中;

public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
  ... 
    RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
        writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
    HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
  ...
}
​
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);
    ...
      return candidate;
    }
}
  
  /**
   * 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,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    ...
    
    // 嘗試從connectionPool中獲取可用connection
    Internal.instance.acquire(connectionPool, address, this, null);
    if (connection != null) {
    foundPooledConnection = true;
    result = connection;
    } else {
    selectedRoute = route;
    }
    
   ...
   
    if (!foundPooledConnection) {
      ... 
      // 如果最終沒有可復(fù)用的connection,則創(chuàng)建一個(gè)新的
        result = new RealConnection(connectionPool, selectedRoute);
    }
  ...
}

這些源碼都是基于okhttp3.13版本的代碼,3.14版本開始這些邏輯有修改。

StreamAllocation類中最終獲取connection是在findConnection方法中,優(yōu)先復(fù)用已有連接,沒可用的才新建立連接。獲取可復(fù)用的連接是在ConnectionPool類中;

/**
 * Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that
 * share the same {@link Address} may share a {@link Connection}. This class implements the policy
 * of which connections to keep open for future use.
 */
public final class ConnectionPool {

  private final Runnable cleanupRunnable = () -> {
    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) {
          }
        }
      }
    }
  };

  // 用一個(gè)隊(duì)列保存當(dāng)前的連接
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  
  
  /**
   * Create a new connection pool with tuning parameters appropriate for a single-user application.
   * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
   * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
   */
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
  ...
  }
  
  void acquire(Address address, StreamAllocation streamAllocation, @Nullable Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return;
      }
    }
  }

由上面源碼可知,ConnectionPool默認(rèn)最大維持5個(gè)空閑的connection,每個(gè)空閑connection5分鐘后自動(dòng)釋放。如果connection數(shù)量超過最大數(shù)5個(gè),則會(huì)移除最舊的空閑connection。

最終判斷空閑的connection是否匹配,是在RealConnection的isEligible方法中;

/**
   * Returns true if this connection can carry a stream allocation to {@code address}. If non-null
   * {@code route} is the resolved route for a connection.
   */
  public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

    // At this point we don't have a hostname match. But we still be able to carry the request if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;

    // 3. This connection's server certificate's must cover the new host.
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    // 4. Certificate pinning must match the host.
    try {
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }

    return true; // The caller's address can be carried by this connection.
  }

這塊代碼比較直白,簡單解釋下比較條件:

如果該connection已達(dá)到承載的流上限(即一個(gè)connection可以承載幾個(gè)請求,http1默認(rèn)是1個(gè),http2默認(rèn)是Int最大值)則不符合;

如果2個(gè)Address除Host之外的屬性有不匹配,則不符合(如果2個(gè)請求用的okhttpClient不同,復(fù)寫了某些重要屬性,或者服務(wù)端端口等屬性不一樣,那都不允許復(fù)用);

如果host相同,則符合,直接返回true(其它字段已經(jīng)在上一條比較了);

如果是http2,則判斷無代理、服務(wù)器IP相同、證書相同等條件,如果都符合也返回true;

整體看下來,出問題的地方應(yīng)該就是ConnectionPool 的隊(duì)列容量太小導(dǎo)致的。游戲中心業(yè)務(wù)復(fù)雜,進(jìn)入首頁后,觸發(fā)了很多接口請求,導(dǎo)致連接池直接被占滿,于是在啟動(dòng)頁做好的預(yù)連接被釋放了。通過調(diào)試驗(yàn)證了下,進(jìn)入詳情頁時(shí),ConnectionPool中的確已經(jīng)沒有之前預(yù)連接的connection了。

四、優(yōu)化

在http1.1中,瀏覽器一般都是限定一個(gè)域名最多保留5個(gè)左右的空閑連接。然而okhttp的連接池并沒有區(qū)分域名,整體只做了默認(rèn)最大5個(gè)空閑連接,如果APP中不同功能模塊涉及到了多個(gè)域名,那這默認(rèn)的5個(gè)空閑連接肯定是不夠用的。有2個(gè)修改思路:

  • 重寫ConnectionPool,將連接池改為根據(jù)域名來限定數(shù)量,這樣可以完美解決問題。然而OkHttp的ConnectionPool是final類型的,無法直接重寫里面邏輯,另外OkHttp不同版本上,ConnectionPool邏輯也有區(qū)別,如果考慮在編譯過程中使用ASM等字節(jié)碼編寫技術(shù)來實(shí)現(xiàn),成本很大,風(fēng)險(xiǎn)很高。
  • 直接調(diào)大連接池?cái)?shù)量和超時(shí)時(shí)間。這個(gè)簡單有效,可以根據(jù)自己業(yè)務(wù)情況適當(dāng)調(diào)大這個(gè)連接池最大數(shù)量,在構(gòu)建OkHttpClient的時(shí)候就可以傳入這個(gè)自定義的ConnectionPool對象。

我們直接選定了方案2。

五、問答

1、如何確認(rèn)連接池最大數(shù)量值?

這個(gè)數(shù)量值有2個(gè)參數(shù)作為參考:頁面最大同時(shí)請求數(shù),App總的域名數(shù)。也可以簡單設(shè)定一個(gè)很大的值,然后進(jìn)入APP后,將各個(gè)主要頁面都點(diǎn)一遍,看看當(dāng)前ConnectionPool中留存的connection數(shù)量,適當(dāng)做一下調(diào)整即可。

2、調(diào)大了連接池會(huì)不會(huì)導(dǎo)致內(nèi)存占用過多?

經(jīng)測試:將connectionPool最大值調(diào)成50,在一個(gè)頁面上,用了13個(gè)域名鏈接,總共重復(fù)4次,也就是一次發(fā)起52個(gè)請求之后,ConnectionPool中留存的空閑connection平均22.5個(gè),占用內(nèi)存為97Kb,ConnectionPool中平均每多一個(gè)connection會(huì)占用4.3Kb內(nèi)存。

3、調(diào)大了連接池會(huì)影響到服務(wù)器嗎?

理論上是不會(huì)的。連接是雙向的,即使客戶端將connection一直保留,服務(wù)端也會(huì)根據(jù)實(shí)際連接數(shù)量和時(shí)長調(diào)整,自動(dòng)關(guān)閉連接的。比如服務(wù)端常用的nginx就可以自行設(shè)定最大保留的connection數(shù)量,超時(shí)也會(huì)自動(dòng)關(guān)閉舊連接。因此如果服務(wù)器定義的最大連接數(shù)和超時(shí)時(shí)間比較小,可能我們的預(yù)連接會(huì)無效,因?yàn)檫B接被服務(wù)端關(guān)閉了。

用charles可以看到這種連接被服務(wù)端關(guān)閉的效果:TLS大類中Session Resumed里面看到復(fù)用信息。

這種情況下,客戶端會(huì)重新建立連接,會(huì)有tcp和tls連接時(shí)長信息。

4、預(yù)連接會(huì)不會(huì)導(dǎo)致服務(wù)器壓力過大?

由于進(jìn)入啟動(dòng)頁就發(fā)起了網(wǎng)絡(luò)請求進(jìn)行預(yù)連接,接口請求數(shù)增多了,服務(wù)器肯定會(huì)有影響,具體需要根據(jù)自己業(yè)務(wù)以及服務(wù)器壓力來判斷是否進(jìn)行預(yù)連接。

5、如何最大化預(yù)連接效果?

由上面第3點(diǎn)問題可知,我們的效果實(shí)際是和服務(wù)器配置息息相關(guān),此問題涉及到服務(wù)器的調(diào)優(yōu)。

服務(wù)器如果將連接超時(shí)設(shè)置的很小或關(guān)閉,那可能每次請求都需要重新建立連接,這樣服務(wù)器在高并發(fā)的時(shí)候會(huì)因?yàn)椴粩鄤?chuàng)建和銷毀TCP連接而消耗很多資源,造成大量資源浪費(fèi)。

服務(wù)器如果將連接超時(shí)設(shè)置的很大,那會(huì)由于連接長時(shí)間未釋放,導(dǎo)致服務(wù)器服務(wù)的并發(fā)數(shù)受到影響,如果超過最大連接數(shù),新的請求可能會(huì)失敗。

可以考慮根據(jù)客戶端用戶訪問到預(yù)連接接口平均用時(shí)來調(diào)節(jié)。比如游戲中心詳情頁接口預(yù)連接,那可以統(tǒng)計(jì)一下用戶從首頁平均瀏覽多長時(shí)間才會(huì)進(jìn)入到詳情頁,根據(jù)這個(gè)時(shí)長和服務(wù)器負(fù)載情況來適當(dāng)調(diào)節(jié)。

以上就是探究Android客戶端網(wǎng)絡(luò)預(yù)連接優(yōu)化機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Android客戶端網(wǎng)絡(luò) 預(yù)連接優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Android仿微信群聊頭像效果

    Android仿微信群聊頭像效果

    這篇文章主要為大家詳細(xì)介紹了Android仿微信群聊頭像效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-07-07
  • Android進(jìn)程間使用Intent進(jìn)行通信

    Android進(jìn)程間使用Intent進(jìn)行通信

    Android進(jìn)程間通信(IPC,Inter-Process Communication)底層采用的是 Binder 機(jī)制,具體到應(yīng)用層有網(wǎng)友根據(jù)安卓四大組件將進(jìn)程間通信方式分為對應(yīng)的四種方式 Activity, Broadcast, ContentProvider, Service
    2023-02-02
  • xUtils3.0框架學(xué)習(xí)筆記分享

    xUtils3.0框架學(xué)習(xí)筆記分享

    這篇文章主要為大家分享了xUtils3.0框架學(xué)習(xí)筆記,xUtils開源框架的功能介紹,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • 打造酷炫的AndroidStudio插件

    打造酷炫的AndroidStudio插件

    這篇文章主要為大家詳細(xì)介紹了如何打造酷炫的AndroidStudio插件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • Android?Compose之Animatable動(dòng)畫停止使用詳解

    Android?Compose之Animatable動(dòng)畫停止使用詳解

    這篇文章主要為大家介紹了Android?Compose之Animatable動(dòng)畫停止使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • Android自定義頂部導(dǎo)航欄控件實(shí)例代碼

    Android自定義頂部導(dǎo)航欄控件實(shí)例代碼

    這篇文章主要介紹了Android自定義頂部導(dǎo)航欄控件實(shí)例代碼,需要的朋友可以參考下
    2017-12-12
  • Android獲取內(nèi)外置存儲(chǔ)卡的方法

    Android獲取內(nèi)外置存儲(chǔ)卡的方法

    這篇文章主要為大家詳細(xì)介紹了Android獲取內(nèi)外置存儲(chǔ)卡的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-04-04
  • Android讀取服務(wù)器圖片的三種方法

    Android讀取服務(wù)器圖片的三種方法

    這篇文章主要為大家詳細(xì)介紹了Android讀取服務(wù)器圖片的三種方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • Android Viewpager實(shí)現(xiàn)輪播廣告圖

    Android Viewpager實(shí)現(xiàn)輪播廣告圖

    這篇文章主要為大家詳細(xì)介紹了Android Viewpager實(shí)現(xiàn)輪播廣告圖,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • Android開發(fā)之TabHost選項(xiàng)卡及相關(guān)疑難解決方法

    Android開發(fā)之TabHost選項(xiàng)卡及相關(guān)疑難解決方法

    這篇文章主要介紹了Android開發(fā)之TabHost選項(xiàng)卡及相關(guān)疑難解決方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android開發(fā)中TabHost選項(xiàng)卡的常見用法以及相關(guān)疑難問題解決方法,需要的朋友可以參考下
    2019-03-03

最新評論

雅江县| 澄迈县| 乐都县| 新田县| 嘉祥县| 阿勒泰市| 洛南县| 溧水县| 黔江区| 洛隆县| 西城区| 连云港市| 兴义市| 白银市| 读书| 峨山| 伊宁市| 南丰县| 苍山县| 郁南县| 宜阳县| 许昌市| 桓台县| 南溪县| 新龙县| 邹平县| 曲阜市| 卢氏县| 清丰县| 云梦县| 句容市| 安吉县| 左权县| 榆林市| 资溪县| 彩票| 乌什县| 蓬安县| 虞城县| 台北县| 广安市|