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

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

最近公司服务器后台程序,在访问第三方数据接口的时候,出现了占用连接数过多,导致本地端口占用过多以及超过Linux系统单进程的打开文件限制数。
公司服务器是利用common-httpclient工具访问第三方服务器,代码结构类似如下(具体程序公司机密,以相似结构的程序代替):
HttpClient client = new HttpClient();
HttpMethod method = new GetMethod("http://www.apache.org");
try {
  client.executeMethod(method);
  byte[] responseBody = method.getResponseBody();
  String returnData = new String(responseBody,”utf-8”);
  System.out.println(returnData);
} catch (HttpException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}finally{
  method.releaseConnection();
}
针对上述代码,公司程序要同时处理很多请求,每次请求都会执行上述的代码,导致问题如下:
1. 在Linux服务器上,利用命令netstat -pnt |grep :80 查看连接数,发现有大量TIME_WAIT的连接存在。
2. 后来发现第一个问题的原因,是由于连接没有释放,所以采用method.releaseConnection();每次请求完,将连接释放掉。但是,此时又出现一个问题,就是在访问高峰 期,出现大量的CLOSE_WAIT的连接。虽然这个连接可以释放,但是释放周期还是相对较长,不能满足短时间大量并发访问的需求。
3. 还有一个问题,就是当并发数超过一定数量后,程序再次发送http请求是,会出现java.net.SocketException: Too many open files 的异常,导致请求失败。


我们已经知道第一个问题的原因,是因为在每次请求之后,没有主动释放该次请求所致。但是原理如何,当时并未深究,直到最近服务器频繁出现第二个问题,所以不得不放下工作,深入源码跟踪一下,究竟是什么原因导致的问题。
首先,根据第一个问题的解决办法,找到httpclient的GetMethod类,查看其releaseConnection()的释放连接原理,源码部分如下:
// 该方法在基类HttpMethodBase中,由GetMethod继承过来使用
public void releaseConnection() {
        try {
            if (this.responseStream != null) {
                try {
                    // 只关闭响应流,无关连接
                    this.responseStream.close();
                } catch (IOException ignore) {
                }
            }
        } finally {
// 关闭连接的方法
            ensureConnectionRelease();
        }
    }
由ensureConnectionRelease()一路追踪,最终在httpclient的默认连接管理器中找到真正关闭连接的逻辑代码:
// @SimpleHttpConnectionManager
// 该方法默认情况下,并没有主动关闭连接,而只是清空了响应流,以便复用
public void releaseConnection(HttpConnection conn) {
        if (conn != httpConnection) {
            throw new IllegalStateException("Unexpected release of an unknown connection.");
        }

// 这个判断才是关键所在:
// 重点是alwaysClose这个参数,在旧版的common-httpclient中,并没有设置这个参数的地方
// 在新版的Apache的httpclient中,才可以在创建HttpClient对象时,通过连接管理器指定该参数的值
// alwaysClose这个参数表明,如果设置这个参数为true,那么就会每次都关闭连接,
// 如果没有设置,默认为false,那么就不关闭连接,留着复用
        if (this.alwaysClose) {
// 这个才是真正关闭连接啊,尼玛不让随便执行!
// 看到最后才知道其实大有深意
            httpConnection.close();
        } else {
// 看看下面的原注释,就明白了,被坑了!
            // make sure the connection is reuseable
            finishLastResponse(httpConnection);
        }
        
        inUse = false;

// 这一点也尤为重要,牵涉到一个彻底关闭连接的方式 
// 为了不打断思路,下面再详细说
        idleStartTime = System.currentTimeMillis();
    }
