1. 程式人生 > >Android 網路程式設計(8): 原始碼解析 OkHttp 中篇[複用連線池]

Android 網路程式設計(8): 原始碼解析 OkHttp 中篇[複用連線池]

1.引子

在瞭解OkHttp的複用連線池之前,我們首先要了解幾個概念。

TCP三次握手

通常我們進行HTTP連線網路的時候我們會進行TCP的三次握手,然後傳輸資料,然後再釋放連線。

20160827190206350

TCP三次握手的過程為:

  • 第一次握手:建立連線。客戶端傳送連線請求報文段,將SYN位置為1,Sequence Number為x;然後,客戶端進入SYN_SEND狀態,等待伺服器的確認;
  • 第二次握手:伺服器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認,設定Acknowledgment Number為x+1(Sequence Number+1);同時,自己自己還要傳送SYN請求資訊,將SYN位置為1,Sequence Number為y;伺服器端將上述所有資訊放到一個報文段(即SYN+ACK報文段)中,一併傳送給客戶端,此時伺服器進入SYN_RECV狀態;
  • 第三次握手:客戶端收到伺服器的SYN+ACK報文段。然後將Acknowledgment Number設定為y+1,向伺服器傳送ACK報文段,這個報文段傳送完畢以後,客戶端和伺服器端都進入ESTABLISHED狀態,完成TCP三次握手。

TCP四次分手

當客戶端和伺服器通過三次握手建立了TCP連線以後,當資料傳送完畢,斷開連線就需要進行TCP四次分手:

  • 第一次分手:主機1(可以使客戶端,也可以是伺服器端),設定Sequence Number和Acknowledgment
    Number,向主機2傳送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有資料要傳送給主機2了;
  • 第二次分手:主機2收到了主機1傳送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number為Sequence
  • 第三次分手:主機2向主機1傳送FIN報文段,請求關閉連線,同時主機2進入LAST_ACK狀態;
  • 第四次分手:主機1收到主機2傳送的FIN報文段,向主機2傳送ACK報文段,然後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段以後,就關閉連線;此時,主機1等待2MSL後依然沒有收到回覆,則證明Server端已正常關閉,那好,主機1也可以關閉連線了。

來看下面的圖加強下理解:

20160828170442731

keepalive connections

當然大量的連線每次連線關閉都要三次握手四次分手的很顯然會造成效能低下,因此http有一種叫做keepalive connections的機制,它可以在傳輸資料後仍然保持連線,當客戶端需要再次獲取資料時,直接使用剛剛空閒下來的連線而不需要再次握手。

20160828174519845

Okhttp支援5個併發KeepAlive,預設鏈路生命為5分鐘(鏈路空閒後,保持存活的時間)。

2.連線池(ConnectionPool)分析

引用計數

在okhttp中,在高層程式碼的呼叫中,使用了類似於引用計數的方式跟蹤Socket流的呼叫,這裡的計數物件是StreamAllocation,它被反覆執行aquire與release操作,這兩個函式其實是在改變RealConnection中的List<Reference<StreamAllocation>> 的大小。(StreamAllocation.Java)

Java
123 publicvoidacquire(RealConnection connection){connection.allocations.add(newWeakReference<>(this));}
Java
12345678910 privatevoidrelease(RealConnection connection){for(inti=0,size=connection.allocations.size();i<size;i++){Reference<StreamAllocation>reference=connection.allocations.get(i);if(reference.get()==this){connection.allocations.remove(i);return;}}thrownewIllegalStateException();}

RealConnection是socket物理連線的包裝,它裡面維護了List<Reference<StreamAllocation>>的引用。List中StreamAllocation的數量也就是socket被引用的計數,如果計數為0的話,說明此連線沒有被使用就是空閒的,需要通過下文的演算法實現回收;如果計數不為0,則表示上層程式碼仍然引用,就不需要關閉連線。

主要變數

連線池的類位於okhttp3.ConnectionPool:

Java
12345678910111213 privatestaticfinalExecutor executor=newThreadPoolExecutor(0/* corePoolSize */,Integer.MAX_VALUE/* maximumPoolSize */,60L/* keepAliveTime */,TimeUnit.SECONDS,newSynchronousQueue<Runnable>(),Util.threadFactory("OkHttp ConnectionPool",true));/** The maximum number of idle connections for each address. *///空閒的socket最大連線數privatefinalintmaxIdleConnections;//socket的keepAlive時間privatefinallongkeepAliveDurationNs;// 雙向佇列privatefinalDeque<RealConnection>connections=newArrayDeque<>();finalRouteDatabase routeDatabase=newRouteDatabase();booleancleanupRunning;

