HttpClient實戰二:單線程和多線程連接池實例


為什么使用HTTP連接池?

隨着系統架構風格逐漸向前后端分離架構,微服務架構轉變,RestFul風格API的開發與設計,同時SpringMVC也很好的支持了REST風格接口。各個系統之間服務的調用大多采用HTTP+JSONHTTPS+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連接池實現類,實現了HttpClientConnectionManagerConnPoolControl接口。類層次結構如下圖所示:
 
PoolingHttpClientConnectionManager類層次結構

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

  • RequestConfig請求參數配置類
RequestConfig方法與內部類Builder

常用方法
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請求

主要步驟:

  1. 創建HTTP的連接池管理對象cm,如下所示
1 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();

2.設置最大連接數和每個路由的默認最大連接數等參數,如下所示

1 //將最大連接數增加到200
2  cm.setMaxTotal(200);
3 //將每個路由的默認最大連接數增加到20
4  cm.setDefaultMaxPerRoute(20);

3.模擬發送HttpGet請求或者HttpPost請求,注意這里創建HttpClients對象的時候需要從連接池中獲取,即要設置連接池對象,當執行發生IO異常需要處理時,要實現HttpRequestRetryHandler接口;需要注意的是這里處理完請求響應后,不能關閉HttpClient對象,如果關閉連接池也會銷毀。HttpClients對象創建對象所示:

1  CloseableHttpClient httpClient = HttpClients.custom()
2                 .setConnectionManager(connectionManager)
3                 .setRetryHandler(retryHandler(5)).build();

