為什么使用HTTP連接池?
隨着系統架構風格逐漸向前后端分離架構,微服務架構轉變,RestFul風格API的開發與設計,同時SpringMVC也很好的支持了REST風格接口。各個系統之間服務的調用大多采用HTTP+JSON
或HTTPS+JSON
方式。
HTTP1.1默認是持久連接,HTTP1.0也可以通過在請求頭中設置Connection:keep-alive
使得連接成為長連接。既然HTTP協議支持長連接,那么HTTP連接同樣可以使用連接池技術來管理和維護連接建立和銷毀。 但是由於每次HTTP連接請求實際上都是在傳輸層建立的TCP連接,利用的socket進行通信,HTTP連接的保持和關閉實際上都同TCP連接的建立和關閉有關,所有每次HTTP請求都有經過TCP連接的三次握手(建立連接)和四次揮手(釋放連接)的過程。所以采用HTTP連接池有以下優勢:
- 降低了頻繁建立HTTP連接的時間開銷,減少了TCP連接建立和釋放時socket通信服務器端資源的浪費;
- 支持更高的並發量;
常用HttpClient連接池API
本文使用的是目前最新版本的HttpClient4.5.3,所以下文的內容都是基於該版本書寫。
- PoolingHttpClientConnectionManager連接池管理實現類
PoolingHttpClientConnectionManager
是一個HttpClient連接池實現類,實現了HttpClientConnectionManager
和ConnPoolControl
接口。類層次結構如下圖所示:

構造方法:PoolingHttpClientConnectionManager()
:無參構造方法,從源碼中可以看到該方法調用了getDefaultRegistry()來注冊http協議和https協議。
常用方法:public void setMaxTotal(int max)
:該方法定義在ConnPoolControl接口中,表示設置最大連接數為max。public void setDefaultMaxPerRoute(int max)
:該方法也是定義在ConnPoolControl接口中,表示將每個路由的默認最大連接數設置為maxpublic void setMaxPerRoute(HttpRoute route,int max)
:設置某個指定路由的最大連接數,這個配置會覆蓋setDefaultMaxPerRoute的某個路由的值。
- RequestConfig請求參數配置類

