- 浏览: 5974 次
文章分类
最新评论
HttpClient 源码解读
前面写了两篇HttpClient和HtmlUnit和文章,然后就很久没有更新了,真的是有事,现在闲下来,把N久没动的博客也更新一下吧,因为上次的HttpClient方面讲的比较少嘛,这篇文章也正好补一下。
写博客之前也看了下网上其他人写的文章,也有很多解读HttpClient源码的,但是都是版本HttpClient4.1之前的,这里我先把我看到的一篇比较好的解读HttpClient源码的文章和大家一起分享一下:http://www.educity.cn/wenda/147389.html。
今天主要是讲解HttpClient4.3版本的源码,4.3改动还是有一点的,主要是对于“链”的概念更加浓重了。
一、HttpClient 执行步骤
先来看一下,HttpClient4.0大致的时序图。
(看的清楚么,我画的时候已经很注意字体大小以及截图的时候的清晰度了,如果实在看不清只能自己纯看文字了。)
你也可以参考下我上面分享的那篇文章里面的时序图,对比下HttpClient4.3版本做了哪些改变。
已请求一个正常的POST流为例,我们来看下HttpClient是如何执行的:
1、首先你需要建立一个HttpClient,设置这个HttpClient的一些基本属性,这其中包括HTTPS处理策略,连接超时时间,Cookie策略,连接池等等,这些都可以通过搜索HttpClient4.0+ 的教程学习。
2、执行HttpClient的execute方法,这个时候你会发现实际走向是 HttpClient ->CloseableHttpClient ->InternalHttpClient ,然后在doExecute()方法中进行了一些基本判断,初始化值之后,就进入了ClientExecChain(MainClientExec)的execute()方法。
3、然后我们可以简单看下MainClientExec的execute方法。
// @CaiBo public CloseableHttpResponse execute(final HttpRoute route, final HttpRequestWrapper request, final HttpClientContext context, final HttpExecutionAware execAware) throws IOException, HttpException { // 判断是否为空(Args这个工具类将经常出现) Args.notNull(route, "HTTP route"); Args.notNull(request, "HTTP request"); Args.notNull(context, "HTTP context"); // 看到Auth相关的直接跳过,这和认证(HTTPS等)相关,我们暂时不理会,直接往下看 AuthState targetAuthState = context.getTargetAuthState(); if (targetAuthState == null) { targetAuthState = new AuthState(); context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState); } AuthState proxyAuthState = context.getProxyAuthState(); if (proxyAuthState == null) { proxyAuthState = new AuthState(); context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState); } // 如果是环式实体,则将其请求进行加强(返回代理) // 按我说,意思就是如果这个HTTP请求实体是有entity的,那么这个entity写完之后,执行关闭,这些是通过代理模式实现的。 if (request instanceof HttpEntityEnclosingRequest) { Proxies.enhanceEntity((HttpEntityEnclosingRequest) request); } /* * 这是HttpClient中一个标识,它可以用来保证某些连接一段时间内只为某个用户所使用,默认为null * 连接池的连接都有state,其实就是这里的userToken(准确来说不全是),所有的连接的state默认都是null * 举个例子,当向连接池请求userToken为“zstu”的连接时,它会返回一个连接,并将连接状态标记为zstu, * 这样同一个线程下次再来请求连接的时候,就会优先拿到这个连接 */ Object userToken = context.getUserToken(); // 向连接池请求一个连接,这里用到了JUC编程的相关知识,返回的是一个future final ConnectionRequest connRequest = connManager.requestConnection(route, userToken); /* * execAware是什么呢,其实就是这次HTTP请求的本体,HttpRequest。 * 那你一定好奇,HttpRequestWrapper是什么呢?看名字像是HttpRequest的包装,为什么要搞两个呢? * 是这样:HttpRequestWrapper是HttpRequest的一份拷贝,execAware则是真正的请求。 * 这是由于HttpRequestWrapper在后面的操作中都会发生一些变化, 而HttpClient又不想这些变化被使用者知道或者捕获,所以拷贝了一份 * 然而使用者可以随时取消网络执行或者释放连接等等,所以又不得不用到原生的对象,以便响应使用者的请求。 */ if (execAware != null) { if (execAware.isAborted()) { connRequest.cancel(); throw new RequestAbortedException("Request aborted"); } else { execAware.setCancellable(connRequest); } } // 就是你前面建立HttpClient时可以设置的属性之一,有长连接时间,超时时间等可以设置。 final RequestConfig config = context.getRequestConfig(); // 如果你使用的是同步线程池(PoolingHttpClientConnectionManager)的话,这里真正跑的是LoggingManagedHttpClientConnection // 当然说的是大部分情况下是这个。 final HttpClientConnection managedConn; try { final int timeout = config.getConnectionRequestTimeout(); // 这里通过future特性来获取值 managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS); } catch (final InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new RequestAbortedException("Request aborted", interrupted); } catch (final ExecutionException ex) { Throwable cause = ex.getCause(); if (cause == null) { cause = ex; } throw new RequestAbortedException("Request execution failed", cause); } // 这个地方将这个连接设置到上下文中,暴露连接 // 这样的话,使用者可以方便的在调用处获得本次执行所使用的连接 // 只需要自己创建一个context,然后让HttpClient使用你的上下文即可 // 在执行过程中,HttpClient会将诸多和本次执行相关的引用都设置到你的上下文中,包括执行用的连接 context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn); if (config.isStaleConnectionCheckEnabled()) { // validate connection if (managedConn.isOpen()) { this.log.debug("Stale connection check"); // 这个函数比较重要,如果你想看的话,它的实现在BHttpConnectionBase类中 // 它的作用是检查连接是否有效(但是它也不能完全的保证连接有效),可以查看这里,我就不复制了。 // http://hc.apache.org/httpcomponents-client-4.2.x/tutorial/html/connmgmt.html#d5e652 if (managedConn.isStale()) { this.log.debug("Stale connection detected"); managedConn.close(); } } } // 新建一个连接持有类,主要就是操作连接的N多代码都是重复的,而且操作连接比较频繁 // 所以将对连接的操作都封装到这个类中 final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn); try { if (execAware != null) { // 大家平时用完HttpClient.execute(post)之后,一般都会调用类似post.releaseConnection()方法吧 // 这个方法之所以有效,就是在这里设置了connHolder,你调用的最终方法其实是ConnectionHolder提供的。 execAware.setCancellable(connHolder); } HttpResponse response; // 真的开始执行了,可以看到,这里是个循环,有可能会执行N次 for (int execCount = 1;; execCount++) { // 如果发现已经执行过了并且这个请求是不可重复的,那么就报错了。 // 那么什么叫请求不可重复呢?这主要是和流有关,大家知道InputStream有很多种,有些流可以反复读(PushbackInputStream),但很多流只能读一次 // 类似的HttpEntity也存在这样的情况,平常的StringEntity(就是纯字符串)是可以随便读多少次的,因为就是个字符串嘛,读了不会消失。 // 然后上传文件等操作就没这么简单了,如果你是使用InputStreamEntity,那么就不能重复请求了。 // 所有建议大家在上传文件时使用FileEntity,如果想做到通用(所有流)则使用ByteArrayEntity。 if (execCount > 1 && !Proxies.isRepeatable(request)) { throw new NonRepeatableRequestException("Cannot retry request " + "with a non-repeatable request entity."); } if (execAware != null && execAware.isAborted()) { throw new RequestAbortedException("Request aborted"); } // 这个连接如果并没有打开,则要建立网络连接(sockt连接)。 // 这里你可能好奇,这个连接明明是连接池给我的,为什么还会存在没有打开的情况呢? // 这个就涉及到连接池是怎么给你连接的,我们以后会分析。 // 这里只要知道,连接池给的连接有可能是没建立“连接”的连接。 if (!managedConn.isOpen()) { this.log.debug("Opening connection " + route); try { establishRoute(proxyAuthState, managedConn, route, request, context); } catch (final TunnelRefusedException ex) { if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage()); } response = ex.getResponse(); break; } } final int timeout = config.getSocketTimeout(); if (timeout >= 0) { managedConn.setSocketTimeout(timeout); } // 是不是总是看到这句话,所以我上面说我们可以“随时”取消请求嘛。 if (execAware != null && execAware.isAborted()) { throw new RequestAbortedException("Request aborted"); } if (this.log.isDebugEnabled()) { this.log.debug("Executing request " + request.getRequestLine()); } // 这下面两个也是认证相关的,没什么用,暂时跳过。 if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) { if (this.log.isDebugEnabled()) { this.log.debug("Target auth state: " + targetAuthState.getState()); } this.authenticator.generateAuthResponse(request, targetAuthState, context); } if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) { if (this.log.isDebugEnabled()) { this.log.debug("Proxy auth state: " + proxyAuthState.getState()); } this.authenticator.generateAuthResponse(request, proxyAuthState, context); } // 调用执行,其实这个requestExecutor没干太多事,模拟一个HTTP请求该有的材料都有了。 // 这个类只不过是拿到这些材料,然后将他们组装组装,用起来,并返回一个结果 // 说白了就是个加工工厂 response = requestExecutor.execute(request, managedConn, context); // 执行完了,判断这个连接是不是要保持长连接的 // 主要是通过返回的消息头判断 // 如果是需要保持长连接的,则将其标记为可重用 // The connection is in or can be brought to a re-usable state. if (reuseStrategy.keepAlive(response, context)) { // Set the idle duration of this connection final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); if (this.log.isDebugEnabled()) { final String s; if (duration > 0) { s = "for " + duration + " " + TimeUnit.MILLISECONDS; } else { s = "indefinitely"; } this.log.debug("Connection can be kept alive " + s); } connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); connHolder.markReusable(); } else { connHolder.markNonReusable(); } // 认证相关,跳过 if (needAuthentication(targetAuthState, proxyAuthState, route, response, context)) { // Make sure the response body is fully consumed, if present final HttpEntity entity = response.getEntity(); if (connHolder.isReusable()) { EntityUtils.consume(entity); } else { managedConn.close(); if (proxyAuthState.getState() == AuthProtocolState.SUCCESS && proxyAuthState.getAuthScheme() != null && proxyAuthState.getAuthScheme().isConnectionBased()) { this.log.debug("Resetting proxy auth state"); proxyAuthState.reset(); } if (targetAuthState.getState() == AuthProtocolState.SUCCESS && targetAuthState.getAuthScheme() != null && targetAuthState.getAuthScheme().isConnectionBased()) { this.log.debug("Resetting target auth state"); targetAuthState.reset(); } } // discard previous auth headers final HttpRequest original = request.getOriginal(); if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) { request.removeHeaders(AUTH.WWW_AUTH_RESP); } if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) { request.removeHeaders(AUTH.PROXY_AUTH_RESP); } } else { break; } } if (userToken == null) { userToken = userTokenHandler.getUserToken(context); context.setAttribute(HttpClientContext.USER_TOKEN, userToken); } if (userToken != null) { connHolder.setState(userToken); } // check for entity, release connection if possible final HttpEntity entity = response.getEntity(); // 如果返回响应是流则释放掉这个连接,其他则要abort连接 // 这里又使用到了代理模式,这个在网络编程中特别常见。 // 这里代理的主要作用就是:如果你读取完了响应,那么这个响应会关闭 if (entity == null || !entity.isStreaming()) { // connection not needed and (assumed to be) in re-usable state connHolder.releaseConnection(); return Proxies.enhanceResponse(response, null); } else { return Proxies.enhanceResponse(response, connHolder); } // 任何异常发生,都要保证连接被关闭,不然会滞留在服务器上,可能会引起服务器端口被占用完 } catch (final ConnectionShutdownException ex) { final InterruptedIOException ioex = new InterruptedIOException("Connection has been shut down"); ioex.initCause(ex); throw ioex; } catch (final HttpException ex) { connHolder.abortConnection(); throw ex; } catch (final IOException ex) { connHolder.abortConnection(); throw ex; } catch (final RuntimeException ex) { connHolder.abortConnection(); throw ex; } }
*我把注释都写在代码里了,大家看起来方便一点,可能讲的不是特别的详细,后面还会一一补充,同时也鉴于第一张时序图的效果(几乎看不清),也就不画图了,都用文字描述吧。
4、到这里基本上就完了,本来还想把请求的封装,响应的返回,连接的处理分开说的,结果看看这个代码里面基本都包含了,那也就不啰嗦了。这个MainClientExec#execute是HttpClient执行HTTP请求的主要方法,以它为核心,扩散着去看整个HttpClient,应该就能看个大概了。
---------先把文章发一下,以后再来更新。
看了这一章,你应该对HttpClient整个执行的过程有一定的了解,接下来具体讲讲经过的每一个类都干了哪些事情,以及它们是如何做这些事情的,用了哪些技巧和特性。
版权声明:本文为博主原创文章,未经博主允许不得转载。
相关推荐
总结,SpringCloud Eureka源码解读涉及到服务注册、心跳机制、服务查询、服务剔除策略以及客户端组件等多个核心概念。通过源码学习,可以更深入地理解Eureka如何在微服务架构中实现高效可靠的服务发现,有助于开发者...
Java作为一门广泛使用的编程语言,其底层知识点和源码解读对于深入理解并优化代码性能至关重要。本主题将探讨以下几个方面: 1. **Java虚拟机(JVM)**: JVM是Java程序运行的基础,它负责字节码的解释执行,内存...
7. **测试与调试**:Web API易于测试,可以使用HttpClient发送请求并接收响应。此外,Visual Studio和Visual Studio Code等IDE提供了方便的调试工具。 8. **版本管理**:为了支持服务的演化,Web API允许通过URL...
"数据分析"则涉及对采集到的数据进行处理和解读,可能包括清洗、转换、统计分析和可视化。在ASP.NET中,可以使用诸如LINQ(Language Integrated Query)进行数据查询,或者引入更专业的数据分析库如NumPy.NET(C#...
该压缩包文件“esp8266太空人网络天气时间源码(白色款).zip”包含了一套基于ESP8266微控制器的项目,主要用于实现一个具有网络天气和时间显示功能的智能设备,我们可以称之为“太空人网络天气时间钟”。...
以下是对该源码资源的详细解读: 1. **安卓编程基础**:作为一款安卓应用,此源码涵盖了Android SDK的基础知识,包括Activity的生命周期管理、Intent的使用、布局文件XML的编写等。学习者可以通过分析源码,了解...
本篇文章将详细解读“Android 炫丽书架源码”项目,帮助读者深入理解Android应用的开发技巧,尤其在UI设计和动画实现方面。 首先,我们要明确,这个项目是针对Android平台的,主要编程语言为Java,同时可能涉及到...
理解这些数据格式和对应的处理逻辑是解读源码的重要部分。 5. **地图渲染**:如果项目包含地图展示功能,那么地图渲染技术,如OpenLayers、Leaflet或Mapbox GL JS(尽管它们是JavaScript库,但Java应用可能通过Web...
本篇文章将详细讲解如何使用C#语言调用高德、百度和Google地图的API来实现这些功能,并提供相关源码的解读。 首先,让我们来看一下C#与高德地图API的结合。高德地图提供了丰富的SDK和API,包括JavaScript、Android...
在给定的文件列表中,“源码必读.pdf”可能是对整个系统的源码进行解读的文档,对于初学者来说,通过阅读源码,可以了解到实际开发中的代码组织结构、设计模式以及关键函数的实现。而“HotelBook_7c27cf80-c4d6-427a...
以下是对该软件及其相关技术的详细解读: 1. C#编程语言:C#是由微软开发的一种面向对象的编程语言,它被广泛用于构建Windows桌面应用、Web应用以及游戏开发。在QQ相册查看器中,C#被用来实现用户界面、事件处理、...
本篇文章将详细解读标题为“WP7手机归属地查询源码2012429”的项目,帮助读者理解如何在Windows Phone 7(WP7)平台上实现这一功能。 WP7,全称Windows Phone 7,是微软针对智能手机推出的操作系统,其应用程序主要...
以下是对这个项目的详细解读: 首先,我们需要理解HtmlAgilityPack的基本用法。这个库提供了一个`HtmlDocument`类,可以加载HTML字符串或从URL获取HTML内容。一旦加载完成,我们可以使用各种方法来查询和操作DOM...
本篇文章将详细解读基于.NET平台的电子面单接口批量打印Demo源码,帮助开发者理解如何集成和利用这些接口实现高效、便捷的电子面单打印功能。 首先,我们要明确`.NET电子面单接口批量打印Dem源码o.rar`这个压缩包的...
以下是对这个Java实例开发源码的详细解读。 1. **Java编程基础**: - Java是一种广泛使用的面向对象的编程语言,具有跨平台性、安全性和高性能的特点,适合开发各种类型的应用程序。 - 在这个项目中,开发者使用...
以上是根据给定的标题和描述可能涵盖的知识点,具体实现方式需查看压缩包中的源码才能详细解读。这个实例对于理解和实践Android与PHP服务器交互是一个很好的起点,尤其对于毕业设计或论文项目的学习者来说。
以下是对这些章节涉及知识点的详细解读: 1. **Chapter 11:Android用户界面设计** - Activity和Intent:Activity是Android应用的基本组件,用于处理用户交互。Intent则作为Activity间的通信桥梁,用于启动其他...
这个"24个C#关于系统操作的实例程序源码合集"提供了丰富的示例,帮助开发者深入理解并熟练掌握C#中涉及系统操作的API和方法。以下是对这些实例的详细解读: 1. 文件和目录操作:C#中的System.IO命名空间包含了大量...
本篇文章将详细解读一个基于C#的车票系统课程设计,帮助你理解并掌握C#在开发实际项目中的应用。 首先,C#是一种面向对象的编程语言,由微软公司开发,主要用于Windows平台的应用程序开发。它具有丰富的类库、高效...
本篇文章将详细解读".NET QQ农场助手源代码",帮助初学者理解C#语言在实际项目中的应用,并提供一个有趣的实践案例。 QQ农场是一款深受用户喜爱的在线游戏,而".NET QQ农场助手"则是针对这款游戏设计的一款辅助工具...