commons-httpclient 3.x如何按照host单独配置连接数和超时参数


以下内容是个人工作中对commons-httpclient分析的小结。

jakarta commons-httpclient是常用的HTTP Client实现,基于HTTP的协议比如SOAP的一些实现比如XFire也有使用commons-httpclient。作为一个常用的类库,学习如何正确和高效地使用是非常有必要的。

首先,按照官方网站的建议:为了提高性能,建议使用单个httpclient实例。(个人认为开启多个Http Client就像开启多个浏览器实例一样……)

第二,并发情况下使用MultiThreadedHttpConnectionManager,这条不难理解。

接下来是参数配置。官方网站提供的配置文档 中提供了很多参数,这里给出连接数和超时配置(大部分都是配置在HttpConnectionManager级别的:

MultiThreadedHttpConnectionManager manager = 
    new MultiThreadedHttpConnectionManager();
// 默认单个host最大连接数
manager.getParams().setDefaultMaxConnectionsPerHost(10);
// 最大总连接数
manager.getParams().setMaxTotalConnections(1000);
// 默认连接超时时间
manager.getParams().setConnectionTimeout(3000);
// 默认读取超时时间
manager.getParams().setSoTimeout(2000);

额外还有一个超时时间,是从HttpConnectionManager获取连接的超时时间

HttpClient client = new HttpClient(manager);
client.getParams().setConnectionManagerTimeout(2000);

到这里你可能会注意到commons-httpclient的配置似乎没有单独针对主机的?一般来说,针对主机单独配置超时和连接数不是什么非常不合理的需求。个人的回答是,看下HttpConnectionManager的接口定义:

HttpConnection getConnection(HostConfiguration hostConfiguration);
HttpConnection getConnectionWithTimeout(HostConfiguration hostConfiguration, long timeout)

可以看到,HttpConnectionManager提供了针对host的单独的从连接池取出connection的超时时间。这里有个新对象HostConfiguration。HostConfiguration是什么?顾名思义就是主机配置。你可能会问,我们平时都没遇到HostConfiguration,commons-httpclient是如何处理的呢?答案是HttpClient实例中默认有一个HostConfiguration,用HostConfiguration里面的常量来讲就是ANY_HOST_CONFIGURATION。

只是连接池超时时间还不够。刚才的MultiThreadHttpConnectionManager其实提供了我们需要的针对host的连接数设置:

MultiThreadHttpConnectionManager#getParams()返回的是HttpConnectionManagerParams,这个类中存在如下方法:

setMaxConnectionsPerHost(HostConfiguration, int) // 第二个参数就是这个HostConfiguration的最大连接数

很遗憾,commons-httpclient 3.x的API中针对HostConfiguration只能配置到这里,虽然MaxTotalConnections对单host无意义可以忽略,但是针对host的ConnectionTimeout和SoTimeout没法单独设置。如果你确实要设置的话,一种选择是使用4.x版本的API,第二种是hack 3.x。这里介绍第二种方案:

如果你仔细看官方配置文档,你会发现HttpMethod层有一个叫做http.socket.timeout,这个参数和connection manager级别的参数含义是一样的,而且必须应用在connection上,那么commons-httpclient 3.x是怎么把这个参数传递过去的呢?

开源的好处是你可以了解内部实现过程。通过对HttpClient的源代码分析,个人了解到commons-httpclient中有一个包可见的类,叫做HttpMethodDirector,而它有一个方法applyConnectionParams,这个方法完成了从method到connection的配置继承。

那么是否可以通过类似方法继承ConnectionTimeout呢?个人认为比较困难,因为commons-httpclient设计上似乎并不容易扩展,而且HttpMethodDirector是包可见更加增加了难。当然个人还是尝试了下。在说明个人的修改方案之前,先回到大家经常接触的HttpClient上。

HttpClient是大家用commons-httpclient最直接接触的类,但是不知道有多少人了解过HttpClient重载的executeMethod方法。

public int executeMethod(HttpMethod method)
public int executeMethod(final HostConfiguration hostConfiguration, final HttpMethod method)
public int executeMethod(HostConfiguration hostconfig, final HttpMethod method, final HttpState state)

大家最常用的应该是第一个,而且大家应该也知道重载方法一般都会调用最后一个参数最多的方法,事实上前两个最终调用的就是第三个方法。

大家是否看到了HostConfiguration?是的,这个就是最终调用HttpConnectionManager的那个HostConfiguration。当然你传入的有可能是null,这时HttpClient就会用默认的HostConfiguration。

顺便说一句,第三个HttpState是什么?从类注释上来看,是用来保存cookie和认证信息的。和HostConfiguration一样,如果没传的话,会用HttpClient默认的HttpState。我记得之前有听过cookie共享引起的问题,解决方法好像是新建HttpClient?其实按照API,只要新建HttpState理论上就不会导致cookie共享了。

言归正传,如果想要做针对HostConfiguration的超时设置,就要考虑修改HttpMethodDirector。而这个HttpMethodDirector就是HttpClient第三个executeMethod实际调用的类。至此,连接管理上的调用串起来了。改造方案也是基于这个调用路径:

核心内容是继承HttpClient写一个HackedHttpClient,HackedHttpClient调用HackedHttpMethodDirector。由于HttpMethodDirector是包可见,所以HackedHttpMethodDirector所在包必须和HttpMethodDirector一样。

接下来个人尝试修改applyConnectionParams,但是没有成功。这里面实际上有点调用先后关系的问题,HttpMethodDirector会先打开socket再设置SoTimeout,但是ConnectionTimeout需要在打开socket之前设置,所以必须在第一次打开socket之前从HostConfiguration那边获取ConnectionTimeout并成功设置的。实际核心代码如下:

// in HackedHttpMethodDirector
private void executeWithRetry(final HttpMethod method) throws IOException,
      HttpException {
 
    // hacking start
    Object connectTimeout = hostConfiguration.getParams().getParameter(HttpConnectionParams.CONNECTION_TIMEOUT);
    if (connectTimeout != null) conn.getParams().setConnectionTimeout((Integer) connectTimeout);
    
    // ….
}

下面是如何使用,这其实也是按照文档和源代码得到的可能是比较合适的使用方式。

MultiThreadedHttpConnectionManager manager = new MultiThreadedHttpConnectionManager();
manager.getParams().setDefaultMaxConnectionsPerHost(100); // 默认最大连接数
manager.getParams().setMaxTotalConnections(1000); // 默认最大总连接数
manager.getParams().setConnectionTimeout(3000); // 默认连接超时
manager.getParams().setSoTimeout(2000); // 默认读取超时
manager.getParams().setMaxConnectionsPerHost(createHostConfig("http://xnnyygn.in"), 17); // 17为单个host最大连接数
manager.getParams().setMaxConnectionsPerHost(createHostConfig("http://www.google.com/search"), 20);
manager.getParams().setMaxConnectionsPerHost(createHostConfig("https://www.alipay.com"), 20);

createHostConfig代码如下:

HostConfiguration config = new HostConfiguration();
config.setHost(new URI(uri, true));
return config;

在这里请重新看下MultiThreadedHttpConnectionManager的配置,第一个参数DefaultMaxConnectionsPerHost可以认为就是共享的连接数。第二个参数是连接总数,在有多个Host的情况这个参数的意义体现出来了。第三和第四个是默认的超时配置,可以认为是用于共享的配置。

可以看到这里没有针对Host的超时时间,原因是超时时间并不是初期设置的,而且是executeMethod连接时设置的。连接代码:

HackedHttpClient client = new HackedHttpClient(createHttpConnectionManager());
GetMethod get = new GetMethod();
// 2002为连接超时,1502为读取超时
int statusCode = client.executeMethod(createHostConfig("http://xnnyygn.in", 2002, 1502), get, new HttpState());
System.out.println(statusCode);
System.out.println(get.getResponseBodyAsString());

createHostConfig重载方法实现如下:

HostConfiguration config = new HostConfiguration();
config.setHost(new URI(uri, true));
config.getParams().setIntParameter(CONNECTION_TIMEOUT, connectionTimeout);
config.getParams().setIntParameter(SO_TIMEOUT, readTimeout);
return config;

代码中的2002和1502分别是连接超时时间和读取超时时间。这里可能有一个疑问,HostConfiguration的比较问题,实际代码中HostConfiguration不比较配置(见equals方法),只比较地址、代理地址和本地地址(后两个可以不管)。

至此,hack 3.x实现针对host的连接数和超时配置完成。源代码在这里