OkHttp的运用与原理(cookie、缓存、源码解析)

简介

作为当下最流行的网络请求底层框架,如何战胜其他框架立于不败之地,被广大人们所认可呢?相较于其他网络框架来说,其具有的优势:

  • 支持对数据的gizp压缩与解压
  • 支持http1.0,http2.0,SPDY_3,QUIC
  • 支持网络响应缓存
  • 连接复用
  • 同一地址的请求共用一个连接(socket)
  • 重试与重定向机制

相较于其他一些网络底层框架而言,对网络请求与响应的处理更加地完善,专业.

如何使用

首先看图
在这里插入图片描述

该图反应了okhttp的基本使用流程,之后会根据该图的每一步来了解okhttp的原理

一 创建OkHttpClient

		private List<Cookie> cookie;
		OkHttpClient client = new OkHttpClient.Builder()   
                .cookieJar(new CookieJar() {  //cookie一般用来保存用户信息,进行网页跟踪等,用户使用喜好等
                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {  

					// 当请求得到服务端响应后,回调该方法
					//url: 请求的地址
					//coolies:需要保存的响应请求头信息
					
				     cookie = cookies;  //使用方式 - 可以将响应存储到内存当中或本地文件
                    }

                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                          return cookie;  //在下一次网络请求中便可以将本次获取的cookie数据传入header
                    }
                })
                .cache(new Cache(getCacheDir(),1024*1024)) //添加缓存实现类,指定缓存文件路径及大小,默认没有缓存
                .addInterceptor(new Interceptor() {  //添加自定义拦截器
                    @Override
                    public Response intercept(Chain chain) throws IOException {

                        return chain.proceed(chain.request());
                    }
                })
                .build();

大多人在创建此类时习惯直接new一个对象来获取实例,其实在OkHttpClient的构造方法中依旧创建一个Builder对象来进行实例化在这里插入图片描述

一.1 cookieJar

cookie是用来存储用户的登陆信息,用户的爱好设置,网页浏览跟踪等,以key/value的键值对形式来存储,是由服务器生成后存储在客户端的数据信息,客户端在访问网站时,在请求头header中携带cookie后,服务器根据该信息返回相应的用户数据信息.

由于考虑到数据的安全,在获取到cookie以后,可以使用自己的加密算法来进行保存,防止黑客恶意破坏数据.这里我们仅做简单示范
在OKHttp中,cookie是将url作为key,响应头header作为value来进行存储的,其默认初始化为cookieJar = CookieJar.NO_COOKIES;(在builder中初始化)不使用cookie,所以需要用户手动来添加.
看一下在拦截器中是如何被调用

 public Response intercept(Chain chain) throws IOException {
 	....
 	List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    ....
     HttpHeaders.receiveHeaders(cookieJar, userRequest.url(),networkResponse.headers());
    ....
 }

cookiejar.loadForRequest()是在用户发送请求时,拼接request header时候回调的,根据传递的本地cookies来添加"Cookie"头信息到请求头中

cookie.saveFromResponse()则是在发送请求,服务器响应以后回调的,会将服务器的响应头header信息转换为集合回调到客户端

public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {
    if (cookieJar == CookieJar.NO_COOKIES) return;
    List<Cookie> cookies = Cookie.parseAll(url, headers);
    if (cookies.isEmpty()) return;
    cookieJar.saveFromResponse(url, cookies);
  }

上述代码是在OkHttp的拦截器BridgeInterceptor中执行的

一.2 cache缓存

OkHttp中有一套自己的缓存机制,用户请求服务器时,成功获取响应的同时,会将缓存存储到本地,在下次请求网络时,如果命中并且缓存没有过期也并未修改,OkHttp将会将本地缓存文件读取后返回给客户端,这样便可以大大增加程序的运行效率,减轻服务器的压力.要注意的是 OkHttp中默认是未开启缓存的,若想使用缓存机制,则需要在OkHttpclient.builder中添加cache(newCache(getCacheDir(),1024*1024))来手动添加缓存, getCacheDIr()为缓存存放路径,参数二为文件存储的大小,单位为bit.

  Request request = new Request.Builder()
                .cacheControl(CacheControl.FORCE_CACHE)
                .url("https://xxx/408/1/json.....")
                .get()
                .build();
        client.newCall(request).enqueue(new okhttp3.Callback() {
        ...
        //省略
        }

我们随便找一json数据网址写一个request请求发送,运行程序,并成功访问后,会在data—>data,
在这里插入图片描述
项目包所在文件目录—>cache文件目录下看到三个文件
在这里插入图片描述
保存到本地后
在这里插入图片描述
分别打开三个文件

第一个文件365350f3bc4919e77e578fbaa9b5a942.0
在这里插入图片描述
通过图片可以看出该文件存储了一个完整的响应header,此文件名是将url编码后添加.0命名

文件二365350f3bc4919e77e578fbaa9b5a942.1
在这里插入图片描述
如图,该文件存储服务器返回的响应体数据,该文件名也是url编码后命名,只不过加了xxx.1
文件三journal
在这里插入图片描述

这个为缓存的日志文件,存储当前缓存的版本,缓存文件数及对缓存的操作,DIRTY,CLEAN等为DiskLruCache处理缓存文件的状态分别为创建/修改文件和缓存操作成功,另外还有两个状态REMOVE表示对应缓存文件删除,READ缓存文件被访问.

Request request = new Request.Builder()
                .cacheControl(CacheControl.FORCE_NETWORK)
                .url("https://xxx/408/1/json").build();

在request请求中,我们还可以通过参数来控制缓存,在CacheControl中默认提供两个Cache的实例

  • public static final CacheControl FORCE_NETWORK = new Builder().noCache().build(); 强制每次使用缓存前访问网络
  • public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) .build();如果有缓存,则强制使用缓存,否则返回504