主要的變數有必要說明一下:

  • executor執行緒池,類似於CachedThreadPool,需要注意的是這種執行緒池的工作佇列採用了沒有容量的SynchronousQueue,不瞭解它的請檢視Java併發程式設計(六)阻塞佇列這篇文章。
  • Deque<RealConnection>,雙向佇列,雙端佇列同時具有佇列和棧性質,經常在快取中被使用,裡面維護了RealConnection也就是socket物理連線的包裝。
  • RouteDatabase,它用來記錄連線失敗的Route的黑名單,當連線失敗的時候就會把失敗的線路加進去。

建構函式

Java
12345678910111213 publicConnectionPool(){//預設空閒的socket最大連線數為5個,socket的keepAlive時間為5秒this(5,5,TimeUnit.MINUTES);}publicConnectionPool(intmaxIdleConnections,longkeepAliveDuration,TimeUnit timeUnit){this.maxIdleConnections=maxIdleConnections;this.keepAliveDurationNs=timeUnit.toNanos(keepAliveDuration);// Put a floor on the keep alive duration, otherwise cleanup will spin loop.if(keepAliveDuration<=0){thrownewIllegalArgumentException("keepAliveDuration <= 0: "+keepAliveDuration);}}

通過建構函式可以看出ConnectionPool預設的空閒的socket最大連線數為5個,socket的keepAlive時間為5秒。

例項化

ConnectionPool例項化是在OkHttpClient例項化時進行的:

Java
123 publicOkHttpClient(){this(newBuilder());}

在OkHttpClient的建構函式中呼叫了new Builder():

Java
123456 publicBuilder(){dispatcher=newDispatcher();...省略connectionPool=newConnectionPool();...省略}

快取操作

ConnectionPool提供對Deque<RealConnection>進行操作的方法分別為put、get、connectionBecameIdle和evictAll幾個操作。分別對應放入連線、獲取連線、移除連線和移除所有連線操作,這裡我們舉例put和get操作。

put操作

Java
12345678 voidput(RealConnection connection){assert(Thread.holdsLock(this));if(!cleanupRunning){cleanupRunning=true;executor.execute(cleanupRunnable);}connections.add(connection);}

在新增到Deque<RealConnection>之前首先要清理空閒的執行緒,這個後面會講到。

get操作

Java
123456789101112 RealConnection get(Address address,StreamAllocation streamAllocation){assert(Thread.holdsLock(this));for(RealConnection connection:connections){if(connection.allocations.size()<connection.allocationLimit&&address.equals(connection.route().address)&&!connection.noNewStreams){streamAllocation.acquire(connection);returnconnection;}}returnnull;}

遍歷connections快取列表,當某個連線計數的次數小於限制的大小並且request的地址和快取列表中此連線的地址完全匹配。則直接複用快取列表中的connection作為request的連線。

自動回收連線

okhttp是根據StreamAllocation引用計數是否為0來實現自動回收連線的。我們在put操作前首先要呼叫executor.execute(cleanupRunnable)來清理閒置的執行緒。我們來看看cleanupRunnable到底做了什麼:

Java
123456789101112131415161718 privatefinalRunnable cleanupRunnable=newRunnable(){@Overridepublicvoidrun(){while(true){longwaitNanos=cleanup(System.nanoTime());if(waitNanos==-1)return;if(waitNanos>0){longwaitMillis=waitNanos/1000000L;waitNanos-=(waitMillis*1000000L);synchronized(ConnectionPool.this){try{ConnectionPool.this.wait(waitMillis,(int)waitNanos);}catch(InterruptedException ignored){}}}}}};

執行緒不斷的呼叫cleanup來進行清理,並返回下次需要清理的間隔時間,然後呼叫wait進行等待以釋放鎖與時間片,當等待時間到了後,再次進行清理,並返回下次要清理的間隔時間,如此迴圈下去,接下來看看cleanup方法:

Java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 longcleanup(longnow){intinUseConnectionCount=0;intidleConnectionCount=0;RealConnection longestIdleConnection=null;longlongestIdleDurationNs=Long.MIN_VALUE;// Find either a connection to evict, or the time that the next eviction is due.synchronized(this){//遍歷連線for(Iterator<RealConnection>i=connections.iterator();i.hasNext();){