看过上面的代码才恍然大悟,原来我们一直追求的出发点有问题(问题的详细情况,放在最后说)。我们公司服务器是单台服务器频繁访问第三方服务器,所以按需求来说,应该保持长连接(在http里面,就是实现连接的复用,并非真正意义上的长连接)才好,而不应该追求每次都把连接关闭掉,因为每次开启和释放http连接都很消耗时间和系统资源。鉴于这一点,下面我来说说上述代码注释里提到的idleStartTime = System.currentTimeMillis();问题,为什么要加这一句,为什么这一句可以解决关闭连接的问题,权当是插播吧:
通过对httpclient的研究发现,默认情况下idleStartTime= Long.MAX_VALUE,而源程序判断连接空闲时间的标准是:
// @SimpleHttpConnectionManager
// 在连接空闲一定时间后,关闭掉连接
// 参数是用户设定的允许空闲时间
public void closeIdleConnections(long idleTimeout) {
// 这段逻辑相信你懂得
        long maxIdleTime = System.currentTimeMillis() - idleTimeout;
// 试想,如果把idleStartTime的值设置为Long.MAX_VALUE,这个连接还能关掉吗?
// 所以,读到这里应该知道,httpclient默认就是拼命想保持长连接的
// 除非用户不想保持,自己设置关闭
        if (idleStartTime <= maxIdleTime) {
            httpConnection.close();
        }
    }
我在程序里悲剧的发现,没有源程序主动调用closeIdleConnections()的地方,所以这个方法估计是给我们自己用的。所以,解决关闭连接问题的一个方法就是
先调用method.releaseConnection();
再调用client.getHttpConnectionManager().closeIdleConnections(0);
其他还有几个关闭连接的方法,会在最后列举出来,此处重点解说原理。
下面接着说保持http长连接的问题(我一直想达到的目标)。为了叙述具备一些条理性,还是从开头的程序讲起吧。
1. HttpClient client = new HttpClient();
这句话是为了创建一个httpclient的整体对象,之后的一切操作其实都是作为这个对象的参数或者执行对象,在这个对象的控制范围内执行。httpclient实例化的过程如下:
public HttpClient() {
this(new HttpClientParams());
}
// 上面的构造函数调用下面的构造函数
public HttpClient(HttpClientParams params) {
super();
if (params == null) {
throw new IllegalArgumentException("Params may not be null");  
}
this.params = params;
this.httpConnectionManager = null;
Class clazz = params.getConnectionManagerClass();
if (clazz != null) {
try {
this.httpConnectionManager = (HttpConnectionManager) clazz.newInstance();
} catch (Exception e) {
LOG.warn("Error instantiating connection manager class, defaulting to"
+ " SimpleHttpConnectionManager", 
e);
}
}
if (this.httpConnectionManager == null) {
// 默认创建一个SimpleHttpConnectionManager管理器
// 这个管理器是一个简单连接池,默认只维护一个连接
this.httpConnectionManager = new SimpleHttpConnectionManager();
}
if (this.httpConnectionManager != null) {
this.httpConnectionManager.getParams().setDefaults(this.params);
}
}


2. client.executeMethod(method);
上述语句具体执行连接方法,我们跟踪一下,看看这个方法里面,到底发生了什么:
// 部分代码
// 实例化方法管理者,由方法管理者去执行方法
HttpMethodDirector methodDirector = new HttpMethodDirector(
getHttpConnectionManager(),
hostconfig,
this.params,
(state == null ? getState() : state)); 
methodDirector.executeMethod(method);
return method.getStatusCode();
关于HttpMethodDirector,源码注释是这样说的:Handles the process of executing a method including authentication, redirection and retries.
进入methodDirector.executeMethod(method)查看,部分重点代码如下:
第一点,根据请求的目的配置,选择是否要重用上次的连接
// 1
// 重用连接,如果本次请求的目的服务器同上次请求的目的服务器不一致
// 则释放上个连接,以便重新创建
if (this.conn != null && !hostConfiguration.hostEquals(this.conn)) {
this.conn.setLocked(false);
// 释放原先的连接
this.conn.releaseConnection();
this.conn = null;
}
在这里,提到了释放连接的方法:releaseConnection()。跟踪这个方法,层层进入,最后进入SimpleHttpConnectionManager#releaseConnection(),这个方法的源码在前面已经分析过,翻到前面看一下就会发现,系统默认的alwaysClose为false,也就是默认这个连接不被释放,而是准备重用。 那么我有一个疑问没弄明白:在上面第一点的代码中,已经明确表示当前连接已经不需要了,需要再创建一个全新的连接了,但是这里为什么不把当前连接完全关闭,而是要保留下来?保留下来的这个连接已经不再使用,会在什么时候被关闭掉?当这个连接还没有释放,就置为null,那么这个连接会被Java虚拟机垃圾回收吗?最重要的一个问题,如果这个连接的目的配置都没有变,但是连接超时被关掉了,那么系统如何知道该链接已经不可用,如果不可用了怎么解决?啊,痛苦!

