HttpClient 工具类

  1package cn.linuxcrypt.utils;
  2
  3import org.apache.http.*;
  4import org.apache.http.client.ClientProtocolException;
  5import org.apache.http.client.ResponseHandler;
  6import org.apache.http.client.config.CookieSpecs;
  7import org.apache.http.client.config.RequestConfig;
  8import org.apache.http.client.entity.UrlEncodedFormEntity;
  9import org.apache.http.client.methods.CloseableHttpResponse;
 10import org.apache.http.client.methods.HttpGet;
 11import org.apache.http.client.methods.HttpPost;
 12import org.apache.http.client.methods.HttpRequestBase;
 13import org.apache.http.client.protocol.HttpClientContext;
 14import org.apache.http.config.ConnectionConfig;
 15import org.apache.http.config.Registry;
 16import org.apache.http.config.RegistryBuilder;
 17import org.apache.http.conn.ConnectionKeepAliveStrategy;
 18import org.apache.http.conn.socket.ConnectionSocketFactory;
 19import org.apache.http.conn.socket.PlainConnectionSocketFactory;
 20import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 21import org.apache.http.entity.ContentType;
 22import org.apache.http.impl.client.CloseableHttpClient;
 23import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
 24import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 25import org.apache.http.message.BasicHeader;
 26import org.apache.http.message.BasicHeaderElementIterator;
 27import org.apache.http.message.BasicNameValuePair;
 28import org.apache.http.protocol.HTTP;
 29import org.apache.http.protocol.HttpContext;
 30import org.apache.http.ssl.SSLContextBuilder;
 31import org.apache.http.util.Args;
 32import org.apache.http.util.EntityUtils;
 33
 34import javax.net.ssl.SSLContext;
 35import javax.net.ssl.TrustManager;
 36import javax.net.ssl.X509TrustManager;
 37import java.io.IOException;
 38import java.nio.charset.Charset;
 39import java.nio.charset.CodingErrorAction;
 40import java.security.cert.X509Certificate;
 41import java.util.*;
 42
 43public final class HttpClients {
 44    // 设置整个连接池最大连接数
 45    private static int POOL_MAX_TOTAL = 2;
 46    /**
 47     * 设置整个连接池最大连接数
 48     *
 49     * @param maxTotal
 50     */
 51    public static void setPoolMaxTotal(int maxTotal) {
 52        synchronized (HttpClients.class) {
 53            POOL_MAX_TOTAL = maxTotal;
 54            HttpClientPool.setMaxTotal(maxTotal);
 55        }
 56    }
 57
 58    /**
 59     * http 连接池
 60     */
 61    static class HttpClientPool {
 62        private static PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = null;
 63        // 设置每个路由上的默认连接个数,setMaxPerRoute则单独为某个站点设置最大连接个数。
 64        private static final int POOL_MAX_PER_ROUTER = 1;
 65        private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:60.0) Gecko/20100101 Firefox/60.0";
 66        // keepalive
 67        private static int DEFAULT_KEEP_ALIVE = 30 * 1000;
 68        /**
 69         * 从连接池中获取请求连接的超时时间 单位毫秒
 70         * -1: 系统默认的超时时间,内核级配置
 71         * 0: 无限制。
 72         * 具体参考{@link org.apache.http.client.config.RequestConfig#getConnectionRequestTimeout()}
 73         */
 74        public static final int DEFAULT_CONNECTION_REQUEST_TIMEOUT = -1;
 75
 76        // 默认连接超时时间
 77        public static final int DEFAULT_CONNECT_TIMEOUT = 10000;
 78
 79        // 默认socket读取数据超时时间,具体的长耗时请求中(如文件传送等)必须覆盖此设置
 80        public static final int DEFAULT_SO_TIMEOUT = 15000;
 81
 82        static {
 83            Registry<ConnectionSocketFactory> socketFactoryRegistry = null;
 84            try {
 85                final SSLContext sslContext = SSLContextBuilder.create().build();
 86                sslContext.init(null, new TrustManager[]{
 87                        new X509TrustManager() {
 88                            public X509Certificate[] getAcceptedIssuers() {
 89                                return null;
 90                            }
 91
 92                            public void checkClientTrusted(X509Certificate[] certs, String authType) {
 93                            }
 94
 95                            public void checkServerTrusted(X509Certificate[] certs, String authType) {
 96                            }
 97                        }
 98                }, null);
 99                socketFactoryRegistry = RegistryBuilder
100                        .<ConnectionSocketFactory>create()
101                        .register("http", PlainConnectionSocketFactory.INSTANCE)
102                        .register("https", new SSLConnectionSocketFactory(sslContext)).build();
103            } catch (Exception e) {
104
105            }
106
107            poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
108
109            //连接池的最大连接数
110            poolingHttpClientConnectionManager.setMaxTotal(POOL_MAX_TOTAL);
111
112            /**
113             * 设置每个路由上的默认连接个数,setMaxPerRoute则单独为某个站点设置最大连接个数。
114             *
115             * DefaultMaxPerRoute是根据连接到的主机对MaxTotal的一个细分;比如:
116             * MaxtTotal=400 DefaultMaxPerRoute=200
117             * 而我只连接到http://a.com时,到这个主机的并发最多只有200;而不是400;
118             * 而我连接到http://a.com 和 http://b.com时,到每个主机的并发最多只有200;即加起来是400(但不能超过400;所以起作用的设置是DefaultMaxPerRoute。
119             */
120            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(POOL_MAX_PER_ROUTER);
121
122            // 默认连接配置
123            ConnectionConfig connectionConfig = ConnectionConfig.custom()
124                    .setMalformedInputAction(CodingErrorAction.IGNORE)
125                    .setUnmappableInputAction(CodingErrorAction.IGNORE)
126                    .setCharset(Consts.UTF_8)
127                    .build();
128            poolingHttpClientConnectionManager.setDefaultConnectionConfig(connectionConfig);
129        }
130
131        public static void setMaxTotal(int maxTotal) {
132            poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
133        }
134
135        /**
136         * 增加默认的http 头
137         *
138         * @return
139         * @{link https://www.cnblogs.com/lwhkdash/archive/2012/10/14/2723252.html}
140         */
141        private static Set<Header> defaultHeaders() {
142            Set<Header> header = new HashSet<>();
143
144            Header accept = new BasicHeader(HttpHeaders.ACCEPT,
145                    "text/html,application/xhtml+xml,application/json,application/xml;q=0.9,*/*;q=0.8");
146            header.add(accept);
147            Header acceptEncoding = new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate");
148            header.add(acceptEncoding);
149            Header acceptLanguage = new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
150            header.add(acceptLanguage);
151            Header connect = new BasicHeader(HttpHeaders.CONNECTION, "keep-alive");
152            header.add(connect);
153            Header acceptCharset = new BasicHeader(HttpHeaders.ACCEPT_CHARSET, Consts.UTF_8.name());
154            header.add(acceptCharset);
155
156            // DO NOT TRACK的缩写,要求服务器程序不要跟踪记录用户信息。DNT: 1 (开启DNT) DNT: 0 (关闭DNT)火狐,safari,IE9都支持这个头域,并且于2011年3月7日被提交至IETF组织实现标准化
157            Header dnt = new BasicHeader("DNT", "1");
158            header.add(dnt);
159
160            return header;
161        }
162
163        /**
164         * 获取 HttpClient
165         * @return
166         */
167        public static CloseableHttpClient getHttpClient() {
168            return getHttpClient(DEFAULT_SO_TIMEOUT, DEFAULT_CONNECT_TIMEOUT, 0);
169        }
170
171        /**
172         * 默认keepAlive策略:如果响应中存在服务器端的keepAlive超时时间则返回该时间否则返回默认的
173         */
174        public static class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
175            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
176                HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
177                while (it.hasNext()) {
178                    HeaderElement he = it.nextElement();
179                    String param = he.getName();
180                    String value = he.getValue();
181                    if (value != null && param.equalsIgnoreCase("timeout")) {
182                        try {
183                            return Long.parseLong(value) * 1000;
184                        } catch (NumberFormatException ignore) {
185                        }
186                    }
187                }
188                return DEFAULT_KEEP_ALIVE; //默认30秒
189            }
190        }
191
192        public static CloseableHttpClient getHttpClient(int socketTimeout, int connectTimeout, int retryCount) {
193            RequestConfig globalConfig = RequestConfig.custom()
194                    .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
195                    .setSocketTimeout(socketTimeout)
196                    .setConnectionRequestTimeout(DEFAULT_CONNECTION_REQUEST_TIMEOUT)
197                    .setConnectTimeout(connectTimeout)
198                    .build();
199
200            CloseableHttpClient closeableHttpClient = org.apache.http.impl.client.HttpClients
201                    .custom()
202                    .setConnectionManager(poolingHttpClientConnectionManager)
203                    .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
204                    // 另外设置http client的重试次数,默认是3次;当前是禁用掉(如果项目量不到,这个默认即可)
205                    .setRetryHandler(new DefaultHttpRequestRetryHandler(retryCount, false))
206                    .setUserAgent(DEFAULT_USER_AGENT)
207                    .setDefaultHeaders(defaultHeaders())
208                    .setDefaultRequestConfig(globalConfig)
209                    .setConnectionManagerShared(true)
210                    .evictExpiredConnections()// 开启超时清理线程
211                    .build();
212
213            return closeableHttpClient;
214        }
215    }
216
217    /**
218     * 对于特殊请求(比如请求涉及到cookie的处理,鉴权认证等),默认的一些配置已经满足不了了,
219     * 这时就可以使用一个独立于全局的配置来执行请求,这个独立于全局,又不会干扰其他线程的请求执行的机制就是使用HttpClientContext,
220     * 该设置类用于对已经提供的一个基于全局配置的副本,来设置一些配置(见HttpClientContext.setXxx)
221     */
222    public static interface HttpClientContextSetter {
223        public void setHttpClientContext(HttpClientContext context);
224    }
225
226    /**
227     * <p>执行http请求</p>
228     *
229     * @param httpMethod              - HTTP请求(HttpGet、HttpPost等等)
230     * @param httpClientContextSetter - 可选参数,请求前的一些参数设置(如:cookie、鉴权认证等)
231     * @param responseHandler         - 必选参数,响应处理类(如针对httpstatu的各种值做一些策略处理等等)
232     * @return 推荐使用 org.apache.http.impl.client.CloseableHttpClient#execute( org.apache.http.HttpHost,
233     * org.apache.http.HttpRequest,
234     * org.apache.http.client.ResponseHandler,
235     * org.apache.http.protocol.HttpContext)
236     */
237    public static <T> T doHttpRequest(HttpRequestBase httpMethod, HttpClientContextSetter httpClientContextSetter, ResponseHandler<T> responseHandler) {
238        Args.notNull(httpMethod, "Parameter 'httpMethod' can not be null!");
239        Args.notNull(responseHandler, "Parameter 'responseHandler' can not be null!");
240        CloseableHttpResponse response = null;
241        try {
242            if (httpClientContextSetter != null) {
243                HttpClientContext context = HttpClientContext.create();
244                httpClientContextSetter.setHttpClientContext(context);
245                response = HttpClientPool.getHttpClient().execute(httpMethod, context);
246            } else {
247                response = HttpClientPool.getHttpClient().execute(httpMethod);
248            }
249            return response == null ? null : responseHandler.handleResponse(response);
250        } catch (Exception e) {
251            throw new RuntimeException(e.getMessage(), e);
252        } finally {
253            if (response != null) {
254                try {
255                    response.close();
256                } catch (IOException e) {
257                }
258            }
259        }
260    }
261
262    /**
263     * 默认的处理返回值为String的ResponseHandler
264     */
265    public static class DefaultStringResponseHandler implements ResponseHandler<String> {
266        /**
267         * 默认响应html字符集编码
268         */
269        private Charset defaultCharset = Consts.UTF_8;
270
271        public DefaultStringResponseHandler() {
272            super();
273        }
274
275        public DefaultStringResponseHandler(String defaultCharset) {
276            super();
277            this.defaultCharset = Charset.forName(defaultCharset);
278        }
279
280        public Charset getDefaultCharset() {
281            return defaultCharset;
282        }
283
284        public void setDefaultCharset(Charset defaultCharset) {
285            this.defaultCharset = defaultCharset;
286        }
287
288        public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
289            HttpEntity httpEntity = response.getEntity();
290            if (httpEntity != null) {
291                return EntityUtils.toString(httpEntity, defaultCharset == null ? ContentType.getOrDefault(httpEntity).getCharset() : defaultCharset);
292            }
293            return null;
294        }
295    }
296
297    /**
298     * <p>根据URL和参数创建HttpPost对象</p>
299     *
300     * @param url
301     * @param paramMap
302     * @return
303     */
304    public static HttpPost createHttpPost(String url, Map<String, String> paramMap) {
305        try {
306            HttpPost httpPost = new HttpPost(url);
307            if (paramMap != null && !paramMap.isEmpty()) {
308                List<NameValuePair> params = new ArrayList<NameValuePair>();
309                for (Map.Entry<String, String> entry : paramMap.entrySet()) {
310                    params.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
311                }
312                UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(params, Consts.UTF_8.name());
313                httpPost.setEntity(formEntity);
314            }
315            return httpPost;
316        } catch (Exception e) {
317            throw new RuntimeException(e.getMessage(), e);
318        }
319    }
320
321    public static String get(String url) {
322        HttpGet httpGet = new HttpGet(url);
323        String value = doHttpRequest(httpGet, null, new DefaultStringResponseHandler());
324        return value;
325    }
326
327    public static String post(String url, Map<String, String> param){
328        HttpPost post = createHttpPost(url, param);
329        return doHttpRequest(post, null, new DefaultStringResponseHandler());
330    }
331}

