http://frodoking.github.io/2015/03/12/android-okhttp/html
Android为咱们提供了两种HTTP交互的方式:HttpURLConnection 和 Apache HTTP Client,虽然二者都支持HTTPS,流的上传和下载,配置超时,IPv6和链接池,已足够知足咱们各类HTTP请求的需求。但更高效的使用HTTP可让您的应用运行更快、更节省流量。而OkHttp库就是为此而生。java
OkHttp是一个高效的HTTP库:android
- 支持 SPDY ,共享同一个Socket来处理同一个服务器的全部请求
- 若是SPDY不可用,则经过链接池来减小请求延时
- 无缝的支持GZIP来减小数据流量
- 缓存响应数据来减小重复的网络请求
会从不少经常使用的链接问题中自动恢复。若是您的服务器配置了多个IP地址,当第一个IP链接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。nginx
使用 OkHttp 无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpURLConnection同样的API。若是您用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块。git
OKHttp源码位置https://github.com/square/okhttp
github
##使用web
简单使用代码apache
1 |
private final OkHttpClient client = new OkHttpClient(); |
在这里使用不作详细介绍,推荐一篇关于OKHttp的详细使用教程,下面转入源码的分析。编程
##整体设计
上面是OKHttp整体设计图,主要是经过Diapatcher不断从RequestQueue中取出请求(Call),根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据。该引擎有同步和异步请求,同步请求经过Call.execute()直接返回当前的Response,而异步请求会把当前的请求Call.enqueue添加(AsyncCall)到请求队列中,并经过回调(Callback)的方式来获取最后结果。json
接下来会介绍一些比较重要的类,另一些基础IO方面的内容主要来之iohttp这个包。这些类的解释大部分来至文档介绍自己,因此在此不会翻译成中文,本人以为英语原文更能准确表达它自身的做用。
##OKHttp中重要的类
1.Route.java
The concrete route used by a connection to reach an abstract origin server.
When creating a connection the client has many options:
- HTTP proxy: a proxy server may be explicitly configured for the client. Otherwise the {@linkplain java.net.ProxySelector proxy selector} is used. It may return multiple proxies to attempt.
- IP address: whether connecting directly to an origin server or a proxy, opening a socket requires an IP address. The DNS server may return multiple IP addresses to attempt.
- TLS configuration: which cipher suites and TLS versions to attempt with the HTTPS connection.
Each route is a specific selection of these options.
其实就是对地址的一个封装类,可是很重要。
2.Platform.java
Access to platform-specific features.
- Server name indication (SNI): Supported on Android 2.3+.
- Session Tickets: Supported on Android 2.3+.
- Android Traffic Stats (Socket Tagging): Supported on Android 4.0+.
- ALPN (Application Layer Protocol Negotiation): Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was unstable.
Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
这个类主要是作平台适应性,针对Android2.3到5.0后的网络请求的适配支持。同时,在这个类中能看到针对不一样平台,经过java反射不一样的class是不同的。
3.Connnection.java
The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be used for multiple HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.
Typically instances of this class are created, connected and exercised automatically by the HTTP client. Applications may use this class to monitor HTTP connections as members of a ConnectionPool.
Do not confuse this class with the misnamed HttpURLConnection, which isn’t so much a connection as a single request/response exchange.
Modern TLS
There are tradeoffs when selecting which options to include when negotiating a secure connection to a remote host. Newer TLS options are quite useful:
- Server Name Indication (SNI) enables one IP address to negotiate secure connections for multiple domain names.
- Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used for different HTTP and SPDY protocols.
Unfortunately, older HTTPS servers refuse to connect when such options are presented. Rather than avoiding these options entirely, this class allows a connection to be attempted with modern options and then retried without them should the attempt fail.
4.ConnnectionPool.java
Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP requests that share the same Address may share a Connection. This class implements the policy of which connections to keep open for future use.
The system-wide default uses system properties for tuning parameters:
- http.keepAlive true if HTTP and SPDY connections should be pooled at all. Default is true.
- http.maxConnections maximum number of idle connections to each to keep in the pool. Default is 5.
- http.keepAliveDuration Time in milliseconds to keep the connection alive in the pool before closing it. Default is 5 minutes. This property isn’t used by HttpURLConnection.
The default instance doesn’t adjust its configuration as system properties are changed. This assumes that the applications that set these parameters do so before making HTTP connections, and that this class is initialized lazily.
5.Request.java
An HTTP request. Instances of this class are immutable if their body is null or itself immutable.(Builder模式)
6.Response.java
An HTTP response. Instances of this class are not immutable: the response body is a one-shot value that may be consumed only once. All other properties are immutable.
7.Call.java
A call is a request that has been prepared for execution. A call can be canceled. As this object represents a single request/response pair (stream), it cannot be executed twice.
8.Dispatcher.java
Policy on when async requests are executed.
Each dispatcher uses an ExecutorService to run calls internally. If you supply your own executor, it should be able to run configured maximum number of calls concurrently.
9.HttpEngine.java
Handles a single HTTP request/response pair. Each HTTP engine follows this
lifecycle:
- It is created.
- The HTTP request message is sent with sendRequest(). Once the request is sent it is an error to modify the request headers. After sendRequest() has been called the request body can be written to if it exists.
- The HTTP response message is read with readResponse(). After the response has been read the response headers and body can be read. All responses have a response body input stream, though in some instances this stream is empty.
The request and response may be served by the HTTP response cache, by the network, or by both in the event of a conditional GET.
10.Internal.java
Escalate internal APIs in {@code com.squareup.okhttp} so they can be used from OkHttp’s implementation packages. The only implementation of this interface is in {@link com.squareup.okhttp.OkHttpClient}.
11.Cache.java
Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and bandwidth.
Cache Optimization
To measure cache effectiveness, this class tracks three statistics:
- Request Count: the number of HTTP requests issued since this cache was created.
- Network Count: the number of those requests that required network use.
- Hit Count: the number of those requests whose responses were served by the cache.
Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of the response, the client will issue a conditional GET. The server will then send either the updated response if it has changed, or a short ‘not modified’ response if the client’s copy is still valid. Such responses increment both the network count and hit count.
The best way to improve the cache hit rate is by configuring the web server to return cacheable responses. Although this client honors all HTTP/1.1 (RFC 2068) cache headers, it doesn’t cache partial responses.
Force a Network Response
In some situations, such as after a user clicks a ‘refresh’ button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh, add the {@code no-cache} directive:
1 |
connection.addRequestProperty("Cache-Control", "no-cache") |
If it is only necessary to force a cached response to be validated by the server, use the more efficient {@code max-age=0} instead:
1 |
connection.addRequestProperty("Cache-Control", "max-age=0"); |
Force a Cache Response
Sometimes you’ll want to show resources if they are available immediately, but not otherwise. This can be used so your application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources, add the {@code only-if-cached} directive:
1 |
try { |
This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the {@code max-stale} directive with the maximum staleness in seconds:
1 |
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale |
12.OkHttpClient.java
Configures and creates HTTP connections. Most applications can use a single OkHttpClient for all of their HTTP requests - benefiting from a shared response cache, thread pool, connection re-use, etc.
Instances of OkHttpClient are intended to be fully configured before they’re shared - once shared they should be treated as immutable and can safely be used to concurrently open new connections. If required, threads can call clone to make a shallow copy of the OkHttpClient that can be safely modified with further configuration changes.
##详细类关系图
因为整个设计类图比较大,因此本人将从核心入口client、cache、interceptor、网络配置、链接池、平台适配性…这些方面来逐一进行分析源代码的设计。
下面是核心入口OkHttpClient的类设计图
从OkHttpClient类的总体设计来看,它采用门面模式来。client知晓子模块的全部配置以及提供须要的参数。client会将全部从客户端发来的请求委派到相应的子系统去。
在该系统中,有多个子系统、类或者类的集合。例如上面的cache、链接以及链接池相关类的集合、网络配置相关类集合等等。每一个子系统均可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另一个客户端而已。同时,OkHttpClient能够看做是整个框架的上下文。
经过类图,其实很明显反应了该框架的几大核心子系统;路由、链接协议、拦截器、代理、安全性认证、链接池以及网络适配。从client大大下降了开发者使用难度。同时很是明了的展现了该框架在全部须要的配置以及获取结果的方式。
在接下来的几个Section中将会结合子模块核心类的设计,从该框架的总体特性上来分析这些模块是如何实现各自功能。以及各个模块之间是如何相互配合来完成客户端各类复杂请求。
##同步与异步的实现
在发起请求时,整个框架主要经过Call来封装每一次的请求。同时Call持有OkHttpClient和一份HttpEngine。而每一次的同步或者异步请求都会有Dispatcher的参与,不一样的是:
- 同步
Dispatcher会在同步执行任务队列中记录当前被执行过得任务Call,同时在当前线程中去执行Call的getResponseWithInterceptorChain()方法,直接获取当前的返回数据Response;- 异步
首先来讲一下Dispatcher,Dispatcher内部实现了懒加载无边界限制的线程池方式,同时该线程池采用了SynchronousQueue这种阻塞队列。SynchronousQueue每一个插入操做必须等待另外一个线程的移除操做,一样任何一个移除操做都等待另外一个线程的插入操做。所以此队列内部其 实没有任何一个元素,或者说容量是0,严格说并非一种容器。因为队列没有容量,所以不能调用peek操做,由于只有移除元素时才有元素。显然这是一种快速传递元素的方式,也就是说在这种状况下元素老是以最快的方式从插入者(生产者)传递给移除者(消费者),这在多任务队列中是最快处理任务的方式。对于高频繁请求的场景,无疑是最适合的。
异步执行是经过Call.enqueue(Callback responseCallback)来执行,在Dispatcher中添加一个封装了Callback的Call的匿名内部类Runnable来执行当前的Call。这里必定要注意的地方这个AsyncCall是Call的匿名内部类。AsyncCall的execute方法仍然会回调到Call的getResponseWithInterceptorChain方法来完成请求,同时将返回数据或者状态经过Callback来完成。
接下来继续讲讲Call的getResponseWithInterceptorChain()方法,这里边重点说一下拦截器链条的实现以及做用。
##拦截器有什么做用
先来看看Interceptor自己的文档解释:观察,修改以及可能短路的请求输出和响应请求的回来。一般状况下拦截器用来添加,移除或者转换请求或者回应的头部信息。
拦截器接口中有intercept(Chain chain)方法,同时返回Response。所谓拦截器更像是AOP设计的一种实现。下面来看一个okhttp源码中的一个引导例子来讲明拦截器的做用。
1 |
public final class LoggingInterceptors { |
返回信息
1 |
三月 19, 2015 2:11:29 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept |
从这里的执行来看,拦截器主要是针对Request和Response的切面处理。
那再来看看源码到底在什么位置作的这个处理呢?为了更加直观的反应执行流程,本人截图了一下执行堆栈
另外若是还有同窗对Interceptor比较敢兴趣的能够去源码的simples模块看看GzipRequestInterceptor.java针对HTTP request body的一个zip压缩。
在这里再多说一下关于Call这个类的做用,在Call中持有一个HttpEngine。每个不一样的Call都有本身独立的HttpEngine。在HttpEngine中主要是各类链路和地址的选择,还有一个Transport比较重要
##缓存策略
在OkHttpClient内部暴露了有Cache和InternalCache。而InternalCache不该该手动去建立,因此做为开发使用者来讲,通常用法以下:
1 |
public final class CacheResponse { |
返回信息
1 |
信息: Cache file path D:\work\workspaces\workspaces_intellij\workspace_opensource\okhttp\CacheResponse.tmp |
上边这一段代码一样来之于simple代码CacheResponse.java,反馈回来的数据重点看一下缓存日志。第一次是来至网络数据,第二次来至缓存。
那在这一节重点说一下整个框架的缓存策略如何实现的。
在这里继续使用上一节中讲到的运行堆栈图。从Call.getResponse(Request request, boolean forWebSocket)执行Engine.sendRequest()和Engine.readResponse()来详细说明一下。
sendRequest()
此方法是对可能的Response资源进行一个预判,若是须要就会开启一个socket来获取资源。若是请求存在那么就会为当前request添加请求头部而且准备开始写入request body。
1 |
public void sendRequest() throws IOException { |
readResponse()
此方法发起刷新请求头部和请求体,解析HTTP回应头部,而且若是HTTP回应体存在的话就开始读取当前回应头。在这里有发起返回存入缓存系统,也有返回和缓存系统进行一个对比的过程。
1 |
public void readResponse() throws IOException { |
##HTTP链接的实现方式(说说链接池)
外部网络请求的入口都是经过Transport接口来完成。该类采用了桥接模式将HttpEngine和HttpConnection来链接起来。由于HttpEngine只是一个逻辑处理器,同时它也充当了请求配置的提供引擎,而HttpConnection是对底层处理Connection的封装。
OK如今重点转移到HttpConnection(一个用于发送HTTP/1.1信息的socket链接)这里。主要有以下的生命周期:
一、发送请求头;
二、打开一个sink(io中有固定长度的或者块结构chunked方式的)去写入请求body;
三、写入而且关闭sink;
四、读取Response头部;
五、打开一个source(对应到第2步的sink方式)去读取Response的body;
六、读取并关闭source;
下边看一张关于链接执行的时序图:
这张图画得比较简单,详细的过程以及链接池的使用下面大体说明一下:
一、链接池是暴露在client下的,它贯穿了Transport、HttpEngine、Connection、HttpConnection和SpdyConnection;在这里目前默认讨论HttpConnection;
二、ConnectionPool有两个构建参数是maxIdleConnections(最大空闲链接数)和keepAliveDurationNs(存活时间),另外链接池默认的线程池采用了Single的模式(源码解释是:一个用于清理过时的多个链接的后台线程,最多一个单线程去运行每个链接池);
三、发起请求是在Connection.connect()这里,实际执行是在HttpConnection.flush()这里进行一个刷入。这里重点应该关注一下sink和source,他们建立的默认方式都是依托于同一个socket:
this.source = Okio.buffer(Okio.source(socket));
this.sink = Okio.buffer(Okio.sink(socket));
若是再进一步看一下io的源码就能看到:
Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout);
Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
这下我想你们都应该明白这里究竟是真么回事儿了吧?
相关的sink和source还有相应的细分,若是有兴趣的朋友能够继续深刻看一下,这里就再也不深刻了。否则真的说不完了。。。
其实链接池这里仍是有不少值得细看的地方,因为时间有限,到这里已经花了不少时间搞这事儿了。。。
##重连机制
这里重点说说链接链路的相关事情。说说自动重连究竟是如何实现的。
照样先来看看下面的这个自动重连机制的实现方式时序图
同时回到Call.getResponse()方法提及
1 |
Response getResponse(Request request, boolean forWebSocket) throws IOException { |
相信这一段代码能让同窗们清晰的看到自动重连机制的实现方式,那么咱们来看看详细的步骤:
一、HttpEngine.recover()的实现方式是经过检测RouteSelector是否还有更多的routes能够尝试链接,同时会去检查是否能够恢复等等的一系列判断。若是能够会为从新链接从新建立一份新的HttpEngine,同时把相应的链路信息传递过去;
二、当恢复后的HttpEngine不为空,那么替换当前Call中的当前HttpEngine,执行while的continue,发起下一次的请求;
三、再重点强调一点HttpEngine.sendRequest()。这里以前分析过会触发connect()方法,在该方法中会经过RouteSelector.next()再去找当前适合的Route。多说一点,next()方法会传递到nextInetSocketAddress()方法,而此处一段重要的执行代码就是network.resolveInetAddresses(socketHost)。这个地方最重要的是在Network这个接口中有一个对该接口的DEFAULT的实现域,而该方法经过工具类InetAddress.getAllByName(host)来完成对数组类的地址解析。
因此,多地址能够采用[“http://aaaaa","https://bbbbbb"]的方式来配置。
##Gzip的使用方式
在源码引导RequestBodyCompression.java中咱们能够看到gzip的使用身影。经过拦截器对Request 的body进行gzip的压缩,来减小流量的传输。
Gzip实现的方式主要是经过GzipSink对普通sink的封装压缩。在这个地方就再也不贴相关代码的实现。有兴趣同窗对照源码看一下就ok。
强大的Interceptor设计应该也算是这个框架的一个亮点。
##安全性
链接安全性主要是在HttpEngine.connect()方法。上一节油讲到地址相关的选择,在HttpEngine中有一个静态方法createAddress(client, networkRequest),在这里经过获取到OkHttpClient中关于SSLSocketFactory、HostnameVerifier和CertificatePinner的配置信息。而这些信息大部分采用默认状况。这些信息都会在后面的重连中做为对比参考项。
同时在Connection.upgradeToTls()方法中,有对SSLSocket、SSLSocketFactory的建立活动。这些建立都会被记录到ConnectionSpec中,当发起ConnectionSpec.apply()会发起一些列的配置以及验证。
建议有兴趣的同窗先了解java的SSLSocket相关的开发再来了解本框架中的安全性,会更能理解一些。
##平台适应性
讲了不少,终于来到了平台适应性了。Platform是整个平台适应的核心类。同时它封装了针对不一样平台的三个平台类Android和JdkWithJettyBootPlatform。
代码实如今Platform.findPlatform中
1 |
private static Platform findPlatform() { |
这里采用了JAVA的反射原理调用到class的method。最后在各自的平台调用下发起invoke来执行相应方法。详情请参看继承了Platform的Android类。
固然要作这两种的平台适应,必需要知道当前平台在内存中相关的class地址以及相关方法。
##总结
一、从总体结构和类内部域中均可以看到OkHttpClient,有点相似与安卓的ApplicationContext。看起来更像一个单例的类,这样使用好处是统一。可是若是你不是高手,建议别这么用,缘由很简单:逻辑牵连太深,若是出现问题要去追踪你会有深深地罪恶感的;
二、框架中的一些动态方法、静态方法、匿名内部类以及Internal的这些代码至关规整,每一个不一样类的不一样功能能划分在不一样的地方。很值得开发者学习的地方;
三、从平台的兼容性来说,也是很不错的典范(若是你之后要从事API相关编码,那更得好好注意对兼容性的处理);
四、因为时间不是很富裕,因此本人对细节的把握仍是不够,这方面还得多多努力;
五、对于初学网络编程的同窗来讲,可能一开始学习都是从简单的socket的发起而后获取响应开始的。由于没有很好的场景能让本身知道网络编程到底有多么的重要,固然估计也没感觉到网络编程有多么的难受。我想这是不少刚入行的同窗们的一种心里痛苦之处;
六、不足的地方是没有对SPDY的方式最详细跟进剖析(手头还有工做的事情,后面若是有时间再补起来吧)。
##结束语
很早前都打算花点时间好好来看一个值得学习的框架,今天终于算是弄得差很少了。我相信从框架的前期使用、到代码的介入、再到源码分模块的剖析、最后到整理成文章。我想这都是一个很好的学习和成长的过程。
但愿看到这篇文章的同窗能作出评价,而且给出一些好的剖析点。
我也是一个普普统统的编码人,只是心里多了一点点不“安分” ^.^。
##后续
最近看到一些网友建议把okhttp的链接池对Connection的重用维护机制以及HTTP和SPDY协议如何获得区分这两部份内容作深刻的分析
有须要的同窗请移步:OKHttp源码解析-ConnectionPool对Connection重用机制&Http/Https/SPDY协议选择