深入学习 HttpClient 5:源码分析、封装工具类和注意点
随着现代应用程序对网络通信需求的增加,以及对更灵活、高性能和可定制的 HTTP 客户端的需求,HttpClient 5 成为了 Java 开发者们的首选工具。它提供了强大的功能和灵活的配置选项,使得发送 HTTP 请求和处理响应变得更加简单和高效。
在本篇技术博客中,我们将深入学习 HttpClient 5,并从以下几个方面入手:源码分析、封装工具类和最佳实践与注意点。通过这些深入的学习,你将能够更好地理解 HttpClient 5 的内部工作原理,灵活地封装和使用 HttpClient 5,以及遵循最佳实践来提升性能和稳定性。
1. 源码分析
httpclient官网: hc.apache.org/httpcompone… 源码版本5.1.3
HttpClient
是顶级接口,有三个实现类CloseableHttpClient
、InternalHttpClient
、MinimalHttpClient
HttpClient:
CloseableHttpClient:
HttpClient
的基本实现,也实现了ModalCloseable
接口(立即/优雅的关闭资源)
InternalHttpClient:
CloseableHttpClient
的内部实现
MinimalHttpClient:
CloseableHttpClient
的最小实现。不支持高级HTTP协议功能,如通过执行代理请求、状态管理、身份验证和请求重定向。
HttpClients
HttpClients
是CloseableHttpClient
实现的工厂方法。
一般调用custom()
方法创建HttpClientBuilder用builder模式创建CloseableHttpClient
。
HttpClientBuilder
类中有各种set属性的方法,我们就从这作为切入点来看。
主要的set方法有
- setConnectionManager
- setKeepAliveStrategy
- setRetryStrategy
- setDefaultRequestConfig
setConnectionManager
public final HttpClientBuilder setConnectionManager(final HttpClientConnectionManager connManager) {
this.connManager = connManager;
return this;
}
setConnectionManager()
方法入参是一个HttpClientConnectionManager
。我们就来看一下这个接口相关的代码。有两个实现类BasicHttpClientConnectionManager
和PoolingHttpClientConnectionManager
。
BasicHttpClientConnectionManager
类是管理单个连接的连接管理器。这个管理器只维护一个活动连接。因此用于创建和管理一次只能由一个线程使用的单个连接。大部分场景还是使用PoolingHttpClientConnectionManager
。
PoolingHttpClientConnectionManager
:是一个池化的连接管理器,能够服务来自多个执行线程的请求。连接基于每个路由进行池化。
PoolingHttpClientConnectionManager
主要的方法有:
setKeepAliveStrategy
设置keepAlive策略一般使用默认的默认的new DefaultConnectionKeepAliveStrategy()
这个默认的keepAlive是3s。根据实际情况覆写getKeepAliveDuration()
方法。
setRetryStrategy
设置重试策略默认是new DefaultHttpRequestRetryStrategy()
,重试1次间隔1s。
setDefaultRequestConfig
设置请求信息。常用的RequestConfig
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT)) // 建立连接超时时间
.setResponseTimeout(Timeout.ofSeconds(MAX_TIMEOUT)) // 传输超时
.setConnectionRequestTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT)) // 设置从连接池获取连接实例的超时
.build();
execute执行流程
execute执行流程主要是责任链模式。在创建httpclient的时候初始化责任链。具体在HttpClients._custom_().build()
时使用头插法创建双向链表,形成责任链。
主要代码
public CloseableHttpClient build() {
// ...
final NamedElementChain<ExecChainHandler> execChainDefinition = new NamedElementChain<>();
execChainDefinition.addLast(
new MainClientExec(connManagerCopy, reuseStrategyCopy, keepAliveStrategyCopy, userTokenHandlerCopy),
ChainElement.MAIN_TRANSPORT.name());
execChainDefinition.addFirst(
new ConnectExec(
reuseStrategyCopy,
new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)),
proxyAuthStrategyCopy),
ChainElement.CONNECT.name());
// ...
execChainDefinition.addFirst(
new ProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy),
ChainElement.PROTOCOL.name());
// Add request retry executor, if not disabled
if (!automaticRetriesDisabled) {
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
if (retryStrategyCopy == null) {
retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new HttpRequestRetryExec(retryStrategyCopy),
ChainElement.RETRY.name());
}
// ...
if (!contentCompressionDisabled) {
if (contentDecoderMap != null) {
final List<String> encodings = new ArrayList<>(contentDecoderMap.keySet());
final RegistryBuilder<InputStreamFactory> b2 = RegistryBuilder.create();
for (final Map.Entry<String, InputStreamFactory> entry: contentDecoderMap.entrySet()) {
b2.register(entry.getKey(), entry.getValue());
}
final Registry<InputStreamFactory> decoderRegistry = b2.build();
execChainDefinition.addFirst(
new ContentCompressionExec(encodings, decoderRegistry, true),
ChainElement.COMPRESS.name());
} else {
execChainDefinition.addFirst(new ContentCompressionExec(true), ChainElement.COMPRESS.name());
}
}
// Add redirect executor, if not disabled
if (!redirectHandlingDisabled) {
RedirectStrategy redirectStrategyCopy = this.redirectStrategy;
if (redirectStrategyCopy == null) {
redirectStrategyCopy = DefaultRedirectStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new RedirectExec(routePlannerCopy, redirectStrategyCopy),
ChainElement.REDIRECT.name());
}
// Optionally, add connection back-off executor
if (this.backoffManager != null && this.connectionBackoffStrategy != null) {
execChainDefinition.addFirst(new BackoffStrategyExec(this.connectionBackoffStrategy, this.backoffManager),
ChainElement.BACK_OFF.name());
}
}
调用execChainDefinition.addLast()
和execChainDefinition.addFirst()
方法初始化一个BackoffStrategyExec
->RedirectExec
->ContentCompressionExec
->HttpRequestRetryExec
->ProtocolExec
->ConnectExec
->MainClientExec
。然后按顺序执行他们的execute()方法,完成自己的任务。
BackoffStrategyExec
:Request execution handler in the classic request execution chain that is responsible for execution of an ConnectionBackoffStrateg(经典请求执行链中的请求执行处理程序,负责ConnectionBackoffStrateg的执行)。对出现连接或者响应超时异常的route进行降级,缩小该route上连接数,能使得服务质量更好的route能得到更多的连接。降级的速度可以通过因子设置,默认是每次降级减少一半的连接数,即降级因子是0.5。RedirectExec
:Request execution handler in the classic request execution chain responsible for handling of request redirects(经典请求执行链中的请求执行处理程序,负责处理请求重定向)。ContentCompressionExec
:Request execution handler in the classic request execution chain that is responsible for automatic response content decompression(经典请求执行链中的请求执行处理程序,负责自动响应内容解压缩)HttpRequestRetryExec
:Request executor in the request execution chain that is responsible for making a decision whether a request that failed due to an I/O exception or received a specific response from the target server should be re-executed.(请求执行链中的请求执行器,负责决定由于I/O异常而失败的请求或从目标服务器接收到特定响应的请求是否应该重新执行)ProtocolExec
:Request execution handler in the classic request execution chain that is responsible for implementation of HTTP specification requirements(经典请求执行链中的请求执行处理程序,负责HTTP规范需求的实现)对http消息编码/解码。这个操作在处理请求和相应之前。我们可以往HttpRequestInterceptor和HttpResponseInterceptor中添加我们自己定义的拦截器。ConnectExec
:Request execution handler in the classic request execution chain that is responsible for establishing connection to the target origin server as specified by the current connection route(经典请求执行链中的请求执行处理程序,该处理程序负责建立到由当前连接路由指定的目标源服务器的连接)。MainClientExec
:Usually the last request execution handler in the classic request execution chain that is responsible for execution of request / response exchanges with the opposite endpoint.(通常是经典请求执行链中的最后一个请求执行处理程序,负责执行与对面端点的请求/响应交换)连接的池化处理在这。
2. 代码片段
import com.google.common.base.Throwables;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpResponseException;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HeaderElements;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicHeaderElementIterator;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
/**
* @Description TODO
* @Author 张弛
* @Datee 2022/8/22
* @Version 1.0
**/
public class HttpUtil {
private static final int MAX_TIMEOUT = 400;
private static final CloseableHttpClient httpClient;
static {
// 自定义 SSL 策略
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", createSSLConnSocketFactory())
.build();
// 设置连接池
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(registry);
connMgr.setMaxTotal(100); // 设置连接池大小
connMgr.setDefaultMaxPerRoute(connMgr.getMaxTotal()); // 设置每条路由的最大并发连接数
connMgr.setValidateAfterInactivity(TimeValue.ofSeconds(600)); // 设置长连接
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT)) // 建立连接超时时间
.setResponseTimeout(Timeout.ofSeconds(MAX_TIMEOUT)) // 传输超时
.setConnectionRequestTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT)) // 设置从连接池获取连接实例的超时
.build();
httpClient = HttpClients.custom()
.setConnectionManager(connMgr)
.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
.setRetryStrategy(new DefaultHttpRequestRetryStrategy()) // 重试 1 次,间隔1s
.setDefaultRequestConfig(requestConfig)
.build();
}
public static byte[] doGet(String url) throws IOException, ParseException {
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
// int statusCode = response.getStatusLine().getStatusCode();
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
String message = EntityUtils.toString(response.getEntity());
throw new HttpResponseException(statusCode, message);
}
byte[] bytes = EntityUtils.toByteArray(response.getEntity());
return bytes;
} catch (Exception e) {
throw e;
} finally {
if (response != null) {
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
}
}
}
}
private static SSLConnectionSocketFactory createSSLConnSocketFactory() {
SSLConnectionSocketFactory sslsf = null;
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
}).build();
sslsf = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true; // 信任所有证书
}
});
} catch (GeneralSecurityException e) {
throw Throwables.propagate(e);
}
return sslsf;
}
}
3. 注意点
- 设置合理的超时时间:连接超时、读取超时、从连接池中获取连接的超时时间。
- 设置合理的连接池大小:连接池大小和读取耗时、QPS 有关,一般等于峰值 QPS * 耗时(单位是秒)。
- 设置合理的长连接有效时间:使用连接池就默认使用长连接,长连接有效时间应该和服务端的长连接有效时间保持一致。如果客户端设置的有效时间过长,则会在服务端连接断开的时客户端依然去请求时导致NoHttpResponseException。也可以通过设RequestConfig.setStaleConnectionCheckEnabled 参数让客户端每次请求之前都检查长连接有效性,但是这样会导致性能的下降。
- 设置合理的重试策略:合理的重试,可以提升应用的可用性。默认的重试策略不会对超时进行重试,然而超时是十分从常见的问题,服务端异常或网络抖动都可能导致超时。
长连接
使用长连接能够减少建立和销毁连接的消耗,三次握手和四次挥手对性能影响还是很大的。一般RPC都是使用长连接。
一般Http服务前面都会挂nginx做负载均衡,那么长连接的设置也就分为从客户端到nginx,nginx到服务端两部分。
apache httpclient 5.x直接使用DefaultConnectionKeepAliveStrategy这个类就行,不行根据这个类重写策略。大部分的博客写的都是3.下或者4.x版本的。5.x版本的看这个类就行。
nginx请求后端服务时启用长连接配置:
proxy_http_version 1.1;
proxy_set_header Connection "";
keepalive_timeout 120s 120s;
keepalive_requests 10000;
keepalive_timeout:不活跃多长时间后断开。
keepalive_requests:keepalive的requests的最大数量。当请求达到最大数量时,连接被关闭。nginx和tomcat的默认数查一下,好像是100。对于 QPS 较高的应用来说 100 有些太小了。
springboot的tomcat配置在这个里面找。