美文网首页Netandroid网络开发资源贴
Okhttp使用指南与源码分析

Okhttp使用指南与源码分析

作者: 背影杀手不太冷 | 来源:发表于2016-06-24 10:29 被阅读5937次

    Okhttp使用指南与源码分析

    标签(空格分隔): Android


    使用指南篇#

    为什么使用okhttp###

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

     - 支持 SPDY ,共享同一个Socket来处理同一个服务器的所有请求,socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数
     
     - 拥有队列线程池,轻松写并发
     
     - 如果SPDY不可用,则通过连接池来减少请求延时
     
     - 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING),无缝的支持GZIP来减少数据流量
     
     - 基于Headers的缓存策略,缓存响应数据来减少重复的网络请求
     
    -会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。
    
    -使用 OkHttp 无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpURLConnection一样的API。如果您用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块。
    

    Okio库###

    Okio库是一个由square公司开发的,它补充了Java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和处理你的数据。而OkHttp的底层也使用该库作为支持。而在开发中,使用该库可以大大给你带来方便。
    因为okhttp用到了Okio库,所以在Gradle的配置也要引入Okio
    compile 'com.squareup.okio:okio:1.6.0' //具体版本以最新的为准


    用法细节###

    同步就用execute,用这个实现异步,要自己写线程所以还不如用下面的enqueue实现异步
    异步就用enqueue,okHttp内部自动实现了线程,自带工作线程池

    取消操作
      网络操作中,经常会使用到对请求的cancel操作,okhttp的也提供了这方面的接口,当call没有必要的时候,使用这个api可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的call都可以取消。
      call的cancel操作。使用Call.cancel()可以立即停止掉一个正在执行的call。如果一个线程正在写请求或者读响应,将会引发IOException,
      同时可以通过Request.Builder.tag(Objec tag)给请求设置一个标签,并使用OkHttpClient.cancel(Object tag)来取消所有带有这个tag的call。但如果该请求已经在做读写操作的时候,cancel是无法成功的,会抛出IOException异常。

    具体用法请见以下的博客::####

    [博客一][1]
    [博客二][2]
    [也可以看一下鸿洋的封装自己的Okhttp库的文章,在前面的部分也提及到一些基础用法][3]
    [稀土掘金的翻译文章][4]
    注意上面的几篇博客对应的OkHttp版本已经过时了,例如

     private void postRequest() {
            final OkHttpClient client = new OkHttpClient();
           // RequestBody formBody = new FormEncodingBuilder()//在3.0版本FormEncodingBuilder已经被FormBody代替
            RequestBody formBody = new FormBody.Builder()
                    .add(Constant.PUSHID,pushID)
                    .build();
    
            final Request request = new Request.Builder()
                    .url(Constant.TUTOR_LOOKQUESKTION)
                    .post(formBody)
                    .build();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Response response = null;
                    try {
                        response = client.newCall(request).execute();
                        if (response.isSuccessful()) {
                            String result=response.body().string();
                            //String result2=response.body().string();//每个 body 只能被消费一次,多次消费会抛出异常;例如这里的result2会报错,因为是第二次获取body了
                            response.body().close();//body 必须被关闭,否则会发生资源泄漏;
                            Log.i("WY","打印POST响应的数据:" +result );
                            try {
                                getQuestionData(result,1);
                            }
                            catch (Exception e){
                                e.printStackTrace();
                            }
                        } else {
                            throw new IOException("Unexpected code " + response);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
        }
    

    缓存

    Okhttp已经内置了缓存,使用DiskLruCache;默认是不使用的,如果想使用缓存我们需要手动设置。要建立缓存,要有以下条件:

    • 可以读写的缓存目录
    • 缓存大小的限制
    • 缓存目录应该是私有的,不信任的程序不能读取缓存内容
    • 全局用户唯一的缓存访问实例。okhttp框架全局必须只有一个OkHttpClient实例(new OkHttpClient()),并在第一次创建实例的时候,配置好缓存。
    OkHttp内部维护着清理线程池,实现对缓存文件的自动清理,而且内部的DiskLrucache结合了LinkedHashMap使用LRU算法,从而实现对缓存文件的有效管理
    
    

    如果服务器支持缓存,请求返回的Response会带有这样的Header:Cache-Control, max-age=xxx,这种情况下我们只需要手动给okhttp设置缓存就可以让okhttp自动帮你缓存了。这里的max-age的值代表了缓存在你本地存放的时间,可以根据实际需要来设置其大小。
    例如可以在reponse的header设置max-age的值,,一般用在服务器不支持缓存,然后需要使用Interceptor来重写Respose的头部信息 ,见下文

     Request request = chain.request();
            Response response = chain.proceed(request);
            Response response1 = response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    //cache for 30 days
    .header("Cache-Control", "max-age=" + 3600 * 24 * 30)
    //设置max-age的值       
                    .build();
    

    也可以在request的header设置max-age的值,一般用在服务器支持缓存

    int maxStale = 60 * 60 * 24 * 28; //4周
    Request request = new Request.Builder()
            .header("Cache-Control", "max-stale=" + maxStale)
            .url("http://publicobject.com/helloworld.txt")
            .build();
    

    注:HTTP header中的max-age 和max-stale区别:
    max-age 指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
    max-stale 指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。


    如果服务器支持缓存:###

    开启缓存,并且设置缓存目录

    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    File cacheDirectory = new File("cache");
    //出于安全性的考虑,在Android中我们推荐使用Context.getCacheDir()来作为缓存的存放路径
    if (!cacheDirectory.exists()) {
        cacheDirectory.mkdirs();
    }
    Cache cache = new Cache(cacheDirectory, cacheSize);
    OkHttpClient newClient = okHttpClient.newBuilder()
            .Cache(cache)
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .build();
    
    

    如果服务器不支持缓存:###

    如果服务器不支持缓存就可能没有指定这个头部,或者指定的值是如no-store等,但是我们还想在本地使用缓存的话要怎么办呢?这种情况下我们就需要使用Interceptor来重写Respose的头部信息,从而让okhttp支持缓存。
    如下所示,我们重写的Response的Cache-Control字段

    public class CacheInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response response = chain.proceed(request);
            Response response1 = response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    //cache for 30 days
                .header("Cache-Control", "max-age=" + 3600 * 24 * 30)
                    .build();
            return response1;
        }
    }
    

    然后将该Intercepter作为一个NetworkInterceptor加入到okhttpClient中:

     OkHttpClient okHttpClient = new OkHttpClient();
    
    OkHttpClient newClient = okHttpClient.newBuilder()
            .addNetworkInterceptor(new CacheInterceptor())
            .cache(cache)
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .build();
    

    这样我们就可以在服务器不支持缓存的情况下使用缓存了。
    [参考文章][5]


    • 强制使用网络响应
           Request request = new Request.Builder()
           .header("Cache-Control", "no-cache") // 刷新数据
           .url("http://publicobject.com/helloworld.txt")
           .build();
    
    • 通过服务器验证缓存数据是否有效
    Request request = new Request.Builder()
            .header("Cache-Control", "max-age=0")
            .url("http://publicobject.com/helloworld.txt")
            .build();
    
    • 强制使用缓存响应
    Request request = new Request.Builder()
            .header("Cache-Control", "only-if-cached")
            .url("http://publicobject.com/helloworld.txt")
            .build();
    
    • 指定缓存数据过时的时间
    int maxStale = 60 * 60 * 24 * 28; //4周
    Request request = new Request.Builder()
            .header("Cache-Control", "max-stale=" + maxStale)
            .url("http://publicobject.com/helloworld.txt")
            .build();
    

    [参考文章][6]


    okhttp框架获取响应数据有三种方法:

    • 返回网络上的数据。如果没有使用网络,则返回null。

    public Response networkResponse();//从网络返回的response取数据

    • 返回缓存中的数据。如果不使用缓存,则返回null。对应发送的GET请求,缓存响应和网络响应 有可都非空。
    public Response priorResponse()```
    
    

    networkResponse()与cacheResponse()是互斥的,要是networkResponse()返回的不为空null,那么cacheResponse()就会返回为nul;反之亦然。具体为什么我还没搞清楚??????

    所以在我们用request去请求网络数据的步骤,可以这样写:
    

    if (null != response.cacheResponse()) {
    String str = response.cacheResponse().toString();
    //这里有一个问题就是为什么要用一个reponse对象去拿缓存呢?不是应该要用request,像volley一样吗???????
    Log.i("wangshu", "cache---" + str);
    } else {
    response.body().string();
    String str=response.networkResponse().toString();
    Log.i("wangshu", "network---" + str);
    }

    
    ---
    下面是强制情况:
    但有时候即使在有缓存的情况下我们依然需要去后台请求最新的资源(比如资源更新了)这个时候可以使用强制走网络来要求必须请求网络数据
    
    

    request = request.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build();

    同样的我们可以使用 FORCE_CACHE 强制只要使用缓存的数据
    

    request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();

    注意:
    如果开启了缓存。但是要强制去网络更新即配置了CacheControl.FORCE_NETWORK的话,程序不会报错。这时对应的缓存会清掉(注意不是所有的缓存喔!)即cacheResponse()返回为空,等同于不使用缓存;
    但如果请求必须从网络获取才有数据,却配置了CacheControl.FORCE_CACHE的话就会返回504错误,
    综上所述,可以这样理解:FORCE_CACHE是第一道门,而FORCE_NETWORK是第二道门。如果使用了第一道门,即配置了CacheControl.FORCE_CACHE,所有的请求都会从cache缓存里面取,这时要请求网络的话是过不了第一道门的;但是如果使用的是第二道门的话,这时是可以过第一道门即绕过缓存,而且会把对应的缓存清掉,然后从网络取数据

    
    关于OkHttp的封装请看鸿洋的博客
    [ Android OkHttp完全解析 是时候来了解OkHttp了][7] //这篇文章我还分析完》》》未完待续
    
    ---
    
    
    
    [参考博客][8]
    
    
      ---
      
    #源码分析篇#
    
    
    ##更新补充##
    推荐在分析之前先看看这篇文章
    一片分析OkHttp不错的文章:[拆轮子系列:拆 OkHttp][18]
    ##博客一##
    [参考博客][9]
    ![总体设计图][10]
    
    上面是OKHttp总体设计图,主要是通过Diapatcher不断从RequestQueue中取出请求(Call),根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据。该引擎有同步和异步请求,一个是普通的同步单线程;另一种是使用了队列进行并发任务的分发(Dispatch)与回调。同步请求通过Call.execute()直接返回当前的Response,而异步请求会把当前的请求Call.enqueue添加(AsyncCall)到请求队列中,并通过回调(Callback)的方式来获取最后结果。
    
    注意:虽然在使用的没有那么明显,但是okhttp也有一个RequestQueue
    
    ![请求流程图][11]
    
    
    ![类设计图][12]
      从OkHttpClient类的整体设计来看,它采用```***门面模式***```来。client知晓子模块的所有配置以及提供需要的参数。client会将所有从客户端发来的请求委派到相应的子系统去。
      在该系统中,有多个子系统、类或者类的集合。例如上面的cache、连接以及连接池相关类的集合、网络配置相关类集合等等。每个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。同时,OkHttpClient可以看作是整个框架的上下文。
      通过类图,其实很明显反应了该框架的几大核心子系统;路由、连接协议、拦截器、代理、安全性认证、连接池以及网络适配。从client大大降低了开发者使用难度。同时非常明了的展示了该框架在所有需要的配置以及获取结果的方式。
    
    ##同步与异步的实现##
    在发起请求时,整个框架主要通过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来完成。
    
    ##拦截器有什么作用##
    先来看看Interceptor本身的文档解释:观察,修改以及可能短路的请求输出和响应请求的回来。通常情况下拦截器用来添加,移除或者转换请求或者回应的头部信息。
    从这里的执行来看,拦截器主要是针对Request和Response的切面处理。
    
    

    在这里再多说一下关于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();
    }
    }
    第一次是来至网络数据,第二次来至缓存

    从Call.getResponse(Request request, boolean forWebSocket)执行Engine.sendRequest()和Engine.readResponse()来详细说明一下。
    具体的sendRequest()和readResponse()请看博客
    sendRequest()
    此方法是对可能的Response资源进行一个预判,如果需要就会开启一个socket来获取资源。如果请求存在那么就会为当前request添加请求头部并且准备开始写入request body。
    readResponse()
    此方法发起刷新请求头部和请求体,解析HTTP回应头部,并且如果HTTP回应体存在的话就开始读取当前回应头。在这里有发起返回存入缓存系统,也有返回和缓存系统进行一个对比的过程。
    
    ##HTTP连接的实现方式(说说连接池)##
    外部网络请求的入口都是通过Transport接口来完成。该类采用了```桥接模式```将HttpEngine和HttpConnection来连接起来。因为HttpEngine只是一个逻辑处理器,同时它也充当了请求配置的提供引擎,而HttpConnection是对底层处理Connection的封装。
    HttpConnection(一个用于发送HTTP/1.1信息的socket连接)这里。主要有如下的生命周期:
    

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

    ![连接执行时序图][13]
    

    1、连接池是暴露在client下的,它贯穿了Transport、HttpEngine、Connection、HttpConnection和SpdyConnection;在这里目前默认讨论HttpConnection;
    2、ConnectionPool有两个构建参数是maxIdleConnections(最大空闲连接数)和keepAliveDurationNs(存活时间),另外连接池默认的线程池采用了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);
    source负责读取,sink负责写入

    
    ---
    
    ##简单总结##
    1、从整体结构和类内部域中都可以看到OkHttpClient,有点类似与安卓的ApplicationContext。看起来更像一个单例的类,这样使用好处是统一。但是如果你不是高手,建议别这么用,原因很简单:逻辑牵连太深,如果出现问题要去追踪你会有深深地罪恶感的;
    
    ##自己理解##
    

    1.首先Okhttp有自己的线程池,所以可以有效管理线程,OkHttpClient自带并发光环,虽然Volley相比Okhttp是高级的封装库,但是Volley没有线程池,而且工作线程是自己维护的,那么就有可能存在线程由于异常退出之后,没有下一个工作线程补充的风险(线程池可以弥补这个缺陷),相对底层的Okhttp却没有这个风险。

      ---
      
      
    ##博客二##
    [参考博客][14]
    ###***因为Okhttp不同于Volley这样的高层库,它是类似于UrlHttpConnection的底层库,所以要涉及到一些数据传输的知识,类似socket***###
    
    ##第一部分:请求的分发和任务队列##
    
    
    ##主要对象##
    Connections: 对JDK中的socket进行了封装,用来控制socket连接
    Streams: 维护HTTP的流,用来对Requset/Response进行IO操作
    Calls: HTTP请求任务封装
    StreamAllocation: 用来控制Connections/Streams的资源分配与释放
    
    
    ##请求的分发##
    当我们用OkHttpClient.newCall(request)进行execute/enenqueue时,实际是将请求Call放到了Dispatcher中,okhttp使用Dispatcher进行线程分发,它有两种方法,一个是普通的同步单线程;另一种是使用了队列进行并发任务的分发(Dispatch)与回调,我们下面主要分析第二种,也就是队列这种情况,这也是okhttp能够竞争过其它库的核心功能之一
    
    
    ##Dispatcher的结构:##
    maxRequests = 64: 最大并发请求数为64
    maxRequestsPerHost = 5: 每个主机最大请求数为5
    Dispatcher: 分发者,也就是生产者(默认在主线程)
    AsyncCall: 队列中需要处理的Runnable(包装了异步回调接口)
    ExecutorService:消费者池(也就是线程池)
    Deque<readyAsyncCalls>:缓存(用数组实现,可自动扩容,无大小限制)
    Deque<runningAsyncCalls>:正在运行的任务,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存
      根据生产者消费者模型的模型理论,当入队(enqueue)请求时,如果满足(runningRequests<64 && runningRequestsPerHost<5),那么就直接把AsyncCall直接加到runningCalls的队列中,并在线程池中执行。如果消费者缓存满了,就放入readyAsyncCalls进行缓存等待。
      当任务执行完成后,调用finished的promoteCalls()函数,手动移动缓存区(这里是主动清理的,而不会发生死锁)
      
    ##OkHttp配置的线程池##
    在OkHttp,使用如下构造了单例线程池
    

    public synchronized ExecutorService executorService() {
    if (executorService == null) {
    executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
    }

    可以看出,在Okhttp中,构建了一个阀值为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒,它使用了一个不存储元素的阻塞工作队列,一个叫做"OkHttp Dispatcher"的线程工厂。
    

    这里解释一下不存储元素的阻塞工作队列SynchronousQueue,Dispatcher内部实现了懒加载无边界限制的线程池方式,同时该线程池采用了SynchronousQueue这种阻塞队列。SynchronousQueue每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其 实没有任何一个元素,或者说容量是0,严格说并不是一种容器。由于队列没有容量,因此不能调用peek操作,因为只有移除元素时才有元素。显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入者(生产者)传递给移除者(消费者),这在多任务队列中是最快处理任务的方式。对于高频繁请求的场景,无疑是最适合的。

    
    也就是说,在实际运行中,当收到10个并发请求时,线程池会创建十个线程,当工作全部完成后,线程池会在60s后相继关闭所有线程。
    
    ---
    ##自己对于为什么Okhttp的线程管理优越于Volley的一些理解##
    首先是Volley中的工作线程是自己维护的,那么就有可能存在线程由于异常退出之后,没有下一个工作线程补充的风险(线程池可以弥补这个缺陷),那okhtyp是怎样避免volley的这个缺陷呢?首先是okhttp是有线程池的,其次这个线程池采用了SynchronousQueue这种阻塞队列。(SynchronousQueue每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。)所以当一个线程发生异常要退出,但是因为在SynchronousQueue的管理下,所以不会马上退出,还要等新的线程加入后才会退出,所以避免了线程异常退出后没有线程补充的问题
    然后是线程池会自动关闭空闲的线程,注意这里是关闭而已,不是销毁,所以线程池还是会有线程的
    
    ---
    
    ##反向代理模型##
    在OkHttp中,使用了与Nginx类似的反向代理与分发技术,这是典型的单生产者多消费者问题。
    我们知道在Nginx中,用户通过HTTP(Socket)访问前置的服务器,服务器会自动转发请求给后端,并返回后端数据给用户。通过将工作分配给多个后台服务器,可以提高服务的负载均衡能力,实现非阻塞、高并发连接,避免资源全部放到一台服务器而带来的负载,速度,在线率等影响。
    ![此处输入图片的描述][15]
    而在OkHttp中,非常类似于上述场景,它使用Dispatcher作为任务的转发器,线程池对应多台后置服务器,用AsyncCall对应Socket请求,用Deque<readyAsyncCalls>对应Nginx的内部缓存
    ![此处输入图片的描述][16]
    
    可以发现请求是否进入缓存的条件如下:
    ```(runningRequests<64 && runningRequestsPerHost<5)```
    如果满足条件,那么就直接把AsyncCall直接加到runningCalls的队列中,并在线程池中执行(线程池会根据当前负载自动创建,销毁,缓存相应的线程)。反之就放入readyAsyncCalls进行缓存等待。
    
    当任务执行完成后,无论是否有异常,finally代码段总会被执行,也就是会调用Dispatcher的finished函数,打开源码,发现它将runningAsyncCalls中对应的Call移除后,接着执行promoteCalls()函数
    
    ##第一部分:Summary请求的分发和任务队列##
    通过上述的分析,我们知道了:
    
    OkHttp采用Dispatcher技术,类似于Nginx,与线程池配合实现了高并发,低阻塞的运行
    Okhttp采用Deque作为缓存,按照入队的顺序先进先出
    OkHttp最出彩的地方就是在try/finally中调用了finished函数,可以主动控制队列的移动,而不是采用锁,极大减少了编码复杂性
    
    ---
    
    ##第二部分:Socket管理##
    ###科普###
    通常我们进行http连接时,首先进行tcp握手,然后传输数据,最后释放
    这种方法的确简单,但是在复杂的网络内容中就不够用了,创建socket需要进行3次握手,而释放socket需要2次握手(或者是4次)。重复的连接与释放tcp连接就像每次仅仅挤1mm的牙膏就合上牙膏盖子接着再打开接着挤一样。而每次连接大概是TTL一次的时间(也就是ping一次),甚至在TLS环境下消耗的时间就更多了。很明显,当访问复杂网络时,延时(而不是带宽)将成为非常重要的因素。
    当然,上面的问题早已经解决了,在http中有一种叫做keepalive connections的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手
    当然keepalive也有缺点,在提高了单个客户端性能的同时,复用却阻碍了其他客户端的链路速度
    
    ---
    在okhttp中,socket连接池对用户,甚至开发者都是透明的。它自动创建socket连接池,自动进行泄漏连接回收,自动帮你管理线程池,提供了put/get/clear的接口,甚至调用都帮你写好了。
    我们知道在socket连接中,也就是Connection中,本质是封装好的流操作,除非手动close,基本不会被gc掉,非常容易引发内存泄露。但是Okhttp通过引用计数法,实现将没有被使用的Socket关闭。java内部有垃圾回收GC,okhttp有socket回收SocketClean;垃圾回收是根据对象的引用树实现的,而okhttp是根据RealConnection的虚引用StreamAllocation引用计数是否为0实现的。
    ##总结##
    通过上面的分析,我们可以总结,okhttp使用了类似于引用计数法与标记擦除法的混合使用,当连接空闲或者释放时,StreamAllocation的数量会渐渐变成0,从而被线程池监测到并回收,这样就可以保持多个健康的keep-alive连接,Okhttp的黑科技就是这样实现的。
    
    ---
    ##第三部分:HTTP请求序列化/反序列化##
    1. 获得HTTP流(httpStream)
    我们已经在上文第二部分的RealConnection通过connectSocket()构造HttpStream对象并建立套接字连接(完成三次握手)
    ```httpStream = connect();```
    在connect()有非常重要的一步,它通过***okio库***与远程socket建立了I/O连接,为了更好的理解,我们可以把它看成管道
    

    /source 用于获取response
    source = Okio.buffer(Okio.source(rawSocket));
    //sink 用于write buffer 到server
    sink = Okio.buffer(Okio.sink(rawSocket));

    ***Okhttp的I/O使用的是Okio库,它是java中最好用的I/O API***
    
    2. 拼装Raw请求与Headers(writeRequestHeaders)
    我们通过Request.Builder构建了简陋的请求后,可能需要进行一些修饰,这时需要使用Interceptors对Request进行进一步的拼装了。
    
    ***拦截器***是okhttp中强大的流程装置,它可以用来监控log,修改请求,修改结果,甚至是对用户透明的GZIP压缩。类似于函数式编程中的flatmap操作。在okhttp中,内部维护了一个Interceptors的List,通过InterceptorChain进行多次拦截修改操作。
    ![此处输入图片的描述][17]
    
    ---
    ##更新补充##
    推荐在分析之前先看看这篇文章
    一片分析OkHttp不错的文章:[拆轮子系列:拆 OkHttp][18]
    
    
      [1]: http://codecloud.net/android-okhttp-6425.html
      [2]: http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html
      [3]: http://blog.csdn.net/lmj623565791/article/details/47911083
      [4]: http://gold.xitu.io/entry/5728441d128fe1006058b6b9
      [5]: http://mushuichuan.com/2016/03/01/okhttpcache/
      [6]: http://aiwoapp.blog.51cto.com/8677066/1619654
      [7]: http://blog.csdn.net/lmj623565791/article/details/47911083
      [8]: http://blog.csdn.net/chenzujie/article/details/46994073
      [9]: http://frodoking.github.io/2015/03/12/android-okhttp/
      [10]: http://o6uwc0k25.bkt.clouddn.com/%E6%80%BB%E4%BD%93%E8%AE%BE%E8%AE%A1.jpg
      [11]: http://o6uwc0k25.bkt.clouddn.com/%E8%AF%B7%E6%B1%82%E6%B5%81%E7%A8%8B%E5%9B%BE.jpg
      [12]: http://o6uwc0k25.bkt.clouddn.com/%E7%B1%BB%E8%AE%BE%E8%AE%A1%E5%9B%BE.jpg
      [13]: http://o6uwc0k25.bkt.clouddn.com/%E8%BF%9E%E6%8E%A5%E6%89%A7%E8%A1%8C%E6%97%B6%E5%BA%8F%E5%9B%BE.jpg
      [14]: http://www.jianshu.com/p/aad5aacd79bf
      [15]: http://o6uwc0k25.bkt.clouddn.com/%E5%8D%95%E7%94%9F%E4%BA%A7%E8%80%85%E5%A4%9A%E6%B6%88%E8%B4%B9%E8%80%85.jpg
      [16]: http://o6uwc0k25.bkt.clouddn.com/diapatcher.jpg
      [17]: http://o6uwc0k25.bkt.clouddn.com/%E6%8B%A6%E6%88%AA%E5%99%A8.jpg
      [18]: http://mp.weixin.qq.com/s?__biz=MzA4MjU5NTY0NA==&mid=2653419018&idx=1&sn=932eec802048861e616a10fb8aca083b&scene=1&srcid=0818NBtIt2B6T6VtfUcQOb85#rd

    相关文章

      网友评论

      • 6b31b8838b70:有了一个比较清醒的认识
      • Answer_yzpppp:楼主大神,想问下http keep-alive和socket连接池的一个关系。socket连接池中的连接都是默认为keep-alive的吗?

      本文标题:Okhttp使用指南与源码分析

      本文链接:https://www.haomeiwen.com/subject/iwyodttx.html