OkHttp使用总结 & 关键源码分析

#.整体介绍

    OkHttp是Android开发中使用非常普遍的一个网络请求库,它封装和实现了Http协议的相关功能。
    官方地址: https://github.com/square/okhttp
     既然是对Http协议的实现,那就绕不开Http协议的原理与报文结构,Http报文结构可参考笔记:  Http协议报文格式_丞恤猿的博客-CSDN博客_http协议请求报文的完整格式
     很多网络库都是围绕某种网络协议,实现了网络协议的相关功能,并且提供对应接口,封装了内部各种细节,供使用者简化开发过程。Android从最初用HttpUrlConnection到现在普遍适用OkHttp、Retrofit,网络库会不断推陈出新,但作为其内核的Http网络协议则鲜有重大变化,所以要理解这些网络库的原理,首先要先基本了解对应的网络协议。

##.OkHttp优点

1.OkHttp内部在TCP层的连接是用Socket来实现的,内置socket连接池,能够高效复用TCP连接,减少了握手次数和请求延迟。

   而且允许上层连接到同一个主机地址的Http请求共享Socket,减少了在TCP层对服务器的请求次数。

2.内置线程池,高效管理异步请求时的线程使用;

3.支持缓存,可减少不必要的网络请求;

4.支持gzip对传输数据进行压缩/解压缩。

5.提供了强大的API支持,能够进行各种自定义配置。

##.OkHttp与Http协议的对应:

Request : 封装Http请求报文
Response : 封装Http响应报文
Call : 在OkHttp架构中,对应一次网络请求任务
OkHttpClient : 在OkHttp架构中,承担客户端角色,通过它可进行网络请求的各种通用设置,例如超时时长、缓存策略、是否设置代理、各种拦截器等。
(OkHttp内部在TCP层的连接是用Socket来实现的,内部会自动维护socket连接池,减少握手次数,减少了请求延迟。允许上层连接到同一个主机地址的所有Http请求共享Socket,减少了在TCP层对服务器的请求次数。)

##.OkHttp的基本使用步骤简介:

1.创建OkHttpClient,设置各种网络请求参数,所有未手动设置的,在OkHttpClient构造时内部都会指定一个默认参数。
2.创建Request对象并设置对应参数,实际上是在设置Http请求报文。
3.用Request来创建对应的Call对象,一个Call对应一次网络请求任务
4.调用Call的同步请求/异步请求方法,获取网络请求结果Response,该对象封装了Http响应报文。按照自己的需求对Response中的数据做解析。

##.OkHttp提供的功能支持简介:

1.OkHttp内部通过Dispatcher类(调度器)来支持Call的同步请求/异步请求
1.1Call的同步请求:会在当前线程中执行网络请求并等待请求结果;
1.2.Call的异步请求:会在额外的工作线程中执行网络请求并在回调接口中返回结果。
      这些额外的线程在OkHttp内部通过线程池管理和复用,默认支持的最大同时网络请求数是64.
2.OkHttp提供了一堆拦截器(Interceptor),针对Http协议中的各个步骤,调整Request和Response中的参数。
   这些拦截器是链式执行的,如下图所示。
    在发起网络请求时,由上往下执行,最先执行的是自定义的拦截器,这个过程会不断地在各个拦截器根据需要调整Request中的请求参数; 
    在网络请求数据返回后,由下往上执行,最后执行的是自定义的拦截器,这个过程这个过程会不断地在各个拦截器根据需要调整Response中的数据。
    这样,开发者可调整某个步骤的拦截器逻辑来满足自己的需求。比方说,可以新增一个自定义拦截器,在网络请求最初填入一些通用的请求参数,在网路请求结果返回时把数据解析成自己需要的数据对象。
(图片来自网络)

#.使用步骤讲解

一、构建OkHttpClient

//        //方式1.直接构建客户端对象OkHttpClient,所有的参数都使用默认值
//        mHttpClient = new OkHttpClient();

        //方式2.用构造器模式创建OkHttpClient实例
        //首先设置好构造器的相关参数
        OkHttpClient.Builder builder = new OkHttpClient.Builder();

        builder.connectTimeout(5, TimeUnit.SECONDS)//连接超时,针对过程:建立TCP连接的过程(三次握手)。默认时长是10s
                //读操作超时,这个值用于两个过程:1.TCP层连接后,Socket若超过该时长仍无数据可读,则超时;2.IO操作,从Source对象读数据的过程
                .readTimeout(10, TimeUnit.SECONDS)
                //写操作超时,针对过程:IO操作,用Sink对象写数据的过程
                .writeTimeout(10, TimeUnit.SECONDS);
