1. 程式人生 > >Http 持久連線與 HttpClient 連線池

Http 持久連線與 HttpClient 連線池

一、背景

HTTP協議是無狀態的協議,即每一次請求都是互相獨立的。因此它的最初實現是,每一個http請求都會開啟一個tcp socket連線,當互動完畢後會關閉這個連線。

HTTP協議是全雙工的協議,所以建立連線與斷開連線是要經過三次握手與四次揮手的。顯然在這種設計中,每次傳送Http請求都會消耗很多的額外資源,即連線的建立與銷燬。

於是,HTTP協議的也進行了發展,通過持久連線的方法來進行socket連線複用。

從圖中可以看到:

  1. 在序列連線中,每次互動都要開啟關閉連線

  2. 在持久連線中,第一次互動會開啟連線,互動結束後連線並不關閉,下次互動就省去了建立連線的過程。

持久連線的實現有兩種:HTTP/1.0+的keep-alive與HTTP/1.1的持久連線。

二、HTTP/1.0+的Keep-Alive

從1996年開始,很多HTTP/1.0瀏覽器與伺服器都對協議進行了擴充套件,那就是“keep-alive”擴充套件協議。

注意,這個擴充套件協議是作為1.0的補充的“實驗型持久連線”出現的。keep-alive已經不再使用了,最新的HTTP/1.1規範中也沒有對它進行說明,只是很多應用延續了下來。

使用HTTP/1.0的客戶端在首部中加上”Connection:Keep-Alive”,請求服務端將一條連線保持在開啟狀態。服務端如果願意將這條連線保持在開啟狀態,就會在響應中包含同樣的首部。如果響應中沒有包含”Connection:Keep-Alive”首部,則客戶端會認為服務端不支援keep-alive,會在傳送完響應報文之後關閉掉當前連線。

通過keep-alive補充協議,客戶端與伺服器之間完成了持久連線,然而仍然存在著一些問題:

在HTTP/1.0中keep-alive不是標準協議,客戶端必須傳送Connection:Keep-Alive來啟用keep-alive連線。

代理伺服器可能無法支援keep-alive,因為一些代理是”盲中繼”,無法理解首部的含義,只是將首部逐跳轉發。所以可能造成客戶端與服務端都保持了連線,但是代理不接受該連線上的資料。

三、HTTP/1.1的持久連線

HTTP/1.1採取持久連線的方式替代了Keep-Alive。

HTTP/1.1的連線預設情況下都是持久連線。如果要顯式關閉,需要在報文中加上Connection:Close首部。即在HTTP/1.1中,所有的連線都進行了複用。

然而如同Keep-Alive一樣,空閒的持久連線也可以隨時被客戶端與服務端關閉。不傳送Connection:Close不意味著伺服器承諾連線永遠保持開啟。

四、HttpClient如何生成持久連線

HttpClien中使用了連線池來管理持有連線,同一條TCP鏈路上,連線是可以複用的。HttpClient通過連線池的方式進行連線持久化。

其實“池”技術是一種通用的設計,其設計思想並不複雜:

  1. 當有連線第一次使用的時候建立連線

  2. 結束時對應連線不關閉,歸還到池中

  3. 下次同個目的的連線可從池中獲取一個可用連線

  4. 定期清理過期連線

所有的連線池都是這個思路,不過我們看HttpClient原始碼主要關注兩點:

  • 連線池的具體設計方案,以供以後自定義連線池參考

  • 如何與HTTP協議對應上,即理論抽象轉為程式碼的實現

4.1 HttpClient連線池的實現

HttpClient關於持久連線的處理在下面的程式碼中可以集中體現,下面從MainClientExec摘取了和連線池相關的部分,去掉了其他部分:

public class MainClientExec implements ClientExecChain {

    @Override