有关CacheControl可以读者可以单独做了解,此为request请求的header中属性,其对应有多个value值,客户端可以通过在header中添加对应value来控制缓存

通过上述我们认识到OkHttp的缓存基本使用,在之后的文章当中博主会从源码角度来详细分析OkHttp的cache机制

一.3 Interceptor自定义拦截器

OkHttp中通过拦截器来对网络请求与响应进行处理的,其内部有五个默认拦截器

  • RetryAndFollowUpInterceptor : (重试和重定向),若有网络响应时间长,路由不通,网络连接流等出现问题,会尝试重试,当服务端返回响应码3开头并带有目标资源新地址时,客户端根据服务器返回的地址进行重定向
  • BridgeInterceptor : (桥拦截器),作用是将客户端的请求体进行完善,合成一个完整的header信息存入request中并解析服务器返回的响应response数据信息
  • CacheInterceptor : (缓存拦截器),用于缓存服务端返回的response到本地,以便在下一次访问该地址时能够从本地直接获取
  • ConnectInterceptor :(连接拦截器) 与服务器建立连接,通过socket连接池可以对连接进行复用,减少tcp连接握手的耗时操作
  • CallServerInterceptor : (读写拦截器)与服务器进行io交互,实际对服务器进行数据交互

有关拦截器内容我们准备在下一篇文章中讲解.这里addInterceptor()是向拦截器链中添加一个自定义的拦截器,其目的是在内置拦截器执行前加入用户自己的操作,根据自己的需求来操作请求与响应,与之对应的还有addNetWorkInterceptor()两者区别在于执行顺序不同,前者是在内置拦截器执行前先执行,后者则是在ConnectInterceptor执行结束后调用的,顺序的不同导致其性质不同,有兴趣可以做了解,不很常用
以上为OkHttpClient的创建,我们继续来看request

二 创建request请求

 Request request = new Request.Builder()
                .cacheControl(CacheControl.FORCE_NETWORK)
.url("https://zzz/list/408/1/json").build();//原始请求

创建一个request,使用构建者模式,可以添加header,post,put等参数,对于header,我们并没有声明参数,那么在桥拦截器中,会自动将需要的请求头信息加入到request中,完善header信息,最后build获取request对象.

三 创建RealCall对象

client.newCall(request)方法内执行RealCall.newRealCall()初始化一个RealCall对象

private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }

我们跟进源码后,可以看到RealCall构造方法中创建了一个RetryAndFollowUpInterceptor重试重定向拦截器,并将client,request和是否使用WebSocket存入变量

WebSocket是一个基于TCP连接的双向通道,它的优点是可以使客户端和服务端之间的连接开销减少,即只需要一次握手一次就可以建立一个持久性的连接,并进行双向数据传输.同时相较于一般的tcp来说,控制开销较少,客户端与服务端进行数据交换时,协议中包含的数据头包比较少,只需要携带2~10字节的包头,而http协议每次通信需要携带完整的头部信息

四 执行enqueue()方法

	client.newCall(request).enqueue(new okhttp3.Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {              
            }
            @Override
            public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
            }
        });

在第三点中获得RealCall对象后接着便调用了其内部的enqueue()方法,此方法是发送一个异步请求,与其对应的还有一个同步请求方法execute(),两者的主要区别是,前者使用线程池创建的线程来执行网络请求,而后者直接在主线程中发送网络请求,所以若想使用则需要手动来添加一个Thread线程,因为UI线程是不允许这些耗时操作执行的

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
    //... 
    eventListener.callStart(this);  //请求发送后回调
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
  

