HttpClient以及连接池的使用

一、开始使用httpclient

本文档需要使用的依赖有如下几个:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.10</version>
</dependency>
<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpmime</artifactId>
  <version>4.5.10</version>
</dependency>
<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpcore</artifactId>
  <version>4.4.11</version>
</dependency>

1、创建HttpClient对象。

创建httpclient对象的方式有两种,

第一种是使用默认配置创建:

CloseableHttpClient client = HttpClients.createDefault();

第二种是使用HttpClients.custom()定制:

HttpClients.custom().setDefaultRequestConfig(RequestConfig)

这其中RequestConfig的设计方式可以学习一下。RequestConfig只有一个默认访问修饰符的构造函,这就意味着我们在使用这个类的时候不能之间构建示例;RequestConfig有很多私有属性,这些属性没有get/set方法,所以,即便是我们拿到了RequestConfig类的实例,也不能去修改其中的属性值。那么RequestConfig是如何构造实例并且为属性赋值的呢?RequestConfig在实例化的时候会利用其公共内部类Builder。使用方式为RequestConfig.custom().setXXX().builder()。调用custom方法返回一个Builder类实例,改类里有设置RequestConfig同名属性的各种方法,设置完毕后调用builder方法构造一个新的RequestConfig实例。从这个过程中可以看出来,RequestConfig对象的属性是不可变的。

有几个参数我们自己必须设置一下:

(1)connectionRequestTimeout:从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出:
ConnectionPoolTimeoutException: Timeout waiting for connection from pool

(2)connectTimeout:连接上服务器(握手成功)的时间,超出该时间抛出connect timeout

(3)socketTimeout:服务器返回数据(response)的时间,超过该时间抛出read timeout

以上3个超时相关的参数如果未配置,默认为-1,意味着无限大,就是一直阻塞等待。

2、创建请求方法的实例

创建请求方法的实例并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象:

HttpGet httpGet = new HttpGet("https://www.baidu.com");

3、组装请求参数

对于get请求,可以直接在构建HttpGet对象的时候在url中加入,也可以使用URIBuilder构建参数,具体可以参考下面的两种方式:

1 URIBuilder builder = new URIBuilder("http://example.com/");
2 builder.setParameter("var1", "value1").setParameter("var2", "value2");
3 HttpGet request = new HttpGet(builder.build());

或使用了建造者模式的URIBuilder

URI uri = new URIBuilder()
            .setScheme("http")
            .setHost("www.example.cn")
            .setPath("/search")
            .setParameter("var1", "value1")
            .build();

对于HttpPost对象而言,可调用setEntity(HttpEntity entity)方法来设置请求参数:

//拼接参数
List<NameValuePair> params= new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("username", "vip"));
params.add(new BasicNameValuePair("password", "secret"));
httpPost.setEntity(new UrlEncodedFormEntity(params));

其中setEntity的参数种类可以有很多种,比如:

JSONObject params = new JSONObject();
params.put("var1", "value1");
params.put("var2", "value2");
httpPost.setEntity(new StringEntity(params.toJSONString(), "utf-8"));

或者使用mutltipar形式传输参数:

InputStream in = httpResponse.getEntity().getContent();
InputStreamBody inputStreamBody = new InputStreamBody(in, "filename");
HttpEntity entity = MultipartEntityBuilder.create()
            .addPart("image", inputStreamBody)
            .addPart("sign", new StringBody("", ContentType.APPLICATION_XML))
            .build();

使用MultipartEntityBuilder需要依赖httpmime包。其中的inputStreamBody是从http查询返回中获得的一个图片的输入流。

4、发送请求

调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse:

CloseableHttpResponse httpResponse = httpClient.execute(httpPost);

5、获取返回内容

调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。

// 获取响应状态码
int statusCode = httpResponse.getStatusLine().getStatusCode();
// 获取返回内容
HttpEntity httpEntity = httpResponse.getEntity();
String content = EntityUtils.toString(httpEntity);

6、释放连接。

无论执行方法是否成功,都必须释放连接

httpClient.close();

二、http连接池

1、 为什么使用连接池

  1. 降低延迟:如果不采用连接池,每次连接发起Http请求的时候都会重新建立TCP连接(经历3次握手),用完就会关闭连接(4次挥手),如果采用连接池则减少了这部分时间损耗
  2. 支持更大的并发:如果不采用连接池,每次连接都会打开一个端口,在大并发的情况下系统的端口资源很快就会被用完,导致无法建立新的连接

2、 创建连接池

PoolingHttpClientConnectionManager cm = null;
LayeredConnectionSocketFactory sslsf = null;
try {
    sslsf = new SSLConnectionSocketFactory(SSLContext.getDefault());
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
}

Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
        .register("https", sslsf)
        .register("http", new PlainConnectionSocketFactory())
        .build();

cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(20);

CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

 

三、注意事项

1、 错误关闭连接

连接池中连接都是在发起请求的时候建立,并且都是长连接。Client.java[附录]中的in.close()作用就是将用完的连接释放,下次请求可以复用,这里特别注意的是,如果不使用in.close()而仅仅使用response.close(),结果就是连接会被关闭,并且不能被复用,这样就失去了采用连接池的意义。
连接池释放连接的时候,并不会直接对TCP连接的状态有任何改变,只是维护了两个Set,leased和avaliabled,leased代表被占用的连接集合,avaliabled代表可用的连接的集合,释放连接的时候仅仅是将连接从leased中remove掉了,并把连接放到avaliabled集合中。

2、 未关闭连接

try{

HttpClient httpClient = HttpClientBuilder.create().build();
    HttpPost request = new HttpPost(httpUrl);
    StringEntity params = new StringEntity(new ObjectMapper().writeValueAsString(body), "UTF-8");
    request.addHeader("content-type", "application/json");
    request.setEntity(params);
    httpClient.execute(request); //问题代码
} catch (Exception ex) {
    logger.error("Http post error. body={}", body, ex);
}

上段代码中,httpClient.execute()执行的返回值response没有被close,导致线程一直在等待。可以改用CloseableHttpClient,并保证httpClient.execute()执行的response最终被close掉。而httpClient无需close,可以重复使用。如果使用CloseableHttpResponse,则response的close也不用显示调用。

CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost request = new HttpPost(httpUrl);
StringEntity params = new StringEntity(new ObjectMapper().writeValueAsString(body), "UTF-8");
request.addHeader("content-type", "application/json");
request.setEntity(params);
//httpClient.execute(request);
try {
    CloseableHttpResponse response = httpClient.execute(request);
    //response.close()会被自动调用
} catch (Exception ex) {
    logger.error("Http post execute error. body={}", body, ex);
}

 

四、附录

1、client.java

public class Client {
    HttpConnectionManager connManager;

    public <T> T get(String path,Class<T> clazz){
        CloseableHttpClient httpClient=connManager.getHttpClient();
        HttpGet httpget = new HttpGet(path);
        String json=null;
        CloseableHttpResponse response=null;
        try {
            response = httpClient.execute(httpget);
            InputStream in=response.getEntity().getContent();
            json=IOUtils.toString(in);
            in.close();
        } catch (UnsupportedOperationException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(response!=null){
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return JSON.parseObject(json, clazz);
    }
}

 

参考:

HttpClient中文网.http://www.httpclient.cn/

 

posted @ 2021-01-08 15:20  不吃陈皮  阅读(5348)  评论(0编辑  收藏  举报