讨论这个主题的起因是源于在学习Apache hc的连接池时,碰见max per route参数,即每路由最大连接数。对这里的“路由”产生了疑惑。
连接池设置这个参数的方法为:
public void setMaxPerRoute(final HttpRoute route, final int max)
从HttpRoute入手,其类图如下。HttpRoute对象是immutable的,包含的数据有目标主机、本地地址、代理链、是否tunnulled、是否layered、是否是安全路由。
RouteTracker和HttpRoute实现相同的接口RouteInfo,而且RouteTracker的字段和HttpRoute几乎相同,但RouteTracker提供了一些修改字段值得方法。仅从RouteTracker的类图来看,第一感觉RouteTracker是HttpRoute的构建器(toRoute是个典型的构建方法)。但是为什么叫RouteTracker而不是RouteBuilder?(问题1)
在继续后文的阅读前,建议先了解HttpClient的请求执行链。可以参考我之前的笔记: Apache HttpComponents学习笔记(二):HttpClient 接口
在HttpClient的请求执行过程中,HttpRoute首先出现在请求执行链的入口处,摘取InternalHttpClient的代码片断如下。在进入请求执行链之前,使用routePlanner来根据目标主机、请求和请求上下文来计算出HttpRoute.
@Override
protected CloseableHttpResponse doExecute(
final HttpHost target,
final HttpRequest request,
final HttpContext context) throws IOException, ClientProtocolException {
...
<strong>final HttpRoute route = determineRoute(target, wrapper, localcontext);</strong>
// 开始调用请求执行链
return this.execChain.execute(<strong>route</strong>, wrapper, localcontext, execAware);
...
}
private HttpRoute determineRoute(
final HttpHost target,
final HttpRequest request,
final HttpContext context) throws HttpException {
HttpHost host = target;
if (host == null) {
host = (HttpHost) request.getParams().getParameter(ClientPNames.DEFAULT_HOST);
}
<strong>return this.routePlanner.determineRoute(host, request, context);</strong>
}
routePlanner是HttpClientBuilder在构建HttpClient时装配的,摘取HttpClientBuilder相关代码片断如下。Apache hc默认提供了三种routePlanner的实现,它们的源码比较简单。Apache hc默认的计算HttpRoute的算法大致为:
- 如果在请求上下文context里能找到代理,直接用来创建HttpRoute对象;
- 如果没有设置代理,并且没有使用系统属性,用DefaultRoutePlanner,计算出的HttpRoute的proxyChain为null;
- 如果设置了代理,用DefaultProxyRoutePlanner,计算出的HttpRoute的proxyChain里只有一个,就是设置的代理;
- 如果没有设置代理,但使用系统数据,用SystemDefaultRoutePlanner,它使用JDK的ProxySelector,从JVM配置和系统配置里获取代理链,再从代理链里寻找第一个HTTP类型的代理,来创建HttpRoute对象。如果没有找到HTTP类型的代理,计算出的HttpRoute的proxyChain为null.
我们可以发现,Apache hc默认的计算HttpRoute的逻辑所产生的HttpRoute对象,其proxyChain里最多只有一个代理。为什么会是这样?HttpRoute的proxyChain字段被设计为List类型,但只含有一个代理。难道Apache hc不支持多代理?(问题2)
// 设置代理
public final HttpClientBuilder setProxy(final HttpHost proxy) {
this.<strong>proxy</strong> = proxy;
return this;
}
// 装配routePlanner
public final HttpClientBuilder setRoutePlanner(final HttpRoutePlanner routePlanner) {
this.<strong>routePlanner </strong>= routePlanner;
return this;
}
// 使用系统属性
public final HttpClientBuilder useSystemProperties() {
<strong>systemProperties</strong> = true;
return this;
}
// 构建HttpClient
public CloseableHttpClient build() {
...
HttpRoutePlanner routePlanner = this.routePlanner;
if (routePlanner == null) {
SchemePortResolver schemePortResolver = this.schemePortResolver;
if (schemePortResolver == null) {
schemePortResolver = DefaultSchemePortResolver.INSTANCE;
}
if (proxy != null) {
routePlanner = new <strong>DefaultProxyRoutePlanner</strong>(<strong>proxy</strong>, schemePortResolver);
} else if (<strong>systemProperties</strong>) {
routePlanner = new <strong>SystemDefaultRoutePlanner</strong>(
schemePortResolver, ProxySelector.getDefault());
} else {
routePlanner = new <strong>DefaultRoutePlanner</strong>(schemePortResolver);
}
}
...
return new InternalHttpClient(
execChain,
connManager,
<strong>routePlanner</strong>,
cookieSpecRegistry,
authSchemeRegistry,
defaultCookieStore,
defaultCredentialsProvider,
defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
closeables != null ? new ArrayList<Closeable>(closeables) : null);
}
HttpRoute在请求执行链入口处创建出来后,传递给请求执行链。顺着执行链走,在链条的最后一环MainClientExec里找到对HttpRoute对象的使用。
现在这段话,只是我的一点体悟,读者可以直接跳过,不影响阅读本文。其实在请求执行链里寻找使用HttpRoute的环节时,作者本人并没有从上至下看链条里各个环节的代码,也没有利用IDE里的Find all references,而是直接跳到了最后一环MainClientExec里。理由有两点:第一,“路由”的概念对应网络OSI模型的网络层。在请求执行链条里,除了最后一环,前面的各环,诸如重试、重导向、HTTP协议处理等,其功能对应OSI模型里较高的层次,只有最后一环的MainClientExec完成的是低层的处理,如建立连接、完成请求和响应的交换等;第二,前文提到,HttpRoute对象是immutable的,那么在链条传递过程中,HttpRoute对象里的字段不会发生变化。我的感悟是,一个合格的软件工程师,不仅要能掌握一两门编程语言,还应该具备一些必要的计算机知识。
MainClientExec使用HttpRoute对象的代码片断较复杂,我用抽取出来并作简化并加上注释,如下。
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
...
// 从connection manager里获取一个链接
final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
final HttpClientConnection managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
...
if (!managedConn.isOpen()) {
// 如果连接没有打开,根据HttpRoute来打开连接
establishRoute(proxyAuthState, managedConn, route, request, context);
}
...
}
void establishRoute(
final AuthState proxyAuthState,
final HttpClientConnection managedConn,
final HttpRoute route,
final HttpRequest request,
final HttpClientContext context) throws HttpException, IOException {
<strong>final RouteTracker tracker = new RouteTracker(route);</strong>
int step;
do {
<strong>final HttpRoute fact = tracker.toRoute();
step = this.routeDirector.nextStep(route, fact);</strong>
switch (step) {
case HttpRouteDirector.CONNECT_TARGET:
// 打开到目标主机的连接
this.connManager.connect(managedConn, route, timeout > 0 ? timeout : 0, context);
<strong>tracker.connectTarget(route.isSecure())</strong>;
break;
case HttpRouteDirector.CONNECT_PROXY:
// 打开到代理机的连接
this.connManager.connect(managedConn, route, timeout > 0 ? timeout : 0, context);
final HttpHost proxy = route.getProxyHost();
<strong>tracker.connectProxy(proxy, false);</strong>
break;
case HttpRouteDirector.TUNNEL_TARGET: {
// 打开到目标主机的Tunnel连接
final boolean secure = createTunnelToTarget(proxyAuthState, managedConn, route, request, context);
this.log.debug("Tunnel to target created.");
<strong>tracker.tunnelTarget(secure);</strong>
} break;
case HttpRouteDirector.TUNNEL_PROXY: {
// 在有多个代理的路由里,打开到下一条代理机的Tunnel连接
final int hop = fact.getHopCount()-1; // the hop to establish
final boolean secure = createTunnelToProxy(route, hop, context);
this.log.debug("Tunnel to proxy created.");
<strong>tracker.tunnelProxy(route.getHopTarget(hop), secure);</strong>
} break;
case HttpRouteDirector.LAYER_PROTOCOL:
// 把连接重新绑定到layered socket
this.connManager.upgrade(managedConn, route, context);
<strong>tracker.layerProtocol(route.isSecure());</strong>
break;
case HttpRouteDirector.UNREACHABLE:
// 路由不可达,抛出异常
throw new HttpException("Unable to establish route: " + "planned = " + route + "; current = " + fact);
case HttpRouteDirector.COMPLETE:
// 路由完成
this.connManager.routeComplete(managedConn, route, context);
break;
default:
// 非法
throw new IllegalStateException("Unknown step indicator " + step + " from RouteDirector.");
}
} while (step > HttpRouteDirector.COMPLETE);
}
establishRoute方法的作用是打开连接,其主体逻辑是个循环。循环的终止条件由每次迭代调用的routeDirector.nextStep的返回结果step来控制:循环之前,先用HttpRoute来创建RouteTracker对象,此时RouteTracker只用HttpRoute的目标主机和本地地址来初始化。进入循环后,每次迭代向都调用RouteTracker的toRoute方法来获取RouteTracker当前跟踪的路由信息,然后routeDirector通过比较期望的路由和当前的路由,来计算得出下一步要进行的操作类型step,根据step,对连接采取相应操作,然后调用RouteTracker对应的方法来更改RouteTracker里的字段值。由此可以回答第一个问题:RouteTracker的命名表明了它的作用是作为一个跟踪器,在打开连接的过程中跟踪记录实际的路由。顺手贴一张RouteTracker的状态图。
routeDirector.nextStep是如何根据期望路由和实际路由计算出下一步的操作的?Apache hc提供了默认的BasicRouteDirector,其源代码比较简单,读者可以自行去看看,如果我有时间,会上一幅图描述。
现在值得关注的是上面代码的55 ~ 61行,routeDirector.nextStep会在下面所示的情况下会得出下一步操作的类型为TUNNEL_TARGET:
期望的路由:Source -> P1 -> P2 -> Target (3 hops),当前实际路由:Source -> P1 -> Target (2 hops)
而前文提到,Apache hc默认的计算HttpRoute的逻辑所产生的HttpRoute对象,其proxyChain里最多只有一个代理,不会出现Source -> P1 -> P2 -> Target (3 hops)的情况,因此这部分代码是不会执行的。那我们手贱看看58行调用的createTunnelToProxy方法里在做什么:
private boolean createTunnelToProxy(
final HttpRoute route,
final int hop,
final HttpClientContext context) throws HttpException {
// Have a look at createTunnelToTarget and replicate the parts
// you need in a custom derived class. If your proxies don't require
// authentication, it is not too hard. But for the stock version of
// HttpClient, we cannot make such simplifying assumptions and would
// have to include proxy authentication code. The HttpComponents team
// is currently not in a position to support rarely used code of this
// complexity. Feel free to submit patches that refactor the code in
// createTunnelToTarget to facilitate re-use for proxy tunnelling.
throw new HttpException("Proxy chains are not supported.");
}
Apache hc明确告诉你不支持。问题2得到回答。
总结一下,Apache hc根据请求对象和用户配置来创建的HttpRoute,用于在请求执行链里传递的路由信息,并在MainClientExec这一环用路由信息来完成连接的打开。但是,目前Apache hc仅支持HttpRoute里最多一个代理,所以在Apache hc里,路由这个概念只关注是否是直接访问目标地址,还是通过设置代理来访问,其结构简化如下:
Source -> P(optional) -> Target
需要澄清的是,这里的目标主机和代理机的概念,与网络实际的物理拓扑是不同的:在网络里,客户端的请求会经过多跳才能到达目的主机,中间会经过各种网关、代理等。Apache hc里的代理更贴近于正常代理的概念,而且是请求传递过程中,HttpClient要明确感知到的第一个正向代理,并对随后是否有代理以及代理的拓扑结构,Apache hc不关心。举个简单的例子,通常在使用HttpClient向目标主机发送请求时,URI所代表的目标主机其实是负载均衡(反向代理),但是在HttpClient看来,这个负载均衡就是Target,而不是代理,此时的HttpClient构建的路由就是直连的。如果我们显示的通过HttpClientBuilder.setProxy来告诉HttpClient一个正向代理的存在,HttpClient所构建的路由就有一个跳转的代理。
留点思考题:routerPlanner不仅在InternalHttpClient里用来构建HttpRoute,还被RetryExec使用,为什么?