调用了dispatcher()的enqueue(),首先来说一下dispatcher
在这里插入图片描述
调度器,顾名思义用来对线程进行控制管理的类,内部维持三个双向队列,readyAsyncCalls等待队列,当请求超过最大数时,会将任务暂存到此队列中等待,runningAsyncCalls正在运行的队列,用于存放正在运行的任务,当任务执行结束,则会在此类中回收,也就是出队,同时从等待队列中获取任务继续执行.前两队列都是异步执行时使用,runningSyncCalls则是在使用同步请求execute()方法时使用,可以发现异步使用的是AsynCall,与同步不同,该类最后我们也会简单介绍
接下来看调度器dispatcher中的enqueue()方法

 synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {//1
      runningAsyncCalls.add(call);//2
      executorService().execute(call);//3
    } else {
      readyAsyncCalls.add(call);//4
    }
  }
  1. OkHttp中,默认允许的最大请求数为64,访问同一主机的最大请求数为5,当然这个数值也是可以通过客户端来修改.首先判断当前正在运行的请求数是否在最大范围
  2. 如果请求数未达到最大值,添加到runningAsyncCalls队列中
  3. 执行executorService().execute(call);executorService()返回一个线程池对象:
  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;  

返回一个0核心线程,int最大值的非核心线程和SynchronousQueue队列,每个线程有60秒的空闲时间,这么做的好处是,当有任务进入时,无需进行等待排队而能够快速的创建一个线程来执行此任务,这样就防止了一些任务在排队很长时间后得不到执行的问题,有关于线程池ThreadPoolExecutor,我在之前的文章中曾详细分析过,有兴趣读者可以去看看
最后调用线程池的executor()方法执行这个任务

  1. 如果超出最大请求数或主机访问最大数后,会将该任务存入readyAsyncCalls队列中等待

总体看逻辑还是比较简单的,先是创建RealCall对象,接着调用了内部enqueue()并传入一个callback回调接口,而在enqueue()中是通过OkHttp调度器的enqueue(),注意这里传入的是一个AsyncCall对象,我们稍后来说,在enqueue()中向队列中添加了任务,并且创建线程池执行传入的call任务,就结束了,那么客户端传入的callback呢?在哪回调了它呢?

四.1. Call对象

上面我们留了一处AsyncCall没有说,该类是RealCall对象一个内部类,间接继承自Runnable对象,这么说大概就懂了吧?既然继承自Runnable对象,内部一定有run()方法,那么当线程池执行任务时,就会回调这个run()方法

@Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }
  //抽象方法
  protected abstract void execute();

AsyncCall中我们只找到了execute()方法,而在他的直接父类NamedRunnable中找到了它的run(),该类是一个抽象类,内部只有一个run方法和一个抽象方法execute(),在run()中被调用,直接继承此类的不就是AsyncCall类吗,里面不正好有这个方法吗?
OK,现在再理一下思路,当执行到dispatcher.enqueue(new AsyncCall(responseCallback))方法时,内部通过线程池执行了这个call任务,具体一点则是回调这个AsynCall对象的run()(因为此类是一个Runnable)方法,由于AsynCall自身没有run()所以调用到父类NamedRunnablerun()方法,内部回调子类的execute(),我们来看

protected void execute() {
//..省略
      try {
        //OkHttp网络连接与交互的核心方法 拦截器调用
        Response response = getResponseWithInterceptorChain();
        
        if (retryAndFollowUpInterceptor.isCanceled()) {//网络重试或重定向取消,说明网络访问失败,回调失败方法
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
        //否则返回响应
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
       }catch (IOException e) {
       //请求网络时发生异常,回调失败方法
 		  eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
		}
//		....省略
        finally {
        //最终调用调度器回收此call对象,如果等待队列有任务,则继续下一个call
        client.dispatcher().finished(this);
      }
}

getResponseWithInterceptorChain()方法为OkHttp与服务器进行交互核心,内部调用了上面介绍的各拦截器,具体会在之后的文章中做介绍,responseCallback是客户端传入的Callback回调接口,如果进行重试或重定向并且成功访问到网络或者未进行重试重定向很顺利访问到网络则得到响应回调callback对象传入响应.

简单提一下重试重定向,重试为访问网络时超时,路由不通或者在建立远程连接后在连接流中出现异常会尝试重新发送请求,执行默认的有限次重试操作后仍然无济于事,网络访问失败,重定向是访问服务器成功,但是目标资源的地址已经发生迁移,所以服务器通过响应头返回迁移后的新地址,客户端拿到后新地址重新发起请求,并获取数据资源,例如客户端请求使用"http://xxx"访问目标服务器,而服务器资源地址已经转换为"https://",服务器便会返回转换后的新地址.

总结

本文我们在OkHttp的基本使用的基础上讲述了其原理,包括缓存cache,cookiejar,拦截器简介,RealCall对象.本文只介绍了enqueue()异步请求,关于OkHttp的同步请求,要比异步简单的多,内部直接调用了RealCall对象的execute()方法,不涉及dispatcher调度器和线程池,方法内容与AsynCall中execute()方法类似,少了调度器管理这一环节,也不再重复阐述.这篇文章也只是浅层的分析了OkHttp使用原理,想真正了解OkHttp的底层,需要仔细研究其getResponseWithInterceptorChain(),那么紧随其后,会更新对拦截器的源码分析.如果文章中有什么不对的地方,请指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

问心彡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值