//                //call超时,针对过程:从Call请求开始执行/入异步队列开始,到获取到返回数据生成ResponseBody的整个过程。默认没有超时时间。
//                .callTimeout(60, TimeUnit.SECONDS)
        // 非测试包, 设置为禁止代理抓包
        if (!BuildConfig.DEBUG) {
            builder.proxy(Proxy.NO_PROXY);
        }

        //构建客户端对象OkHttpClient
        mHttpClient = builder.build();
OkHttpClient的一些参数:
    final @Nullable Proxy proxy;  //代理
    final List<Protocol> protocols; //支持的协议,默认支持的协议为HTTP_1.1 和 HTTP_2
    final List<Interceptor> interceptors; //拦截器集合
    final EventListener.Factory eventListenerFactory; //监听器,整个网络请求过程的监听器
    final @Nullable Cache cache; //缓存
    final boolean followRedirects; //是否允许重定向
    final boolean retryOnConnectionFailure; //连接失败是否重试
    final int callTimeout; //整个请求过程的超时,默认没有超时
    final int connectTimeout; //连接超时,默认10秒
    final int readTimeout; //读超时,默认10秒
    final int writeTimeout; //写超时,默认10秒

二、创建Request对象并设置对应参数(实际上是在设置Http请求报文)

1.GET请求

GET请求对应的请求报文不含数据体,其参数附在Url后面,格式Url?参数1=值1&参数2=值2……,存储在Http请求报文请求行的URL字段。
//构造Http 请求报文对象
        Request request = new Request.Builder()
                .url(url)
                //指明是GET请求
                .get()
//                //设置请求头,只会有一个值,后面设置的值会覆盖前面的值
//                .header("User-Agent", "OkHttp Headers.java")
//                //添加请求头,可有多个值,后面设置的值不会覆盖前面的值
//                .addHeader("Accept", "application/json; charset=utf-8")
//                .addHeader("Accept", "application/xxxxxxxxx")
                .build();

2.POST请求

GET请求对应的请求报文包含数据体,数据体的对应类是RequestBody。发送不同数据时,数据体在创建时,要设置不同的数据类型。
//        //方式1:自己构造json字符串并传入
//        String json = "{\"tagExample\":\"valueExample\"}";
//        //构造Http POST请求报文数据体对象
//        RequestBody requestBody1 = RequestBody.create(MediaType.get("application/json; charset=utf-8"), json);

//        //方式2:通过new FormBody()调用build方法,创建一个RequestBody,可以用add添加键值对
//        RequestBody  requestBody2 = new FormBody.Builder()
//                .add("name","xxxx")
//                .add("age","25")
//                .build();

//        //方式3:上传文件
//        File file = new File(Environment.getExternalStorageDirectory(), "zhuangqilu.png");
//        //通过RequestBody.create 创建requestBody对象,application/octet-stream 表示文件是任意二进制数据流
//        RequestBody requestBody3 =RequestBody.create(MediaType.parse("application/octet-stream"), file);

        //方式4:以多媒体表单形式上传多种类型的数据,此处是:一些参数+图片文件
        File file = new File(AppUtils.getFileDirPicture(), "xxxx.png");
        //通过new MultipartBody build() 创建requestBody对象,
        RequestBody  requestBody4 = new MultipartBody.Builder()
                //一定要设置类型是表单。这里设置的值,最终会影响Http请求报文的Content-Type头部字段。
                .setType(MultipartBody.FORM)
                //添加数据
                .addFormDataPart("username","xxx")
                .addFormDataPart("age","25")
                //addFormDataPart()方法的第一个参数就是类似于键值对的键,是供服务端使用的,第二个参数是文件的本地的名字,
                //        第三个参数是RequestBody,里面包含了我们要上传的文件的MidiaType以及路径。
                .addFormDataPart("image","xxxx.png",
                        RequestBody.create(MediaType.parse("image/png"),file))
//                //也可以添加其它RequestBody做为表单的一部分
//                .addPart(requestBody1)
                .build();

        //构造Http 请求报文对象
        Request request = new Request.Builder()
                .url(url)
                //指明是POST请求,并设置请求数据体
                .post(requestBody4)
                .build();