4.可以開啟線程來監聽連接池中空閑連接,並清理無效連接,線程監聽頻率可以自行設置。
Java實現源碼:

  1 package com.liangpj.develop.httpclient;
  2 import org.apache.http.HttpEntityEnclosingRequest;
  3 import org.apache.http.HttpRequest;
  4 import org.apache.http.NoHttpResponseException;
  5 import org.apache.http.client.HttpRequestRetryHandler;
  6 import org.apache.http.client.config.RequestConfig;
  7 import org.apache.http.client.methods.CloseableHttpResponse;
  8 import org.apache.http.client.methods.HttpGet;
  9 import org.apache.http.client.protocol.HttpClientContext;
 10 import org.apache.http.conn.ConnectTimeoutException;
 11 import org.apache.http.conn.HttpClientConnectionManager;
 12 import org.apache.http.impl.client.CloseableHttpClient;
 13 import org.apache.http.impl.client.HttpClients;
 14 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 15 import org.apache.http.protocol.HttpContext;
 16 import org.apache.http.util.EntityUtils;
 17 import javax.net.ssl.SSLException;
 18 import javax.net.ssl.SSLHandshakeException;
 19 import java.io.IOException;
 20 import java.io.InterruptedIOException;
 21 import java.net.UnknownHostException;
 22 
 23 /**
 24  * 單線程-使用連接池管理HTTP請求
 25  * @author: liangpengju
 26  * @version: 1.0
 27  */
 28 public class HttpConnectManager {
 29 
 30     public static void main(String[] args) throws Exception {
 31         //創建HTTP的連接池管理對象
 32         PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
 33         //將最大連接數增加到200
 34         connectionManager.setMaxTotal(200);
 35         //將每個路由的默認最大連接數增加到20
 36         connectionManager.setDefaultMaxPerRoute(20);
 37         //將http://www.baidu.com:80的最大連接增加到50
 38         //HttpHost httpHost = new HttpHost("http://www.baidu.com",80);
 39         //connectionManager.setMaxPerRoute(new HttpRoute(httpHost),50);
 40 
 41         //發起3次GET請求
 42         String url ="https://www.baidu.com/s?word=java";
 43         long start = System.currentTimeMillis();
 44         for (int i=0;i<100;i++){
 45             doGet(connectionManager,url);
 46         }
 47         long end = System.currentTimeMillis();
 48         System.out.println("consume -> " + (end - start));
 49 
 50         //清理無效連接
 51         new IdleConnectionEvictor(connectionManager).start();
 52     }
 53 
 54     /**
 55      * 請求重試處理
 56      * @param tryTimes 重試次數
 57      * @return
 58      */
 59     public static HttpRequestRetryHandler retryHandler(final int tryTimes){
 60 
 61         HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
 62             @Override
 63             public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
 64                 // 如果已經重試了n次,就放棄
 65                 if (executionCount >= tryTimes) {
 66                     return false;
 67                 }
 68                 // 如果服務器丟掉了連接,那么就重試
 69                 if (exception instanceof NoHttpResponseException) {
 70                     return true;
 71                 }
 72                 // 不要重試SSL握手異常
 73                 if (exception instanceof SSLHandshakeException) {
 74                     return false;
 75                 }
 76                 // 超時
 77                 if (exception instanceof InterruptedIOException) {
 78                     return false;
 79                 }
 80                 // 目標服務器不可達
 81                 if (exception instanceof UnknownHostException) {
 82                     return true;
 83                 }
 84                 // 連接被拒絕
 85                 if (exception instanceof ConnectTimeoutException) {
 86                     return false;
 87                 }
 88                 // SSL握手異常
 89                 if (exception instanceof SSLException) {
 90                     return false;
 91                 }
 92                 HttpClientContext clientContext = HttpClientContext .adapt(context);
 93                 HttpRequest request = clientContext.getRequest();
 94                 // 如果請求是冪等的,就再次嘗試
 95                 if (!(request instanceof HttpEntityEnclosingRequest)) {
 96                     return true;
 97                 }
 98                 return false;
 99             }
100         };
101         return httpRequestRetryHandler;
102     }
103 
104     /**
105      * doGet
106      * @param url 請求地址
107      * @param connectionManager
108      * @throws Exception
109      */
110     public static void doGet(HttpClientConnectionManager connectionManager,String url) throws Exception {
111         //從連接池中獲取client對象,多例
112         CloseableHttpClient httpClient = HttpClients.custom()
113                 .setConnectionManager(connectionManager)
114                 .setRetryHandler(retryHandler(5)).build();
115 
116         // 創建http GET請求
117         HttpGet httpGet = new HttpGet(url);
118         // 構建請求配置信息
119         RequestConfig config = RequestConfig.custom().setConnectTimeout(1000) // 創建連接的最長時間
120                 .setConnectionRequestTimeout(500) // 從連接池中獲取到連接的最長時間
121                 .setSocketTimeout(10 * 1000) // 數據傳輸的最長時間10s
122                 .setStaleConnectionCheckEnabled(true) // 提交請求前測試連接是否可用
123                 .build();
124         // 設置請求配置信息
125         httpGet.setConfig(config);
126 
127         CloseableHttpResponse response = null;
128         try {
129             // 執行請求
130             response = httpClient.execute(httpGet);
131             // 判斷返回狀態是否為200
132             if (response.getStatusLine().getStatusCode() == 200) {
133                 String content = EntityUtils.toString(response.getEntity(), "UTF-8");
134                 System.out.println("內容長度:" + content.length());
135             }
136         } finally {
137             if (response != null) {
138                 response.close();
139             }
140             // 此處不能關閉httpClient,如果關閉httpClient,連接池也會銷毀
141             // httpClient.close();
142         }
143     }
144 
145     /**
146      * 監聽連接池中空閑連接,清理無效連接
147      */
148     public static class IdleConnectionEvictor extends Thread {
149 
150         private final HttpClientConnectionManager connectionManager;
151 
152         private volatile boolean shutdown;
153 
154         public IdleConnectionEvictor(HttpClientConnectionManager connectionManager) {
155             this.connectionManager = connectionManager;
156         }
157 
158         @Override
159         public void run() {
160             try {
161                 while (!shutdown) {
162                     synchronized (this) {
163                         //3s檢查一次
164                         wait(3000);
165                         // 關閉失效的連接
166                         connectionManager.closeExpiredConnections();
167                     }
168                 }
169             } catch (InterruptedException ex) {
170                 // 結束
171                 ex.printStackTrace();
172             }
173         }
174 
175         public void shutdown() {
176             shutdown = true;
177             synchronized (this) {
178                 notifyAll();
179             }
180         }
181     }
182 }

