关于httpclient:HttpClient-在vivo内销浏览器的高并发实践优化

作者:vivo 互联网服务器团队- Zhi GuangquanHttpClient作为Java程序员最罕用的Http工具,其对Http连贯的治理能简化开发,并且晋升连贯重用效率;在失常状况下,HttpClient能帮忙咱们高效治理连贯,但在一些并发高,报文体较大的状况下,如果再遇到网络稳定,如何保障连贯被高效利用,有哪些优化空间。 一、问题景象北京工夫X月X日,浏览器信息流服务监控出现异常,次要体现在以下三个方面: 从某个工夫点开始,云监控显示局部Http接口的熔断器被关上,而且从明细列表能够发现问题机器: 2. 从PAAS平台Hystrix熔断治理界面中能够进一步确认问题机器的所有Http接口调用均呈现了熔断: 3. 日志核心有大量从Http连接池获取连贯的异样:org.apache.http.impl.execchain.RequestAbortedException: Request aborted。 二、问题定位综合以上三个景象,大略能够揣测出问题机器的TCP连贯治理出了问题,可能是虚拟机问题,也可能是物理机问题;与运维与零碎侧沟通后,发现虚拟机与物理机均无显著异样,第一工夫分割运维重启了问题机器,线上问题失去解决。 2.1 长期解决方案几天当前,线上局部其余机器也陆续呈现了上述景象,此时根本能够确认是服务自身有问题;既然问题与TCP连贯相干,于是分割运维在问题机器上建设了一个作业查看TCP连贯的状态散布: netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'后果如下: 如上图,问题机器的CLOSE\_WAIT状态的连接数曾经靠近200左右(该服务Http连接池最大连接数设置的250),那问题间接起因根本能够确认是CLOSE\_WAIT状态的连贯过多导致的;本着第一工夫先解决线上问题的准则,先把连接池调整到500,而后让运维重启了机器,线上问题临时失去解决。 2.2 起因剖析调整连接池大小只是临时解决了线上问题,然而具体起因还不确定,依照以往教训,呈现连贯无奈失常开释根本都是开发者使用不当,在应用实现后没有及时敞开连贯;但很快这个想法就被否定了,起因不言而喻:以后的服务曾经在线上运行了一周左右,两头没有经验过发版,以浏览器的业务量,如果是连贯应用完没有及时关。 闭,250的连接数连一分钟都撑不到就会被打爆。那么问题就只能是一些异样场景导致的连贯没有开释;于是,重点排查了下近期上线的业务接口,尤其是那种数据包体较大,响应工夫较长的接口,最终把指标锁定在了某个详情页优化接口上;先查看处于CLOSE_WAIT状态的IP与端口连贯对,确认对方服务器IP地址。 netstat-tulnap|grep CLOSE_WAIT 通过与合作方确认,指标IP均来自该合作方,与咱们的揣测是相符的。 2.3 TCP抓包在定位问题的同时,也让运维共事帮忙抓取了TCP的数据包,结果表明的确是客户端(浏览器服务端)没返回ACK完结握手,导致挥手失败,客户端处于了CLOSE_WAIT状态,数据包的大小也与狐疑的问题接口相符。 为了不便大家了解,我从网上找了一张图,大家能够作为参考: CLOSE\_WAIT是一种被动敞开状态,如果是SERVER被动断开的连贯,那么就会在CLIENT呈现CLOSE\_WAIT的状态,反之同理; 通常状况下,如果客户端在一次http申请实现后没有及时敞开流(tcp中的流套接字),那么超时后服务端就会被动发送敞开连贯的FIN,客户端没有被动敞开,所以就停留在了CLOSE_WAIT状态,如果是这种状况,很快连接池中的连贯就会被耗尽。 所以,咱们明天遇到的状况(处于CLOSE_WAIT状态的连接数每天都在迟缓增长),更像是某一种异样场景导致的连贯没有敞开。 2.4 独立连接池为了不影响其余业务场景,防止出现系统性危险,咱们先把问题接口连接池进行了独立治理。 2.5 深入分析带着2.3的疑难咱们认真查看一下业务调用代码: try { httpResponse = HttpsClientUtil.getHttpClient().execute(request); HttpEntity httpEntity = httpResponse.getEntity(); is = httpEntity.getContent(); }catch (Exception e){ log.error(""); }finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(httpResponse); }这段代码存在一个显著的问题:既敞开了数据传输流( IOUtils.closeQuietly(is)),也敞开了整个连贯(IOUtils.closeQuietly(httpResponse)),这样咱们就没方法进行连贯的复用了;然而却更让人纳闷了:既然每次都手动敞开了连贯,为什么还会有大量CLOSE_WAIT状态的连贯存在呢? 如果问题不在业务调用代码上,那么只能是这个业务接口具备的某种特殊性导致了问题的产生;通过抓包剖析发现该接口有一个显著特色:接口返回报文较大,均匀在500KB左右。那么问题就极有可能是报文过大导致了某种异样,造成了连贯不能被复用也不能被开释。 2.6 源码剖析开始剖析之前,咱们须要理解一个基础知识:Http的长连贯和短连贯。所谓长连贯就是建设起连贯之后,能够复用连贯屡次进行数据传输;而短连贯则是每次都须要从新建设连贯再进行数据传输。 ...

August 15, 2022 · 1 min · jiezi

关于httpclient:Appache-httpclient证书认证过程

tcp三次握手后,client开始和server进行SSL连贯,默认client用jdk的证书库对服务器证书进行认证,如果证书非法抛异样。如下示例代码 public static void main(String[] args) throws Exception { HttpClientBuilder builder = HttpClients.custom(); SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustStrategy() { @Override public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { return false; } }).build();// builder.setSSLContext(sslContext); CloseableHttpClient client = builder.build(); HttpGet get = new HttpGet("https://expired.badssl.com/");// HttpGet get = new HttpGet("https://baidu.com"); try (CloseableHttpResponse resp = client.execute(get)) { System.out.println(resp.getStatusLine()); } }抛出证书异样,异样堆栈如下 Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: timestamp check failed at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509) at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979) at sun.security.ssl.Handshaker.process_record(Handshaker.java:914) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:436) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384) at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) at com.learn.http.HttpClientTest.main(HttpClientTest.java:39)Caused by: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: timestamp check failed at sun.security.validator.PKIXValidator.doValidate(PKIXValidator.java:352) at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:249) at sun.security.validator.Validator.validate(Validator.java:260) at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324) at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229) at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1491) ... 20 moreCaused by: java.security.cert.CertPathValidatorException: timestamp check failed at sun.security.provider.certpath.PKIXMasterCertPathValidator.validate(PKIXMasterCertPathValidator.java:135) at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:219) at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:140) at sun.security.provider.certpath.PKIXCertPathValidator.engineValidate(PKIXCertPathValidator.java:79) at java.security.cert.CertPathValidator.validate(CertPathValidator.java:292) at sun.security.validator.PKIXValidator.doValidate(PKIXValidator.java:347) ... 26 moreCaused by: java.security.cert.CertificateExpiredException: NotAfter: Mon Apr 13 07:59:59 CST 2015 at sun.security.x509.CertificateValidity.valid(CertificateValidity.java:274) at sun.security.x509.X509CertImpl.checkValidity(X509CertImpl.java:629) at sun.security.provider.certpath.BasicChecker.verifyTimestamp(BasicChecker.java:190) at sun.security.provider.certpath.BasicChecker.check(BasicChecker.java:144) at sun.security.provider.certpath.PKIXMasterCertPathValidator.validate(PKIXMasterCertPathValidator.java:125) ... 31 more证书验证相干类类有两个局部组成,一个是appache的httpclient,还有一个是jsse.jar包,httpclient次要利用jsse.jar来实现SSL通信,浅蓝色背景是类是jsse的类,其余是httpclient 包的类 ...

July 3, 2022 · 2 min · jiezi

关于httpclient:Go-httpclient-常用操作

模块介绍httpclient 是基于 net/http  封装的 Go HTTP 客户端申请包,反对罕用的申请形式、罕用设置,比方: 反对设置 Mock 信息反对设置失败时告警反对设置失败时重试反对设置我的项目外部的 Trace反对设置超时工夫、Header 等申请阐明办法名形容httpclient.Get()GET 申请httpclient.PostForm()POST 申请,form 模式httpclient.PostJSON()POST 申请,json 模式httpclient.PutForm()PUT 申请,form 模式httpclient.PutJSON()PUT 申请,json 模式httpclient.PatchForm()PATCH 申请,form 模式httpclient.PatchJSON()PATCH 申请,json 模式httpclient.Delete()DELETE 申请配置阐明配置项配置办法设置 TTL 本次申请最大超时工夫httpclient.WithTTL(ttl time.Duration)设置 Header 信息httpclient.WithHeader(key, value string)设置 Logger 信息httpclient.WithLogger(logger *zap.Logger)设置 Trace 信息httpclient.WithTrace(t trace.T)设置 Mock 信息httpclient.WithMock(m Mock)设置失败时告警httpclient.WithOnFailedAlarm(alarmTitle string, alarmObject AlarmObject, alarmVerify AlarmVerify)设置失败时重试httpclient.WithOnFailedRetry(retryTimes int, retryDelay time.Duration, retryVerify RetryVerify)设置 TTL// 设置本次申请最大超时工夫为 5shttpclient.WithTTL(time.Second*5),设置 Header 信息能够调用屡次进行设置多对 key-value 信息。 // 设置多对 key-value 信息,比方这样:httpclient.WithHeader("Authorization", "xxxx"),httpclient.WithHeader("Date", "xxxx"),设置 Logger 信息传递的 logger 便于 httpclient 打印日志。 ...

January 25, 2021 · 2 min · jiezi

关于httpclient:Java之HttpClient调用WebService接口发送短信源码实战

摘要Java之HttpClient调用WebService接口发送短信源码实战 一:接口文档 二:WSDL 三:HttpClient办法HttpClient办法一 HttpClient办法二 HttpClient办法三 HttpClient办法四 四:封装soapXml办法(外围办法) 五:调用WebService接口 六:调用后果 七:短信后果 完结以上就是Java之通过HttpClient形式调用WebService接口的实现流程 作者:迷彩出处:Java源码网原文地址:https://javaymw.com/post/108

October 29, 2020 · 1 min · jiezi

Apache-httpclient的execute方法调试

因为工作需要,想研究一下execute执行的逻辑。 在这一行调用execute: response = getHttpClient().execute(get);getHttpClient的实现: private HttpClient getHttpClient() { if (this.m_httpClient == null) { this.m_httpClient = HttpClientBuilder.create().build(); } return this.m_httpClient; }我在代码里声明的HttpClient只是一个接口, 实现类是InternalHttpClient。 首先根据传入的请求决定出目标-target host 投递到RedirectExec执行。 后者又投递到RetryExec执行。 收到307重定向: redirectsEnabled标志位为true: 再看当前的请求确实被redirect了吗? original url: 我的后台服务器返回的307,落到了分支HttpStatus.SC_TEMPORARY_REDIRECT处: 看来Apache的库认为只有HEAD和GET才能被redirect: 重定向最大次数:50 准备重试了: 要获取更多Jerry的原创文章,请关注公众号"汪子熙":

May 25, 2019 · 1 min · jiezi

爬虫实现:根据IP地址反查域名