三、创建Call对象,并发起网络请求,获取请求结果

1.同步请求:会在当前线程中执行网络请求并等待请求结果

try {
    //执行网络请求,并获取Http响应报文对象
    Response response = mHttpClient.newCall(request).execute();
    if(response == null){
        return;
    }
    //如果请求成功
    if(response.code() == HttpURLConnection.HTTP_OK){
        //获取Http响应报文数据体
        ResponseBody responseBody = response.body();
        //.....做自己的数据解析.....
    }
} catch (IOException e) {
    e.printStackTrace();
}

2.异步请求:会在额外的工作线程中执行网络请求并在回调接口中返回结果

    异步请求会将本次网络请求(Call)加入调度队列,在稍后可执行时去执行该网络请求,并在请求结果返回后执行设置的回调方法。
    如果当前满足执行条件的话,会立即开始执行本次网络请求(一般情况)。具体的流程和条件后面会分析。
//创建Call对象,一个Call可以理解为一次网络请求任务
        Call call = mHttpClient.newCall(request);

        //异步请求方式
        //将本次网络请求加入调度队列,在稍后可执行时去执行该网络请求,并在请求结果返回后执行设置的回调方法
        //如果当前满足执行条件的话,会立即开始执行本次网络请求(一般情况)
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
               
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if(response == null){
                    return;
                }
                //获取响应报文的数据体。响应报文的数据体只能使用该方法消费一次。
                ResponseBody responseBody = response.body();
                //根据需要的数据类型来选择数据输出方式
//                responseBody.bytes();//以byte数组形式输出
//                responseBody.string();//以String形式输出
//                responseBody.charStream();//返回对应的char字符流
//                responseBody.byteStream();//返回对应的String字节流
                //......进行自己的数据解析.......
                //注意不要再这里直接操作View,因为这里执行在工作线程而非主线程
            }
        });

#.关键源码分析(RealCall、Dispatcher)

一、请求过程分析

    Call是一个接口,其实际功能完成者是RealCall。每个RealCall内部有字段标记是否已经发起过请求,发起过就不能再次发起请求。如果希望同样的Call再发起一次请求,可以使用Call.clone()获取一个同样的Call对象。
    调度器Dispatcher中有三个请求缓存队列:
​
public final class Dispatcher {
    //允许同时执行的最大异步网络请求数量
    private int maxRequests = 64;
    //针对单个主机允许同时执行的的最大异步网络请求数量
    //Host示例:www.xxxxxxxxx.com
    private int maxRequestsPerHost = 5;
    //线程池
    private @Nullable ExecutorService executorService;
    //异步请求队列,异步请求开始时会加入该队列,但并未真正开始执行网络请求
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
    //正在执行的异步请求队列。加入该队列后,会去真正执行网络请求。
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
    //同步请求队列
    //Call发起访问时加入该队列,访问完毕后移出该队列
    private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
​

1.同步请求的过程

同步请求比较简单,就是调用同步请求时,加入Dispatcher的同步请求队列,然后网络请求完毕后,移出同步请求队列。    
    

2.异步请求的过程

2.1调用异步请求时,Call会被封装成AsyncCall,并加入异步请求队列,但并未真正开始去做网络请求。
(AsyncCall是实现了Runnable的类,可被线程池中线程去执行。)
2.2接下去会执行Dispatcher的 promoteAndExecute (),该方法内部会过滤出异步请求队列中满足以下条件的AsyncCall:
当前正在执行的异步请求数小于64,且针对目标Host上正执行的异步请求数小于5。
满足条件的AsyncCall会被移动到正在执行异步队列中,从线程池中调配线程来执行其run()方法。
2.3执行完毕后,会执行Dispatcher的 finished()方法,再次触发 promoteAndExecute ()方法,进行下一轮筛选,不断重复这个过程,最终将异步请求队列中还未执行的AsyncCall执行完。

3.关键源码

同步请求会执行:
@Override public Response execute() throws IOException {
    synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    transmitter.timeoutEnter();
    transmitter.callStart();
    try {
        client.dispatcher().executed(this);
        return getResponseWithInterceptorChain();
    } finally {
        client.dispatcher().finished(this);
    }
}

异步请求最终会执行:

下面的responseCallback就是做异步请求时传入的Callback。
无论同步请求还是异步请求,最终实际完成网络请求的都是getResponseWithInterceptorChain(),后面讲拦截器是会分析该方法。
@Override protected void execute() {
    boolean signalledCallback = false;
    transmitter.timeoutEnter();
    try {
        Response response = getResponseWithInterceptorChain();
        signalledCallback = true;
        responseCallback.onResponse(okhttp3.RealCall.this, response);
    } catch (IOException e) {
        if (signalledCallback) {
            // Do not signal the callback twice!
            Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
            responseCallback.onFailure(okhttp3.RealCall.this, e);
        }
    } catch (Throwable t) {
        cancel();
        if (!signalledCallback) {
            IOException canceledException = new IOException("canceled due to " + t);
            canceledException.addSuppressed(t);
            responseCallback.onFailure(okhttp3.RealCall.this, canceledException);
        }
        throw t;
    } finally {
        client.dispatcher().finished(this);
    }
}

二、拦截器分析

##.拦截器内的处理逻辑概括

public class HttpInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        //1.获取当前传递给当前拦截器的Request对象(封装了请求报文的参数和数据)
        Request request = chain.request();
        //.......根据需要对Request对象做调整........

