如果您对这一系列的文章感兴趣,可以关注此专栏:android网络框架OkHttp+Retrofit源码分析
废话不多说先上图:OkHttp拦截器链
结合上图中的说明我们对OkHttp拦截器链上的各个拦截器的作用进行说明
RetryAndFollowUpInterceptor拦截器
retryAndFollowUpInterceptor此拦截器顾名思义就是主要负责失败重连工作,但是并不是所有的网络请求都会进行失败重连的,在此拦截器内部会进行网络请求的异常检测和响应码的判断,如果都在限制范围内,那么就可以进行失败重连
主要执行工作:
1、创建StreamAllocation对象
2、调用RealInterceptorChainrealChain.proceed(request, streamAllocation, null, null); 通过chain对象调用下一个拦截器BridgeInterceptor
3、从下一个拦截器那里接收传递过来的response,根据异常和响应结果判断是否重连pan
4、处理完成之后将response返回给上一个拦截器
桥接拦截器:主要负责设置内容长度、编码方式、设置gzip压缩、添加请求头、cookie等相关功能
那么它是怎么实现这种桥接转换的呢,因为我们知道除了请求Url之外,浏览器还需要请求头等信息,如下图所示:
桥接拦截器实现的是桥接模式,用来桥接客户端代码和网络代码,或者说是链接客户端代码和网络代码的桥梁
既然BridgeInterceptor会根据用户请求创建真正的Network Request,那么这些请求头信息也是少不了的
@Override public Response intercept(Chain chain) { //获取用户构建的Request对象 Request userRequest = chain.request(); ..... //设置Host if (userRequest.header("Host") == null) { requestBuilder.header("Host", hostHeader(userRequest.url(), false));} //设置Connection头,Keep-Alive 保持连接状态 if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive"); } // 如果设置了压缩,也会构建解压缩参数 boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } //上面的代码逻辑也很简单,就是为Request设置User-Agent、Cookie、Accept-Encoding等相关请求头信息 // 到此为止一个完整的NetWork Request 就构建完毕 //拦截器链继续往下运行 Response networkResponse =chain.proceed(requestBuilder.build()); ..... //判断服务器是否支持gzip压缩格式,如果支持则交给kio压缩 ..... }
主要功能:
1、负责将用户构建的一个Request请求转化为能够进行网络访问的请求(桥接)
2、将这个符合网络请求的Request继续向下传递进行网络请求
3、将网络请求回来的响应Response转化为用户可以使用的Response(支持gzip压缩/解压)
okhttp的缓存策略
为什么要使用缓存:
一个优点就是让客户端下一次的网络请求节省更多的时间,更快的展示数据
如何开启和使用缓存功能的呢?
直接使用OkHttpClient的build来设置缓存:
new OkHttpClient.Builder() .cache(new Cache(new File("cache"),24*1024*1024)) .build();
当CacheInterceptor拦截到请求之后会调用到cache类的put方法,cache类实现了InternalCache接口,此接口定义了缓存的增删改查等方法,下面我们看一下如何将response缓存到cache里面
@Nullable CacheRequest put(Response response) { //获取到请求方法 String requestMethod = response.request().method(); //判断该请求方法是否不符合缓存需求 if (HttpMethod.invalidatesCache(response.request().method())) { remove(response.request());//移除这个请求 return null; } //非get方法不需要缓存,或者说因为post不能被缓存,get请求可以被CDN缓存,可以大大减少web服务器的负担 if (!requestMethod.equals("GET")) { return null; } if (HttpHeaders.hasVaryAll(response)) { return null;} //开始缓存 Entry entry = new Entry(response); DiskLruCache.Editor editor = null;//DiskLruCache缓存策略 try {//把这个请求url(经过转换MD5加密然后转十六进制)作为key editor = cache.edit(key(response.request().url())); //真正的开始缓存 entry.writeTo(editor); return new CacheRequestImpl(editor); ..... }
Cache类的put方法定义了如何通过传入的response潘墩是否需要需要缓存,以及创建Entry对象,把需要的一些属性 方法 地址 头部 信息 code 等写入缓存
我们知道OkHttp 是通过OkIo进行IO操作的,当然OkHttp的缓存自然也离不开OkIo的支持,接下来我们通过对entry.writeTo()方法的分析,来看一下如何使用OkIo进行缓存处理
public void writeTo(DiskLruCache.Editor editor) throws IOException { //使用的是okio BufferedSink sink =Okio.buffer(editor.newSink(ENTRY_METADATA)); //缓存的请求地址 sink.writeUtf8(url).writeByte('\n'); //缓存的请求方法 sink.writeUtf8(requestMethod).writeByte('\n'); //对header头部进行遍历 for (int i = 0, size = varyHeaders.size(); i < size; i++) { sink.writeUtf8(varyHeaders.name(i)).writeUtf8(": ") .writeUtf8(varyHeaders.value(i)).writeByte('\n'); } //缓存http的响应行StatusLine sink.writeUtf8(new StatusLine(protocol, code, message).toString()) .writeByte('\n'); ...... } if (isHttps()) {//判断是否是Https请求 //相应的握手 等缓存 sink.writeByte('\n'); ...... } //关闭 sink.close(); }
在上面的put方法里面我们看到了CacheRequestImpl对象
CacheRequestImpl实现了CacheRequest接口,主要暴漏给CacheInterceptor,CacheInterceptor可以根据这个 CacheRequestImpl直接写入和更新缓存数据
put()方法总结:
1、首先判断缓存请求方法是否是get方法
2、如果符合缓存策略,那么创建一个Entry对象—>用于我们所要包装的缓存信息
3、最终使用DiskLruCache进行缓存
4、最后通过返回一个CacheRequestImpl对象,这个对象主要用于CacheInterceptor拦截器服务的
Cache类的作用,根据缓存策略创建entry类包裹response放入DiskLruCache缓存,然后在需要缓存的时候将根据request寻找到对应的entry并转成response返回
当然寻找缓存的操作封装在了get()请求里面:
get()方法总结:
1、根据请求url获取key(MD5解密)
2、通过key值获取 DiskLruCache.Snapshot(缓存快照)
3、通过获取到的这个snapshot获取到Source,最终创建entry对象
4、通过entry和snapshot(缓存快照)获取缓存的response并返回
前面总结了Cache的get和put方法,HTTP的缓存的工作是通过CacheInterceptor拦截器来完成的
OkHttp源码解读-->CacheInterceptor拦截器
如果当前未使用网络 并且缓存不可以使用,通过构建者模式创建一个Response响应 抛出504错误
如果有缓存 但是不能使用网络 直接返回缓存结果这是在进行网络请求之前所做的事情,当网络请求完成,得到下一个拦截器返回的response之后,判断response的响应码是否是HTTP_NOT_MODIFIED = 304 (未改变)是则从缓存中读取数据
此拦截器负责主要负责打开服务器之间的TCP链接,正式开启okhttp的网络请求
@Override public Response intercept(Chain chain) { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); //StreamAllocation创建 StreamAllocation streamAllocation = realChain.streamAllocation(); boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); }
从代码上来看该拦截器的主要功能都交给了StreamAllocation处理,且这个类是从拦截器链对象(RealInterceptorChain对象)上获取的
我们知道RealInterceptorChain是在RealCall的getResponseWithInterceptorChain方法初始化的,但是去追踪源码的时候我们发现在那里StreamAllocation并没有被创建出来
其实在之前我们就了解过拦截器的原理,在整个拦截链条上所有的拦截器都在一定程度上共享了一份环境Chain(创建下一个chain的时候使用自身参数进行初始化)
因此实际上StreamAllocation是在第一个拦截器RetryAndFollowUpInterceptor里面进行创建的
StreamAllocation用来建立http请求所需要的所有网络组件,在RetryAndFollowUpInterceptor 重试连接器中 只是初始化了 但是没有使用,它只是把创建好的StreamAllocation进行了向下传递 最终由ConnectInterceptor拦截器获取和使用
这里再次验证了我们之前的说法Chain是OkHttp的拦截器链的环境,在拦截器链上所有的拦截器都可以对环境进行特定的处理,也就是改变chain中任务或者其他组件的状态
StreamAllocation的构造器里面有两个重要的参数:
1.使用了Okhttp的连接池ConnectionPool
2.通过url创建了一个Address对象。
在Okhttp内部的连接池实现类为ConnectionPool,该类持有一个ArrayDeque队列作为缓存池,该队列里的元素为RealConnection(通过这个名字应该不难猜出是用来干嘛的)
总结:
1、ConnectInterceptor从责任链的环境chain中取出StreamAllocation对象,通过StreamAllocation.newStream()方法获取HttpCodec对象,这个HttpCodec对象用于编码request和解码response,并且对不同http协议(http1.1和http/2)的请求和响应做处理
2、newStream方法主要做的工作:
1)从缓冲池ConnectionPool获取一个RealConnection对象,如果缓冲池里面没有就创建一个RealConnection对象并且放入缓冲池中,具体的说是放入ConnectionPool的ArrayDeque队列中。
2)获取RealConnection对象后并调用其connect打开Socket链接
3、将刚才创建好的用于网络IO的RealConnection对象,以及对于与服务器交互最为关键的HttpCodec等对象传递给后面的拦截
这个类大体上做了如下几个工作:
1、StreamAllocation的connection能复用就复用之
2、如果connection不能复用,则从连接池中获取RealConnection,获取成功则返回,从连接池中获取RealConnection的方法调用了两次 ,第一次没有传Route,第二次传了
3,如果连接池里没有则创建一个RealConnection对象,并放入连接池中
4.最终调用RealConnection的connect方法打开一个socket链接(此处暂且说结论,至于为何断定是socket链接,篇幅有限另外结合别的知识点另开博文说明)
终于到了OkHttp最后一个拦截器CallServerInterceptor
OkHttp源码解读—>CallServerInterceptor拦截器
向服务器发送请求,并最终返回Response对象供客户端使用
该拦截器的拦截方法首先获取了httpCodec对象,该对象的主要功能是对不同http协议(http1.1和http/2)的请求和响应做处理,该对象的初始化是在ConnectIntercepor的intercept里面
Okhttp的提供了两种HttpCodec的实现类,如果使用了http2协议则返回Http2Codec,否则返回Http1Codec!并且设置了超时时间,本篇就以Http1Codec对象来进行分析
1、建立TCP链接
2、客户端向web服务器发送请求命令:
形如GET /login/login.jsp?username=android&password=123 HTTP/1.1的信息
在Okhttp中ConnectInterceptor负责第一个步骤,那么第二个步骤是如何实现的呢?
答案就是httpcodec的writeRequestHeaders方法,在这个方法里面我们可以发现Okhttp是通过OkIO的Sink对象(该对象可以看做Socket的OutputStream对象)来向服务器发送请求的
我们知道HTTP支持post,delete,get,put等方法,而post,put等方法是需要请求体的(在Okhttp中用RequestBody来表示)。所以接着writeRequestHeaders之后Okhttp对请求体也做了响应的处理
通过上面的代码可以发现Okhttp对Expect头部也做了支持,上面代码对客户端是否使用该头部做了判断
“100 continue”的作用就是:客户端有一个RequestBody(比如post或者PUT方法)要发给服务器,但是客户端希望在发送RequestBody之前查看服务器是否接受这个body,服务端在接受到这个请求后必须进行响应。客户端通过Expect首部来发送这个消息,当然如果客户端没有实体发送,就不应该发送100 continue 首部,因为这样会使服务器误以为客户端有body要发送。所以okhttp在发送这个之前要permitsRequestBody来判断。当然常规的get请求是不会走这个方法的
ConnectInterceptor执行完请求之后接着做的就是读取服务器响应的数据,构建response.builder对象
主要做了两个工作:
1、调用HttpCodec的readResponseHeaders方法读取服务器响应的数据,构建Response.Builder对象(以Hppt1Codec)
2、通过ResopnseBuilder对象来最终创建Response对象并返回。主要是调用Http1Codec对象的openResponseBody方法,此方法将Socket的输入流InputStream对象交给OkIo的Source对象,然后封装成RealResponseBody(该类是ResponseBody的子类)作为Response的body
(在本篇博文中只需简单的将Sink作为Socket的输出流,Source作为Socket的输入流看待即可)
到此为止CallServerInterceptor简单分析完毕,总结下主要做了如下工作:
1、获取HttpCodec对象,对Http1.1或者http/2不同协议的http请求进行处理
2、发送http请求数据,构建Resposne.Builder对象,然后构建Response并返回