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