        //2.将修改后Request传递给链条中的下一个拦截器,并最终触发下一个拦截器的intercept(Chain chain)方法
        //  获取从下一个拦截器处返回的数据
        Response response = chain.proceed(request);

        //3......根据需要,对从下一个拦截器处返回的数据Response做调整......

        return response;
    }
}

##.拦截器链条的处理过程分析

    OkHttp拦截器的设计采用了责任链模式,每个拦截器在链条中只处理自己那一环的输入和输出。
    在代码实现中,就是先创建一个列表,把自定义和预设的拦截器按顺序加入到列表中。
    用一个索引值来标明当前该那个拦截器来处理了,每次取出对应的拦截器触发其intercept()方法,然后将索引值+1。
    再具体点儿,就是每次都会创建一个RealInterceptorChain对象,这个对象中存储着当前拦截器索引、拦截器列表、当前拦截器的传入Request对象等,然后调用chain.proceed(Request)。第一次的chain.proceed(Request)是由getResponseWithInterceptorChain()触发的,后面都是由拦截器中的intercept(Chain chain)触发的。
Response getResponseWithInterceptorChain() throws IOException {
    //0.创建一个拦截器链列表
    List<Interceptor> interceptors = new ArrayList<>();
    //1.添加自定义拦截器
    interceptors.addAll(client.interceptors());
    //2.添加重试和重定向的拦截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //3.添加处理请求头和响应体的拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //4.添加处理缓存逻辑的拦截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //5.添加处理连接的拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
        interceptors.addAll(client.networkInterceptors());
    }
    //6.添加发送请求报文、解析响应报文的拦截器
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
            originalRequest, this, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
        Response response = chain.proceed(originalRequest);
chain.proceed(originalRequest)里的关键代码,如果index索引值未到达列表尾部,会构建一个新的RealInterceptorChain。该RealInterceptorChain的索引值是下一个拦截器的索引值,但其中的Request是当前要执行的拦截器的输入Request,然后触发当前拦截器的intercept(Chain chain),当前拦截器的intercept(Chain chain)又会触发下一个chain.proceed(originalRequest),然后又会触发下一个拦截器的intercept(Chain chain),……周而复始……,直到所有拦截器都被触发完。
//构建新的RealInterceptorChain,其中的索引值是下一个拦截器的索引值,其中的Request是当前要执行的拦截器的输入Request
RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
    index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);

##.添加自定义的拦截器

其实很简单,以上面自定义的HttpInterceptor为例,在构造OkHttpClient时:
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(5, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                //添加自定义的拦截器
                .addInterceptor(new HttpInterceptor());
        //构建客户端对象OkHttpClient
        mHttpClient = builder.build();

#.OkHttp内部的五个拦截器功能介绍

1.RetryAndFollowUpInterceptor:处理重试和重定向

根据Response的返回码,来判断是否进行重新请求或者重定向。
当返回码是200时,说明请求成功,向上层返回结果。
当返回码是307、407时,说明是重定向,此时服务端向客户端返回了新的请求新的url地址,放在头部的Location字段,OkHttp会取出该url,封装新的request并发起请求。
其它情况时,一般是访问失败,会重新发起请求。最多只能重试20次。

2.BridgeInterceptor:处理请求对象的头部字段、处理结果数据

添加一些默认的请求头,如Cookie、Connection、Content-Type、Content-Length等;
对返回数据做处理,例如当返回数据被压缩时,会解压数据。

3.CacheInterceptor:根据缓存策略进行缓存相关的处理

    OkHttp在 CacheInterceptor中按照请求、响应报文中设置的缓存策略,来进行数据缓存并使用这些缓存数据。
    缓存策略由Http报文中一些头部字段来决定,最常见的就是Cache-Control字段,可以在请求报文中指定、也可以在响应报文中指定。OkHttp默认构造的Request是不设置这些字段的,如果要用,需要自己设置该头部字段。如果服务端返回的相应报文中配置了缓存策略,OkHttp也会根据缓存策略进行相应的处理。

3.1相关头部字段中可设置的值:

3.1.1请求报文中:
no-store: 不缓存任何内容,永远去服务端获取最新内容;
no-cache: 并非完全不缓存,而是每次都要去服务端确认客户端的缓存数据是否有效未过期,有效则可使用本地缓存。
max-age: 表示可使用过期一定时间的缓存数据,同指定了参数的max-stale;
max-stale: 表示可使用过期的缓存,如后面未指定参数,则表示永远接收缓存数据。如max-stale: 3600, 表示可接受过期1小时内的数据;
min-fresh: 表示可使用指定时间内的缓存数据,不考虑其是否过期,只要是缓存后未超过这个时间就可用。
only-if-cache: 表示直接获取缓存数据,若没有数据返回,则返回504(Gateway Timeout)
no-transform:不得对响应进行转换或转变
3.1.2响应报文中
public: 可向任一方提供缓存数据;
private: 只向指定用户提供缓存数据;
no-cache: 缓存前需确认其有效性;
no-store: 不缓存请求或响应的任何内容;
max-age: 表示缓存的最大时间,在此时间范围内,访问该资源时,直接返回缓存数据。不需要对资源的有效性进行确认;
must-revalidate: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale将无效。

3.1.3使用示例

        //创建缓存对象,指定缓存路径和大小
        Cache cache = new Cache(new File(AppUtils.getFileDirDocuments(),"cacheFile"),10 * 1024 * 1024);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(5, TimeUnit.SECONDS)/
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                //指定缓存路径和大小
                .cache(cache)
                //添加自定义的拦截器
                .addInterceptor(new HttpInterceptor());
        //构建客户端对象OkHttpClient
        mHttpClient = builder.build();

自定义的拦截器,来为Request添加缓存设置。有网络,使用网络请求;无网络,使用未过期的缓存数据;

public class HttpInterceptor implements Interceptor {

    //自定义的拦截器,来为Request添加缓存设置
    //有网络,使用网络请求;
    //无网络,使用未过期的缓存数据;
    @Override
    public Response intercept(Chain chain) throws IOException {
        //1.获取当前传递给当前拦截器的Request对象(封装了请求报文的参数和数据)
        Request request = chain.request();
        //.......根据需要对Request对象做调整........


        //2.将修改后Request传递给链条中的下一个拦截器,并最终触发下一个拦截器的intercept(Chain chain)方法
        //  获取从下一个拦截器处返回的数据
        Response response;
        if (DeviceUtils.isNetworkConnected()) {
            //有网络,使用网络
            response = chain.proceed(request);
        } else {
            //没网络,使用未过期的缓存数据
            //使用CacheControl类来设置缓存策略
            CacheControl cacheControl = new CacheControl.Builder()
                    .onlyIfCached()
                    .maxStale(3600, TimeUnit.SECONDS)
                    .build();

            Request newRequest = request.newBuilder()
                    //此处的缓存策略
                    //方式1:官方推荐的方式,使用提供的CacheControl类
                    .cacheControl(cacheControl)
                    //方式2:直接设置头部
//                    .header("Cache-Control", "only-if-cached, max-stale=" + 3600)
                    .build();
            response = chain.proceed(newRequest);
        }

        //3......根据需要,对从下一个拦截器处返回的数据Response做调整......

        return response;
    }
}

4.ConnectInterceptor:处理底层Socket连接的连接和复用

OkHttp在TCP层的连接采用Socket来实现,内部管理着一个Socket连接池,会尝试最大限度的复用Socket连接来提升连接效率。

5.CallServerInterceptor:负责向服务端发起网络请求,并解析服务端返回的Http相应报文

(部分图片来自网络,侵删)

猜你喜欢

转载自blog.csdn.net/u013914309/article/details/125147429