httpclient(4.5.x)默認是啓動鏈接池的, 其下降時耗(避免鏈接初3次握手, 以及關閉4次握手的消耗), 顯著提高高併發處理能力(大量減小time_wait), 確實扮演了重要的角色. 可是封裝httpclient, 須要瞭解很多細節, 還要根據業務合理配置參數.
這裏結合這段時間深刻httpclient(4.5.x)源碼分析, 結合網上的代碼案例, 以及線下測試的結果. 嘗試寫一個可用的的httpclient封裝類, 兼顧性能, 接口友好. 感謝cctv, ^_^.html
1. HttpClient官方sample代碼的深刻分析(鏈接池)java
啥也不說了, 直接貼代碼了.api
public class PooledHttpClientAdaptor { private static final int DEFAULT_POOL_MAX_TOTAL = 200; private static final int DEFAULT_POOL_MAX_PER_ROUTE = 200; private static final int DEFAULT_CONNECT_TIMEOUT = 500; private static final int DEFAULT_CONNECT_REQUEST_TIMEOUT = 500; private static final int DEFAULT_SOCKET_TIMEOUT = 2000; private PoolingHttpClientConnectionManager gcm = null; private CloseableHttpClient httpClient = null; // 鏈接池的最大鏈接數 private final int maxTotal; // 鏈接池按route配置的最大鏈接數 private final int maxPerRoute; // tcp connect的超時時間 private final int connectTimeout; // 從鏈接池獲取鏈接的超時時間 private final int connectRequestTimeout; // tcp io的讀寫超時時間 private final int socketTimeout; public PooledHttpClientAdaptor() { this( PooledHttpClientAdaptor.DEFAULT_POOL_MAX_TOTAL, PooledHttpClientAdaptor.DEFAULT_POOL_MAX_PER_ROUTE, PooledHttpClientAdaptor.DEFAULT_CONNECT_TIMEOUT, PooledHttpClientAdaptor.DEFAULT_CONNECT_REQUEST_TIMEOUT, PooledHttpClientAdaptor.DEFAULT_SOCKET_TIMEOUT ); } public PooledHttpClientAdaptor( int maxTotal, int maxPerRoute, int connectTimeout, int connectRequestTimeout, int socketTimeout ) { this.maxTotal = maxTotal; this.maxPerRoute = maxPerRoute; this.connectTimeout = connectTimeout; this.connectRequestTimeout = connectRequestTimeout; this.socketTimeout = socketTimeout; Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(); gcm = new PoolingHttpClientConnectionManager(registry); gcm.setMaxTotal(this.maxTotal); gcm.setDefaultMaxPerRoute(this.maxPerRoute); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(this.connectTimeout) // 設置鏈接超時 .setSocketTimeout(this.socketTimeout) // 設置讀取超時 .setConnectionRequestTimeout(this.connectRequestTimeout) // 設置從鏈接池獲取鏈接實例的超時 .build(); HttpClientBuilder httpClientBuilder = HttpClients.custom(); httpClient = httpClientBuilder .setConnectionManager(gcm) .setDefaultRequestConfig(requestConfig) .build(); } public String doGet(String url) { return this.doGet(url, Collections.EMPTY_MAP, Collections.EMPTY_MAP); } public String doGet(String url, Map<String, Object> params) { return this.doGet(url, Collections.EMPTY_MAP, params); } public String doGet(String url, Map<String, String> headers, Map<String, Object> params ) { // *) 構建GET請求頭 String apiUrl = getUrlWithParams(url, params); HttpGet httpGet = new HttpGet(apiUrl); // *) 設置header信息 if ( headers != null && headers.size() > 0 ) { for (Map.Entry<String, String> entry : headers.entrySet()) { httpGet.addHeader(entry.getKey(), entry.getValue()); } } CloseableHttpResponse response = null; try { response = httpClient.execute(httpGet); if (response == null || response.getStatusLine() == null) { return null; } int statusCode = response.getStatusLine().getStatusCode(); if ( statusCode == HttpStatus.SC_OK ) { HttpEntity entityRes = response.getEntity(); if (entityRes != null) { return EntityUtils.toString(entityRes, "UTF-8"); } } return null; } catch (IOException e) { } finally { if ( response != null ) { try { response.close(); } catch (IOException e) { } } } return null; } public String doPost(String apiUrl, Map<String, Object> params) { return this.doPost(apiUrl, Collections.EMPTY_MAP, params); } public String doPost(String apiUrl, Map<String, String> headers, Map<String, Object> params ) { HttpPost httpPost = new HttpPost(apiUrl); // *) 配置請求headers if ( headers != null && headers.size() > 0 ) { for (Map.Entry<String, String> entry : headers.entrySet()) { httpPost.addHeader(entry.getKey(), entry.getValue()); } } // *) 配置請求參數 if ( params != null && params.size() > 0 ) { HttpEntity entityReq = getUrlEncodedFormEntity(params); httpPost.setEntity(entityReq); } CloseableHttpResponse response = null; try { response = httpClient.execute(httpPost); if (response == null || response.getStatusLine() == null) { return null; } int statusCode = response.getStatusLine().getStatusCode(); if ( statusCode == HttpStatus.SC_OK ) { HttpEntity entityRes = response.getEntity(); if ( entityRes != null ) { return EntityUtils.toString(entityRes, "UTF-8"); } } return null; } catch (IOException e) { } finally { if (response != null) { try { response.close(); } catch (IOException e) { } } } return null; } private HttpEntity getUrlEncodedFormEntity(Map<String, Object> params) { List<NameValuePair> pairList = new ArrayList<NameValuePair>(params.size()); for (Map.Entry<String, Object> entry : params.entrySet()) { NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry .getValue().toString()); pairList.add(pair); } return new UrlEncodedFormEntity(pairList, Charset.forName("UTF-8")); } private String getUrlWithParams(String url, Map<String, Object> params) { boolean first = true; StringBuilder sb = new StringBuilder(url); for (String key : params.keySet()) { char ch = '&'; if (first == true) { ch = '?'; first = false; } String value = params.get(key).toString(); try { String sval = URLEncoder.encode(value, "UTF-8"); sb.append(ch).append(key).append("=").append(sval); } catch (UnsupportedEncodingException e) { } } return sb.toString(); } }
這個版本基本沒啥問題, 可是當流量爲0時, 你會發現存在處於ClOSE_WAIT的鏈接. 究其緣由是, httpclient清理過時/被動關閉的socket, 是採用懶惰清理的策略. 它是在鏈接從鏈接池取出使用的時候, 檢測狀態並作相應處理. 若是沒有流量, 那這些socket將一直處於CLOSE_WAIT(半鏈接的狀態), 系統資源被浪費.
不過解決方案也至關的簡單, 官方的建議引入一個清理線程, 按期主動處理過時/空閒鏈接, 這樣就OK了.安全
private class IdleConnectionMonitorThread extends Thread { private final HttpClientConnectionManager connMgr; private volatile boolean exitFlag = false; public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) { this.connMgr = connMgr; setDaemon(true); } @Override public void run() { while (!this.exitFlag) { synchronized (this) { try { this.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } // 關閉失效的鏈接 connMgr.closeExpiredConnections(); // 可選的, 關閉30秒內不活動的鏈接 connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } } public void shutdown() { this.exitFlag = true; synchronized (this) { notify(); } } }
直接整合貼代碼, ^_^.併發
public class PooledHttpClientAdaptor { private static final int DEFAULT_POOL_MAX_TOTAL = 200; private static final int DEFAULT_POOL_MAX_PER_ROUTE = 200; private static final int DEFAULT_CONNECT_TIMEOUT = 500; private static final int DEFAULT_CONNECT_REQUEST_TIMEOUT = 500; private static final int DEFAULT_SOCKET_TIMEOUT = 2000; private PoolingHttpClientConnectionManager gcm = null; private CloseableHttpClient httpClient = null; private IdleConnectionMonitorThread idleThread = null; // 鏈接池的最大鏈接數 private final int maxTotal; // 鏈接池按route配置的最大鏈接數 private final int maxPerRoute; // tcp connect的超時時間 private final int connectTimeout; // 從鏈接池獲取鏈接的超時時間 private final int connectRequestTimeout; // tcp io的讀寫超時時間 private final int socketTimeout; public PooledHttpClientAdaptor() { this( PooledHttpClientAdaptor.DEFAULT_POOL_MAX_TOTAL, PooledHttpClientAdaptor.DEFAULT_POOL_MAX_PER_ROUTE, PooledHttpClientAdaptor.DEFAULT_CONNECT_TIMEOUT, PooledHttpClientAdaptor.DEFAULT_CONNECT_REQUEST_TIMEOUT, PooledHttpClientAdaptor.DEFAULT_SOCKET_TIMEOUT ); } public PooledHttpClientAdaptor( int maxTotal, int maxPerRoute, int connectTimeout, int connectRequestTimeout, int socketTimeout ) { this.maxTotal = maxTotal; this.maxPerRoute = maxPerRoute; this.connectTimeout = connectTimeout; this.connectRequestTimeout = connectRequestTimeout; this.socketTimeout = socketTimeout; Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(); this.gcm = new PoolingHttpClientConnectionManager(registry); this.gcm.setMaxTotal(this.maxTotal); this.gcm.setDefaultMaxPerRoute(this.maxPerRoute); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(this.connectTimeout) // 設置鏈接超時 .setSocketTimeout(this.socketTimeout) // 設置讀取超時 .setConnectionRequestTimeout(this.connectRequestTimeout) // 設置從鏈接池獲取鏈接實例的超時 .build(); HttpClientBuilder httpClientBuilder = HttpClients.custom(); httpClient = httpClientBuilder .setConnectionManager(this.gcm) .setDefaultRequestConfig(requestConfig) .build(); idleThread = new IdleConnectionMonitorThread(this.gcm); idleThread.start(); } public String doGet(String url) { return this.doGet(url, Collections.EMPTY_MAP, Collections.EMPTY_MAP); } public String doGet(String url, Map<String, Object> params) { return this.doGet(url, Collections.EMPTY_MAP, params); } public String doGet(String url, Map<String, String> headers, Map<String, Object> params ) { // *) 構建GET請求頭 String apiUrl = getUrlWithParams(url, params); HttpGet httpGet = new HttpGet(apiUrl); // *) 設置header信息 if ( headers != null && headers.size() > 0 ) { for (Map.Entry<String, String> entry : headers.entrySet()) { httpGet.addHeader(entry.getKey(), entry.getValue()); } } CloseableHttpResponse response = null; try { response = httpClient.execute(httpGet); if (response == null || response.getStatusLine() == null) { return null; } int statusCode = response.getStatusLine().getStatusCode(); if ( statusCode == HttpStatus.SC_OK ) { HttpEntity entityRes = response.getEntity(); if (entityRes != null) { return EntityUtils.toString(entityRes, "UTF-8"); } } return null; } catch (IOException e) { } finally { if ( response != null ) { try { response.close(); } catch (IOException e) { } } } return null; } public String doPost(String apiUrl, Map<String, Object> params) { return this.doPost(apiUrl, Collections.EMPTY_MAP, params); } public String doPost(String apiUrl, Map<String, String> headers, Map<String, Object> params ) { HttpPost httpPost = new HttpPost(apiUrl); // *) 配置請求headers if ( headers != null && headers.size() > 0 ) { for (Map.Entry<String, String> entry : headers.entrySet()) { httpPost.addHeader(entry.getKey(), entry.getValue()); } } // *) 配置請求參數 if ( params != null && params.size() > 0 ) { HttpEntity entityReq = getUrlEncodedFormEntity(params); httpPost.setEntity(entityReq); } CloseableHttpResponse response = null; try { response = httpClient.execute(httpPost); if (response == null || response.getStatusLine() == null) { return null; } int statusCode = response.getStatusLine().getStatusCode(); if ( statusCode == HttpStatus.SC_OK ) { HttpEntity entityRes = response.getEntity(); if ( entityRes != null ) { return EntityUtils.toString(entityRes, "UTF-8"); } } return null; } catch (IOException e) { } finally { if (response != null) { try { response.close(); } catch (IOException e) { } } } return null; } private HttpEntity getUrlEncodedFormEntity(Map<String, Object> params) { List<NameValuePair> pairList = new ArrayList<NameValuePair>(params.size()); for (Map.Entry<String, Object> entry : params.entrySet()) { NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry .getValue().toString()); pairList.add(pair); } return new UrlEncodedFormEntity(pairList, Charset.forName("UTF-8")); } private String getUrlWithParams(String url, Map<String, Object> params) { boolean first = true; StringBuilder sb = new StringBuilder(url); for (String key : params.keySet()) { char ch = '&'; if (first == true) { ch = '?'; first = false; } String value = params.get(key).toString(); try { String sval = URLEncoder.encode(value, "UTF-8"); sb.append(ch).append(key).append("=").append(sval); } catch (UnsupportedEncodingException e) { } } return sb.toString(); } public void shutdown() { idleThread.shutdown(); } // 監控有異常的連接 private class IdleConnectionMonitorThread extends Thread { private final HttpClientConnectionManager connMgr; private volatile boolean exitFlag = false; public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) { this.connMgr = connMgr; setDaemon(true); } @Override public void run() { while (!this.exitFlag) { synchronized (this) { try { this.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } // 關閉失效的鏈接 connMgr.closeExpiredConnections(); // 可選的, 關閉30秒內不活動的鏈接 connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } } public void shutdown() { this.exitFlag = true; synchronized (this) { notify(); } } } }
其實沒啥難道, 主要就是買個安心, 這樣寫是安全的, 是經得起線上考驗的.app