    public CloseableHttpResponse execute(

            final HttpRoute route,

            final HttpRequestWrapper request,

            final HttpClientContext context,

            final HttpExecutionAware execAware) throws IOException, HttpException {

     //從連線管理器HttpClientConnectionManager中獲取一個連線請求ConnectionRequest

        final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;

        final int timeout = config.getConnectionRequestTimeout();

        //從連線請求ConnectionRequest中獲取一個被管理的連線HttpClientConnection

        managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);

     //將連線管理器HttpClientConnectionManager與被管理的連線HttpClientConnection交給一個ConnectionHolder持有

        final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);

        try {

            HttpResponse response;

            if (!managedConn.isOpen()) {

          //如果當前被管理的連線不是出於開啟狀態,需要重新建立連線

                establishRoute(proxyAuthState, managedConn, route, request, context);

            }

       //通過連線HttpClientConnection傳送請求

            response = requestExecutor.execute(request, managedConn, context);

       //通過連線重用策略判斷是否連線可重用         

            if (reuseStrategy.keepAlive(response, context)) {

                //獲得連線有效期

                final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);

                //設定連線有效期

                connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);

          //將當前連線標記為可重用狀態

                connHolder.markReusable();

            } else {

                connHolder.markNonReusable();

            }

        }

        final HttpEntity entity = response.getEntity();

        if (entity == null || !entity.isStreaming()) {

            //將當前連線釋放到池中,供下次呼叫

            connHolder.releaseConnection();

            return new HttpResponseProxy(response, null);

        } else {

            return new HttpResponseProxy(response, connHolder);

        }

}

這裡看到了在Http請求過程中對連線的處理是和協議規範是一致的,這裡要展開講一下具體實現。

PoolingHttpClientConnectionManager是HttpClient預設的連線管理器,首先通過requestConnection()獲得一個連線的請求,注意這裡不是連線。

public ConnectionRequest requestConnection(

            final HttpRoute route,

            final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null);

        return new ConnectionRequest() {

            @Override

            public boolean cancel() {

                return future.cancel(true);

            }

            @Override

            public HttpClientConnection get(

                    final long timeout,

                    final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {

                final HttpClientConnection conn = leaseConnection(future, timeout, tunit);

                if (conn.isOpen()) {

                    final HttpHost host;

                    if (route.getProxyHost() != null) {

                        host = route.getProxyHost();

                    } else {

                        host = route.getTargetHost();

                    }

                    final SocketConfig socketConfig = resolveSocketConfig(host);

                    conn.setSocketTimeout(socketConfig.getSoTimeout());

                }

                return conn;

            }

        };

    }

可以看到返回的ConnectionRequest物件實際上是一個持有了Future<CPoolEntry>,CPoolEntry是被連線池管理的真正連線例項。

從上面的程式碼我們應該關注的是:

  • Future<CPoolEntry> future = this.pool.lease(route, state, null)

    如何從連線池CPool中獲得一個非同步的連線,Future<CPoolEntry>

  • HttpClientConnection conn = leaseConnection(future, timeout, tunit)

    如何通過非同步連線Future<CPoolEntry>獲得一個真正的連線HttpClientConnection

4.2 Future<CPoolEntry>

看一下CPool是如何釋放一個Future<CPoolEntry>的,AbstractConnPool核心程式碼如下:

