HttpClient 源码解读

原创 2015年07月08日 15:38:03

前面写了两篇HttpClient和HtmlUnit和文章,然后就很久没有更新了,真的是有事,现在闲下来,把N久没动的博客也更新一下吧,因为上次的HttpClient方面讲的比较少嘛,这篇文章也正好补一下。


写博客之前也看了下网上其他人写的文章,也有很多解读HttpClient源码的,但是都是版本HttpClient4.1之前的,这里我先把我看到的一篇比较好的解读HttpClient源码的文章和大家一起分享一下:http://www.educity.cn/wenda/147389.html。


今天主要是讲解HttpClient4.3版本的源码,4.3改动还是有一点的,主要是对于“链”的概念更加浓重了。


一、HttpClient 执行步骤


先来看一下,HttpClient4.0大致的时序图。


(看的清楚么,我画的时候已经很注意字体大小以及截图的时候的清晰度了,如果实在看不清只能自己纯看文字了。)

你也可以参考下我上面分享的那篇文章里面的时序图,对比下HttpClient4.3版本做了哪些改变。


已请求一个正常的POST流为例,我们来看下HttpClient是如何执行的:


1、首先你需要建立一个HttpClient,设置这个HttpClient的一些基本属性,这其中包括HTTPS处理策略,连接超时时间,Cookie策略,连接池等等,这些都可以通过搜索HttpClient4.0+ 的教程学习。


2、执行HttpClient的execute方法,这个时候你会发现实际走向是 HttpClient -> CloseableHttpClient -> InternalHttpClient ,然后在doExecute()方法中进行了一些基本判断,初始化值之后,就进入了ClientExecChain(MainClientExec)的execute()方法。


3、然后我们可以简单看下MainClientExec的execute方法。