总结

  1. DefaultMaxPerRouteMaxTotal配置 DefaultMaxPerRoute是根据连接到的主机对MaxTotal的一个细分;比如: MaxtTotal=400 DefaultMaxPerRoute=200 而我只连接到http://a.com时,到这个主机的并发最多只有200;而不是400; 而我连接到http://a.com 和 http://b.com时,到每个主机的并发最多只有200;即加起来是400(但不能超过400;所以起作用的设置是DefaultMaxPerRoute。
  2. 超时设置
  • connectionRequestTimeout: 从连接池中获取请求连接的超时时间 单位毫秒, -1:系统默认的超时时间,内核级配置; 0:无限制
  • connectTimeout: 默认连接超时时间
  • soTimeout: 默认socket读取数据超时时间,具体的长耗时请求中(如文件传送等)必须覆盖此设置
  1. 策略
  • pool.evictExpiredConnections(true): 后台启动一个线程,进行超时连接处理
  • 当获取可用连接时,采用LRU进行处理连接。
  1. 池中池
  • httpclient在初始化时,设置的MaxTotal的参数为总的连接池
  • 在最大的池中,根据主机的名字(route)进行小池的划分
  • 在动态获取可用连接的时候采用LRU算法,清理或者释放连接
  1. available集合和leased集合
  • 见 org.apache.http.pool.AbstractConnPool
  • leased集合当前租用的连接
  • available集合当前可用的连接
  • LRU会根据MaxTotal、leased集合总数、available集合总数进行LRU淘汰。
  1. Future 进行一步接收数据
  2. 持久连接
  • HTTP/1.1采取持久连接的方式替代了Keep-Alive
  • HTTP/1.1的连接默认情况下都是持久连接。如果要显式关闭,需要在报文中加上Connection:Close首部。即在HTTP/1.1中,所有的连接都进行了复用
  • 两种方式,空闲的持久连接也可以随时被客户端与服务端关闭。不发送Connection:Close不意味着服务器承诺连接永远保持打开。
  1. EntityUtils.toString(HttpEntity, …)和EntityUtils.consume(HttpEntity);
  • 注意此方法会关闭InputStream,因为是多路复用,每次读取完必须关闭,否则不能被复用
  • CloseableHttpResponse在每次请求完进行reponse.close()
  1. HttpClient不能关闭,否则连接都会重新建立,会发起tcp的3次握手连接和4次握手断开

测试

抓包测试池化后网络连接3次握手建立连接和4次握手断开连接(抓包只看到3次)

参考