热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

httpclient源码分析-如何重用连接最近公司服务器后台程序,在访问第三方数据接口的时候,出现了占用连接数过多,导致本地端口占用过多以及超过Linux系统单进程的打开文件限制数。
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











推荐阅读
  • 本文详细介绍了GetModuleFileName函数的用法,该函数可以用于获取当前模块所在的路径,方便进行文件操作和读取配置信息。文章通过示例代码和详细的解释,帮助读者理解和使用该函数。同时,还提供了相关的API函数声明和说明。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • Windows7 64位系统安装PLSQL Developer的步骤和注意事项
    本文介绍了在Windows7 64位系统上安装PLSQL Developer的步骤和注意事项。首先下载并安装PLSQL Developer,注意不要安装在默认目录下。然后下载Windows 32位的oracle instant client,并解压到指定路径。最后,按照自己的喜好对解压后的文件进行命名和压缩。 ... [详细]
author-avatar
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有