常用方法static RequestConfig.Builder custom()
:靜態方法,用於構建Builder 對象,然后設置相應的參數;int getConnectionRequestTimeout()
:獲取從連接池獲取連接的最長時間,單位是毫秒;int getConnectTimeout()
:獲取創建連接的最長時間,單位是毫秒;int getSocketTimeout()
:獲取數據傳輸的最長時間,單位是毫秒;
RequestConfig有一個靜態內部類Builder,用於構建RequestConfig對象並設置請求參數,該類有以下常用方法:public RequestConfig.Builder setConnectionRequestTimeout(int connectionRequestTimeout)
:設置從連接池獲取連接的最長時間,單位是毫秒;public RequestConfig.Builder setConnectTimeout(int connectTimeout)
:設置創建連接的最長時間,單位是毫秒;public RequestConfig.Builder setSocketTimeout(int socketTimeout)
:設置數據傳輸的最長時間,單位是毫秒;
- HttpRequestRetryHandler請求重試接口
boolean retryRequest(IOException exception, int executionCount, org.apache.http.protocol.HttpContext context)
:實現該接口的,必須實現該方法,決定了如果一個方法執行時發生了IO異常,是否應該重試,重試executionCount次。
單線程-使用連接池管理HTTP請求
主要步驟:
- 創建HTTP的連接池管理對象cm,如下所示
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
2.設置最大連接數和每個路由的默認最大連接數等參數,如下所示
//將最大連接數增加到200 cm.setMaxTotal(200); //將每個路由的默認最大連接數增加到20 cm.setDefaultMaxPerRoute(20);
3.模擬發送HttpGet請求或者HttpPost請求,注意這里創建HttpClients對象的時候需要從連接池中獲取,即要設置連接池對象,當執行發生IO異常需要處理時,要實現HttpRequestRetryHandler接口;需要注意的是這里處理完請求響應后,不能關閉HttpClient對象,如果關閉連接池也會銷毀。HttpClients對象創建對象所示:
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setRetryHandler(retryHandler(5)).build();
4.可以開啟線程來監聽連接池中空閑連接,並清理無效連接,線程監聽頻率可以自行設置。
Java實現源碼:
package com.liangpj.develop.httpclient; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; import org.apache.http.NoHttpResponseException; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.io.InterruptedIOException; import java.net.UnknownHostException; /** * 單線程-使用連接池管理HTTP請求 * @author: liangpengju * @version: 1.0 */ public class HttpConnectManager { public static void main(String[] args) throws Exception { //創建HTTP的連接池管理對象 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); //將最大連接數增加到200 connectionManager.setMaxTotal(200); //將每個路由的默認最大連接數增加到20 connectionManager.setDefaultMaxPerRoute(20); //將http://www.baidu.com:80的最大連接增加到50 //HttpHost httpHost = new HttpHost("http://www.baidu.com",80); //connectionManager.setMaxPerRoute(new HttpRoute(httpHost),50); //發起3次GET請求 String url ="https://www.baidu.com/s?word=java"; long start = System.currentTimeMillis(); for (int i=0;i<100;i++){ doGet(connectionManager,url); } long end = System.currentTimeMillis(); System.out.println("consume -> " + (end - start)); //清理無效連接 new IdleConnectionEvictor(connectionManager).start(); } /** * 請求重試處理 * @param tryTimes 重試次數 * @return */ public static HttpRequestRetryHandler retryHandler(final int tryTimes){ HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { @Override public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { // 如果已經重試了n次,就放棄 if (executionCount >= tryTimes) { return false; } // 如果服務器丟掉了連接,那么就重試 if (exception instanceof NoHttpResponseException) { return true; } // 不要重試SSL握手異常 if (exception instanceof SSLHandshakeException) { return false; } // 超時 if (exception instanceof InterruptedIOException) { return false; } // 目標服務器不可達 if (exception instanceof UnknownHostException) { return true; } // 連接被拒絕 if (exception instanceof ConnectTimeoutException) { return false; } // SSL握手異常 if (exception instanceof SSLException) { return false; } HttpClientContext clientContext = HttpClientContext .adapt(context); HttpRequest request = clientContext.getRequest(); // 如果請求是冪等的,就再次嘗試 if (!(request instanceof HttpEntityEnclosingRequest)) { return true; } return false; } }; return httpRequestRetryHandler; } /** * doGet * @param url 請求地址 * @param connectionManager * @throws Exception */ public static void doGet(HttpClientConnectionManager connectionManager,String url) throws Exception { //從連接池中獲取client對象,多例 CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setRetryHandler(retryHandler(5)).build(); // 創建http GET請求 HttpGet httpGet = new HttpGet(url); // 構建請求配置信息 RequestConfig config = RequestConfig.custom().setConnectTimeout(1000) // 創建連接的最長時間 .setConnectionRequestTimeout(500) // 從連接池中獲取到連接的最長時間 .setSocketTimeout(10 * 1000) // 數據傳輸的最長時間10s .setStaleConnectionCheckEnabled(true) // 提交請求前測試連接是否可用 .build(); // 設置請求配置信息 httpGet.setConfig(config); CloseableHttpResponse response = null; try { // 執行請求 response = httpClient.execute(httpGet); // 判斷返回狀態是否為200 if (response.getStatusLine().getStatusCode() == 200) { String content = EntityUtils.toString(response.getEntity(), "UTF-8"); System.out.println("內容長度:" + content.length()); } } finally { if (response != null) { response.close(); } // 此處不能關閉httpClient,如果關閉httpClient,連接池也會銷毀 // httpClient.close(); } } /** * 監聽連接池中空閑連接,清理無效連接 */ public static class IdleConnectionEvictor extends Thread { private final HttpClientConnectionManager connectionManager; private volatile boolean shutdown; public IdleConnectionEvictor(HttpClientConnectionManager connectionManager) { this.connectionManager = connectionManager; } @Override public void run() { try { while (!shutdown) { synchronized (this) { //3s檢查一次 wait(3000); // 關閉失效的連接 connectionManager.closeExpiredConnections(); } } } catch (InterruptedException ex) { // 結束 ex.printStackTrace(); } } public void shutdown() { shutdown = true; synchronized (this) { notifyAll(); } } } }
多線程-HttpClient連接池管理HTTP請求實例
主要步驟:
- 創建HTTP的連接池管理對象cm,如下所示
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
2.設置最大連接數和每個路由的默認最大連接數等參數,如下所示
//將最大連接數增加到200 cm.setMaxTotal(200); //將每個路由的默認最大連接數增加到20 cm.setDefaultMaxPerRoute(20);
3.創建HttpClients對象的並設置連接池對象,HttpClients對象創建對象所示:
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
4.繼承Thread類實現一個執行Get請求線程類GetThread,重載run()方法,執行HttpGet請求;
5.定義要請求的URI地址為數組形式,為每一個URI創建一個GetThread線程,並啟動所有線程;
Java實現源碼:
package com.liangpj.develop.httpclient; import org.apache.http.HttpEntity; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.protocol.HttpContext; import java.io.IOException; /** * 多線程-HttpClient連接池管理HTTP請求實例 */ public class MultiThreadHttpConnManager { public static void main(String[] args) { //連接池對象 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); //將最大連接數增加到200 connectionManager.setMaxTotal(200); //將每個路由的默認最大連接數增加到20 connectionManager.setDefaultMaxPerRoute(20); //HttpClient對象 CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build(); //URIs to DoGet String[] urisToGet = { "https://www.baidu.com/s?word=java", "https://www.baidu.com/s?word=java", "https://www.baidu.com/s?word=java", "https://www.baidu.com/s?word=java" }; //為每一個URI創建一個線程 GetThread[] threads = new GetThread[urisToGet.length]; for (int i=0;i<threads.length;i++){ HttpGet httpGet = new HttpGet(urisToGet[i]); threads[i] = new GetThread(httpClient,httpGet); } //啟動線程 for (int j=0;j<threads.length;j++){ threads[j].start(); } //join 線程 for(int k=0;k<threads.length;k++){ try { threads[k].join(); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 執行Get請求線程 */ public static class GetThread extends Thread{ private final CloseableHttpClient httpClient; private final HttpContext context; private final HttpGet httpget; public GetThread(CloseableHttpClient httpClient, HttpGet httpget) { this.httpClient = httpClient; this.context = HttpClientContext.create(); this.httpget = httpget; } @Override public void run() { try { CloseableHttpResponse response = httpClient.execute(httpget,context); try { HttpEntity entity = response.getEntity(); }finally { response.close(); } }catch (ClientProtocolException ex){ //處理客戶端協議異常 }catch (IOException ex){ //處理客戶端IO異常 } } } }
想要獲取HttpClient實戰的所有實例代碼,可以關注Java技術日志訂閱號后,在消息框回復關鍵字:httpclient可以獲取代碼的地址。