热门标签 | 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











推荐阅读
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 如何提高PHP编程技能及推荐高级教程
    本文介绍了如何提高PHP编程技能的方法,推荐了一些高级教程。学习任何一种编程语言都需要长期的坚持和不懈的努力,本文提醒读者要有足够的耐心和时间投入。通过实践操作学习,可以更好地理解和掌握PHP语言的特异性,特别是单引号和双引号的用法。同时,本文也指出了只走马观花看整体而不深入学习的学习方式无法真正掌握这门语言,建议读者要从整体来考虑局部,培养大局观。最后,本文提醒读者完成一个像模像样的网站需要付出更多的努力和实践。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 标题: ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • MPLS VP恩 后门链路shamlink实验及配置步骤
    本文介绍了MPLS VP恩 后门链路shamlink的实验步骤及配置过程,包括拓扑、CE1、PE1、P1、P2、PE2和CE2的配置。详细讲解了shamlink实验的目的和操作步骤,帮助读者理解和实践该技术。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 本文介绍了Java中Currency类的getInstance()方法,该方法用于检索给定货币代码的该货币的实例。文章详细解释了方法的语法、参数、返回值和异常,并提供了一个示例程序来说明该方法的工作原理。 ... [详细]
author-avatar
顽童0006_648
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有