1. 程式人生 > >Android妙用SPDY協議提高移動端網路請求效能

Android妙用SPDY協議提高移動端網路請求效能

    本文旨在提出一種提高移動端網路效能的可行方案。我們知道目前移動端使用的網路請求協議基本上都是http。用的最多的是http/1.1,http/2.0正在逐漸壯大,實際上http/2.0是基於google提出的SPDY協議改進而來。廢話不多說,馬上進入正題。

     關於SPDY協議的詳細介紹,請參看 OkHttp完全解析(七)SPDY協議詳細介紹
    關於OkHttp的使用及原始碼分析網上相關的資料很多,推薦OkHttp完全解析(八)原始碼解析一 系列文章。寫的很不錯。
     從速度上來講,Http/1.1用明文傳輸,無需加密驗證,速度快;SPDY加入了SSL加密握手,但是SPDY協議允許一個TCP連線複用,可以一個域名只提供一個TCP連線即可完成通訊,雖然由於TCP連線複用,但是SSL加密握手協商過程又耗費了一定的時間;Http/2.0是Http的加密版本,也是SPDY的升級版本,如果追求高安全性,可以選用Http/2.0,速度上Http/2.0比Http/1.1稍慢。實驗證明單純的將Http/1.1升級到SPDY,速度提升並不明顯,原因在於SSL加密層的新增,一定程度上拖慢了SPDY協議的效率。本文在基於OkHttp開源庫的基礎上,使用SPDY協議,但是強制忽略了SSL握手驗證過程,強制使用SPDY/3.1協議,這樣就將SPDY的速度優勢發揮出來了。Okhttp的原始碼量比較大,邏輯比較複雜,我費了不少時間,看懂了大致的思路。請在閱讀本文之前閱讀前面的OkHttp原始碼系列文章。
     針對某個公司的產品,伺服器端可以針對域名進行配置SPDY協議的使用。針對自家產品完全可以免去SSL協商過程,直接預設使用SPDY協議。關於伺服器怎麼配置,在此不過多贅述,自行查閱相關文章。