多線程-HttpClient連接池管理HTTP請求實例

主要步驟:

  1. 創建HTTP的連接池管理對象cm,如下所示
1 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();

2.設置最大連接數和每個路由的默認最大連接數等參數,如下所示

1  //將最大連接數增加到200
2  cm.setMaxTotal(200);
3 //將每個路由的默認最大連接數增加到20
4  cm.setDefaultMaxPerRoute(20);

3.創建HttpClients對象的並設置連接池對象,HttpClients對象創建對象所示:

1 CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();

4.繼承Thread類實現一個執行Get請求線程類GetThread,重載run()方法,執行HttpGet請求;
5.定義要請求的URI地址為數組形式,為每一個URI創建一個GetThread線程,並啟動所有線程;
Java實現源碼:

 1 package com.liangpj.develop.httpclient;
 2 import org.apache.http.HttpEntity;
 3 import org.apache.http.client.ClientProtocolException;
 4 import org.apache.http.client.methods.CloseableHttpResponse;
 5 import org.apache.http.client.methods.HttpGet;
 6 import org.apache.http.client.protocol.HttpClientContext;
 7 import org.apache.http.impl.client.CloseableHttpClient;
 8 import org.apache.http.impl.client.HttpClients;
 9 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
10 import org.apache.http.protocol.HttpContext;
11 import java.io.IOException;
12 
13 /**
14  * 多線程-HttpClient連接池管理HTTP請求實例
15  */
16 public class MultiThreadHttpConnManager {
17     public static void main(String[] args) {
18         //連接池對象
19         PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
20       //將最大連接數增加到200
21         connectionManager.setMaxTotal(200);
22         //將每個路由的默認最大連接數增加到20
23         connectionManager.setDefaultMaxPerRoute(20);
24         //HttpClient對象
25         CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
26         //URIs to DoGet
27         String[] urisToGet = {
28                 "https://www.baidu.com/s?word=java",
29                 "https://www.baidu.com/s?word=java",
30                 "https://www.baidu.com/s?word=java",
31                 "https://www.baidu.com/s?word=java"
32         };
33         //為每一個URI創建一個線程
34         GetThread[] threads = new GetThread[urisToGet.length];
35         for (int i=0;i<threads.length;i++){
36             HttpGet httpGet = new HttpGet(urisToGet[i]);
37             threads[i] = new GetThread(httpClient,httpGet);
38         }
39         //啟動線程
40         for (int j=0;j<threads.length;j++){
41             threads[j].start();
42         }
43         //join 線程
44         for(int k=0;k<threads.length;k++){
45             try {
46                 threads[k].join();
47             } catch (InterruptedException e) {
48                 e.printStackTrace();
49             }
50         }
51     }
52 
53     /**
54      * 執行Get請求線程
55      */
56     public static class GetThread extends Thread{
57         private final CloseableHttpClient httpClient;
58         private final HttpContext context;
59         private final HttpGet httpget;
60         public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
61             this.httpClient = httpClient;
62             this.context = HttpClientContext.create();
63             this.httpget = httpget;
64         }
65         @Override
66         public void run() {
67             try {
68                 CloseableHttpResponse response = httpClient.execute(httpget,context);
69                 try {
70                     HttpEntity entity = response.getEntity();
71                 }finally {
72                     response.close();
73                 }
74             }catch (ClientProtocolException ex){
75                 //處理客戶端協議異常
76             }catch (IOException ex){
77                 //處理客戶端IO異常
78             }
79         }
80     }
81 }

想要獲取HttpClient實戰的所有實例代碼,可以關注Java技術日志訂閱號后,在消息框回復關鍵字:httpclient可以獲取代碼的地址。



轉自:https://www.jianshu.com/p/187dde35336d


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM