OKHttp源码解析

Android为我们提供了两种HTTP交互的方式:HttpURLConnection和Apache Http Client,虽然两者都支持HTTPS,流的上传和下载,配置超市,IPv6和连接池,已足够满足我们各种http请求的需求,但更高效的使用http可以让您的应用运行更快、更节省流量。而OKHttp库就是为此而生。

OkHttp是一个高效的HTTP库:

  • 支持SPDY,共享同一个Socket来处理同一个服务器的所有请求
  • 如果SPDY不可用,则通过谅解吃来减少请求延时
  • 无缝的支持GZIP来减少数据流量
  • 缓存响应数据来减少重复的网络请求

会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OKHttp会自动尝试下一个IP,OkHttp还处理了代理服务器问题和SSL握手失败的问题。

使用OkHttp无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpUrlConnection一样的API,如果您用了Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache模块。

OkHttp源码这里写链接内容

使用

简单使用代码

    private final OkHttpClient client = new OkHttpClient();
    public void run() throws Exception {
        Request request = new Request.Builder()
                .url("https://api.github.com/repos/square/okhttp/issues")
                .header("User-Agent", "OkHttp Headers.java")
                .addHeader("Accept", "application/json; q=0.5")
                .addHeader("Accept", "application/vnd.github.v3+json")
                .build();
        Response response = client.newCall(request).execute();
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
        System.out.print("Server: " + response.header("Server"));
        System.out.print("Date: " + response.header("Date"));
        System.out.print("Vary: " + response.header("Vary"));
    }

如果想了解更多关于OkHttp的用法,请参考这里写链接内容,这里不做更多的介绍,下面进入源码的分析。
总体设计
这里写图片描述
上面是OkHttp总体设计图,主要是通过Dispatcher不断从RequestQueue中取出请求(Call),根据是否已缓存调用Cache或Network者两类数据获取接口之一,从内存缓存或是服务器你取得请求的数据。该引擎有同步和异步请求,同步请求通过Call.execute()直接返回当前的Response,而异步请求会把当前的请求Call.enqueue添加(AsyncCall)到请求队列中,并通过回调的方式来获取最后结果。

接下来介绍一些比较重要的类,另外一些基础IO方面的内容主要来自IOHttp这个包,这些类的解释大部分来自文档介绍本身

OkHttp中重要的类

1.Route.java

连接到抽象服务器的具体路由器,当创建一个连接时,有一些选项:

  • HTTP Proxy: 可以为客户端显式配置代理服务器。否则使用{@林肯plain java.net.ProxySelector proxy selector}。它可能返回多个代理去尝试。
  • IP address: 无论是直接连接到原始服务器还是代理服务器,打开Socket都需要一个IP地址,DNS服务器可能会返回多个IP去尝试。
  • TLS configuration: 是指那些尝试使用Https连接的密码套件和TLS版本。

其实就是对地址的一个封装类,但是很重要。

2、PlatForm.java
访问平台特定的功能。

  • Server name indication(SNI): 支持版本Android2.3以上
  • Session Tickets: 支持版本Android2.3以上
  • Android流量统计信息(Socket Tagging):支持版本Android4.0以上
  • ALPN(Application Layer Protocol Negotiation):支持版本Android5.0以上,API从Android4.4就开始支持,但是不稳定。

这个类主要是做平台适应性,针对Android2.3到5.0后的网络请求的适配支持。同事,这个类中能看到针对不同平台,通过java反射不同的class是不一样的。

3、Connection.java
Http、HTTPS或HTTPS+SPDY连接的Socket和流。可用于多个Http请求/相应交换。Connections可以是直接连接到源服务器或代理服务器。
通常,该类的实例由Http客户端自动创建,连接和运行。应用程序可以使用此类作为ConnectionPool成员来监控Http请求。
不要将这个类与错误的HttpUrlConnection混淆,这不是一个的单一的请求/相应交互的连接。
Modern TLS
当选择在协商与远程主机的安全连接时要包括哪些选项是,有权衡。较新的TLS选项非常有用:

  • Server Name Indication(SNI) 可以使一个IP地址与多个域名安全连接。
  • 应用层协议协商(ALPN)使Https端口(443)可用于不同的Http和SPDY协议。

不幸的是,这些选项可能会被较老的HTTPS服务器拒绝。而不是完全避免他们。这个类允许使用这些选项尝试连接,然后如果尝试失败,继续重试。

4、ConnectionPool.java
管理HTTP和SPDY连接的重用,以减少网络延迟。共享相同地址的HTTP请求可能共享一个连接。该类实现了保持开放以供将来使用的连接的策略。
全系统默认使用的系统属性:

  • http.keepAlive如果HTTP和SPDY连接完全合并,则为true,默认值是true。
  • http.maxConnections线程池保持最大的空闲连接数,默认为5.
  • http.keepAliveDuration 连接存活时间(以时间为单位),默认为5分钟。HttpUrlConnection不使用此属性。

5、Request.java
Http请求,如果他们的Body是空的或者本事是不可变的时候这个实例是不可变的(Builder模式)。

6、Response.java
HTTP响应。当响应体是一次性的值的时候这个类的实例是不可变的,所有其他属性都是不可变的。

7、Call.java
Call是准备好执行请求的Request,可以被取消。由于此对象标识单个请求/响应对(stream),因此无法执行两次。

8、Dispatcher.java
执行异步请求的策略。
每个Dispatcher都使用ExecutorService在内部运行调用。如果您提供自己的执行程序,他会运行配置的最大值。

9、HttpEngine.java
处理单个Http请求/响应对,每个Http engine都遵循这个生命周期:

  • 他要被创建
  • Http请求消息与sendRequest()一起发送。一旦请求发送后,就不可以修改请求headers。在sendRequest()被调用后,如果请求主题存在,可以将其写入。
  • 使用readResponse()读取HTTP响应消息。响应已被读取后,可以读取响应标题和正文。所有的响应都包含响应主体输入流,但在某些情况下,此流为空。

Request和Response可以由HTTP响应缓存、网络,或者两者在Get条件下提供。

10、Internal.java
在{@code com.squareup.okhttp}中升级内部API,以便他们可以从OkHttp的实现包中使用。此接口的唯一实现是在{@link com.squareup.okhttp.OkHttpClien)中。

11、Cache.java
缓存HTTP和HTTPS对文件系统的响应,从而可以重用,从而节省时间和带宽。

缓存优化
为了测量缓存的有效性,该类监控三个统计信息:

  • 请求计数:自此缓存创建以来发出的HTTP请求数。
  • 网络计数:需要使用网络的请求数。
  • 命中计数:由缓存提供相应的请求数。

    有时,某些请求条件会导致缓存命中。如果缓存包含相应的陈旧副本,则客户端将发出GET请求,如果客户端的副本仍然有效,服务器将发送更新的(如果已更改)响应或短的”未修改”响应。这样的响应增加了网络计数和命中次数。
    提高缓存命中率的最佳方法是配置Web服务器以返回可缓存的响应。虽然这个客户端组中所有HTTP/1.1(RFC3.68)缓存头,但它并不是所有响应都缓存。

强制网络响应
在某些情况下,例如在用户单击”刷新”按钮之后,可能需要跳过缓存并直接从服务器获取数据,要强制完全刷新,请天假{@code no-cache}指令:

connection.addRequestProperty("Cache-Control", "no-cache")

如果仅需要强制缓存的响应由服务器验证,则使用更有效的{@code max-age=0}:

connection.addRequestProperty("Cache-Control", "max-age=0");

强制缓存响应
有时,如果可以立即显示资源,但是没有其他资料。用这个方式可以使你的应用程序可以在等待最新数据下载时显示一些东西。要限制本地缓存资源的请求,请天假{@code only-if-cached}指令:

try {
    connection.addRequestProperty("Cache-Control", "only-if-cached");
    InputStream cached = connection.getInputStream();
} catch (FileNotFoundException e) {
    // the resource was not cached
}

这种技术在陈旧的响应比没有响应更好的情况下运行的更好。要允许陈旧的缓存相应,请使用{@code max-stale}指令,其最大值为秒:

int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);

12、OkHttpClient.java
配置和创建HTTP连接。大多数应用程序可以使用单个OkHttpClient来处理所有的Http请求-受益于共享响应缓存,线程池,连接重用等。
OkHttpClient的实例旨在在共享之前进行完全配置-一旦共享,他们应被视为不可变的,并且可以安全的用于同时打开新连接。如果需要,线程可以调用克隆来创建OkHttpClient的浅层副本,可以通过进一步的修改配置以便将来安全的修改。

请求流程图

下面是关于OkHttp的请求流程图:
这里写图片描述

详细类关系图
由于整个设计类图比较大,所以本人将从核心入口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源码中的一个引导例子来说明拦截器的作用。

public final class LoggingInterceptors {
    private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
    private final OkHttpClient client = new OkHttpClient();
    public LoggingInterceptors() {
        client.networkInterceptors().add(new Interceptor() {
            @Override public Response intercept(Chain chain) throws IOException {
                long t1 = System.nanoTime();
                Request request = chain.request();
                logger.info(String.format("Sending request %s on %s%n%s",
                request.url(), chain.connection(), request.headers()));
                Response response = chain.proceed(request);
                long t2 = System.nanoTime();
                logger.info(String.format("Received response for %s in %.1fms%n%s",
                request.url(), (t2 - t1) / 1e6d, response.headers()));
                return response;
            }
        });
    }
    public void run() throws Exception {
        Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();
        Response response = client.newCall(request).execute();
        response.body().close();
    }
    public static void main(String... args) throws Exception {
        new LoggingInterceptors().run();
    }
}

返回信息

三月 19, 2015 2:11:29 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept
信息: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA protocol=http/1.1}
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent:
三月 19, 2015 2:11:30 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept
信息: Received response for https://publicobject.com/helloworld.txt in 275.9ms
Server: nginx/1.4.6 (Ubuntu)
Date: Thu, 19 Mar 2015 06:08:50 GMT
Content-Type: text/plain
Content-Length: 1759
Last-Modified: Tue, 27 May 2014 02:35:47 GMT
Connection: keep-alive
ETag: "5383fa03-6df"
Accept-Ranges: bytes
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1426745489953
OkHttp-Received-Millis: 1426745490198

从这里的执行来看,拦截器主要是针对Request和Response的切面处理,那再来看看源码到底在说很么位置做的这个处理呢?为了更加直观的反应执行流程,本人截图了一下堆栈
这里写图片描述
另外如果还有同学对Interceptor比较感兴趣的可以去源码的simples模块看看GzipRequestInterceptor.java针对HTTP request body的一个zip压缩。

在这里再多说一下关于Call这个类的作用,在Call中持有一个HttpEngine。每一个不同的Call都有自己独立的HttpEngine。在HttpEngine中主要是各种链路和地址的选择,还有一个Transport比较重要。

缓存策略

在OkHttpClient内部暴露了有Cache和InternalCache,而InternalCache不应该手动去创建,所以作为开发使用者来说一般用法如下:

public final class CacheResponse {
    private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
    private final OkHttpClient client;
    public CacheResponse(File cacheDirectory) throws Exception {
        logger.info(String.format("Cache file path %s",cacheDirectory.getAbsoluteFile()));
        int cacheSize = 10 * 1024 * 1024; // 10 MiB
        Cache cache = new Cache(cacheDirectory, cacheSize);
        client = new OkHttpClient();
        client.setCache(cache);
    }
    public void run() throws Exception {
        Request request = new Request.Builder()
            .url("http://publicobject.com/helloworld.txt")
            .build();
        Response response1 = client.newCall(request).execute();
        if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
        String response1Body = response1.body().string();
        System.out.println("Response 1 response: " + response1);
        System.out.println("Response 1 cache response: " + response1.cacheResponse());
        System.out.println("Response 1 network response: " + response1.networkResponse());
        Response response2 = client.newCall(request).execute();
        if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
        String response2Body = response2.body().string();
        System.out.println("Response 2 response: " + response2);
        System.out.println("Response 2 cache response: " + response2.cacheResponse());
        System.out.println("Response 2 network response: " + response2.networkResponse());
        System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
    }
    public static void main(String... args) throws Exception {
        new CacheResponse(new File("CacheResponse.tmp")).run();
    }
}

返回信息

信息: Cache file path D:\work\workspaces\workspaces_intellij\workspace_opensource\okhttp\CacheResponse.tmp
Response 1 response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response: null
Response 1 network response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response: null
Response 2 equals Response 1? true
Process finished with exit code 0

上边这一段代码同样来自于simple代码CacheResponse.java,反馈回来的数据重点看一下缓存日志。第一次是来自网络的数据,第二次来自缓存。
那再这一届重点说一下整个框架的缓存策略是如何实现的。

在这里继续使用上一节中降到的运行堆栈图。从Call.getResponse(Request request, boolean foeWebSocket)执行Engine.sendRequest()和Engine.readResponse()来详细说明一下。

sendRequest()
此方法是对可能的Response资源进行一个预判,如果需要就会开启一个socket来获取资源,如果请求存在那么就会为当前request添加请求头部并且准备开始写入request body。

public void sendRequest() throws IOException {
    if (cacheStrategy != null) {
        return; // Already sent.
    }
    if (transport != null) {
        throw new IllegalStateException();
    }
    //填充默认的请求头部和事务。
    Request request = networkRequest(userRequest);
    //下面一行很重要,这个方法会去获取client中的Cache。同时Cache在初始化的时候会去读取缓存目录中关于曾经请求过的所有信息。
    InternalCache responseCache = Internal.instance.internalCache(client);
    Response cacheCandidate = responseCache != null? responseCache.get(request): null;
    long now = System.currentTimeMillis();
    //缓存策略中的各种配置的封装
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;
    if (responseCache != null) {
        //记录当前请求是来至网络还是命中了缓存
        responseCache.trackResponse(cacheStrategy);
    }
    if (cacheCandidate != null && cacheResponse == null) {
        closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
    if (networkRequest != null) {
        // Open a connection unless we inherited one from a redirect.
        if (connection == null) {
            //连接到服务器、重定向服务器或者通过一个代理Connect to the origin server either directly or via a proxy.
            connect();
        }
        //通过Connection创建一个SpdyTransport或者HttpTransport
        transport = Internal.instance.newTransport(connection, this);
        ...
    } else {
        ...
    }
}

readResponse()
此方法发起刷新请求头部和请求体,解析Http回应头部,并且如果HTTP回应体存在的话就开始读取当前回应头。在这里有发起返回存入缓存系统,也有返回和缓存系统进行一个对比的过程。

public void readResponse() throws IOException {
    ...
    Response networkResponse;
    if (forWebSocket) {
        ...
    } else if (!callerWritesRequestBody) {
        // 这里主要是看当前的请求body,其实真正请求是在这里发生的。
        // 在readNetworkResponse()方法中执行transport.finishRequest()
        // 这里可以看一下该方法内部会调用到HttpConnection.flush()方法
        networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);
    } else {
        ...
    }
    //对Response头部事务存入事务管理中
    receiveHeaders(networkResponse.headers());
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
        //检查缓存是否可用,如果可用。那么就用当前缓存的Response,关闭网络连接,释放连接。
        if (validate(cacheResponse, networkResponse)) {
            userResponse = cacheResponse.newBuilder()
                .request(userRequest)
                .priorResponse(stripBody(priorResponse))
                .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();
            networkResponse.body().close();
            releaseConnection();
            // Update the cache after combining headers but before stripping the
            // Content-Encoding header (as performed by initContentStream()).
            // 更新缓存以及缓存命中情况
            InternalCache responseCache = Internal.instance.internalCache(client);
            responseCache.trackConditionalCacheHit();
            responseCache.update(cacheResponse, stripBody(userResponse));
            // unzip解压缩response
            userResponse = unzip(userResponse);
            return;
        } else {
            closeQuietly(cacheResponse.body());
        }
    }
    userResponse = networkResponse.newBuilder()
        .request(userRequest)
        .priorResponse(stripBody(priorResponse))
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    //发起缓存的地方
    if (hasBody(userResponse)) {
        maybeCache();
        userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
}

HTTP连接的实现方式(说说连接池)
外部网络请求的入口都是通过Transport接口来完成。该类采用了桥接模式将HttpEngine和HttpConnection来连接起来。因为HttpEngine只是一个逻辑处理器,同时他也充当了请求配置的提供引擎,而HttpConnection是对底层处理Connection的封装。
OK现在重点转移到HttpConnection(一个用于发送HTTP/1.1信息的socket连接)这里。主要有如下的生命周期:

  1. 发送请求头
  2. 打开一个sink(io中有固定长度的或者块结构chunked方式的)去写入请求body
  3. 写入并且关闭sink
  4. 读取Response头部
  5. 打开一个source(对应第2步的sink方式)去读取Response的body
  6. 读取并关闭source

下边看一张关于连接执行的时序图:
这里写图片描述

这张图比较简单,详细的过程以及连接池的使用下面大致说明一下:

  1. 连接池是暴露在client下的,它贯穿了Transport、HttpEngine、Connection、HttpConnection和SPDYConnection;在这里目前默认讨论HttpConnection。
  2. ConnectionPool有两个构造参数是maxIdleConnections(最大空间连接数)和keepAliveDurations(存活时间),另外连接池默认的线程池采用了Single的模式(源码解释是:一个用于清理过期的多个连接的后台进程,最多一个单线程去运行每一个连接池)
  3. 发起请求是在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()方法说起

Response getResponse(Request request, boolean forWebSocket) throws IOException {
    ...
    while (true) { // 自动重连机制的循环处理
        if (canceled) {
            engine.releaseConnection();
            return null;
        }
        try {
            engine.sendRequest();
            engine.readResponse();
        } catch (IOException e) {
            //如果上一次连接异常,那么当前连接进行一个恢复。
            HttpEngine retryEngine = engine.recover(e, null);
            if (retryEngine != null) {
                engine = retryEngine;
                continue;//如果恢复成功,那么继续重新请求
            }
            // Give up; recovery is not possible.如果不行,那么就中断了
            throw e;
        }
        Response response = engine.getResponse();
        Request followUp = engine.followUpRequest();
        ...
    }
}

相信这一段代码能让同学们清晰的看到自动重连机制的实现方式,那么我们来看看详细的步骤:

  1. HttpEngine.recover()的实现方式是通过检测RouteSelector是否还有更多的routes可以尝试连接,同时回去检查是否可以恢复等等一系列判断。如果可以会为重新连接重新创建一份新的HttpEngine,同时把相应的链路信息传递过去;
  2. 当恢复后的HttpEngine不为空,那么替换当前Call中的当前HttpEngine,执行while的continue,发起下一次的请求;
  3. 再重点强调一点HttpEngine.sendRequest()。这里之前分析过会触发connect()方法,而此处一段重要的执行代码就是network.resolveInetAddresses(socketHost),这个地方最重要的是在Network这个接口中有一个对该接口的Default的实现域,而该方法通过工具类InetAddress.getAllByName(host)来完成对数组类的地址解析。 所以,多地址可以采用【”http://aaaa“,”http://bbbb“】的方式来配置.

Gzip的使用方式
在源码一你到RequestBodyCompression.java中我们可以看到gzip的使用身影。通过拦截器对Request的body进行gzip的压缩,来减少流量的传输。
Gzip实现的方式主要是通过GzipSink对普通sink的封装压缩,在这个地方就不再贴相关代码的实现。有兴趣的同学对照源码看一下就ok。

强大的Interceptor设计应该也算是这个框架的一个亮点。

安全性

连接安全性主要是在HttpEngine.connect()方法,上一届有降到地址相关的选择,在HttpEngine中有一个静态方法,createAddress(client, networkRequest),在这里通过获取到OkHttpClient中关于SSLSocketFactory、HonstnameVerifier和CertificatePinner的配置信息。而这些信息大部分采用默认情况。这些信息都会在后面的重连中作为对比参考项。

同时在COnnection.upgradeToTls()方法中,有对SSLSocket、SSLSocketFactory的创建活动。这些创建都会被记录到ConnectionSPen中,当发起ConnectionSpec.apply()会发起一系列的配置以及验证。

建议有兴趣的同学先了解java的SSLSocket相关的开发再来了解本框架中的安全性。会更能理解一些。

平台适应性

降了很多,终于来到平台适应性了。Platform是整个平台适应的核心类。同时它封装了针对不同平台的三个平台类Android和JdkWithJettyBootPlatform。
代码实现在Platform.findPlatform中

private static Platform findPlatform() {
    // Attempt to find Android 2.3+ APIs.
    try {
        try {
            Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
        } catch (ClassNotFoundException e) {
            // Older platform before being unbundled.
            Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
        }
        OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(null, "setUseSessionTickets", boolean.class);
        OptionalMethod<Socket> setHostname = new OptionalMethod<>(null, "setHostname", String.class);
        Method trafficStatsTagSocket = null;
        Method trafficStatsUntagSocket = null;
        OptionalMethod<Socket> getAlpnSelectedProtocol = null;
        OptionalMethod<Socket> setAlpnProtocols = null;
        // Attempt to find Android 4.0+ APIs.
        try {
            //流浪统计类
            Class<?> trafficStats = Class.forName("android.net.TrafficStats");
            trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class);
            trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class);
            // Attempt to find Android 5.0+ APIs.
            try {
                Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
                getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
                setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
            } catch (ClassNotFoundException ignored) {
            }
        } catch (ClassNotFoundException | NoSuchMethodException ignored) {
        }
        return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket,
        trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols);
    } catch (ClassNotFoundException ignored) {
        // This isn't an Android runtime.
    }
    // Find Jetty's ALPN extension for OpenJDK.
    try {
        String negoClassName = "org.eclipse.jetty.alpn.ALPN";
        Class<?> negoClass = Class.forName(negoClassName);
        Class<?> providerClass = Class.forName(negoClassName + "$Provider");
        Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
        Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
        Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
        Method getMethod = negoClass.getMethod("get", SSLSocket.class);
        Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
        return new JdkWithJettyBootPlatform(putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
    } catch (ClassNotFoundException | NoSuchMethodException ignored) {
    }
    return new Platform();
}

这里采用了Java的反射原理调用到class的method。最后在个字的平台调用下发起invoke来执行响应方法。详情请参看继承了Platform的Android类。
当然要做这两种的平台适应,必须要知道当前平台在内存中相关的class地址以及相关方法。

总结

  1. 从整体结构和类内部域中都可以看都OkHttpClient,有点类似于安卓的ApplicationContext。看起来更像一个单例类,这样使用好处是统一。但是如果你不是高手,建议别这么用,原因很简单:逻辑牵涉太深,如果出现问题要去追踪,你会有深深地罪恶感的。
  2. 框架中的一些动态方法、静态方法、匿名内部类以及Internal的这些代码相当规整,每个不同类的不同功能划分在不同的地方。很值得开发者学习的地方。
  3. 从平台的兼容性来讲,也是很不错的典范(如果你以后要从事API相关编码,那更得好好注意对兼容性的处理)
  4. 由于时间不是很富裕,所以本人对细节的把我还是不够,这方面害的多多努力
  5. 对于初学网络编程的同学来捉,可能一开始学习都是从简单的ocket的发起然后获取响应开始的。因为没有很好的场景能让自己知道网络编程到底有多么的重要,当然估计也没感受到网络编程有多么的难受。我想这是很多刚入行的同学们的一种内心痛苦之处。
  6. 不足的地方是没有对SPDY的方式更详细跟进剖析

结束语
老实说,这篇文章大部分都是摘抄别人的,但是图是自己画的,中间一部分英文是翻译了的,文章也是一个字一个字敲出来的,重在学习的过程。最后贴上原创地址http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0326/2643.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值