Java HttpComponents源码阅读1
Java HttpComponents源码阅读2
HttpComponents一直是Java中HTTP请求的常用库,经常用来和OkHttp和Spring RestTemplate拿来比较,今天就来看下该库请求一次http请求会发生什么,由于代码过于庞大和精力时间有限,所以只会翻阅部分代码;
版本:
httpcomponents-client-4.5.x
httpcore-4.4.13
示例
public class Test {
private static CloseableHttpClient DEFAULT_CLIENT;
private static void method3() throws Exception {
DEFAULT_CLIENT = HttpClients.custom().build();
HttpGet get = new HttpGet("http://www.baidu.com");
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(5000)
.setConnectTimeout(5000)
.setConnectionRequestTimeout(5000)
.build();
get.setConfig(requestConfig);
HttpResponse response = DEFAULT_CLIENT.execute(get);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity resEntity = response.getEntity();
String message = EntityUtils.toString(resEntity, "utf-8");
System.out.println(message);
} else {
System.out.println("http code != 200");
}
}
}
最简单的用法,用一个静态单例持有HTTPClient
对象;
HttpClientBuilder
当我们调用HttpClientBuilder#build
时,会走到一个长300多行的方法,里面将处理请求和响应所用到的类,根据用户的配置进行组装,大概用到了下面一些关键的类;
HttpRequestExecutor
核心类,是一个基于阻塞IO模型的客户端HTTP协议处理类,主要负责控制相关类进行send和receive http请求,定义了一个HTTP请求从生到死的生命周期的流程;HttpClientConnectionManager
核心接口,定义了HTTP连接池的操作,包括创建、获取、路由、关闭等操作,这个接口的实现必须是线程安全的,确保一次只能有一个执行线程访问连接池获取连接,但是调用者可以从多线程环境中执行;主要实现是PoolingHttpClientConnectionManager
;ConnectionSocketFactory
用于创建和连接socket的工厂,主要实现有PlainConnectionSocketFactory
和SSLConnectionSocketFactory
,前者用于HTTP
连接,后者用于HTTPS
连接;ClientExecChain
处理HTTP请求执行链,每个实现该接口的类都是都是一个执行者,每个执行者执行完任务后会将请求传输到目标服务器或传递到请求执行链中的下一个执行器来执行请求,主要的实现类是MainClientExec
;HttpHost
描述到主机的HTTP连接所需的所有变量,包括远程主机名、端口和地址等;HttpRoute
Http路由信息,包括HttpHost、InetAddress、是否使用代理、是否使用SSL、是否使用隧道技术等;HttpRoutePlanner
根据http请求计算到目标主机的HttpRoute;HttpClient
此接口仅表示HTTP请求执行的最基本契约。它对请求执行过程没有施加任何限制或细节,并将状态管理、身份验证和重定向处理的具体细节留给各个实现类实现,具体的细节在CloseableHttpClient
和InternalHttpClient
中;ConnectionReuseStrategy
定义决定连接是否可以为后续请求重用的接口,实现类必须是线程安全的;默认实现为DefaultClientConnectionReuseStrategy
,由HTTP请求头中的Connection
、Content-Length
等字样判断重用策略;ConnectionKeepAliveStrategy
定义决定连接在被重用之前可以保持空闲多长时间,实现类必须是线程安全的;默认实现为DefaultConnectionKeepAliveStrategy
,由HTTP请求头中的Keep-Alive
来决定;AuthenticationStrategy
用于确定HTTP响应是否表示由于身份验证失败而发送回客户机的身份验证挑战,TargetAuthenticationStrategy
处理WWW-Authenticate
,ProxyAuthenticationStrategy
处理Proxy-Authenticate
,这两个的含义具体参考RFC-7235UserTokenHandler
用于确定执行上下文是否是特定用户的处理程序。如果上下文是特定于用户的,则此返回的令牌对象将唯一标识当前用户,用户令牌将用于确保特定的用户资源不会被其他用户共享或被其他用户重用。根据设置使用DefaultUserTokenHandler
或NoopUserTokenHandler
;HttpProcessor
协议拦截器集合,用于拦截每个http请求的request和response,即处理HttpRequestInterceptor
和HttpResponseInterceptor
接口,默认实现为ImmutableHttpProcessor
;
拦截器 | 作用 |
---|---|
RequestTargetHost | 负责处理HTTP1.1中HOST请求头 |
RequestUserAgent | 负责处理HTTP中User-Agent请求头 |
RequestDefaultHeaders | 负责处理HTTP用户自定义请求头 |
RequestContent | 负责处理HTTP中和Content相关的请求头如Content-Type、Content-Length和Content-Encoding等 |
RequestClientConnControl | 负责处理HTTP中和Keep-Alive请求头 |
RequestExpectContinue | 负责处理HTTP中和Expect请求头 |
RequestAcceptEncoding | 负责处理HTTP中和Accept-Encoding请求头 |
ResponseProcessCookies | 负责处理HTTP中和Set-Cookie请求头 |
到这里整个build过程遇到的一些重要的类都有了个印象,这个方法的大概流程就是:创建构建InternalHttpClient
所需要的对象,根据用户自定义来决定相关配置,并返回InternalHttpClient
;
内部其中一部分组成部分,其中任务链的起点是从RedirectExec
开始;
执行HTTP请求的流程
InternalHttpClient#doExecute
HttpGet get = new HttpGet("http://api8.iwown.com");
首先是创建请求,HttpGet
的结构如下
主要是下面几个组成
HttpUriRequest
获取HTTP请求体信息的接口,包括Method、Header、URI、RequestLine等信息;HttpExecutionAware
提供一个可以允许HTTP请求被取消操作的入口;AbstractExecutionAwareRequest
HttpExecutionAware的实现类,内部使用AtomicMarkableReference
来保证每个请求只能被取消一次;HttpRequestBase
结合了以上几个接口,也是HttpGet、HttpPost等的基类;
HttpResponse response = DEFAULT_CLIENT.execute(get);
当我们执行这行代码时,首先进入
protected CloseableHttpResponse doExecute(
final HttpHost target,
final HttpRequest request,
final HttpContext context) throws IOException, ClientProtocolException {
Args.notNull(request, "HTTP request");
HttpExecutionAware execAware = null;
if (request instanceof HttpExecutionAware) {
execAware = (HttpExecutionAware) request; // 默认的HttpGet、HttpPost等都是支持HttpExecutionAware操作的
}
try {
// 包装一下request和target
final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request, target);
// 设置httpclient context
final HttpClientContext localcontext = HttpClientContext.adapt(
context != null ? context : new BasicHttpContext());
RequestConfig config = null;
if (request instanceof Configurable) {
config = ((Configurable) request).getConfig();
}
if (config == null) {
final HttpParams params = request.getParams();
if (params instanceof HttpParamsNames) {
if (!((HttpParamsNames) params).getNames().isEmpty()) {
config = HttpClientParamConfig.getRequestConfig(params, this.defaultConfig);
}
} else {
config = HttpClientParamConfig.getRequestConfig(params, this.defaultConfig);
}
}
if (config != null) {
localcontext.setRequestConfig(config);
}
setupContext(localcontext);
// 决定路由
final HttpRoute route = determineRoute(target, wrapper, localcontext);
// 从任务链的第一个执行者开始执行
return this.execChain.execute(route, wrapper, localcontext, execAware);
} catch (final HttpException httpException) {
throw new ClientProtocolException(httpException);
}
}
首先是配置HttpClientContext
,该类实际上是一个线程安全的map,主要负责将Http请求的一些header或者对象映射的属性储存,因为一个http请求所需要的来源于众多接口和类,放在一起方便存取;
然后是配置路由,将目标地址、端口号、是否使用代理、是否使用SSL等信息生成新的路由;
最后开始从任务链的第一个执行者执行任务,在这里是RedirectExec
;
RedirectExec#execute
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
final List redirectLocations = context.getRedirectLocations();
if (redirectLocations != null) {
redirectLocations.clear();
}
final RequestConfig config = context.getRequestConfig();
// 最大重定向次数,默认为50
final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
HttpRoute currentRoute = route;
HttpRequestWrapper currentRequest = request;
for (int redirectCount = 0;;) {
// 唤起下一个执行器执行任务
final CloseableHttpResponse response = requestExecutor.execute(
currentRoute, currentRequest, context, execAware);
try {
// 判断用户设置是否支持重定向 和
// response返回的结果是否是重定向的状态码 如302
if (config.isRedirectsEnabled() &&
this.redirectStrategy.isRedirected(currentRequest.getOriginal(), response, context)) {
// 允许重定向且http响应为302或其他重定向标志
if (!RequestEntityProxy.isRepeatable(currentRequest)) {
// 请求不支持 不可重复请求 重定向,直接返回response
return response;
}
if (redirectCount >= maxRedirects) {
// 超过最大重定向次数
throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
}
redirectCount++;
// 按照重定向策略获取重定向路径
final HttpRequest redirect = this.redirectStrategy.getRedirect(
currentRequest.getOriginal(), response, context);
if (!redirect.headerIterator().hasNext()) {
// 如果header被重置了,补充所有之前请求的headers
final HttpRequest original = request.getOriginal();
redirect.setHeaders(original.getAllHeaders());
}
currentRequest = HttpRequestWrapper.wrap(redirect);
if (currentRequest instanceof HttpEntityEnclosingRequest) {
RequestEntityProxy.enhance((HttpEntityEnclosingRequest) currentRequest);
}
final URI uri = currentRequest.getURI();
final HttpHost newTarget = URIUtils.extractHost(uri);
if (newTarget == null) {
throw new ProtocolException("Redirect URI does not specify a valid host name: " +
uri);
}
// 如果重定向到另一个主机则重置虚拟主机和认证状态
if (!currentRoute.getTargetHost().equals(newTarget)) {
final AuthState targetAuthState = context.getTargetAuthState();
if (targetAuthState != null) {
targetAuthState.reset();
}
final AuthState proxyAuthState = context.getProxyAuthState();
if (proxyAuthState != null && proxyAuthState.isConnectionBased()) {
proxyAuthState.reset();
}
}
// 重新选择路由
currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
// 确保entity被读取,同时关闭inputstream
EntityUtils.consume(response.getEntity());
response.close();
} else {
return response;
}
} catch (final RuntimeException ex) {
response.close();
throw ex;
} catch (final IOException ex) {
response.close();
throw ex;
} catch (final HttpException ex) {
// 出现http协议异常,底层的连接可能被挽救复用;
try {
EntityUtils.consume(response.getEntity());
} catch (final IOException ioex) {
this.log.debug("I/O error while releasing connection", ioex);
} finally {
response.close();
}
throw ex;
}
}
}
故名思义RedirectExec
就是负责http请求重定向的,主要做了以下几件事
- 开启一个循环,次数是用户配置的最大重定向数
maxRedirects
,在每次循环中执行下一个执行器的任务,在这里也就是RetryExec
,等待执行完拿到response; - 如果配置允许重定向,则进入下面步骤,否则直接返回response;
- 解析response拿到
http status code
,如果状态码是302或者301等重定向的状态码,则进行重定向操作,重新生成一个新的请求和路由,在maxRedirects
内重新执行RetryExec#execute
; - 如果遇到异常需要把请求关闭,关闭请求的过程下面在看,同时把异常向上抛出;
RetryExec#execute
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
final Header[] origheaders = request.getAllHeaders();
for (int execCount = 1;; execCount++) {
try {
// 先执行下一个执行器任务
return this.requestExecutor.execute(route, request, context, execAware);
} catch (final IOException ex) {
if (execAware != null && execAware.isAborted()) {
// isAborted 意味着请求已经被关闭
this.log.debug("Request has been aborted");
throw ex;
}
if (retryHandler.retryRequest(ex, execCount, context)) {
if (!RequestEntityProxy.isRepeatable(request)) {
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity", ex);
}
request.setHeaders(origheaders);
} else {
if (ex instanceof NoHttpResponseException) {
final NoHttpResponseException updatedex = new NoHttpResponseException(
route.getTargetHost().toHostString() + " failed to respond");
updatedex.setStackTrace(ex.getStackTrace());
throw updatedex;
}
throw ex;
}
}
}
}
RetryExec
负责决定是否应该重新执行由于IOException
导致的请求失败,比如再流被关闭后继续读取从流中读取数据而抛出的异常;
在执行后续的任务链上面的任务时遇到了异常后,如果请求被关闭则直接抛出异常,否则交由HttpRequestRetryHandler
来判断是否允许重试请求,默认实现为DefaultHttpRequestRetryHandler
,主要的判断逻辑是依据retryCount
和requestSentRetryEnabled
,retryCount
默认为3次,requestSentRetryEnabled
表示请求发生成功后是否允许再被重试,默认为false;
DEFAUTL_HTTP_CLIENT = HttpClients.custom()
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))//forbidden retry
.build();
可以通过上面方式修改默认配置;
ProtocolExec#exec
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException,
HttpException {
final HttpRequest original = request.getOriginal();
URI uri = null;
if (original instanceof HttpUriRequest) {
uri = ((HttpUriRequest) original).getURI();
} else {
final String uriString = original.getRequestLine().getUri();
try {
uri = URI.create(uriString);
} catch (final IllegalArgumentException ex) {
}
}
request.setURI(uri);
// 更新request中的uri属性,即是访问的资源路径
rewriteRequestURI(request, route, context.getRequestConfig().isNormalizeUri());
final HttpParams params = request.getParams();
HttpHost virtualHost = (HttpHost) params.getParameter(ClientPNames.VIRTUAL_HOST);
// 处理virtualhost
if (virtualHost != null && virtualHost.getPort() == -1) {
final int port = route.getTargetHost().getPort();
if (port != -1) {
virtualHost = new HttpHost(virtualHost.getHostName(), port,
virtualHost.getSchemeName());
}
}
HttpHost target = null;
if (virtualHost != null) {
target = virtualHost;
} else {
if (uri != null && uri.isAbsolute() && uri.getHost() != null) {
target = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
}
}
if (target == null) {
target = request.getTarget();
}
if (target == null) {
target = route.getTargetHost();
}
if (uri != null) {
final String userinfo = uri.getUserInfo();
if (userinfo != null) {
// 处理和用户验证的信息
CredentialsProvider credsProvider = context.getCredentialsProvider();
if (credsProvider == null) {
credsProvider = new BasicCredentialsProvider();
context.setCredentialsProvider(credsProvider);
}
credsProvider.setCredentials(
new AuthScope(target),
new UsernamePasswordCredentials(userinfo));
}
}
// 将http请求相关的属性储存到这个请求上下文中
context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
// 处理http请求拦截器
this.httpProcessor.process(request, context);
// MainClientExec发送该请求,注意这里并不会捕捉异常
final CloseableHttpResponse response = this.requestExecutor.execute(route, request,
context, execAware);
try {
// Run response protocol interceptors
context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
// 处理http响应拦截器
this.httpProcessor.process(response, context);
return response;
} catch (final RuntimeException ex) {
response.close();
throw ex;
} catch (final IOException ex) {
response.close();
throw ex;
} catch (final HttpException ex) {
response.close();
throw ex;
}
}
ProtocolExec
是负责实现HTTP规范要求,来看下这个方法的执行流程;
- 解析出请求的uri,比如
http://www.baidu.com/index.html?a=1
,uri是/index.html?a=1
; - 将本次请求所需要的信息储存到请求上下文中,一个http请求对应一个context;
- 处理http请求拦截器,拦截器种类在上面,最主要就是处理http请求的header;
- 执行下一个任务链也就是
MainClientExec
的任务,等待返回请求,注意这里并不会捕捉MainClientExec#exec
的任何异常,通常来说这里只会返回IOExecption
,如果发生异常直接抛给上一个执行器; - 处理http响应的拦截器,默认有两个
拦截器 | 作用 |
---|---|
ResponseProcessCookies | 在HTTP响应中接收到的响应cookie中包含的数据填充当前的CookieStore |
ResponseContentEncoding | 负责处理响应内容编码,通常是自定义的解析,使用方法是在builder中设置setContentDecoderRegistry |
组件
在研究MainClientExec
之前需要先了解一下该执行器遇到的各种组件
SessionInputBufferImpl
似于InputStream类,用于阻塞连接的会话输入缓冲区,这个类在一个内部字节数组中缓冲输入数据,以获得最佳的输入性能。
使用该类时需要和一个InputStream进行绑定后才能从流中读取数据,本质上还是通过SocketInputStream#read
来读取,第一次读取的时候会读取最大长度的数据并缓存起来,最大长度设置是在构造器中指定的,追溯到源头的话是通过ConnectionConfig
设置的,默认大小为8*1024;
SessionInputBufferImpl
提供的最核心的功能就是将数据转换成http协议的格式,比如readLine
可以按照HTTP标准分隔符来区别出HTTP每一行;
SessionOutputBufferImpl
负责输出缓冲区,将HTTP数据写入内部缓存的buffer,直到所有数据写完或者缓冲区满了,就会手动SocketOutputStream#write
将数据写入流中;
HttpMessageWriter
负责HTTP数据的写入,默认实现是DefaultHttpRequestWriter
,内部持有SessionOutputBufferImpl
的HTTP请求写入器;
HttpMessageParser
HTTP消息解析器,默认实现为DefaultHttpResponseParser
,内部持有SessionInputBuffer
,本质上就是解析从流中读取的数据并解析成一个对象代表HTTP响应;
BHttpConnectionBase
这个类充当所有HttpConnection
的基础类,实现并提供客户端和服务器HTTP连接通用的功能,基本上是对Socket
的操作的封装,外部所有对HTTP连接的打开、关闭最终的反应也就是这里的open()
和close()
;
HttpClientConnection
代表了一个客户端的HTTP连接过程,可用于发送请求和接收响应;默认实现为DefaultBHttpClientConnection
,DefaultBHttpClientConnection
是在BHttpConnectionBase
的基础上扩展实现的,除了继承BHttpConnectionBase
的操作外还实现HttpClientConnection
接口,提供了直接发送和获取请求头和请求体的封装,还提供了发送请求、收到响应等钩子函数;
ManagedHttpClientConnection
表示其状态和生命周期由连接管理器管理的托管连接,同时整合了HttpClientConnection
和HttpInetConnection
;
DefaultManagedHttpClientConnection和LoggingManagedHttpClientConnection
ManagedHttpClientConnection
和HttpContext
的默认实现,同时也继承了DefaultBHttpClientConnection
类,该类最重要的功能是提供了一个线程安全map
来缓存HTTP请求上下文,也就是上面代码看到的context#setAttribute
;
LoggingManagedHttpClientConnection
在DefaultManagedHttpClientConnection
的基础上提供了日志功能;
ManagedHttpClientConnection
接口通常最终实现的内部结构如图所示,一个ManagedHttpClientConnection
对象对应着一个HTTP连接;
PoolEntry和CPoolEntry
PoolEntry
是一个包含了一个HTTP连接对象和其路由的数据,同时在构建时提供参数可以设置过期时间,并提供外界获取当前连接的方法;
CPoolEntry
扩展了PoolEntry
,在这基础上增加了对路由是否完成的判断,默认是创建CPoolEntry
时捆绑了一个LoggingManagedHttpClientConnection
对象和HttpRoute
对象;
RouteSpecificPool
RouteSpecificPool
由路由与队列组成一个特定路由的连接池,结构如下
当我们创建一个连接对象时(LoggingManagedHttpClientConnection
),会将其与请求的路由一起组成CPoolEntry
对象,然后放到RouteSpecificPool
池中的租用集合(leased
),代表着该连接被租用了,当连接使用完毕后会将其从租用池中移除;
available
列表是储存的是可重用的连接;
pending
是将因获取连接或者创建连接失败的请求任务缓存到这里,等到有空闲连接或者可创建连接时再取出来执行请求;
ConnFactory和HttpConnectionFactory
ConnFactory
用于阻塞连接池创建的工厂,默认实现是PoolingHttpClientConnectionManager
的内部类InternalConnectionFactory
;
HttpConnectionFactory
是用于创建HttpConnection
的工厂,默认实现是ManagedHttpClientConnectionFactory
;
这两个类的关系如下
InternalConnectionFactory#create
本质上调用的还是ManagedHttpClientConnectionFactory#create
,创建出来的连接是LoggingManagedHttpClientConnection
对象;
ConnPool和ConnPoolControl
ConnPool
接口表示一个共享池连接,可以从其租借并释放回其;
ConnPoolControl
接口来控制连接池的运行时属性,例如最大连接总数或每个路由允许的最大连接数;这两个接口的默认实现类为AbstractConnPool
;
AbstractConnPool
该抽象类提供方法让外部从HTTP连接池中租借指定路由的连接,但是该类自己本身不维护执行线程,而是提供一个Future
接口让调用者自己决定请求连接的时机;
CPool
扩展了AbstractConnPool
抽象类,在构造时增加了连接存活时间属性,当指定了存活时间后,从该池中的创建所有连接的存活时间都是该值;
CPool
中也有租用队列、可用队列和等待队列用来储存请求任务或者连接,但与RouteSpecificPool
不同的是前者保存的是所有路由的连接,而后者只储存特定路由的连接;
HttpClientConnectionOperator
负责执行建立Socket连接操作,默认实现是DefaultHttpClientConnectionOperator
;
里面包含一组ConnectionSocketFactory
注册表、DNS解析器和端口解析器,根据HTTP或者HTTPS来选择PlainConnectionSocketFactory
或者SSLConnectionSocketFactory
来创建Socket对象并连接,同时将Socket和LoggingManagedHttpClientConnection
对象的socket关联起来;
PoolingHttpClientConnectionManager
PoolingHttpClientConnectionManager
是前面提及过HttpClientConnectionManage
的默认实现,承担了最核心的连接池管理操作;内部维护了一个HttpClientConnections
池,并且能够为来自多个执行线程的连接请求提供服务。连接是按请求路由的分隔的,对于已经在池中拥有可用的持久连接的路由请求,将通过从池中租借连接而不是创建一个新的连接。
PoolingHttpClientConnectionManager
在每个路由和总数上维护最大的连接限制。默认情况下,该实现将为每个给定路由创建不超过2个并发连接,并且在池中创建总共不超过20个连接。
总结下来各个组件所处的位置如下