域名解析与IP地址域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务;IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转换过程,该过程由DNS服务器完成(来自百度百科)先来了解两个知识点1、一个域名同一时刻只能对应一个IP地址2、一个IP地址可以解析绑定多个域名,没有限制基于以上知识点,假如我们已知一个IP地址,我们怎么才能获取解析到该IP地址的所有域名信息呢?一种方式是国家工信部能开放查询接口以供查询(不知道会不会开放?);另外一种方式就是接下来我要分享的——爬虫实现:根据IP地址反查域名。实现原理实现原理其实很简单,现在已有网站提供了根据IP地址查询域名的功能,但是需要人为登录网站输入IP地址查询,我想要实现程序自动化查询,所以就想到了爬虫的方式,简单来说,就是模拟人的查询行为,将查询结果解析成我想要的域名列表。以site.ip138.com为例,打开F12,输入一个IP查询,观察控制台请求,看到下图中信息请求地址为:http://site.ip138.com/119.75….请求方式为:GET然后,分析Response,可以看到,在页面上看到的绑定域名信息就是下图红框中<span>的内容,所以只要能将Response的内容解析出来,获取到<span>的内容就可以得到想要的域名列表。上述Response是HTML页面,使用jsoup来解析HTML简直完美。jsoup是什么?jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。//解析成Document对象Document document = Jsoup.parse(result);if (document == null) { logger.error(“Jsoup parse get document null!”);}//根据ID属性“list”获取元素Element对象(有没有感觉很像jQuery?)Element listEle = document.getElementById(“list”);//根据class属性和属性值筛选元素Element集合,并通过eachText()遍历元素内容return listEle.getElementsByAttributeValue(“target”, “blank”).eachText();result的内容通过HttpClient模拟HTTP请求HttpGet httpGet = new HttpGet(url);httpGet.setHeader(“Accept”, “text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8”);httpGet.setHeader(“Accept-Encoding”, “gzip, deflate”);httpGet.setHeader(“Accept-Language”, “zh-CN,zh;q=0.9”);httpGet.setHeader(“Cache-Control”, “max-age=0”);httpGet.setHeader(“Connection”, “keep-alive”);httpGet.setHeader(“Cookie”, “Hm_lvt_d39191a0b09bb1eb023933edaa468cd5=1553090128; BAIDU_SSP_lcr=https://www.baidu.com/link?url=FS0ccst469D77DpdXpcGyJhf7OSTLTyk6VcMEHxT_9&wd=&eqid=fa0e26f70002e7dd000000065c924649; pgv_pvi=6200530944; pgv_si=s4712839168; Hm_lpvt_d39191a0b09bb1eb023933edaa468cd5=1553093270”);httpGet.setHeader(“DNT”, “1”);httpGet.setHeader(“Host”, host);httpGet.setHeader(“Upgrade-Insecure-Requests”, “1”);httpGet.setHeader(“User-Agent”, “Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36”);String result = HttpUtils.doGet(httpGet);HTTP请求工具类public class HttpUtils { private static Logger logger = LoggerFactory.getLogger(HttpUtils.class); public static String doGet(HttpGet httpGet) { CloseableHttpClient httpClient = null; try { httpClient = HttpClients.createDefault(); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000).setConnectionRequestTimeout(10000) .setSocketTimeout(5000).build(); httpGet.setConfig(requestConfig); HttpResponse httpResponse = httpClient.execute(httpGet); if (httpResponse.getStatusLine().getStatusCode() == 200 || httpResponse.getStatusLine().getStatusCode() == 302) { HttpEntity entity = httpResponse.getEntity(); return EntityUtils.toString(entity, “utf-8”); } else { logger.error(“Request StatusCode={}”, httpResponse.getStatusLine().getStatusCode()); } } catch (Exception e) { logger.error(“Request Exception={}:”, e); } finally { if (httpClient != null) { try { httpClient.close(); } catch (IOException e) { logger.error(“关闭httpClient失败”, e); } } } return null; }}新增Controller@RestControllerpublic class DomainSpiderController { private static Logger logger = LoggerFactory.getLogger(DomainSpiderController.class); @Autowired private DomainSpiderService domainSpiderService; /** * @param ip 119.75.217.109 * @return */ @RequestMapping("/spider/{ip}") @ResponseBody public List<String> domainSpider(@PathVariable(“ip”) String ip) { long startTime = System.currentTimeMillis(); List<String> domains = domainSpiderService.domainSpiderOfIp138(ip); if(domains == null || domains.size() == 0) { domains = domainSpiderService.domainSpiderOfAizan(ip); } long endTime = System.currentTimeMillis(); logger.info(“完成爬虫任务总耗时:{}s”, (endTime - startTime) / 1000); return domains; }}启动Spring Boot应用,访问浏览器:http://localhost:8080/spider/119.75.217.109获得返回结果如下:怎么样?是不是很简单?优化改进:有时候仅仅通过一个网站查询的域名数据可能不太准确,甚至查询不到数据,我们也没法判断谁才是正确的,所以,可以通过爬取多个网站的结果结合起来使用,例如:dns.aizhan.com提出疑问:这些提供根据IP反查域名的网站,是怎么实现的呢?我咨询过其他人,他们的回答是这些网站收集了很多IP和域名的对应关系,真实情况是这样的吗?示例源码domain-spider代码已上传至码云和Github上,欢迎下载学习GiteeGithub ...

April 13, 2019 · 1 min · jiezi

通过FD耗尽实验谈谈使用HttpClient的正确姿势