private E getPoolEntryBlocking(

            final T route, final Object state,

            final long timeout, final TimeUnit tunit,

            final Future<E> future) throws IOException, InterruptedException, TimeoutException {

     //首先對當前連線池加鎖,當前鎖是可重入鎖ReentrantLockthis.lock.lock();

        try {

        //獲得一個當前HttpRoute對應的連線池,對於HttpClient的連線池而言,總池有個大小,每個route對應的連線也是個池,所以是“池中池”

            final RouteSpecificPool<T, C, E> pool = getPool(route);

            E entry;

            for (;;) {

                Asserts.check(!this.isShutDown, "Connection pool shut down");

          //死迴圈獲得連線

                for (;;) {

            //從route對應的池中拿連線,可能是null,也可能是有效連線

                    entry = pool.getFree(state);

            //如果拿到null,就退出迴圈

                    if (entry == null) {

                        break;

                    }

            //如果拿到過期連線或者已關閉連線,就釋放資源,繼續迴圈獲取

                    if (entry.isExpired(System.currentTimeMillis())) {

                        entry.close();

                    }

                    if (entry.isClosed()) {

                        this.available.remove(entry);

                        pool.free(entry, false);

                    } else {

              //如果拿到有效連線就退出迴圈

                        break;

                    }

                }

          //拿到有效連線就退出

                if (entry != null) {

                    this.available.remove(entry);

                    this.leased.add(entry);

                    onReuse(entry);

                    return entry;

                }

          //到這裡證明沒有拿到有效連線,需要自己生成一個                

                final int maxPerRoute = getMax(route);

                //每個route對應的連線最大數量是可配置的,如果超過了,就需要通過LRU清理掉一些連線

                final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);

                if (excess > 0) {

                    for (int i = 0; i < excess; i++) {

                        final E lastUsed = pool.getLastUsed();

                        if (lastUsed == null) {

                            break;

                        }

                        lastUsed.close();

                        this.available.remove(lastUsed);

                        pool.remove(lastUsed);

                    }

                }

          //當前route池中的連線數,沒有達到上線

                if (pool.getAllocatedCount() < maxPerRoute) {

                    final int totalUsed = this.leased.size();

                    final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);

            //判斷連線池是否超過上線,如果超過了,需要通過LRU清理掉一些連線

                    if (freeCapacity > 0) {

                        final int totalAvailable = this.available.size();

               //如果空閒連線數已經大於剩餘可用空間,則需要清理下空閒連線

                        if (totalAvailable > freeCapacity - 1) {

                            if (!this.available.isEmpty()) {

                                final E lastUsed = this.available.removeLast();

                                lastUsed.close();

                                final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());

                                otherpool.remove(lastUsed);

                            }

                        }

              //根據route建立一個連線

                        final C conn = this.connFactory.create(route);

              //將這個連線放入route對應的“小池”中

                        entry = pool.add(conn);

              //將這個連線放入“大池”中

                        this.leased.add(entry);

                        return entry;

                    }

                }

         //到這裡證明沒有從獲得route池中獲得有效連線,並且想要自己建立連線時當前route連線池已經到達最大值,即已經有連線在使用,但是對當前執行緒不可用

                boolean success = false;

                try {

                    if (future.isCancelled()) {

                        throw new InterruptedException("Operation interrupted");

                    }

            //將future放入route池中等待

                    pool.queue(future);

            //將future放入大連線池中等待

                    this.pending.add(future);

            //如果等待到了訊號量的通知,success為true

                    if (deadline != null) {

                        success = this.condition.awaitUntil(deadline);

                    } else {

                        this.condition.await();

                        success = true;

                    }

                    if (future.isCancelled()) {

                        throw new InterruptedException("Operation interrupted");

                    }

                } finally {

                    //從等待佇列中移除

                    pool.unqueue(future);

                    this.pending.remove(future);

                }

                //如果沒有等到訊號量通知並且當前時間已經超時,則退出迴圈

                if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {

                    break;

                }

            }

       //最終也沒有等到訊號量通知,沒有拿到可用連線,則拋異常

            throw new TimeoutException("Timeout waiting for connection");

        } finally {

       //釋放對大連線池的鎖

            this.lock.unlock();

        }

    }

上面的程式碼邏輯有幾個重要點:

  • 連線池有個最大連線數,每個route對應一個小連線池,也有個最大連線數

  • 不論是大連線池還是小連線池,當超過數量的時候,都要通過LRU釋放一些連線

  • 如果拿到了可用連線,則返回給上層使用

  • 如果沒有拿到可用連線,HttpClient會判斷當前route連線池是否已經超過了最大數量,沒有到上限就會新建一個連線,並放入池中

  • 如果到達了上限,就排隊等待,等到了訊號量,就重新獲得一次,等待不到就拋超時異常