实例
package com.thinkgem.jeesite.common.utils;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpRequestRetryHandler;
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.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HttpConnectionPoolUtil {
private static Logger logger = LoggerFactory.getLogger(HttpConnectionPoolUtil.class);
/**
* 获取请求超时-毫秒
*/
private static final int REQUESTCONNECT_TIMEOUT = 3*1000;
/**
* 连接超时-毫秒
*/
private static final int CONNECT_TIMEOUT = 3*1000;
/**
* 传输超时(响应超时)-毫秒
*/
private static final int SOCKET_TIMEOUT = 180*1000;
/**
* 连接池最大连接数
*/
private static final int MAX_CONN =128;
/**
* 目标主机的最大连接数(相同 host:port 为同一主机)
*/
private static final int MAX_ROUTE = 128;
/**
* 发送请求的客户端单例
*/
private static CloseableHttpClient httpClient;
/**
* 连接池管理类
*/
private static PoolingHttpClientConnectionManager connManager;
private static ScheduledExecutorService monitorExecutor;
/**
* 相当于线程锁,用于线程安全
*/
private final static Object syncLock = new Object();
public static CloseableHttpClient getHttpClient(String url){
String hostName = url.split("/")[2];
int port = 80;
if (hostName.contains(":")){
String[] args = hostName.split(":");
hostName = args[0];
port = Integer.parseInt(args[1]);
}
logger.info("hostName:{} port:{}",hostName,port);
if (httpClient == null){
//多线程下多个线程同时调用getHttpClient容易导致重复创建httpClient对象的问题,所以加上了同步锁
synchronized (syncLock){
if (httpClient == null){
httpClient = createHttpClient(hostName, port);
//开启监控线程,对异常和空闲线程进行关闭
monitorExecutor = new ScheduledThreadPoolExecutor(3);
monitorExecutor.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
//关闭异常连接
connManager.closeExpiredConnections();
//关闭30s空闲的连接
connManager.closeIdleConnections(30, TimeUnit.SECONDS);
logger.info("close expired and idle for over 30s connection");
}
}, 15,10, TimeUnit.MINUTES);//设置清理任务 首次 15 分钟 第一次运行 随后 10 分钟运行一次
}
}
}
return httpClient;
}
/**
* 根据host和port构建httpclient实例
* @param host 要访问的域名
* @param port 要访问的端口
* @return
*/
public static CloseableHttpClient createHttpClient(String host, int port){
ConnectionSocketFactory plainSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
LayeredConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("http", plainSocketFactory)
.register("https", sslSocketFactory).build();
connManager = new PoolingHttpClientConnectionManager(registry);
/**
* 设置连接参数
*/
//设置最大连接数
connManager.setMaxTotal(MAX_CONN);
//设置每个路由默认基础连接数
connManager.setDefaultMaxPerRoute(MAX_ROUTE);
// 可用空闲连接过期时间,重用空闲连接时会先检查是否空闲时间超过这个时间,如果超过,释放socket重新建立
//connManager.setValidateAfterInactivity(30000);
/**
* 单个host配置 连接数据
*/
// HttpHost httpHost = new HttpHost(host, port);
// connManager.setMaxPerRoute(new HttpRoute(httpHost),Max_PRE_ROUTE);
//请求失败时,进行请求重试
HttpRequestRetryHandler handler = new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException e, int i, HttpContext httpContext) {
if (i > 3){
//重试超过3次,放弃请求
logger.error("retry has more than 3 time, give up request");
return false;
}
if (e instanceof NoHttpResponseException){
//服务器没有响应,可能是服务器断开了连接,应该重试
logger.error("receive no response from server, retry");
return true;
}
if (e instanceof SSLHandshakeException){
// SSL握手异常
logger.error("SSL hand shake exception");
return false;
}
if (e instanceof InterruptedIOException){
//超时
logger.error("InterruptedIOException");
return false;
}
if (e instanceof UnknownHostException){
//目标服务器不可达
logger.error("server host unknown");
return false;
}
if (e instanceof ConnectTimeoutException){
//连接超时被拒绝
logger.error("Connection Time out");
return false;
}
if (e instanceof SSLException){
//ssl 握手异常
logger.error("SSLException");
return false;
}
HttpClientContext context = HttpClientContext.adapt(httpContext);
HttpRequest request = context.getRequest();
// 如果请求是幂等的,就再次尝试
if (!(request instanceof HttpEntityEnclosingRequest)){
//如果请求不是关闭连接的请求
return true;
}
return false;
}
};
/**
* Session 保持会话
*/
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
HeaderElementIterator it = new BasicHeaderElementIterator
(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase
("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return 60 * 1000;//如果没有约定,则默认定义时长为60s
}
};
/**
* 请求参数配置
* connectionRequestTimeout:
* 从连接池中获取连接的超时时间,超过该时间未拿到可用连接,
* 会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
* connectTimeout:
* 连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
* socketTimeout:
* 服务器返回数据(response)的时间,超过该时间抛出read timeout
*
* 以上3个超时相关的参数如果未配置,默认为-1 ,默认为-1,意味着无限大,就是一直阻塞等待!
*/
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(REQUESTCONNECT_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT).build();
CloseableHttpClient client = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connManager)
.setKeepAliveStrategy(myStrategy)
.setRetryHandler(handler).build();
return client;
}
/**
* 设置post请求的参数
* @param httpPost
* @param params
*/
private static void setPostParams(HttpPost httpPost, Map<String, String> params){
ArrayList<NameValuePair> nvps = new ArrayList<NameValuePair>();
Set<String> keys = params.keySet();
for (String key: keys){
nvps.add(new BasicNameValuePair(key, params.get(key)));
}
try {
httpPost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8"));
} catch (UnsupportedEncodingException e) {
logger.error("发送post请求异常",e);
}
}
/**
* 设置post请求的参数
* @param httpPost
* @param jsonStr
*/
private static void setPostString(HttpPost httpPost,String jsonStr){
// 实体
StringEntity entity = new StringEntity(jsonStr, "utf-8");
httpPost.setEntity(entity);
}
public static String post(String url, Map<String, String> params){
HttpPost httpPost = new HttpPost(url);
setPostParams(httpPost, params);
return post(url,httpPost);
}
public static String post(String url,String params){
HttpPost httpPost = new HttpPost(url);
setPostString(httpPost, params);
return post(url,httpPost);
}
private static String post(String url,HttpPost httpPost){
CloseableHttpResponse response = null;
InputStream in = null;
try {
response = getHttpClient(url).execute(httpPost, HttpClientContext.create());
int status_code = response.getStatusLine().getStatusCode();
logger.info("status_code is {} " + status_code);
HttpEntity entity = response.getEntity();
if (entity != null) {
in = entity.getContent();
return IOUtils.toString(in, Consts.UTF_8);
}
}catch (ConnectTimeoutException e) {
logger.error("请求通信[" + url + "]时连接超时,堆栈轨迹如下", e);
} catch (SocketTimeoutException e) {
logger.error("请求通信[" + url + "]时读取超时,堆栈轨迹如下", e);
} catch (ClientProtocolException e) {
// 该异常通常是协议错误导致:比如构造HttpGet对象时传入协议不对(将'http'写成'htp')or响应内容不符合HTTP协议要求等
logger.error("请求通信[" + url + "]时协议异常,堆栈轨迹如下", e);
} catch (ParseException e) {
logger.error("请求通信[" + url + "]时解析异常,堆栈轨迹如下", e);
} catch (IOException e) {
// 该异常通常是网络原因引起的,如HTTP服务器未启动等
logger.error("请求通信[" + url + "]时网络异常,堆栈轨迹如下", e);
} catch (Exception e) {
logger.error("请求通信[" + url + "]时偶遇异常,堆栈轨迹如下", e);
}finally {
try{
if (in != null){
in.close();
}
if (response != null){
response.close();
}
} catch (IOException e) {
logger.error("关闭流异常",e);
}
}
return null;
}
/**
* 关闭连接池
*/
public static void closeConnectionPool(){
try {
httpClient.close();
connManager.close();
monitorExecutor.shutdown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.特点
阻塞 I/O , 是一个 HTTP 客户端的通信实现,用于发送和接收HTTP报文,不会处理内容。
http相关 - 请参考 https://www.runoob.com/http/http-tutorial.html
2.使用
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.4</version>
</dependency>
3. http 请求方式
3.1 请求头,响应
3.1.1 httpClient 常用请求方式
http请求方式 | GET | HEAD | POST | PUT | DELETE | TRACE | OPTIONS |
---|---|---|---|---|---|---|---|
httpClient方法 | HttpGet | HttpHead | HttpPost | HttpPut | HttpDelete | HttpTrace | HttpOptions |
3.1.2 http请求
所有的 HTTP 请求都有一个请求行,包含一个方法名,请求 URI 和 HTTP 协议版本。 请求 URI 是通用资源标识符,用来定位要响应请求的资源。HTTP 请求的 URI 包含协议,主机名,可选的端口号,资源路径, 可选的查询字符串和可选的片段。
URI uri = new URIBuilder()
.setScheme("http")//设置版本信息
.setHost("www.google.com")//设置查询路径
.setPath("/search")
.setParameter("q", "httpclient")//设置参数
.setParameter("btnG", "Google Search")
.setParameter("aq", "f")
.setParameter("oq", "")
.build();
HttpGet httpget = new HttpGet(uri);
或者
HttpGet httpget = new HttpGet("http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
3.1.3 http响应
HTTP 响应是服务器在收到并解析了客户端的请求信息后返回给客户端的信息。消息的第一行包含了协议的版本号,后面有一个数字 表示的状态码,还有一个相关的词语。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK);
System.out.println(response.getProtocolVersion());//HTTP/1.1
System.out.println(response.getStatusLine().getStatusCode());//200
System.out.println(response.getStatusLine().getReasonPhrase());//OK
System.out.println(response.getStatusLine().toString());//HTTP/1.1 200 OK
3.1.4 响应头
HTTP 消息可以包含一些用来描述content length,content type这些的头部信息。HttpClient 提供了检索、添加、移除和枚举头部的方法。
HttpResponse response = enw BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");
Header h1 = response.getFirstHeader("Set-Cookie");
System.out.println(h1);//c1=a; path=/; domain=localhost
Header h2 = response.getLastHeader("Set-Cookie");
System.out.println(h2);//c2=b; path=\"/\", c3=c; domain=\"localhost\"
Header[] hs = response.getHeaders("Set-Cookie");
System.out.println(hs.length);//2
HeaderIterator it = response.headerIterator("Set-Cookie");
/**
* 打印所有Set-Cookie 信息
*/
while(it.hasNext()) {
System.out.println(it.next());
}
/**
* 打印Set-Cookie 内容信息 以 ; 区分为 一组信息(将 HTTP 消息解析成独立的头部元素。)
*/
while(it.hasNext()) {
HeaderElement elem = it.nextElement();
System.out.println(elem.getName() + " = " + elem.getValue());
NameValuePair[] params = elem.getParameters();
for(int i = 0; i < params.length; i++) {
System.out.println(" " + params[i]);
}
}
3.2 http实体
HTTP 消息能够携带与请求或响应相关的内容实体。这些实体能在某些请求和某些响应中找到,因为它们是可选的。使用实体的请求指的是封装请求的实体。HTTP 协议规定了两种实体封装请求方法:POST 和 PUT。响应通常被认为是封装内容的实体。 由于一个实体既可以表示二进制内容,又能表示字符内容,因此它支持字符编码(为了支持后者,即字符内容),当发送封装有内容的请求或者成功收到请求后响应内容要被返回给客户端时,实体就会被创建。 要从实体中读出内容,一种方法是通过HttpEntity#getContent()方法查询输入流,getContent()方法返回一个java.io.InputStream对象。另一种方法是提供一个输出流给HttpEntity#writeTo(OutputStream)方法,这会一次性返回所有被写入给定流内的内容。 通过传入的信息接收到实体后,HttpEntity#getContentType()和 HttpEntity#getContentLength()方法可以用来读出大多数元数据,比如Content-Type和 Content-Length头(如果能够获取的话)。因为Content-Type头可以包含文本多媒体类型的字符编码,像 text/plain 或者是 text/html,HttpEntity#getContentEncoding()方法可以读出这些信息。如果无法获取头部信息的话,返回的长度就会是 -1 ,类型是 NULL。如果Content-Type头部可获得,那么将返回一个 Header 对象。要发送消息时,创建一个实体,需要向实体创建者提供元数据。
3.2.1 请求实体
HttpClient提供了几个类,它们能够通过 HTTP 连接来高效地输出内容。这些类的实例可以与封装请求的实体(比如 POST和 PUT)关联,可以把实体内容封装到输出的 HTTP 请求中。HttpClient提供几个类,适用于大多数常见的数据,诸如字符串,字节数组,输入流和文件: StringEntity,ByteArrayEntity,InputStreamEntity和 FileEntity。
StringEntity 字符串 可用于 JSON等请求
StringEntity entity= new StringEntity("important message", ContentType.create("text/plain", "UTF-8"));
System.out.println(myEntity.getContentType());//Content-Type: text/plain; charset=utf-8
System.out.println(myEntity.getContentLength());//17
System.out.println(EntityUtils.toString(myEntity));//important message
System.out.println(EntityUtils.toByteArray(myEntity).length);//17
/**
*推荐让 HttpClient 根据发送的 HTTP 消息属性来选择最合适的转码。但也有可能要告诉 HttpClient 某个块优先编码,
*将 HttpEntity#setChunked() 为 true 就行。要注意 HttpClient 仅会把这个标志当作提示。当设置的 HTTP
*协议版本不支持块编码时,这个值会变忽略掉,比如 HTTP/1.0。
*/
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/action.do");
httppost.setEntity(entity);
FileEntity字符串 可用于 文件 传输等。
File file = new File("somefile.txt");
FileEntity entity = new FileEntity(file, ContentType.create("text/plain", "UTF-8"));
HttpPost httppost = new HttpPost("http://localhost/action.do");
httppost.setEntity(entity);
注意 InputStreamEntity 不可复用,因为它仅能从基础数据流读取一次。一般来说推荐实现一个自定义的 HttpEntity 类是一个良好的开头,这个类是自包含的而不是使用普通的 InputStreamEntity, FileEntity。
HTML 表单许多应用需要模拟提交 HTML 表单
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("param1", "value1"));
formparams.add(new BasicNameValuePair("param2", "value2"));
/**
* 内容 param1=value1¶m2=value2
*/
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);
3.2.1 响应实体
资源关闭为了确保系统资源的合理释放,必须关闭与实体相关联的内容流或者响应。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.excute(httpget);
try {
HttpEntity entity = response.getEntity();
if(entity != null) {
InputStream instream = entity.getContent();
try {
// do something useful
} finally {
instream.close();
}
}
} finally {
response.close();
}
关闭内容流和关闭响应的区别在于前者会试着通过消耗实体内容来保持基础连接,而后者会立即切断并中止连接。请注意,一旦实体被完全写入到输出流中后, HttpEntity#writeTo(OutputStream) 方法同样需要保证系统资源的释放。如果这个方法要用到一个调用 HttpEntity#getContent() 方法得到的 java.io.InputStream 的实例,那么这个实例也应该在finally 块中关闭。使用流式实体时,可以调用 EntityUtils#consume(HttpEntity) 方法来保证实体内容被完全消耗,也关闭了基础连接。
Response handlers
最简单、最方便的处理响应的方式是使用 ResponseHandler 接口,它包含了 handleResponse(HttpResponse response) 方法。这个方法彻底的解决了用户关于连接管理的担忧。使用 ResponseHandler 时,HttpClient 会自动处理连接,无论执行请求成功或是发生了异常,都确保连接释放到连接管理者
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");
ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() {
@Override
public JsonObject handleResponse(final HttpResponse response) throws IOException {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if(statusLine.getStatusCode() >= 300) {
throw new ClientProtocolException("Response contains no content");
}
Gson gson = new GsonBuilder().create();
ContentType contentType = ContentType.getOrDefault(entity);
Charset charset = contentType.getCharset();
Reader reader = new InputStreamReader(entity.getContent(), charset);
return gson.fromJson(reader, MyJsonObject.class);
}
};
MyJsonObject myjson = client.execute(httpget, rh);
3.3HttpClient 接口
3.3.1 连接管理
HttpClient的实现应该是线程安全的,建议在执行多次请求时复用同一个HttpClient对象。 当不再需要 CloseableHttpClient的实例时,并且要离开作用域时,与之相关联的连接管理器必须要通过 CloseableHttpClient#close()方法关闭
CloseableHttpClient httpclient = HttpClients.createDefault();
try{
// TODO
// <some code>
} finally {
httpclient.close();
}
3.3.2 连接存活时间管理
HttpClient 接口代表了执行 HTTP 请求的最重要的规则。它利用请求执行过程中的无限制或特殊细节。把特定的连接管理,状态管理,授权和重定向处理留给了不同的实现者。这更容易用别的功能(比如缓存响应内容)来完善接口。一般来说 HttpClient 的实现是充当外观,暴露一些特殊意图的处理者或者策略接口,来合理的处理实现 HTTP 协议中特殊的部分,比如重定向、认证或确定连接持久化和持续活跃。这保证了用户可以选择使用自定义的,应用指定的实现方式来替换默认的实现方式。
ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long keepAlive = super.getKeepAliveDuration(respnse, context);
if(keepAlive == -1) {
//保持连接 5 秒
keepAlive = 5*1000;
}
return keepAlive;
}
};
CloseableHttpClient httpclient = HttpClients.custom().setKeepAliveStrategy(keepAliveStrat).build();
3.3.3 http 上下文
HTTP 被设计为弱状态,请求-响应式的协议。但实际应用常常需要能够通过几个逻辑相关的请求-响应交换。来持久化状态信息。为了让应用能保持在处理中的状态,HttpClient 允许在一个特殊的运行上下文中执行 HTTP 请求,这就是 HTTP 运行时上下文。如果在连续请求时复用同一个上下文,能把各种逻辑相关的请求加入到一个逻辑会话中。HTTP 上下文的作用类似于 java.util.Map<String, Object>。它仅是一个键值对集合。应用可以设置上下文中的属性,优先执行请求或者在执行完毕后检验上下文。HttpContext 能包含任意的对象,可能在多线程下共享工作时不太安全。建议每个执行线程都保持自己的上下文。在执行 HTTP 请求 这一节里,HttpClient 向执行上下文中添加了以下属性:
HttpConnection 代表与目标服务器实际连接的实例 HttpHost 代表目标主机的实例 HttpRoute 代表完整的连接路由的实例 HttpRequest 代表实际的 HTTP 请求的实例。执行上下文中的 HttpRequest 对象总是表示明确的消息状态,因为它是发送到服务器中的。每个默认的 HTTP/1.0 和 HTTP/1.1 协议使用相对应的 URI。但如果请求是是通过非通道模式的代理发送的,这个 URI 就是绝对的。 HttpResponse 代表实际 HTTP 响应的实例 java.lang.Boolean 代表实际请求是否被完全发送到目标主机的标志的对象 RequestConfig 代表实际的请求配置的对象 java.util.List 代表在执行请求过程中的所有重定向的地址的对象 可以用 HttpClientContext 适配器类简化与上下文状态的交互。
HttpContext context = <...>
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig config = clientContext.getRequestConfig();
许多代表同一个逻辑关联会话的请求队列应该在同一个 HttpContext 实例下执行,确保在请求间自动传播会话上下文和状态信息。下面的例子中,初始请求设置的请求配置将会被保存到运行时上下文中,并会被应用到该上下文中的其它请求。
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(1000)
.setConnectTimeout(1000)
.build();
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget1 = new HttpGet("http://localhost/1");
httpget1.setConfig(requestConfg);
CloseableHttpResponse response1 = httpclient.excute(httpget1, context);
try{
HttpEntity entity1 = response1.getEntity();
} finally {
response1.close();
}
HttpGet httpget2 = new HttpGet("http://localhost/2");
CloseableHttpResponse response2 = httpclient.excute(httpget2, context);
try {
HttpEntity entity2 = response2.getEntity();
} finally {
response.close();
}
cookie传入上下文
import java.util.List;
import org.apache.http.client.CookieStore;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
/**
* This example demonstrates the use of a local HTTP context populated with
* custom attributes.
*/
public class ClientCustomContext {
public final static void main(String[] args) throws Exception {
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
// Create a local instance of cookie store
CookieStore cookieStore = new BasicCookieStore();
// Create local HTTP context
HttpClientContext localContext = HttpClientContext.create();
// Bind custom cookie store to the local context
localContext.setCookieStore(cookieStore);
HttpGet httpget = new HttpGet("http://localhost/");
System.out.println("Executing request " + httpget.getRequestLine());
// Pass local context as a parameter
CloseableHttpResponse response = httpclient.execute(httpget, localContext);
try {
System.out.println("----------------------------------------");
System.out.println(response.getStatusLine());
List<Cookie> cookies = cookieStore.getCookies();
for (int i = 0; i < cookies.size(); i++) {
System.out.println("Local cookie: " + cookies.get(i));
}
EntityUtils.consume(response.getEntity());
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
3.3.4 HTTP 协议截获器
HTTP 协议截获器是实现了 HTTP 协议特殊方面的程序。通常认为协议截获器作用在接收到的消息的特殊的头部或一组相关联的头部中,或者是用特殊的头部或一组相关的头部构成将要发送的消息。协议截获器可以操作封装了消息透明的内容实体,实现压缩/解压缩就是一个不错的例子。通常这是通过装饰模式来完成的,用包含实体的类来装饰原始的实体。几个协议截获器能够组合形成一个逻辑单元。协议截获器可以通过共享信息来协同工作,比如通过 HTTP 运行上下文共享处理状态。协议截获器能够用 HTTP 上下文来给一个或几个连续的请求存储状态信息。协议截获器必须是线程安全的。类似 servlets,协议截获器不应该使用实例变量,除非这些变量的访问是同步的。这是关于如何使用局部上下文在连续的请求间,持久化处理状态
CloseableHttpClient httpclient = HttpClients.custom()
.addInterceptorLast(new HttpRequestInterceptor(){
public void process(final HttpRequest request, final HttpContext context)
throws HttpException, IOException {
AtomicInteger count = (AtomicInteger) context.getAttribute("count");
request.addHeader("Count", Integer.toString(count.getAndIncrement()));
}
})
.build();
AtomicInteger count = new AtomicInteger();
HttpClientContext localContext = HttpClientContext.create();
localContext.setAttribute("count", count);
HttpGet httpget = new HttpGet("http://localhost/");
for (int i = 0; i < 10; i++) {
CloseableHttpResponse response = httpclient.execute(httpget, localContext);
try {
HttpEntity entity = response.getEntity();
} finally {
response.close();
}
}
3.3.4 HTTP 异常处理
HTTP 协议处理能够抛出两种类型的异常:如果发生了 I/O 错误,抛出 :java.io.IOException异常,比如连接超时或者连接重置,如果发生了 HTTP 错误,抛出 HttpException,比如违反了 HTTP 协议。通常认为 I/O 错误无关紧要并且可以修复,而认为 HTTP 协议错误是致命的并且不能自动修复。请注意 HttpClient 的实现是ClientProtocolException一样重复抛出 HttpException, ClientProtocolException是 java.io.IOException 的子类。这保证 HttpClient的用户在一个 catch 块中既能处理 I/O 错误,也能处理违反协议的错误
自动解决异常。HttpClient 默认会尝试自动解决 I/O 异常。默认的自动解决机制仅限于解决一些已知是安全的异常。 HttpClient 不会尝试解决逻辑错误或者 HTTP 协议错误(源自 HttpException 类的异常) HttpClient 会自动重试那些由于传输异常导致的方法,尽管 HTTP 请求仍然被发送到目标服务器上(比如请求还没有完全发送到服务器) HttpClient 会自动重试幂等性的方法。
HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
public boolean retryRequest (IOException exception, int executionCount, HttpContext context) {
if(executionCount >= 5) {
// Do not retry if over max retry count
return false;
}
if(exception instanceof InterruptedIOException) {
// Timeout
return false;
}
if(exception instanceof UnknownHostException) {
// Unknown host
return false;
}
if(exception instanceof ConnectTimeoutException) {
//Connection refused
return false;
}
if(exception instanceof SSLException) {
// SSL handshake exception
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if(!idempotent) {
// Retry if the request is considered idempotent
return true;
}
return false;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setRetryHandler(myRetryHandler)
.build();
请注意可以使用 StandardHttpRequestRetryHandler代替默认使用的处理器,自动重试那些根据 RFC-2616 定义为幂等性的请求方法: GET,HEAD,PUT,DELETE,OPTIONS和 TRACE
3.3.5 终止请求
有些情况,由于目标服务器的高负载或者很多并发请求发送给客户端,导致 HTTP 请求没能在预期的时间内完成。这时就有必要提前终止请求并且解除执行线程对一个 I/O 操作的占用。HTTPClient 执行的 HTTP 请求可以在任何阶段被打断,调用HttpUriRequest#abort()方法就可以了。这个方法是线程安全的,能被任何线程调用。当一个 HTTP 请求被终止了,它的执行线程哪怕当时占用了一个 I/O 操作,也能确保解锁并抛出 InterruptedIOException 异常。
3.3.6 处理重定向
HttpClient自动处理所有类型的重定向,除了某些 HTTP 规则因需要用户干预而明令禁止的类型。参见其它重定向(状态码:303),HTTP 规则要求将 POST 和 PUT请求被转化为 GET 请求。可以使用自定义的重定向策略,利用 HTTP 规则来解除POST 方法自动重定向的限制。禁止重定向 setRedirectsEnabled(false)即可(默认是true)
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(redirectStrategy)
.build();
HttpClient 一般能在执行过程中重写请求消息,每个默认的 HTTP/1.0 和 HTTP/1.1 一般用相关联的请求 URI。同样的,原始请求可能会被多次重定向到其它地方。最终解译出的 HTTP 绝对地址可以用原始请求和上下文来构建。协助方法 URIUtils#resolve可以用来构建解译的绝对 URI,从而生成最终的请求。这个方法包含了在重定向请求或者原始请求中的最后一个分片校验器。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpget = new HttpGet("http://localhost:8080/");
CloseableHttpResponse respnse = httpclient.excute(httpget, context);
try {
HttpHost target = context.getTargetHost();
List<URI> redirectLocations = context.getRedirectLocations();
URI location = URIUtils.resolve(httpget.getURI(), target, redirectLocations);
System.out.println("Final HTTP location: " + location.toASCIIString());
// Expected to be an absolute URI
} finally {
response.close();
}
3.4 HttpClient的连接管理
两个主机之间建立连接的过程是很复杂的,包括了两个终端之间许多数据包的交换,会消耗了大量时间。对于很小的HTTP报文传输,上层连接的握手也是必须的【译者:上层连接指的是TCP/IP连接】。如果已有的连接能够重复使用,来执行多个请求,将会加大你程序的数据吞吐量。 HTTP/1.1 默认HTTP连接可以重复地执行多次请求。符合HTTP/1.0的终端,也可以使用某些机制来明确地表达为多次请求而优先使用持久连接。HTTP代理中,对于相同的目标主机,需要随后更多的请求,也能够在特定的时间段内保持空闲的持久连接。保持持续连接的能力通常被称为连接持久化。HttpClient完全地支持连接持久化。
3.4.1 路由
RouteInfo 接口代表一个最终的路由连接到目标主机的信息(通过多个步骤和跳)。HttpRoute 是 RouteInfo 的固定的具体实现。 HttpTracker是可变的 RouteInfo 实现,在HttpClient 内部使用来跟踪到最后的路由目标的剩余跳数。HttpTracker 可以在成功执行向路由目标的下一跳之后更新。 HttpRouteDirector是一个帮助类,可以用来计算路由中的下一跳。这个类在 HttpClient 内部使用。 HttpRoutePlanner 是一个接口,它代表计算到基于执行上下文到给定目标完整路由策略。 HttpClient 装载了 两 个 默 认 的 HttpRoutePlanner 实 现 。ProxySelectorRoutePlanner基于 java.net.ProxySelector 。默认情况下 ,它会从系统属性中或从正在运行浏览器中选JVM的代理设置 。DefaultHttpRoutePlanner 实现既不使用任何 Java 系统属性,也不使用系统或浏览器的代理设置。它总是通过默认的相同代理来计算路由。
3.4.2 连接管理器
HTTP 连接是复杂的,有状态的,非线程安全的对象,它需要恰当的管理以便正确地执行功能。 “HTTP 连接”一次仅只能由一个执行线程来使用。 HttpClient 采用一个特殊实体来管理访问 HTTP 连接,这被称为 HTTP 连接管理器, ClientConnectionManager接口就是代表。HTTP 连接管理器的目的是作为工厂创建新的 HTTP 连接,管理持久连接的生命周期和同步访问持久连接(确保同一时间仅有一个线程可以访问一个连接)。内部的 HTTP 连接管理器使用 OperatedClientConnection 实例,这个实例为真正的连接扮演了一个代理,来管理连接状态和控制I/O操作的执行。如果一个管理的连接被释放或者被使用者明确地关闭,潜在的连接就会从它的代理分离,退回到管理器中。 尽管“服务消费者”仍然保存一个代理实例的引用。它也不能执行任何I/O操作、有意或无意地改变真实连接的状态。 BasicHttpClientConnectionManager是一个简单的连接管理器,它保持一次只有一个连接。尽管这个方法是线程安全的,它也应该一次使用一个线程。BasicHttpClientConnectionManager将会城尝试在同一个路由中随后的请求使用同一个连接.如果持续的连接不匹配连接的请求,他将会关闭现有连接并为给定的路由重新打开。如果连接已经被分配,将会抛出java.lang.IllegalStateException异常,连接管理器应该在EJB容器中实现。!
HttpClientContext context = HttpClientContext.create();
HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
// Request new connection. This can be a long process
ConnectionRequest connRequest = connMrg.requestConnection(route, null);
// Wait for connection up to 10 sec
HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
try {
// If not open
if (!conn.isOpen()) {
// establish connection based on its route info
connMrg.connect(conn, route, 1000, context);
// and mark it as route complete
connMrg.routeComplete(conn, route, context);
}
// Do useful things with the connection.
} finally {
connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
}
PoolingHttpClientConnectionManager是一个管理客户端连接更复杂的实现。它也为执行多线程的连接请求提供服务。对于每个基本的路由,连接都是池管理的。对于路由的请求,管理器在池中有可用的持久性连接,将被从池中取出连接服务,而不是创建一个新的连接。 对于每一个基本路由,PoolingHttpClientConnectionManager保持着一个最大限制的连接数。每个默认的实现对每个给定路由将会创建不超过2个的并发连接,一共不会超过 20 个连接。对于真实世界的许多程序,连接数太少了,特别是他们为其服务使用HTTP协议作为传输协议时。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);
CloseableHttpClient httpClient = HttpClients.custom()setConnectionManager(cm).build();
当一个HttpClient实例不再被需要时,而且即将超出使用范围,重要是的,关闭连接管理器来保证由管理器保持活动的所有连接被关闭,并且由连接分配的系统资源被释放。 CloseableHttpClient httpClient = <...> httpClient.close();
3.4.3 回收策略
一个典型阻塞 I/O 的模型的主要缺点是网络socket仅当 I/O 操作阻塞时才可以响应 I/O事件。当一个连接被释放回管理器时,它可以被保持活动状态而却不能监控socket的状态和响应任何 I/O 事件。 如果连接在服务器端关闭,那么客户端连接也不能去侦测连接状态中的变化(也不能关闭本端的socket去作出适当反馈)。HttpClient 通过测试连接是否是连接是过时的来尝试去减轻这个问题。在服务器端关闭的连接已经不再有效了,优先使用之前使用执行 HTTP 请求的连接。过时的连接检查也并不是100%可靠。唯一可行的而不涉及每个空闲连接的socket模型线程的解决方案是:是使用专用的监控线程来收回因为长时间不活动而被认 为 是 过 期 的 连 接 。 监 控 线 程 可 以 周 期 地 调 用ClientConnectionManager#closeExpiredConnections()方法来关闭所有过期 的 连 接 并且从 连 接 池 中 收 回 关 闭 的 连 接 。 它 也 可 以 选 择 性 调 用ClientConnectionManager#closeIdleConnections()方法来关闭所有已经空闲超过给定时间周期的连接。
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
3.4.4 连接保持策略
HTTP 规范没有详细说明一个持久连接可能或应该保持活动多长时间。一些 HTTP 服务器使用非标准的首部Keep-Alive来告诉客户端它们想在服务器端保持连接活动的周期秒数。如果这个信息存在, HttClient 就会使用它。如果响应中首部Keep-Alive 在不存在, HttpClient会假定无限期地保持连接。然而许多 HTTP 服务器的普遍配置是在特定非 活动周期之后丢掉持久连接来保护系统资源,往往这是不通知客户端的。默认的策略是过于乐观的,你必须提供一个自定义的keep-alive策略
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
// Honor 'keep-alive' header
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch(NumberFormatException ignore) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(
HttpClientContext.HTTP_TARGET_HOST);
if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// Keep alive for 5 seconds only
return 5 * 1000;
} else {
// otherwise keep alive for 30 seconds
return 30 * 1000;
}
}
};