一段问题代码实验在进行网络编程时,正确关闭资源是一件很重要的事。在高并发场景下,未正常关闭的资源数逐渐积累会导致系统资源耗尽,影响系统整体服务能力,但是这件重要的事情往往又容易被忽视。我们进行一个简单的实验,使用HttpClient-3.x编写一个demo请求指定的url,看看如果不正确关闭资源会发生什么事。public String doGetAsString(String url) { GetMethod getMethod = null; String is = null; InputStreamReader inputStreamReader = null; BufferedReader br = null; try { HttpClient httpclient = new HttpClient();//问题标记① getMethod = new GetMethod(url); httpclient.executeMethod(getMethod); if (HttpStatus.SC_OK == getMethod.getStatusCode()) { ……//对返回结果进行消费,代码省略 } return is; } catch (Exception e) { if (getMethod != null) { getMethod.releaseConnection(); //问题标记② } } finally { inputStreamReader.close(); br.close(); ……//关闭流时的异常处理代码省略 } return null; }这段代码逻辑很简单, 先创建一个HttpClient对象,用url构建一个GetMethod对象,然后发起请求。但是用这段代码并发地以极高的QPS去访问外部的url,很快就会在日志中看到“打开文件太多,无法打开文件”的错误,后续的http请求都会失败。这时我们用lsof -p &dollar;{javapid}命令去查看java进程打开的文件数,发现达到了655350这么多。分析上面的代码片段,发现存在以下2个问题:(1)初始化方式不对。标记①直接使用new HttpClient()的方式来创建HttpClient,没有显示指定HttpClient connection manager,则构造函数内部默认会使用SimpleHttpConnectionManager,而SimpleHttpConnectionManager的默认参数中alwaysClose的值为false,意味着即使调用了releaseConnection方法,连接也不会真的关闭。(2)在未使用连接池复用连接的情况下,代码没有正确调用releaseConnection。catch块中的标记②是唯一调用了releaseConnection方法的代码,而这段代码仅在发生异常时才会走到,大部分情况下都走不到这里,所以即使我们前面用正确的方式初始化了HttpClient,由于没有手动释放连接,也还是会出现连接堆积的问题。可能有同学会有以下疑问:1、明明是发起Http请求,为什么会打开这么多文件呢?为什么是655350这个上限呢?2、正确的HttpClient使用姿势是什么样的呢?这就涉及到linux系统中fd的概念。什么是fd在linux系统中有“一切皆文件”的概念。打开和创建普通文件、Socket(套接字)、Pipeline(管道)等,在linux内核层面都需要新建一个文件描述符来进行状态跟踪和使用。我们使用HttpClient发起请求,其底层需要首先通过系统内核创建一个Socket连接,相应地就需要打开一个fd。为什么我们的应用最多只能创建655350个fd呢?这个值是如何控制的,能否调整呢?事实上,linux系统对打开文件数有多个层面的限制:1)限制单个Shell进程以及其派生子进程能打开的fd数量。用ulimit命令能查看到这个值。2)限制每个user能打开的文件总数。具体调整方法是修改/etc/security/limits.conf文件,比如下图中的红框部分就是限制了userA用户只能打开65535个文件,userB用户只能打开655350个文件。由于我们的应用在服务器上是以userB身份运行的,自然就受到这里的限制,不允许打开多于655350个文件。# /etc/security/limits.conf##<domain> <type> <item> <value>userA - nofile 65535userB - nofile 655350# End of file3)系统层面允许打开的最大文件数限制,可以通过“cat /proc/sys/fs/file-max”查看。前文demo代码中错误的HttpClient使用方式导致连接使用完成后没有成功断开,连接长时间保持CLOSE_WAIT状态,则fd需要继续指向这个套接字信息,无法被回收,进而出现了本文开头的故障。再识HttpClient我们的代码中错误使用common-httpclient-3.x导致后续请求失败,那这里的common-httpclient-3.x到底是什么东西呢?相信所有接触过网络编程的同学对HttpClient都不会陌生,由于java.net中对于http访问只提供相对比较低级别的封装,使用起来很不方便,所以HttpClient作为Jakarta Commons的一个子项目出现在公众面前,为开发者提供了更友好的发起http连接的方式。然而目前进入Jakarta Commons HttpClient官网,会发现页面最顶部的“End of life”栏目,提示此项目已经停止维护了,它的功能已经被Apache HttpComponents的HttpClient和HttpCore所取代。同为Apache基金会的项目,Apache HttpComponents提供了更多优秀特性,它总共由3个模块构成:HttpComponents Core、HttpComponents Client、HttpComponents AsyncClient,分别提供底层核心网络访问能力、同步连接接口、异步连接接口。在大多数情况下我们使用的都是HttpComponents Client。为了与旧版的Commons HttpClient做区分,新版的HttpComponents Client版本号从4.x开始命名。从源码上来看,Jakarta Commons HttpClient和Apache HttpComponents Client虽然有很多同名类,但是两者之间没有任何关系。以最常使用到的HttpClient类为例,在commons-httpclient中它是一个类,可以直接发起请求;而在4.x版的httpClient中,它是一个接口,需要使用它的实现类。既然3.x与4.x的HttpClient是两个完全独立的体系,那么我们就分别讨论它们的正确用法。HttpClient 3.x用法回顾引发故障的那段代码,通过直接new HttpClient()的方式创建HttpClient对象,然后发起请求,问题出在了这个构造函数上。由于我们使用的是无参构造函数,查看三方包源码,会发现内部会通过无参构造函数new一个SimpleHttpConnectionManager,它的成员变量alwaysClose在不特别指定的情况下默认为false。alwaysClose这个值是如何影响到我们关闭连接的动作呢?继续跟踪下去,发现HttpMethodBase(它的多个实现类分别对应HTTP中的几种方法,我们最常用的是GetMethod和PostMethod)中的releaseConnection()方法首先会尝试关闭响应输入流(下图中的①所指代码),然后在finally中调用ensureConnectionRelease(),这个方法内部其实是调用了HttpConnection类的releaseConnection()方法,如下图中的标记③所示,它又会调用到SimpleHttpConnectionManager的releaseConnection(conn)方法,来到了最关键的标记④和⑤。标记④的代码说明,如果alwaysClose=true,则会调用httpConnection.close()方法,它的内部会把输入流、输出流都关闭,然后把socket连接关闭,如标记⑥和⑦所示。然后,如果标记④处的alwaysClose=false,则会走到⑤的逻辑中,调用finishLastResponse()方法,如标记⑧所示,这段逻辑实际上只是把请求响应的输入流关闭了而已。我们的问题代码就是走到了这段逻辑,导致没能把之前使用过的连接断开,而后续的请求又没有复用这个httpClient,每次都是new一个新的,导致大量连接处于CLOSE_WAIT状态占用系统文件句柄。通过以上分析,我们知道使用commons-httpclient-3.x之后如果想要正确关闭连接,就需要指定always=true且正确调用method.releaseConnection()方法。上述提到的几个类,他们的依赖关系如下图(红色箭头标出的是我们刚才讨论到的几个类):其中SimpleHttpConnectionManager这个类的成员变量和方法列表如下图所示:事实上,通过对commons-httpclient-3.x其他部分源码的分析,可以得知还有其他方法也可以正确关闭连接。方法1:先调用method.releaseConnection(),然后获取到httpClient对象的SimpleHttpConnectionManager成员变量,主动调用它的shutdown()方法即可。对应的三方包源码如下图所示,其内部会调用httpConnection.close()方法。方法2:先调用method.releaseConnection(),然后获取到httpClient对象的SimpleHttpConnectionManager成员变量,主动调用closeIdleConnections(0)即可,对应的三方包源码如下。方法3:由于我们使用的是HTTP/1.1协议,默认会使用长连接,所以会出现上面的连接不释放的问题。如果客户端与服务端双方协商好不使用长连接,不就可以解决问题了吗。commons-httpclient-3.x也确实提供了这个支持,从下面的注释也可以看出来。具体这样操作,我们在创建了method后使用method.setRequestHeader(“Connection”, “close”)设置头部信息,并在使用完成后调用一次method.releaseConnection()。Http服务端在看到此头部后会在response的头部中也带上“Connection: close”,如此一来httpClient发现返回的头部有这个信息,则会在处理完响应后自动关闭连接。HttpClient 4.x用法既然官方已经不再维护3.x,而是推荐所有使用者都升级到4.x上来,我们就顺应时代潮流,重点看看4.x的用法。(1)简易用法最简单的用法类似于3.x,调用三方包提供的工具类静态方法创建一个CloseableHttpClient对象,然后发起调用,如下图。这种方式创建的CloseableHttpClient,默认使用的是PoolingHttpClientConnectionManager来管理连接。由于CloseableHttpClient是线程安全的,因此不需要每次调用时都重新生成一个,可以定义成static字段在多线程间复用。如上图,我们在获取到response对象后,自己决定如何处理返回数据。HttpClient的三方包中已经为我们提供了EntityUtils这个工具类,如果使用这个类的toString()或consume()方法,则上图finally块红框中的respnose.close()就不是必须的了,因为EntityUtils的方法内部会在处理完数据后把底层流关闭。(2)简易用法涉及到的核心类详解CloseableHttpClient是一个抽象类,我们通过HttpClients.createDefault()创建的实际是它的子类InternalHttpClient。/** * Internal class. * * @since 4.3 /@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)@SuppressWarnings(“deprecation”)class InternalHttpClient extends CloseableHttpClient implements Configurable { … …}继续跟踪httpclient.execute()方法,发现其内部会调用CloseableHttpClient.doExecute()方法,实际会调到InternalHttpClient类的doExecute()方法。通过对请求对象(HttpGet、HttpPost等)进行一番包装后,最后实际由execChain.execute()来真正执行请求,这里的execChain是接口ClientExecChain的一个实例。接口ClientExecChain有多个实现类,由于我们使用HttpClients.createDefault()这个默认方法构造了CloseableHttpClient,没有指定ClientExecChain接口的具体实现类,所以系统默认会使用RedirectExec这个实现类。/* * Base implementation of {@link HttpClient} that also implements {@link Closeable}. * * @since 4.3 /@Contract(threading = ThreadingBehavior.SAFE)public abstract class CloseableHttpClient implements HttpClient, Closeable { private final Log log = LogFactory.getLog(getClass()); protected abstract CloseableHttpResponse doExecute(HttpHost target, HttpRequest request, HttpContext context) throws IOException, ClientProtocolException; … …}RedirectExec类的execute()方法较长,下图进行了简化。可以看到如果远端返回结果标识需要重定向(响应头部是301、302、303、307等重定向标识),则HttpClient默认会自动帮我们做重定向,且每次重定向的返回流都会自动关闭。如果中途发生了异常,也会帮我们把流关闭。直到拿到最终真正的业务返回结果后,直接把整个response向外返回,这一步没有帮我们关闭流。因此,外层的业务代码在使用完response后,需要自行关闭流。执行execute()方法后返回的response是一个CloseableHttpResponse实例,它的实现是什么?点开看看,这是一个接口,此接口唯一的实现类是HttpResponseProxy。/* * Extended version of the {@link HttpResponse} interface that also extends {@link Closeable}. * * @since 4.3 /public interface CloseableHttpResponse extends HttpResponse, Closeable {}我们前面经常看到的response.close(),实际是调用了HttpResponseProxy的close()方法,其内部逻辑如下:/* * A proxy class for {@link org.apache.http.HttpResponse} that can be used to release client connection * associated with the original response. * * @since 4.3 / class HttpResponseProxy implements CloseableHttpResponse { @Override public void close() throws IOException { if (this.connHolder != null) { this.connHolder.close(); } } … …}/* * Internal connection holder. * * @since 4.3 /@Contract(threading = ThreadingBehavior.SAFE)class ConnectionHolder implements ConnectionReleaseTrigger, Cancellable, Closeable { … … @Override public void close() throws IOException { releaseConnection(false); }}可以看到最终会调用到ConnectionHolder类的releaseConnection(reusable)方法,由于ConnectionHolder的close()方法调用releaseConnection()时默认传入了false,因此会走到else的逻辑中。这段逻辑首先调用managedConn.close()方法,然后调用manager.releaseConnection()方法。managedConn.close()方法实际是把连接池中已经建立的连接在socket层面断开连接,断开之前会把inbuffer清空,并把outbuffer数据全部传送出去,然后把连接池中的连接记录也删除。manager.releaseConnection()对应的代码是PoolingHttpClientConnectionManager.releaseConnection(),这段代码代码本来的作用是把处于open状态的连接的socket超时时间设置为0,然后把连接从leased集合中删除,如果连接可复用则把此连接加入到available链表的头部,如果不可复用则直接把连接关闭。由于前面传入的reusable已经强制为false,因此实际关闭连接的操作已经由managedConn.close()方法做完了,走到PoolingHttpClientConnectionManager.releaseConnection()中真正的工作基本就是清除连接池中的句柄而已。如果想了解关闭socket的细节,可以通过HttpClientConnection.close()继续往下跟踪,最终会看到真正关闭socket的代码在BHttpConnectionBase中。/* * This class serves as a base for all {@link HttpConnection} implementations and provides * functionality common to both client and server HTTP connections. * * @since 4.0 /public class BHttpConnectionBase implements HttpConnection, HttpInetConnection { … … @Override public void close() throws IOException { final Socket socket = this.socketHolder.getAndSet(null); if (socket != null) { try { this.inbuffer.clear(); this.outbuffer.flush(); try { try { socket.shutdownOutput(); } catch (final IOException ignore) { } try { socket.shutdownInput(); } catch (final IOException ignore) { } } catch (final UnsupportedOperationException ignore) { // if one isn’t supported, the other one isn’t either } } finally { socket.close(); } } } … …}为什么说调用了EntityUtils的部分方法后,就不需要再显示地关闭流呢?看下它的源码就明白了。/* * Static helpers for dealing with {@link HttpEntity}s. * * @since 4.0 /public final class EntityUtils { /* * Ensures that the entity content is fully consumed and the content stream, if exists, * is closed. * * @param entity the entity to consume. * @throws IOException if an error occurs reading the input stream * * @since 4.1 */ public static void consume(final HttpEntity entity) throws IOException { if (entity == null) { return; } if (entity.isStreaming()) { final InputStream instream = entity.getContent(); if (instream != null) { instream.close(); } } } … …}(3)HttpClient进阶用法在高并发场景下,使用连接池有效复用已经建立的连接是非常必要的。如果每次http请求都重新建立连接,那么底层的socket连接每次通过3次握手创建和4次握手断开连接将是一笔非常大的时间开销。要合理使用连接池,首先就要做好PoolingHttpClientConnectionManager的初始化。如下图,我们设置maxTotal=200且defaultMaxPerRoute=20。maxTotal=200指整个连接池中连接数上限为200个;defaultMaxPerRoute用来指定每个路由的最大并发数,比如我们设置成20,意味着虽然我们整个池子中有200个连接,但是连接到"http://www.taobao.com"时同一时间最多只能使用20个连接,其他的180个就算全闲着也不能给发到"http://www.taobao.com"的请求使用。因此,对于高并发的场景,需要合理分配这2个参数,一方面能够防止全局连接数过多耗尽系统资源,另一方面通过限制单路由的并发上限能够避免单一业务故障影响其他业务。private static volatile CloseableHttpClient instance; static { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); // Increase max total connection to 200 cm.setMaxTotal(200); // Increase default max connection per route to 20 cm.setDefaultMaxPerRoute(20); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(1000) .setSocketTimeout(1000) .setConnectionRequestTimeout(1000) .build(); instance = HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(requestConfig) .build(); }官方同时建议我们在后台起一个定时清理无效连接的线程,因为某些连接建立后可能由于服务端单方面断开连接导致一个不可用的连接一直占用着资源,而HttpClient框架又不能百分之百保证检测到这种异常连接并做清理,因此需要自给自足,按照如下方式写一个空闲连接清理线程在后台运行。public class IdleConnectionMonitorThread extends Thread { private final HttpClientConnectionManager connMgr; private volatile boolean shutdown; Logger logger = LoggerFactory.getLogger(IdleConnectionMonitorThread.class); public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) { super(); this.connMgr = connMgr; } @Override public void run() { try { while (!shutdown) { synchronized (this) { wait(5000); // Close expired connections connMgr.closeExpiredConnections(); // Optionally, close connections // that have been idle longer than 30 sec connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } } } catch (InterruptedException ex) { logger.error(“unknown exception”, ex); // terminate } } public void shutdown() { shutdown = true; synchronized (this) { notifyAll(); } }}我们讨论到的几个核心类的依赖关系如下:HttpClient作为大家常用的工具,看似简单,但是其中却有很多隐藏的细节值得探索。本文作者:闲鱼技术-峰明阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 14, 2019 · 3 min · jiezi