// @CaiBo
	public CloseableHttpResponse execute(final HttpRoute route, final HttpRequestWrapper request, final HttpClientContext context,
			final HttpExecutionAware execAware) throws IOException, HttpException {
		// 判断是否为空(Args这个工具类将经常出现)
		Args.notNull(route, "HTTP route");
		Args.notNull(request, "HTTP request");
		Args.notNull(context, "HTTP context");

		// 看到Auth相关的直接跳过,这和认证(HTTPS等)相关,我们暂时不理会,直接往下看
		AuthState targetAuthState = context.getTargetAuthState();
		if (targetAuthState == null) {
			targetAuthState = new AuthState();
			context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState);
		}
		AuthState proxyAuthState = context.getProxyAuthState();
		if (proxyAuthState == null) {
			proxyAuthState = new AuthState();
			context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState);
		}

		// 如果是环式实体,则将其请求进行加强(返回代理)
		// 按我说,意思就是如果这个HTTP请求实体是有entity的,那么这个entity写完之后,执行关闭,这些是通过代理模式实现的。
		if (request instanceof HttpEntityEnclosingRequest) {
			Proxies.enhanceEntity((HttpEntityEnclosingRequest) request);
		}

		/*
		 * 这是HttpClient中一个标识,它可以用来保证某些连接一段时间内只为某个用户所使用,默认为null
		 * 连接池的连接都有state,其实就是这里的userToken(准确来说不全是),所有的连接的state默认都是null
		 * 举个例子,当向连接池请求userToken为“zstu”的连接时,它会返回一个连接,并将连接状态标记为zstu,
		 * 这样同一个线程下次再来请求连接的时候,就会优先拿到这个连接
		 */
		Object userToken = context.getUserToken();

		// 向连接池请求一个连接,这里用到了JUC编程的相关知识,返回的是一个future
		final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);

		/*
		 * execAware是什么呢,其实就是这次HTTP请求的本体,HttpRequest。
		 * 那你一定好奇,HttpRequestWrapper是什么呢?看名字像是HttpRequest的包装,为什么要搞两个呢?
		 * 是这样:HttpRequestWrapper是HttpRequest的一份拷贝,execAware则是真正的请求。
		 * 这是由于HttpRequestWrapper在后面的操作中都会发生一些变化, 而HttpClient又不想这些变化被使用者知道或者捕获,所以拷贝了一份
		 * 然而使用者可以随时取消网络执行或者释放连接等等,所以又不得不用到原生的对象,以便响应使用者的请求。
		 */
		if (execAware != null) {
			if (execAware.isAborted()) {
				connRequest.cancel();
				throw new RequestAbortedException("Request aborted");
			} else {
				execAware.setCancellable(connRequest);
			}
		}

		// 就是你前面建立HttpClient时可以设置的属性之一,有长连接时间,超时时间等可以设置。
		final RequestConfig config = context.getRequestConfig();

		// 如果你使用的是同步线程池(PoolingHttpClientConnectionManager)的话,这里真正跑的是LoggingManagedHttpClientConnection
		// 当然说的是大部分情况下是这个。
		final HttpClientConnection managedConn;
		try {
			final int timeout = config.getConnectionRequestTimeout();
			// 这里通过future特性来获取值
			managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
		} catch (final InterruptedException interrupted) {
			Thread.currentThread().interrupt();
			throw new RequestAbortedException("Request aborted", interrupted);
		} catch (final ExecutionException ex) {
			Throwable cause = ex.getCause();
			if (cause == null) {
				cause = ex;
			}
			throw new RequestAbortedException("Request execution failed", cause);
		}
		// 这个地方将这个连接设置到上下文中,暴露连接
		// 这样的话,使用者可以方便的在调用处获得本次执行所使用的连接
		// 只需要自己创建一个context,然后让HttpClient使用你的上下文即可
		// 在执行过程中,HttpClient会将诸多和本次执行相关的引用都设置到你的上下文中,包括执行用的连接
		context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);

		if (config.isStaleConnectionCheckEnabled()) {
			// validate connection
			if (managedConn.isOpen()) {
				this.log.debug("Stale connection check");
				// 这个函数比较重要,如果你想看的话,它的实现在BHttpConnectionBase类中
				// 它的作用是检查连接是否有效(但是它也不能完全的保证连接有效),可以查看这里,我就不复制了。
				// http://hc.apache.org/httpcomponents-client-4.2.x/tutorial/html/connmgmt.html#d5e652
				if (managedConn.isStale()) {
					this.log.debug("Stale connection detected");
					managedConn.close();
				}
			}
		}
		// 新建一个连接持有类,主要就是操作连接的N多代码都是重复的,而且操作连接比较频繁
		// 所以将对连接的操作都封装到这个类中
		final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
		try {
			if (execAware != null) {
				// 大家平时用完HttpClient.execute(post)之后,一般都会调用类似post.releaseConnection()方法吧
				// 这个方法之所以有效,就是在这里设置了connHolder,你调用的最终方法其实是ConnectionHolder提供的。
				execAware.setCancellable(connHolder);
			}

			HttpResponse response;
			// 真的开始执行了,可以看到,这里是个循环,有可能会执行N次
			for (int execCount = 1;; execCount++) {

				// 如果发现已经执行过了并且这个请求是不可重复的,那么就报错了。
				// 那么什么叫请求不可重复呢?这主要是和流有关,大家知道InputStream有很多种,有些流可以反复读(PushbackInputStream),但很多流只能读一次
				// 类似的HttpEntity也存在这样的情况,平常的StringEntity(就是纯字符串)是可以随便读多少次的,因为就是个字符串嘛,读了不会消失。
				// 然后上传文件等操作就没这么简单了,如果你是使用InputStreamEntity,那么就不能重复请求了。
				// 所有建议大家在上传文件时使用FileEntity,如果想做到通用(所有流)则使用ByteArrayEntity。
				if (execCount > 1 && !Proxies.isRepeatable(request)) {
					throw new NonRepeatableRequestException("Cannot retry request " + "with a non-repeatable request entity.");
				}

				if (execAware != null && execAware.isAborted()) {
					throw new RequestAbortedException("Request aborted");
				}

				// 这个连接如果并没有打开,则要建立网络连接(sockt连接)。
				// 这里你可能好奇,这个连接明明是连接池给我的,为什么还会存在没有打开的情况呢?
				// 这个就涉及到连接池是怎么给你连接的,我们以后会分析。
				// 这里只要知道,连接池给的连接有可能是没建立“连接”的连接。
				if (!managedConn.isOpen()) {
					this.log.debug("Opening connection " + route);
					try {
						establishRoute(proxyAuthState, managedConn, route, request, context);
					} catch (final TunnelRefusedException ex) {
						if (this.log.isDebugEnabled()) {
							this.log.debug(ex.getMessage());
						}
						response = ex.getResponse();
						break;
					}
				}
				final int timeout = config.getSocketTimeout();
				if (timeout >= 0) {
					managedConn.setSocketTimeout(timeout);
				}
				// 是不是总是看到这句话,所以我上面说我们可以“随时”取消请求嘛。
				if (execAware != null && execAware.isAborted()) {
					throw new RequestAbortedException("Request aborted");
				}

				if (this.log.isDebugEnabled()) {
					this.log.debug("Executing request " + request.getRequestLine());
				}

				// 这下面两个也是认证相关的,没什么用,暂时跳过。
				if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) {
					if (this.log.isDebugEnabled()) {
						this.log.debug("Target auth state: " + targetAuthState.getState());
					}
					this.authenticator.generateAuthResponse(request, targetAuthState, context);
				}
				if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) {
					if (this.log.isDebugEnabled()) {
						this.log.debug("Proxy auth state: " + proxyAuthState.getState());
					}
					this.authenticator.generateAuthResponse(request, proxyAuthState, context);
				}
				// 调用执行,其实这个requestExecutor没干太多事,模拟一个HTTP请求该有的材料都有了。
				// 这个类只不过是拿到这些材料,然后将他们组装组装,用起来,并返回一个结果
				// 说白了就是个加工工厂
				response = requestExecutor.execute(request, managedConn, context);

				// 执行完了,判断这个连接是不是要保持长连接的
				// 主要是通过返回的消息头判断
				// 如果是需要保持长连接的,则将其标记为可重用
				// The connection is in or can be brought to a re-usable state.
				if (reuseStrategy.keepAlive(response, context)) {
					// Set the idle duration of this connection
					final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
					if (this.log.isDebugEnabled()) {
						final String s;
						if (duration > 0) {
							s = "for " + duration + " " + TimeUnit.MILLISECONDS;
						} else {
							s = "indefinitely";
						}
						this.log.debug("Connection can be kept alive " + s);
					}
					connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
					connHolder.markReusable();
				} else {
					connHolder.markNonReusable();
				}

				// 认证相关,跳过
				if (needAuthentication(targetAuthState, proxyAuthState, route, response, context)) {
					// Make sure the response body is fully consumed, if present
					final HttpEntity entity = response.getEntity();
					if (connHolder.isReusable()) {
						EntityUtils.consume(entity);
					} else {
						managedConn.close();
						if (proxyAuthState.getState() == AuthProtocolState.SUCCESS && proxyAuthState.getAuthScheme() != null
								&& proxyAuthState.getAuthScheme().isConnectionBased()) {
							this.log.debug("Resetting proxy auth state");
							proxyAuthState.reset();
						}
						if (targetAuthState.getState() == AuthProtocolState.SUCCESS && targetAuthState.getAuthScheme() != null
								&& targetAuthState.getAuthScheme().isConnectionBased()) {
							this.log.debug("Resetting target auth state");
							targetAuthState.reset();
						}
					}
					// discard previous auth headers
					final HttpRequest original = request.getOriginal();
					if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) {
						request.removeHeaders(AUTH.WWW_AUTH_RESP);
					}
					if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) {
						request.removeHeaders(AUTH.PROXY_AUTH_RESP);
					}
				} else {
					break;
				}
			}

			if (userToken == null) {
				userToken = userTokenHandler.getUserToken(context);
				context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
			}
			if (userToken != null) {
				connHolder.setState(userToken);
			}

			// check for entity, release connection if possible
			final HttpEntity entity = response.getEntity();
			// 如果返回响应是流则释放掉这个连接,其他则要abort连接
			// 这里又使用到了代理模式,这个在网络编程中特别常见。
			// 这里代理的主要作用就是:如果你读取完了响应,那么这个响应会关闭
			if (entity == null || !entity.isStreaming()) {
				// connection not needed and (assumed to be) in re-usable state
				connHolder.releaseConnection();
				return Proxies.enhanceResponse(response, null);
			} else {
				return Proxies.enhanceResponse(response, connHolder);
			}
			// 任何异常发生,都要保证连接被关闭,不然会滞留在服务器上,可能会引起服务器端口被占用完
		} catch (final ConnectionShutdownException ex) {
			final InterruptedIOException ioex = new InterruptedIOException("Connection has been shut down");
			ioex.initCause(ex);
			throw ioex;
		} catch (final HttpException ex) {
			connHolder.abortConnection();
			throw ex;
		} catch (final IOException ex) {
			connHolder.abortConnection();
			throw ex;
		} catch (final RuntimeException ex) {
			connHolder.abortConnection();
			throw ex;
		}
	}

