很晚了,但是还是决定打开电脑记录下最近一段时间遇到的问题。
事情是这样的,前段时间公司一个同事ZM休婚假,我就接手了他手上的一家比较重要的保险公司(暂时成为RB吧,我们是一家保险销售平台,上海最会保),其实一开始我是拒绝的,因为这家公司实际上的流程不是正常的,因为它是爬取的,而且据说有很多坑(事后发现并不是那样夸张吧)。
说爬取,其实也就是用httpClient模拟浏览器的行为,然后用htmlParse解析xml,然后在模拟提交表单之类。我在程序入口处将要访问的链接log一下,用浏览器去访问这个链接直接能够打开,但是httpClient却经常抛socketTimeoutException,这就奇怪了,难道是cookie有问题?RB服务器没有检测到所需要的cookie?然后这个http connect就一直被idle了?但是我是访问RB的第一个链接啊,明显不需要任何cookie啊,第一个猜想被pass掉;好吧,继续猜想!难道是没有完全模拟浏览器?为此做了如下httpHeader设置:
private void setHttpHeader(HttpMethod method, String referSuffix, boolean isAjax){
method.setRequestHeader("Accept", "text/html, */*; q=0.01");
method.setRequestHeader("Accept-Encoding", "gzip, deflate");
method.setRequestHeader("Accept-Language","zh-CN,zh;q=0.8,en;q=0.6");
method.setRequestHeader("Cache-Control", "no-cache");
method.setRequestHeader("Connection", "Keep-Alive");
method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;");
method.setRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36");
method.setRequestHeader("Host", "www.epicc.com.cn");
method.setRequestHeader("Referer", BASE_URL + referSuffix);
method.setRequestHeader("Origin","http://www.epicc.com.cn");
if (isAjax){
method.setRequestHeader("X-Requested-With", "XMLHttpRequest");
}
}
每次构造get或者post的时候调用此方法来设置头,然而并没有啥用处,依然socketTimeoutException;
然后我就蒙圈了,各种想,然而并没有什么头绪!其实我当时想的是我应该用log4j打印一下httpClient的debug的log的,或许能够找到原因;
另一个同事WL先tcpdump抓了包,ZM和WL工作8年的老程序员,果然还是经验很重要,然后就分析出来了,分析发现正常情况下,http发送前都需要进行tcp握手建立连接,然后再http发送报文,然而使用了连接池后,每次http请求结束后,这些连接会被放回连接池,直到下次被使用,但是我们不知道的是,可能这个时候服务器已经把这个连接断开了,而第二次如果有请求发现连接池中有连接空闲,就未经检查就使用了这个connection,这样无法发送报文,就好抛出socketTimeoutException,这就是为什么有时候会socketTimeout,有时候不会,因为这个connection不知道有没有被服务器shutdown掉,如果关闭了,则异常呗!
如何使用?
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
HttpConnectionManagerParams params = connectionManager.getParams();
params.setConnectionTimeout(30000);
params.setSoTimeout(60000);
params.setDefaultMaxConnectionsPerHost(1000);
params.setMaxTotalConnections(1000);
httpClient = new HttpClient(connectionManager);
当初使用MultiThreadedHttpConnectionManage(关于)是为了复用连接的连接,解决办法就是不用连接池了呗,每个service实例中都存有一个共享的httpClient,这样多次发起http请求也可以公用这个httpClient;
当然最好的解决办法或者说是使用MultiThreadedHttpConnectionManage的错误是由于没有设置正确的参数,HttpConnectionParams中有一个方法:
/**
* Defines whether stale connection check is to be used. Disabling
* stale connection check may result in slight performance improvement
* at the risk of getting an I/O error when executing a request over a
* connection that has been closed at the server side.
* @param value <tt>true</tt> if stale connection check is to be used,
* <tt>false</tt> otherwise.
*/
public void setStaleCheckingEnabled(boolean value) {
setBooleanParameter(STALE_CONNECTION_CHECK, value);
}
设置true后每次使用连接池中的链接会先检查下链接是否可用;虽然可能会消耗15-30ms,但是这个应该比tcp握手来的快。
其实任然还有很多优化的地方,如失败重试等等;
另外整理了一下HTTP Client MultiThreadedHttpConnectionManager线程安全连接管理类源码解析,
请看: