简介

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

  • 支持对数据的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一般用来保存用户信息,进行网页跟踪等,用户使用喜好等@Overridepublic void saveFromResponse(HttpUrl url, List<Cookie> cookies) {  // 当请求得到服务端响应后,回调该方法//url: 请求的地址//coolies:需要保存的响应请求头信息cookie = cookies;  //使用方式 - 可以将响应存储到内存当中或本地文件}@Overridepublic List<Cookie> loadForRequest(HttpUrl url) {return cookie;  //在下一次网络请求中便可以将本次获取的cookie数据传入header}}).cache(new Cache(getCacheDir(),1024*1024)) //添加缓存实现类,指定缓存文件路径及大小,默认没有缓存.addInterceptor(new Interceptor() {  //添加自定义拦截器@Overridepublic 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() {@Overridepublic void onFailure(okhttp3.Call call, IOException e) {              }@Overridepublic 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) {//1runningAsyncCalls.add(call);//2executorService().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对象,如果等待队列有任务,则继续下一个callclient.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(),那么紧随其后,会更新对拦截器的源码分析.如果文章中有什么不对的地方,请指正

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

  1. Hystrix核心原理和断路器源码解析

    Hystrix运行原理 构造一个HystrixCommand或HystrixObservableCommand对象 执行命令. 检查是否已命中缓存,如果命中直接返回. 检查断路器开关是否打开,如果打开 ...

  2. 稀疏多项式的运算用链表_用最简单的大白话聊一聊面试必问的HashMap原理和部分源码解析...

    HashMap在面试中经常会被问到,一定会问到它的存储结构和实现原理,甚至可能还会问到一些源码 今天就来看一下HashMap 首先得看一下HashMap的存储结构和底层实现原理 如上图所示,HashM ...

  3. matlabeig函数根据什么原理_vue3.0 源码解析二 :响应式原理(下)

    一 回顾上文 上节我们讲了数据绑定proxy原理,vue3.0用到的基本的拦截器,以及reactive入口等等.调用reactive建立响应式,首先通过判断数据类型来确定使用的hander,然后创建p ...

  4. 社区发现算法原理与louvain源码解析

    前言 社区发现(community detection),或者社区切分,是一类图聚类算法,它主要作用是将图数据划分为不同的社区,社区内的节点都是连接紧密或者相似的,而社区与社区之间的节点连接则是稀疏的 ...

  5. Scrapy分布式原理及Scrapy-Redis源码解析(待完善)

    1 Scrapy分布式原理 2 队列用什么维护 首先想到的可能是一些特定数据结构, 数据库, 文件等等. 这里推荐使用Redis队列. 3 怎样来去重 保证Request队列每个request都是唯一 ...

  6. Handler 源码解析——Handler的创建

    前言 Android 提供了Handler和Looper来来满足线程间的通信,而前面我们所说的IPC指的是进程间的通信.这是两个完全不同的概念. Handler先进先出原则,Looper类用来管理特定 ...

  7. 多激光雷达外参标定算法与源码解析(一):基于BLAM的建图模块

    前言 原理文字介绍略微简介,但是在代码注释中非常详细.我相信在代码中学习原理才能理解更加通透. 代码参考自livox sdk: gitcode 一.算法原理 二.源码解析 函数流:main->B ...

  8. Java线程池源码解析及高质量代码案例

    引言 本文为Java高级编程中的一些知识总结,其中第一章对Jdk 1.7.0_25中的多线程架构中的线程池ThreadPoolExecutor源码进行架构原理介绍以及源码解析.第二章则分析了几个违反J ...

  9. 基于postCss的TaiWindCss源码解析

    基于postCss的TaiWindCss源码解析 前言 了解 postCss 什么 是 postCss? postCss的核心原理/工作流 TaiWindCss 源码解析 TaiWindCss 是什么 ...

  10. ❤️缓存集合(一级缓存、二级缓存、缓存原理以及自定义缓存—源码+图文分析,建议收藏) ❤️

    ❤️缓存集合(一级缓存.二级缓存.缓存原理以及自定义缓存-源码+图文分析,建议收藏) ❤️ 查询 : 连接数据库 ,耗资源!一次查询的结果,给他暂存在一个可以直接取到的地方!--> 内存 : 缓 ...

最新文章

  1. Chrome 打印PDF技巧
  2. SSH服务的渗透测试
  3. docker部署mysql项目_docker部署springboot项目(web + mysql)
  4. Office合并字符功能比较(转)
  5. frm ibd文件导入mysql_Mariadb,Mysql如何根据.frm和.ibd文件来恢复数据和表结构
  6. sql azure 语法_使用Azure Data Studio从SQL Server数据创建图表
  7. qnap raid5升级raid6_QNAP TS-419P组建RAID5后重建Transmission!
  8. 小红伞 for 2003
  9. nv驱动版本linux,完善支持NV显卡Linux驱动275.19正式版,275.19增加了对
  10. 百度开放平台Demo提示“Key验证失败...”的问题
  11. Python案例1—人民币与美元的汇率兑换V_6.0
  12. Reac16+Monaco打造代码编辑器(前端部分)
  13. 如何搭建一个简单的个人网站
  14. 区块链调研备份8.1
  15. 【二分】C. Keshi Is Throwing a Party
  16. Ansj添加停用词表
  17. 2016年十大黑客工具
  18. Oracle设置sql执行时的并行度和强制走索引
  19. Java 面试全解析:核心知识点与典型面试题
  20. Echarts气泡图(相邻效果,气泡之间不叠加)

热门文章

  1. 为什么Windows 10电脑运行缓慢?如何解决?
  2. C++ ARX二次开发-BREP库
  3. 苹果cmsv10黑色自适应好看的炫酷简约带会员中心模板
  4. javascript精雕细琢(四):认亲大戏——通过console.log彻底搞清this
  5. 让生气的顾客开心的 10 种方法
  6. AI观察|机器人成为了我们的好伙伴,你愿意吗?
  7. html如何做好看的图片效果,CSS使用图片美化的漂亮菜单效果
  8. JDK 1.8 Sream 分组的使用
  9. java sream 对Map分组排序
  10. 【STM32】ESP8266 WiFi模块实时上报温湿度及控制LED灯项目笔记