痛苦完,接着走!
第二点,如果上个连接无法重用,那么重新创建连接:


// 2
// 如果连接已经改变,无法复用,则获取新连接,通过连接管理器获取
//(此处我们使用的默认连接管理器SimpleHttpConnectManager)
if (this.conn == null) {
// 获取新连接,并将连接保存在该类中
this.conn = connectionManager.getConnectionWithTimeout(hostConfiguration,
  this.params.getConnectionManagerTimeout());
}
进入获取连接的方法查看:

// 部分代码
// 这个方法的最后一个参数timeout,没有看见被使用的地方,默认为0
public HttpConnection getConnectionWithTimeout(
HostConfiguration hostConfiguration, long timeout) {
if (httpConnection == null) {
httpConnection = new HttpConnection(hostConfiguration);
httpConnection.setHttpConnectionManager(this);
httpConnection.getParams().setDefaults(this.params);

}
上面没有什么可说的,接着看:
第三点,实际执行请求的方法

// 3
// 实际执行请求的方法
executeWithRetry(method);
跟踪方法查看

// 2.0
// 这段逻辑负责处理连接请求过程中,出现的问题
// 如果这个请求不成功,那么就一直不停发送请求
// 直到这个请求被成功执行或者 抛出了无法继续执行的异常,这时候才会停止循环


while (true) {
execCount++;
try {

// 2.1
// 这个方法是重要方法,负责检查当前连接是否有效
// 如果当前连接没有被打开,或者已经被第三方服务器关闭
// 那么,就关闭当前连接(关闭该链接中的socket对象)
if (this.conn.getParams().isStaleCheckingEnabled()) {
this.conn.closeIfStale();
}
// 2.3
// 如果当前连接还没有打开,那么当前代码负责打开,
// 这样就与上一部分代码形成呼应
if (!this.conn.isOpen()) {
// 2.3.0
// 在这个open方法里面,重新创建了当前连接的底层socket对象
this.conn.open(); 
}

applyConnectionParams(method); 
method.execute(state, this.conn);
// 如果该方法执行没有异常
// 表示请求成功,那么就中断循环
break;
} catch (HttpException e) {
// 如果抛出跟http协议有关的异常,证明该连接请求非法
// 所以该请求不应该继续被执行,抛出异常中断循环
throw e;
} catch (IOException e) {
// 这个里面包含一大段代码
// 代码的作用就是,如果本次请求不成功,要重试执行
// 如果判断要重试,就类似于ontinue
// 如果判断不能继续重试,就抛出异常
LOG.info("Retrying request");
}
}


我们来分析一下上面的代码:
注释2.0处,明确表述,我这个请求第三方服务器的方法,会用while(true)一直执行,那么什么时候退出呢,如果请求成功了,就退出;如果请求不成功,那么就视情况而定,如果情况很恶劣,比如说你发的请求根本就不是http协议的,那么对不起,程序不会继续循环了,而是抛出异常退出;如果情况不是很恶劣,那么久多次请求,直到成功为止。
注释2.1处,是检查当前的连接是否还有效,是不是已经被第三方服务器关闭了。如果连接还有效,那么就跳过;如果连接已经无效,那么就关闭掉,这个关闭,并不是释放掉这个连接对象,而是将连接底层用的socket对象置为null,并且将连接的状态isOpen置为false。这样做的好处是什么呢,就是这个连接依然可以用,只是当真正发送请求的时候,底层的socket对象再重新创建一个出来,这样就不用换连接对象,但其实底层传输数据的socket对象已经变了。那么,如果执行了这段代码,就证明该连接对象的底层socket对象已经为null了,那么程序又是在什么时候把这个对象重新创建出来的?我们接着看注释2.2处的代码。
注释2.2处,这个地方判断当前的连接是否处于open状态,如果没有处于打开状态,则重新打开连接。就是在这个conn.open()方法里面,程序重新创建了底层socket对象:
if (this.socket == null) {
this.socket = socketFactory.createSocket(host, port, localAddress, 0, this.params);
}
看完这段代码,首先感觉解决我前面的痛苦,原因如下:
看到此处我们应该清楚,在httpclient中,不用刻意去维护通讯的长连接有效性,如果当前连接还没有被关闭,那么就使用当前连接通讯;如果当前连接已经被关闭了,那么再重新创建一个socket对象通讯。这样,我们刚开始创建的连接对象还是同一个不变,但是底层的socket对象其实已经变了。
那么这样做有什么好处呢?我们可以想一下这种情况,在两台频繁通讯的服务器之间,tcp连接不会因为空闲时间过长而被中断,那么就可以一直使用当前tcp连接通讯,不用每次耗费大量资源去重新建立tcp连接;如果两台服务器通讯不频繁了,当期连接被其中一个终止了,那么就自动再创建一个TCP的socket连接通讯。这样的处理方式,对于用户来说是透明的,不用刻意去维持连接有效性,好像一直在保持通讯似的。
但是这个方法也有局限性,就是这样的做法只能保证客户端向服务器发送请求是随时可以且能够请求到,但是服务器不能保证随时能联系上客户端。所以,把这一点用到http协议上,再恰当不过。
但是这样做,还是会有可能创建多个连接,所以httpclient官方文档也给出方案,建议使用网络心跳的方式,一直保持长连接。但是这个必要性根据具体的需求而定。为了方便以后查看,我把这段代码也提供出来,这段代码是看的一个帖子评论里写的:

import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
// 创建线程
IdleConnectionTimeoutThread thread = new IdleConnectionTimeoutThread();
// 注册连接管理器
thread.addConnectionManager(httpClient.getHttpConnectionManager());
// 启动线程
thread.start();
// 在最后,关闭线程
thread.shutdown();
我上面分析的代码,仅仅是针对开头的代码结构分析的,实际上,这个代码结构仅仅是针对单线程处理的,官方也建议不要再多线程中去使用。因为httpclient默认创建的连接管理器同时只维护一个连接,所以多线程有可能降低效能,即使他是线程安全的。而httpclient还有一个可以维护多个连接的连接管理器,运行多线程的时候可以自己创建多连接的管理器。
以上这些是基于common-httpclient-3.0版本分析的,新版的httpclient4.0已经交由Apache的httpclient项目开发了。所以这个版本的研究到此为止,下次研究就面向新版本啦!
再说一下关于我们公司遇到的问题,原因分析了一下,就是程序中,每次请求都会创建一个httpclient对象,而每个httpclient对象都会重新创建一个socket连接,所以在访问的高峰期,socket连接数过多,而又不能完全释放,就造成了同时很多连接处于被打开状态。而在Linux系统中,每一个进程能打开的文件数是有限制的,每个socket在Linux上都是一个文件,所以才会出现本文开头的诸多问题。
这两天我看网上很多人也遇到了这个问题,给出的解决办法有两种:
1. 扩大Linux系统上每个进程能够打开的文件数。这个办法可以在一定程度上暂时解决问题,但是访问量继续增大的话,就又会出现当前问题。
2. 每访问一次就彻底关闭掉链接。这个方法在一方面可以解决1的问题,但是又会频繁耗费系统资源,而且在并发访问量特别大的时候,也可能出现1的问题。
对于以上所总结的内容,仅针对单线程而言,多线程的话,可能会有不一样的结果,仅供参考。

关于上面提到的彻底关闭连接的方法,请参考文章:http://www.iteye.com/topic/234759


参考资料:

http://www.iteye.com/topic/234759

http://blog.csdn.net/javaalpha/article/details/6159442

http://blog.sina.com.cn/s/blog_616e189f01018rpk.html










你可能感兴趣的:(Java,Http)