前段时间琢磨了一下Apache HttpClient框架细节,看了一些源码,方便查阅,这里记录一下。

框架版本

Httpclient 4.x 是生产应用的主流,因此暂时只讨论Httpclient 4.x,具体框架版本如下:

1  <dependency>
2            <groupId>org.apache.httpcomponents</groupId>
3            <artifactId>httpclient</artifactId>
4            <version>4.5.14</version>
5   </dependency>

httpclient会依赖httpcore,httpcore的版本如下:

1<dependency>
2    <groupId>org.apache.httpcomponents</groupId>
3    <artifactId>httpcomponents-core</artifactId>
4    <version>4.4.15</version>
5 </dependency>

官网说明如下:https://hc.apache.org/httpcomponents-client-4.5.x/index.html

典型用例

在生产环境,我们使用httpclient框架时一定会使用到连接池。HttpUtils工具类封装如下:

  1package com.example.demo.utils;
  2
  3
  4import java.util.Map;
  5
  6public class HttpUtils {
  7    private static PoolingHttpClientConnectionManager cm;
  8    private static String EMPTY_STR = "";
  9    private static String UTF_8 = "UTF-8";
 10
 11    private static void init() {
 12        if (cm == null) {
 13            cm = new PoolingHttpClientConnectionManager();
 14            // 整个连接池最大连接数
 15            cm.setMaxTotal(100);
 16            // 每路由最大连接数,默认值是2
 17            cm.setDefaultMaxPerRoute(200);
 18        }
 19    }
 20
 21    /**
 22     * 通过连接池获取HttpClient
 23     *
 24     * @return
 25     */
 26
 27    private static CloseableHttpClient getHttpClient() {
 28        init();
 29        RequestConfig requestConfig = RequestConfig.custom()
 30                // 设置连接超时时间
 31                .setConnectTimeout(3000)
 32                // 设置响应超时时间
 33                .setSocketTimeout(3000)
 34                // 设置从连接池获取链接的超时时间
 35                .setConnectionRequestTimeout(3000)
 36                .build();
 37        return HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(cm).build();
 38
 39    }
 40
 41
 42    /**
 43     * @param url
 44     * @return
 45     */
 46
 47    public static String get(String url) {
 48        HttpGet httpGet = new HttpGet(url);
 49        return getResult(httpGet);
 50    }
 51
 52
 53    public static String get(String url, Map<String, Object> params) throws URISyntaxException {
 54        URIBuilder ub = new URIBuilder(url);
 55        ArrayList<NameValuePair> pairs = covertParams2NVPS(params);
 56        ub.setParameters(pairs);
 57        HttpGet httpGet = new HttpGet(ub.build());
 58        return getResult(httpGet);
 59    }
 60
 61
 62    public static String post(String url) {
 63        HttpPost httpPost = new HttpPost(url);
 64        return getResult(httpPost);
 65    }
 66
 67
 68    public static String post(String url, String json) {
 69
 70        HttpPost httpPost = new HttpPost(url);
 71
 72        StringEntity entity = new StringEntity(json, "utf-8");//解决中文乱码问题
 73
 74        entity.setContentEncoding("UTF-8");
 75
 76        entity.setContentType("application/json");
 77
 78        httpPost.setEntity(entity);
 79
 80        return getResult(httpPost);
 81
 82    }
 83    public static String post(String url, Map<String, Object> params) throws UnsupportedEncodingException {
 84        HttpPost httpPost = new HttpPost(url);
 85        ArrayList<NameValuePair> pairs = covertParams2NVPS(params);
 86        httpPost.setEntity(new UrlEncodedFormEntity(pairs, UTF_8));
 87        return getResult(httpPost);
 88
 89    }
 90
 91    private static ArrayList<NameValuePair> covertParams2NVPS(Map<String, Object> params) {
 92        ArrayList<NameValuePair> pairs = new ArrayList<NameValuePair>();
 93        for (Map.Entry<String, Object> param : params.entrySet()) {
 94            pairs.add(new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue())));
 95        }
 96        return pairs;
 97    }
 98
 99
100    /**
101     * 处理Http请求
102     */
103
104    private static String getResult(HttpRequestBase request) {
105
106        CloseableHttpClient httpClient = getHttpClient();
107
108        try {
109
110            CloseableHttpResponse response = httpClient.execute(request);
111            HttpEntity entity = response.getEntity();
112            if (entity != null) {
113
114                // long len = entity.getContentLength();// -1 表示长度未知
115
116                String result = EntityUtils.toString(entity);
117
118                response.close();
119
120                // httpClient.close();
121
122                return result;
123            }
124        } catch (IOException e) {
125            e.printStackTrace();
126        }
127        return EMPTY_STR;
128    }
129}

在以上工具类中,首先getHttpClient()方法和init方法用于初始化httpclient,使用的连接池管理器是PoolingHttpClientConnectionManager,然后getResult方法是执行具体的GET和POST请求。

关于httpclient的一些认识

  • 生产环境必须使用连接池,httpclient必须单例,否则会产生性能问题

  • response.close();表示的是释放连接,这行代码是必不可少的。释放连接包含两层含义:

    • 第一,当连接满足可复用时,将连接归还给连接池,以便后续请求使用。
    • 第二,当连接不可复用时,直接关闭连接,包括底层的Socket连接。
  • httpclient.close();表示的是关闭连接池,建议放在JVM的shutdown钩子中使用

  • 连接池中的空闲连接,大多数情况下有效时间是有限的,默认是60秒,在每次请求中,httpclient框架底层会去主动检测连接是否有效,但是最佳实践是我们自己开启定时任务去关闭超时的连接。

  • 连接是否复用,取决于很多条件,大体上可以理解成:对于http 1.1版本,请求和响应任何一方的header

    中有 Connection:close ,则不复用连接;对于http 1.0版本,请求和响应都包含 Connection: keep-alive,连接才会复用。因此默认情况霞http 1.1是连接复用的。

    HttpClient 请求头默认携带Connection: keep-alive

    Tomcat服务器响应头默认携带

    • Connection: keep-alive

    • Keep-Alive: timeout=60

    • TCP的长连接和Http中长连接有什么关系

      简单说,不是一回事。TCP长连接是指建立Socket后保活一段时间,默认是2个小时,时间很长。Http的长连接,指的是连接复用。对于Http 1.0来说,每次进行Http请求都会新建一个Socket连接,因此连接不能复用,同时,请求完毕底层的Socket连接也会关闭,因此是TCP短连接。Http1.1 则是相反的情况,不过一般Http连接有60秒的超时,超时后会关闭底层的TCP连接。

    • httpclient连接池和tomcat线程池有什么关系

      httpclient框架中本身没有使用任何线程池,也就是说在从连接池中获取、使用、归还、关闭连接的各个环节,都是在当前的业务线程中执行的。当前业务线程无法从连接池中获取到连接时,就处于睡眠状态,直到被其他线程执行完毕归还连接时给唤醒。

补充说明

接下来我将围绕httpclient是怎么创建的和httpclient是怎么执行的这两个问题层层递进,进行阐述这个框架的关键细节。