*我把注释都写在代码里了,大家看起来方便一点,可能讲的不是特别的详细,后面还会一一补充,同时也鉴于第一张时序图的效果(几乎看不清),也就不画图了,都用文字描述吧。

4、到这里基本上就完了,本来还想把请求的封装,响应的返回,连接的处理分开说的,结果看看这个代码里面基本都包含了,那也就不啰嗦了。这个MainClientExec#execute是HttpClient执行HTTP请求的主要方法,以它为核心,扩散着去看整个HttpClient,应该就能看个大概了。



---------先把文章发一下,以后再来更新。

看了这一章,你应该对HttpClient整个执行的过程有一定的了解,接下来具体讲讲经过的每一个类都干了哪些事情,以及它们是如何做这些事情的,用了哪些技巧和特性。






        



HttpClient源码解析系列:第二篇:极简版实现

从MinimalHttpClient从名字可以看出,是一个极简可以用的版本,是核心设计的原初模型。所以我们就从最精简的开始分析。...

HttpClient源码解析系列:第一篇:总览

HttpClient源码解析系列:第一篇:总览。 最最核心的HttpClient接口。

httpclient源码分析-如何重用连接

本文介绍了关于HttpClient源码中,如何重用连接的原理分析。

[httpcomments-client-4.5.2]--源码分析(HTTP request)

All HTTP requests have a request line consisting a method name, a request URI and an HTTP protocol v...

[httpcomments-client-4.5.2]--源码分析(Get请求)

package org.apache.http.examples.client; //这个是apache最新版本的jar import org.apache.http.client.methods.C...

httpcore和httpclient的源码一点点(1)

因为需要重写自己github上以前的项目(“eat my dog food”),进行一次彻底的技术上的”寻根究底”. 项目中使用了爬虫获取数据,后台接入了多个爬虫的数据源,这前写这些爬虫的时候,有大...

HttpClient 4.3连接池参数配置及源码解读

目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口。最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB->服务端处...
  • umke888
  • umke888
  • 2017年02月05日 16:43
  • 1745

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

修改httpclient-4.3.3源码,并重新打包

一、背景介绍: 在跟踪java后台采用rest_template发起httpclient调用的时候,发现有一个ClientProtocolException,以下为异常信息堆栈: 根据以上堆栈信息...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:HttpClient 源码解读
举报原因:
原因补充:

(最多只允许输入30个字)