HttpClient 理解与实践
HTTPClient 理解与实战
推荐使用 HttpClient 的主要优点:
在 keep-alive 时间内,可以使用同一个 tcp 连接发起多次 http 请求。
如果不使用连接池,在大并发的情况下,每次连接都会打开一个端口,使系统资源很快耗尽,无法建立新的连接,可以限定最多打开的端口数。
httpClient 维护着两个 Set,leased(被占用的连接集合) 和 avaliabled(可用的连接集合) 两个集合,释放连接就是将被占用连接放到可用连接里面。
HTTPClient 组成图
HttpClient 经典配置详解
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpConnectionFactory;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.DefaultHttpResponseParserFactory;
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Configuration
public class RestTemplateConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(RestTemplateConfig.class);
@Bean
public RestTemplate restTemplate() {
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(getHttpClient());
return new RestTemplate(requestFactory);
}
// httpclient 4.5.2使用连接池的经典配置
private CloseableHttpClient getHttpClient() {
// 注册访问协议相关的Socket工厂
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
// HttpConnectionFactory:配置写请求/解析响应处理器
HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connectionFactory = new ManagedHttpClientConnectionFactory(
DefaultHttpRequestWriterFactory.INSTANCE,
DefaultHttpResponseParserFactory.INSTANCE
);
// DNS解析器
DnsResolver dnsResolver = SystemDefaultDnsResolver.INSTANCE;
// 创建连接池管理器
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(socketFactoryRegistry, connectionFactory, dnsResolver);
// 设置默认的socket参数
manager.setDefaultSocketConfig(SocketConfig.custom().setTcpNoDelay(true).build());
// 设置最大连接数。高于这个值时,新连接请求,需要阻塞,排队等待
manager.setMaxTotal(300);
// 路由是对MaxTotal的细分。
// 每个路由实际最大连接数默认值是由DefaultMaxPerRoute控制。
// MaxPerRoute设置的过小,无法支持大并发:ConnectionPoolTimeoutException:Timeout waiting for connection from pool
// 每个路由的最大连接
manager.setDefaultMaxPerRoute(200);
// 在从连接池获取连接时,连接不活跃多长时间后需要进行一次验证,默认为2s
manager.setValidateAfterInactivity(5 * 1000);
// 配置默认的请求参数
RequestConfig defaultRequestConfig = RequestConfig.custom()
// 连接超时设置为2s。建立TCP连接的时间
.setConnectTimeout(2 * 1000)
// 等待数据超时设置为5s。等待服务器返回数据的时间
.setSocketTimeout(5 * 1000)
// 从连接池获取连接的等待超时时间设置为2s
.setConnectionRequestTimeout(2 * 1000)
// 设置代理
.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.2", 1234)))
.build();
CloseableHttpClient closeableHttpClient = HttpClients.custom()
.setConnectionManager(manager)
// 连接池不是共享模式,这个共享是指与其它httpClient是否共享
.setConnectionManagerShared(false)
// 定期回收空闲连接
.evictIdleConnections(60, TimeUnit.SECONDS)
// 回收过期连接
.evictExpiredConnections()
// 连接存活时间,如果不设置,则根据长连接信息决定
.setConnectionTimeToLive(60, TimeUnit.SECONDS)
// 设置默认的请求参数
.setDefaultRequestConfig(defaultRequestConfig)
// 连接重用策略
.setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE)
// 长连接配置,即获取长连接生产多长时间
.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
// 设置重试次数,默认为3次;当前是禁用掉
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
.build();
/**
*JVM停止或重启时,关闭连接池释放掉连接
*/
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
LOGGER.info("closing http client");
closeableHttpClient.close();
LOGGER.info("http client closed");
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
}
}
});
return closeableHttpClient;
}
}
HTTPClient 使用Demo
/**
*普通的GET请求
*/
public class DoGET {
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
//请求体内容
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
//内容写入文件
FileUtils.writeStringToFile(new File("E:\\devtest\\baidu.html"), content, "UTF-8");
System.out.println("内容长度:"+content.length());
}
} finally {
if (response != null) {
response.close();
}
//相当于关闭浏览器
httpclient.close();
}
}
}
import org.apache.commons.io.FileUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.File;
/**
* 常规post请求
* 可以设置Header来伪装浏览器请求
*/
public class DoPOST {
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http POST请求
HttpPost httpPost = new HttpPost("http://www.oschina.net/");
//伪装浏览器请求
httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpPost);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
//内容写入文件
FileUtils.writeStringToFile(new File("E:\\devtest\\oschina.html"), content, "UTF-8");
System.out.println("内容长度:"+content.length());
}
} finally {
if (response != null) {
response.close();
}
httpclient.close();
}
}
}
HttpClient 可能出现的异常
connectionRequestTimeout:从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
connectTimeout:连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
socketTimeout:服务器返回数据(response)的时间,超过该时间抛出read timeout
以上3个超时相关的参数如果未配置,默认为-1,意味着无限大,就是一直阻塞等待!
HttpClients在我们没有指定连接工厂的时候默认使用的是连接池工厂org.apache.http.impl.conn.PoolingHttpClientConnectionManager.PoolingHttpClientConnectionManager(Registry<ConnectionSocketFactory>)
HTTPClient 连接池的连接保持、超时、和失效机制
- 如何判断连接是否可以保持?
检查返回response报文头的Transfer-Encoding字段,若该字段值存在且不为chunked,则连接不保持,直接关闭。
检查返回的response报文头的Content-Length字段,若该字段值为空或者格式不正确(多个长度,值不是整数),则连接不保持,直接关闭。
检查返回的response报文头的Connection字段(若该字段不存在,则为Proxy-Connection字段)值:
如果这俩字段都不存在,则1.1版本默认为保持, 1.0版本默认为连接不保持,直接关闭。
如果字段存在,若字段值为close 则连接不保持,直接关闭;若字段值为keep-alive则连接标记为保持。
- 保持多长时间?
保持时间计时开始时间为连接交换至连接池的时间。 保持时长计算规则为:获取response中 Keep-Alive字段中timeout值,若该存在,则保持时间为 timeout值*1000,单位毫秒。若不存在,则连接保持时间设置为-1,表示为无穷。
- 保持过程中如何保证连接没有失效?
很难保证。传统阻塞I/O模型,只有当I/O操做的时候,socket才能响应I/O事件。当TCP连接交给连接管理器后,它可能还处于“保持连接”的状态,但是无法监听socket状态和响应I/O事件。如果这时服务器将连接关闭的话,客户端是没法知道这个状态变化的,从而也无法采取适当的手段来关闭连接。
针对这种情况,HttpClient采取一个策略,通过一个后台的监控线程定时的去检查连接池中连接是否还“新鲜”,如果过期了,或者空闲了一定时间则就将其从连接池里删除掉。ClientConnectionManager提供了 closeExpiredConnections和closeIdleConnections两个方法。