之前没有认真了解过 HttpClient 如何实现,现在有时间来整理一下使用及中间步骤介绍
整体分为 5 步:
- 创建 HttpClient 对象
- 创建 请求 request 对象
- 执行请求
- 响应分析
- 资源释放
package com.maodou.autotest.utils;
import com.maodou.autotest.Application;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.SpringApplication;
import sun.misc.ObjectInputFilter;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
public class HttpClientUtil {
/**
* 总的connection数量
*/
private final static Integer MAX_CONNECTION = 20;
/**
* 每个route允许最多connection数量
*/
private final static Integer MAX_PRE_ROUTE = MAX_CONNECTION;
/**
* 从连接池中取连接的超时时间(若从连接池中没有拿到可用的连接,则 request 会被阻塞,等待时间超过连接超时时间时
* 则抛出ConnectionPoolTimeoutException异常)
*/
private final static Integer CON_RST_TIME_OUT = 12000;
/**
* 连接超时时间(取得连接之后,接通目标url的连接等待时间,发生超时时会抛出,ConnectionTimeoutException异常)
*/
private final static Integer CON_TIME_OUT = 12000;
/**
* 请求超时时间(连接服务器之后,从服务器获得请求数据的等待时间,即连接上一个 url 之后获取 response 之间的等待时间,若发生超时,则抛出 SocketTimeoutException 异常)
*/
private final static Integer SOCKET_TIME_OUT = 12000;
public static String doGet() throws Exception {
String resultString = "";
CloseableHttpResponse response = null;
//********* 一、创建 HttpClient 对象 ***********
//A、HttpClient 路由计算策略 HttpRoutePlanner
/*
* HttpClient既可以直接、又可以通过多个中转路由(hops)和目标服务器建立连接,
* 在HttpClient中,一个Route指运行环境机器->目标机器host的一条线路,也就是如果目标url的host是同一个,那么它们的route也是一样的。
* HttpRoutePlanner接口可以用来表示基于http上下文情况下,客户端到服务器的路由计算策略,它有两个实现类:
* SystemDefaultRoutePlanner这个类基于java.net.ProxySelector,它默认使用jvm的代理配置信息,这个配置信息一般来自系统配置或者浏览器配置。
* DefaultProxyRoutePlanner这个类既不使用java本身的配置,也不使用系统或者浏览器的配置。它通常通过默认代理来计算路由信息。
*/
/*
* B 、HttpClient 连接管理器: HttpCLientConnectionManager
* 它负责新的 Http 连接的创建、管理连接的生命周期,保证一个http 连接在某个时刻只能被一个线程使用,它有两个实现类:
* 1.BasicHttpClientConnectionManager
* 每次只管理一个connection。不过,虽然它是thread-safe的,但由于它只管理一个连接,所以只能被一个线程使用。
* 每次新来一个请求,如果是相同 route,则会复用旧的连接,但是如果 route 不相同,则会断开旧的连接创建新的连接
* 2.PoolingHttpClientConnectionManager
* 它管理着一个连接池,如果每次来一个新的请求,它会先在连接池查找可用连接,如过存在 route 相同并且可用的连接的话,连接池就会直接复用该连接
* 如果没有找到 route 相同的可用连接时,连接池就会重新创建一个新的连接,如果连接池已经满了的话,就会等待其他连接断开或者直到超时
*/
/*
*多线程执行
* HttpClient已经实现了线程安全。所以我们在实例话 httpClient 时,也要支持为多个请求使用,选择 PoolingHttpClientConnectionManager
*/
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
//默认不对HttpClientBuilder进行配置的话,new出来的CloeableHttpClient实例使用的是PoolingHttpClientConnectionManager。
//这种情况下HttpClientBuilder创建出的HttpClient实例就可以被多个连接和多个线程共用,在应用容器起来的时候实例化一次,在整个应用结束的时候再调用httpClient.close()就行了。
//如果没有显式设置,默认每个route只允许最多2个connection,总的connection数量不超过20
//如果所有的连接请求都是到同一个url,那可以把MaxPerRoute的值设置成和MaxTotal一致,这样就能更高效地复用连接。
//HttpClient设置最大连接数和每个route的最大连接数示例
poolingHttpClientConnectionManager.setMaxTotal(MAX_CONNECTION);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(MAX_PRE_ROUTE);
//C、HttpConfig 表示http request的配置信息
//设置超时时间,如果不设置的话,默认这三个超时时间都为0,也就意味着会无限等待
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(CON_RST_TIME_OUT)
.setConnectTimeout(CON_TIME_OUT)
.setSocketTimeout(SOCKET_TIME_OUT).build();
CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
try {
//********** 二、创建 request 对象 *********
/*
* GET、HEAD、POST、PUT、DELETE、TRACE和OPTIONS。对使用的类为HttpGet、HttpHead、HttpPost、HttpPut、HttpDelete、HttpTrace和HttpOptions
* Request的对象建立很简单,一般用目标url来构造就好了
* 一个Request还可以addHeader、setEntity、setConfig等
*/
//HttpClient提供URIBuilder工具类来简化URIs的创建和修改过程,下列等同于 HttpGet request = new HttpGet("https://wenku.baidu.com/search/interface/getrelatequery?word=%E4%BD%A0%E5%A5%BD");
URI uri = new URIBuilder()
.setScheme("http")
.setHost("wenku.baidu.com")
.setPath("/search/interface/getrelatequery")
.setParameter("word", "%E4%BD%A0%E5%A5%BD")
.build();
HttpGet request = new HttpGet(uri);
//通过 setConfig 设置相关请求配置信息
request.setConfig(requestConfig);
//通过 addHeader 设置请求头
request.addHeader("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)");
//通过 setEntity 设置请求体,见 doPost 请求
//*********** 三、执行请求 ***********
/*
* 最简单的使用方法是调用execute(final HttpUriRequest request)。
* 整个 execute 执行的常规流程为:
*
* new一个 http context
* 取出 Request 和URL
* 根据 HttpRoute 的配置看是否需要重写URL
* 根据 URL 的host、port和scheme设置target
* 在发送前用 http 协议拦截器处理 request 的各个部分
* 取得验证状态、user token来验证身份
* 从连接池中取一个可用的连接
* 根据request的各种配置参数以及取得的connection构造一个connManaged
* 打开managed的connection(包括创建route、dns解析、绑定socket、socket连接等)
* 请求数据(包括发送请求和接收response两个阶段)
* 查看keepAlive策略,判断连接是否要复用,并设置相应标识
* 返回response
* 用http协议拦截器处理response的各个部分
*/
/*
* a、Http 上下文 HttpContext
* HttpClient允许http连接在特定的Http上下文中执行,HttpContext是跟一个连接相关联的,所以它也只能属于一个线程,
* 如果没有特别设定,在execute的过程中,HttpClient会自动为每一个connectionnew一个HttpClientHttpContext。
* HttpContext可以包含任意类型的对象,因此如果在多线程中共享上下文会不安全。推荐每个线程都只包含自己的http上下文
* 在Http请求执行的过程中,HttpClient会自动添加下面的属性到Http上下文中:
*
* HttpConnection的实例,表示客户端与服务器之间的连接
* HttpHost的实例,表示要连接的木包服务器
* HttpRoute的实例,表示全部的连接路由
* HttpRequest的实例,表示Http请求。在执行上下文中,最终的HttpRequest对象会代表http消息的状态。Http/1.0和Http/1.1都默认使用相对的uri。但是如果使用了非隧道模式的代理服务器,就会使用绝对路径的uri。
* HttpResponse的实例,表示Http响应
* java.lang.Boolean对象,表示是否请求被成功的发送给目标服务器
* RequestConfig对象,表示http request的配置信息
* java.util.List<Uri>对象,表示Http响应中的所有重定向地址
* 我们可以使用HttpClientContext这个适配器来简化和上下文交互的过程。
*/
response = httpClient.execute(request);
//********** 四、响应处理 ***********
/*
* HttpReaponse是将服务端发回的Http响应解析后的对象。CloseableHttpClient的execute方法返回的response都是CloseableHttpResponse类型
* getAllHeaders()、getEntity、getRequestLine等,一般这几个方法比较常用
*/
response.getAllHeaders();
StatusLine statusLine = response.getStatusLine();
int status = statusLine.getStatusCode();
if (status == 200) {
/**
* 对于entity的处理需要特别注意一下。一般来说一个response中的entity只能被使用一次,它是一个流,这个流被处理完就不再存在了。
* 先response.getEntity()再使用HttpEntity#getContent()来得到一个java.io.InputStream,然后再对内容进行相应的处理
*/
HttpEntity responseEntity = response.getEntity();
resultString = EntityUtils.toString(responseEntity);
System.out.println(resultString);
}
/*
有一点非常重要,想要复用一个connection就必须要让它占有的系统资源得到正确释放。释放资源有两种方法:
1)关闭和entity相关的content stream
如果使用的是 outputStream 就要保证整个 entity 被 Write Out ,
如果是 inputStream 就要调用inputStream.close(),或者使用 EntityUtils.consume(entity)保证整个 entity 被消耗掉,
entity.toString() 方法也可以实现,也会自动把inputStream close掉的,如上代码(只有确保 entity 不是特别大的情况下才可以)
2)关闭 response
执行response.close()虽然会正确释放掉该connection占用的所有资源,但是这是一种比较暴力的方式,采用这种方式之后,这个connection就不能被重复使用了。
关闭stream和关闭response的区别在于前者会尝试保持底层的连接alive,而后者会直接shut down并且丢弃connection
*/
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
//********** 五、关闭HttpClient **************
/*
调用httpClient.close()会先shut down connection manager,然后再释放该HttpClient所占用的所有资源,关闭所有在使用或者空闲的connection包括底层socket。
由于这里把它所使用的connection manager关闭了,所以在下次还要进行http请求的时候,要重新new一个connection manager来build一个HttpClient
*/
httpClient.close();
return resultString;
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doPost() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try{
//通过 setEntity 设置请求体
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("url", "/search"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);
HttpPost httppost = new HttpPost("https://wenku.baidu.com/message/getnotice");
httppost.setEntity(entity);
response = httpClient.execute(httppost);
int status = response.getStatusLine().getStatusCode();
if(status == 200){
resultString = EntityUtils.toString(response.getEntity());
System.out.println(resultString);
return resultString;
}
}catch (Exception e){
e.printStackTrace();
} finally {
try{
if(null !=response ){
response.close();
}
httpClient.close();
}catch (IOException e){
e.printStackTrace();
}
}
return resultString;
}
/**
* 当一个CloseableHttpClient的实例不再被使用,并且它的作用范围即将失效,和它相关的连接必须被关闭,关闭方法可以调用CloseableHttpClient的close()方法
*/
public static void main(String[] args) throws Exception{
HttpClientUtil.doGet();
// {"status":{"code":0,"msg":null},"data":{"relateQuery":["\u767e\u5206\u53f7\u4e0e\u5343\u5206\u53f7\u6362\u7b97","c\u8bed\u8a00\u5982\u4f55\u6253\u5370\u767e\u5206\u53f7","origin\u4e2d\u767e\u5206\u53f7\u8bbe\u7f6e","\u767e\u5206\u53f7\u600e\u4e48\u5199\u89c4\u8303","\u8ba1\u7b97\u5668\u767e\u5206\u53f7\u600e\u4e48\u6309"]}}
HttpClientUtil.doPost();
// {"status":{"code":0,"msg":"get notice success"},"data":{"system":[],"diy":[],"advert":[],"errstr":"get notice success"}}
}
}