客戶端具體實現過程:
下載okHttp原始碼並進行修改,只需修改一處即可完成。直接上程式碼:
Connection.java中的connect()函式:

  //當新建連線或者可用連線無效的時候進入此函式
  void connect(int connectTimeout, int readTimeout, int writeTimeout,Request request, List<ConnectionSpec> connectionSpecs,boolean connectionRetryEnabled) throws RouteException {
    if (connected)
      throw new IllegalStateException("already connected");
    SocketConnector socketConnector = new
SocketConnector(this, pool); SocketConnector.ConnectedSocket connectedSocket; if (route.address.getSslSocketFactory() != null) { // https:// communication connectedSocket = socketConnector.connectTls(connectTimeout, readTimeout, writeTimeout, request, route, connectionSpecs, connectionRetryEnabled); } else
{ // http:// communication. if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) { throw new RouteException(new UnknownServiceException( "CLEARTEXT communication not supported: " + connectionSpecs)); } connectedSocket = socketConnector.connectCleartext(connectTimeout, readTimeout, route); } socket = connectedSocket.socket; handshake = connectedSocket.handshake; //此處已完成協議的協商,protocol變數的值則決定了下面所走的協議。 protocol = connectedSocket.alpnProtocol == null ? Protocol.HTTP_1_1 : connectedSocket.alpnProtocol; /***********加入程式碼**************/ String hostName = request.httpUrl().host(); if ("xxx.com".equals(hostName) { protocol = Protocol.SPDY_3; } /***********加入的程式碼結束**************/ try { if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) { socket.setSoTimeout(0); // SPDY timeouts are set per-stream. spdyConnection = new SpdyConnection.Builder(route.address.uriHost, true, socket).protocol(protocol).build(); spdyConnection.sendConnectionPreface(); } else { httpConnection = new HttpConnection(pool, this, socket); } } catch (IOException e) { throw new RouteException(e); } connected = true; }

由於時間有限,加上第一次正式寫部落格,很多地方語法用的不是太好,湊合看哈,connect()函式中加入的程式碼:

 String hostName = request.httpUrl().host();
    if ("xxx.com".equals(hostName) {
        protocol = Protocol.SPDY_3;
    }

     只是通過很簡單的邏輯即可實現,xxx.com代表的是伺服器端支援SPDY的域名,此段程式碼的含義即為強制客戶端針對域名選用SPDY協議。值得注意的是,在OkHttp框架中,若為https則預設會進行handshake過程,此時相當於會進行TLS握手過程,此時,這樣新增實際上無法提高效率,且既然用了https加密協議,那TLS握手肯定是不能去掉的,否則https變的毫無意義。
具體的實現則需要追蹤原始碼。這裡給出簡單的說明。Connection.java中的connectAndSetOwner函式是每次請求都會進來的函式,注意此處會進行判斷,當第一次建立連線時,isConnected返回false,此時會進入connect函式,建立連線,選擇協議,完成連線後,會進行isSpdy判斷,如果是Spdy協議,那麼會將此connnection放入連線池中進行共享,下次相同域名下的請求到來時,isConnected判斷就會為true,則不需要進行重複建立連線,實現了連線的共享。而若是http/1.1,一般不會進行連線共享,每次都需要重建連線,效率較低。

  void connectAndSetOwner(OkHttpClient client, Object owner, Request request)throws RouteException {
    setOwner(owner);
    if (!isConnected()) {
      List<ConnectionSpec>  connectionSpecs  =       route.address.getConnectionSpecs();
      connect(client.getConnectTimeout(), client.getReadTimeout(),
          client.getWriteTimeout(), request, connectionSpecs,
          client.getRetryOnConnectionFailure());
      if (isSpdy()) {
        client.getConnectionPool().share(this);
      }
      client.routeDatabase().connected(getRoute());
    }
    setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
  }

下面給出一個簡單的程式碼進行驗證:

public class MainActivity extends Activity {
    private Button myButton;
    private String[] urls = {
            "http://api.caipiao.163.com/clientCommonConfig_getNaviBottomAlert.html?mobileType=android&ver=4.13&channel=netease&apiVer=1.1&apiLevel=19",
            "http://api.caipiao.163.com/lottery_newActivity.html?mobileType=android&ver=4.13&channel=netease&apiVer=1.1&apiLevel=19",
            "http://api.caipiao.163.com/clientHall_hallInfoAll.html?mobileType=android&ver=4.13&channel=netease&apiVer=1.1&apiLevel=19",
            "http://api.caipiao.163.com/getClientNaviLogos.html?mobileType=android&ver=4.13&channel=netease&apiVer=1.1&apiLevel=19",
            "http://api.caipiao.163.com/classifyABTest.html?mobileType=android&ver=4.13&channel=netease&apiVer=1.1&apiLevel=19" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myButton = (Button) findViewById(R.id.myBtn);
        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread() {
                    @Override
                    public void run() {
                        long start = System.currentTimeMillis();
                        for (int i = 0; i < urls.length * 4; i++) {
                            OkHttpClient okHttpClient = new OkHttpClient();
                            Request request = new Request.Builder().url(
                                    urls[i % urls.length]).build();
                            try {
                                Response okHttpResponse = okHttpClient.newCall(
                                        request).execute();
                                Log.i("SpdyTest", "i:" + i + "   protocol:"
                                        + okHttpResponse.protocol().name()
                                        + "code:" + okHttpResponse.code());
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        long end = System.currentTimeMillis();
                        Log.i("aTest", "total time:  " + (end - start) + "ms");

                    }
                }.start();
            }
        });
    }
}

程式碼很簡單,利用已配置好伺服器端的url進行測試,每進行20次請求, 進行一次時間計算。

http協議:每20次請求所用時間

Spdy協議:

每20次請求所用時間

    測試環境為家裡wifi訊號,幾乎同時進行測試,也切換過其他網路,同等環境下,去掉TLS握手的SPDY協議要比Http快大概40%。
當然了對於大多數應用來說,一般情況下不會產生明顯的卡頓,但是效果總不怕好嘛,這裡只給大家提供一個比較可行的思路而已。
    最近在家裡閒來無事,在看okhttp的原始碼,確實學到了很多東西,本來想寫幾篇原始碼分析的文章,發現寫起來真是繁瑣,且網上有很多部落格解釋的很清晰,這裡就不做重複性工作了。
    本文提供的思路幾乎只用一行程式碼就能實現,不過很遺憾,okhttp框架並不能直接設定想要使用的協議,沒有暴露介面給我們,所以我們只能把原始碼下載下來,在進行打jar包啦。
    這是我第一次寫部落格,很多地方沒注意到,看到的請見諒,關於OkHttp這個開源庫,我強烈推薦,親自實踐了它失敗重連等功能,有任何相關問題,歡迎留言交流。