A. Android通过OKhttp从服务器端获取数据
①简单的异棚洞步Get请求
第一步,创建OKHttpClient对象
第二步,创建Request请求
第三步,创建一个Call对象
第四步,将请求添大和贺加到调度中
不多说,直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//okHttp的基本使用 --- get方法
String url = "https://api.douban.com/v2/movie/top250?start=0&count=10";
//1,创建OKHttpClient对象
OkHttpClient mOkHttpClient = new OkHttpClient();
//2,创建一个Request
Request request = new Request.Builder().url(url).build();
//3,创建一个call对象
Call call = mOkHttpClient.newCall(request);
//4,将请求添加到调滚派度中
call.enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
}
@Override
public void onResponse(Response response) throws IOException {
if (response.isSuccessful()) {
final String message = response.body().string();
handler.post(new Runnable() {
@Override
public void run() {
tv_message.setText(message);
progressBar.setVisibility(View.GONE);
}
});
}
}
});
B. OkHttp源码解析 (三)——代理和路由
初看OkHttp源码,由于对Address、Route、Proxy、ProxySelector、RouteSelector等理解不够,读源码非常吃力,看了几遍依然对于寻找复用连接、创建连接、连接服务器、连接代理服务器、创建隧道连接等逻辑似懂非懂,本篇决定梳理一遍相关的概念及基本原理。
● HTTP/1.1(HTTPS)
● HTTP/2
● SPDY
一个http请求的流程(直连):
1、输入url及参数;
2、如果是url是域名则解析ip地址,可能对应多个ip,如果没有指定端口,则用默认端口,http请求用80;
3、创建socket,根据ip和端口连接服务器(socket内部会完成3次TCP握手);
4、socket成功连接后,发送http报文数据。
一个https请求的流程(直连):
1、输入url及参数;
2、如果是url是域名则解析ip地址,可能对应多个ip,如果没有指定端口,则用默认端口,https请求用443;
3、创建socket,根据ip和端口连接服务器(socket内部会完成3次TCP握手);
4、socket成功连接后进行TLS握手,可通过java标准款提供的SSLSocket完成;
5、握手成功后,发送https报文数据。
1、分类
● HTTP代理:普通代理、隧道代理
● SOCKS代理:SOCKS4、SOCKS5
2、HTTP代理分类及说明
普通代理
HTTP/1.1 协议的第一部分。其代理过程为:
● client 请求 proxy
● proxy 解析请求获取 origin server 地址
● proxy 向 origin server 转发请求
● proxy 接收 origin server 的响应
● proxy 向 client 转发响应
其中proxy获取目的服务器地址的标准方法是解析 request line 里的 request-URL。因为proxy需要解析报文,因此普通代理无法适用于https,因为报文都是加密的。
隧道代理
通过 Web 代理服务器用隧道方式传输基于 TCP 的协议。
请求包括两个阶段,一是连接(隧道)建立阶段,二是数据通信(请求响应)阶段,数据通信是基于 TCP packet ,代理服务器不会对请求及响应的报文作任何的处理,都是原封不动的转发,因此可以代理 HTTPS请求和响应。
代理过程为:
● client 向 proxy 发送 CONNET 请求(包含了 origin server 的地址)
● proxy 与 origin server 建立 TCP 连接
● proxy 向 client 发送响应
● client 向 proxy 发送请求,proxy 原封不动向 origin server 转发请求,请求数据不做任何封装,为原生 TCP packet.
3、SOCKS代理分类及说明
● SOCKS4:只支持TCP协议(即传输控制协议)
● SOCKS5: 既支持TCP协议又支持UDP协议(即用户数据包协议),还支持各种身份验证机制、服务器端域名解析等。
SOCK4能做到的SOCKS5都可得到,但反过来却不行,比如我们常用的聊天工具QQ在使用代理时就要求用SOCKS5代理,因为它需要使用UDP协议来传输数据。
有了上面的基础知识,下面分析结合源码分析OkHttp路由相关的逻辑。OkHttp用Address来描述与目标服务器建立连接的配置信息,但请求输入的可能是域名,一个域名可能对于多个ip,真正建立连接是其中一个ip,另外,如果设置了代理,客户端是与代理服务器建立直接连接,而不是目标服务器,代理又可能是域名,可能对应多个ip。因此,这里用Route来描述最终选择的路由,即客户端与哪个ip建立连接,是代理还是直连。下面对比下Address及Route的属性,及路由选择器RouteSelector。
描述与目标服务器建立连接所需要的配置信息,包括目标主机名、端口、dns,SocketFactory,如果是https请求,包括TLS相关的SSLSocketFactory 、HostnameVerifier 、CertificatePinner,代理服务器信息Proxy 、ProxySelector 。
Route提供了真正连接服务器所需要的动态信息,明确需要连接的服务器IP地址及代理服务器,一个Address可能会有很多个路由Route供选择(一个DNS对应对个IP)。
Address和Route都是数据对象,没有提供操作方法,OkHttp另外定义了RouteSelector来完成选择的路由的操作。
1、读取代理配置信息:resetNextProxy()
读取代理配置:
● 如果有指定代理(不读取系统配置,在OkHttpClient实例中指定),则只用1个该指定代理;
● 如果没有指定,则读取系统配置的,可能有多个。
2、获取需要尝试的socket地址(目标服务器或者代理服务器):resetNextInetSocketAddress()
结合Address的host和代理,解析要尝试的套接字地址(ip+端口)列表:
● 直连或者SOCK代理, 则用目标服务器的主机名和端口,如果是HTTP代理,则用代理服务器的主机名和端口;
● 如果是SOCK代理,根据目标服务器主机名和端口号创建未解析的套接字地址,列表只有1个地址;
● 如果是直连或HTTP代理,先DNS解析,得到InetAddress列表(没有端口),再创建InetSocketAddress列表(带上端口),InetSocketAddress与InetAddress的区别是前者带端口信息。
3、获取路由列表:next()
选择路由的流程解析:
● 遍历每个代理对象,可能多个,直连的代理对象为Proxy.DIRECT(实际是没有中间代理的);
● 对每个代理获取套接字地址列表;
● 遍历地址列表,创建Route,判断Route如果在路由黑名单中,则添加到失败路由列表,不在黑名单中则添加到待返回的Route列表;
● 如果最后待返回的Route列表为空,即可能所有路由都在黑名单中,实在没有新路由了,则将失败的路由集合返回;
● 传入Route列表创建Selection对象,对象比较简单,就是一个目标路由集合,及读取方法。
为了避免不必要的尝试,OkHttp会把连接失败的路由加入到黑名单中,由RouteDatabase管理,该类比较简单,就是一个失败路由集合。
1、创建Address
Address的创建在RetryAndFollowUpInteceptor里,每次请求会声明一个新的Address及StreamAllocation对象,而StreamAllocation使用Address创建RouteSelector对象,在连接时RouteSelector确定请求的路由。
每个Requst都会构造一个Address对象,构造好了Address对象只是有了与服务器连接的配置信息,但没有确定最终服务器的ip,也没有确定连接的路由。
2、创建RouteSelector
在StreamAllocation声明的同时会声明路由选择器RouteSelector,为一次请求寻找路由。
3、选择可用的路由Route
下面在测试过程跟踪实例对象来理解,分别测试直连和HTTP代理HTTP2请求路由的选择过程:
● 直连请求流程
● HTTP代理HTTPS流程
请求url: https://www.jianshu.com/p/63ba15d8877a
1、构造address对象
2、读取代理配置:resetNextProxy
3、解析目标服务器套接字地址:resetNextInetSocketAddress
4、选择Route创建RealConnection
5、确定协议
测试方法:
● 在PC端打开Charles,设置端口,如何设置代理,网上有教程,比较简单;
● 手机打开WIFI,选择连接的WIFI修改网络,在高级选项中设置中指定了代理服务器,ip为PC的ip,端口是Charles刚设置的端口;
● OkHttpClient不指定代理,发起请求。
1、构造address对象
2、读取代理配置:resetNextProxy
3、解析目标服务器套接字地址:resetNextInetSocketAddress
4、选择Route创建RealConnection
5、创建隧道
由于是代理https请求,需要用到隧道代理。
从图可以看出,建立隧道其实是发送CONNECT请求,header包括字段Proxy-Connection,目标主机名,请求内容类似:
6、确定协议,SSL握手
1、代理可分为HTTP代理和SOCK代理;
2、HTTP代理又分为普通代理和隧道代理;普通代理适合明文传输,即http请求;隧道代理仅转发TCP包,适合加密传输,即https/http2;
3、SOCK代理又分为SOCK4和SOCK5,区别是后者支持UDP传输,适合代理聊天工具如QQ;
4、没有设置代理(OkHttpClient没有指定同时系统也没有设置),客户端直接与目标服务器建立TCP连接;
5、设置了代理,代理http请求时,客户端与代理服务器建立TCP连接,如果代理服务器是域名,则解释代理服务器域名,而目标服务器的域名由代理服务器解析;
6、设置了代理,代理https/http2请求时,客户端与代理服务器建立TCP连接,发送CONNECT请求与代理服务器建立隧道,并进行SSL握手,代理服务器不解析数据,仅转发TCP数据包。
如何正确使用 HTTP proxy
OkHttp3中的代理与路由
HTTP 代理原理及实现(一)
C. Android网络请求库【OkHttp4.9.3】基本用法与原理分析
OkHttp是一套处理 HTTP 网络请求的依赖库,由 Square 公司设计研发并开源,目前可以在 Java 和 Kotlin 中使用。对于 Android App 来说,OkHttp 现在几乎已经占据了所有的网络请求操作,Retrofit + OkHttp实现网络请求似乎成了一种标配。因此它也是每一个 Android 开发工程师的必备技能,了解其内部实现原理可以更好地进行功能扩展、封装以及优化。
OkHttp的高效性体现在:
第一步:创建OkHttpClient,创建OkHttpClient有两种方式:
OkHttpClient提供了丰富的配置方法,例如添加拦截器、指定连接池、设置请求超时等等。
第二步:创建请求
使用Request.Builder() 构建Request实例
第三步:发起网络请求
OkHttp支持同步和异步两种请求方式
OkHttp的使用方法非常简单,三步操作就可以发起一个简单的同步或异步请求。我们也可以很轻松地对网络请求进行配置,例如添加请求头、设置请求方式、设置请求超时等等,这些配置参数会在源码分析过程中详细介绍。
现在我们已经学会了三步操作发起网络请求,接下来以这三个步骤为切入点,深入到源码中学习OkHttp的实现原理,废话少说马上开车。
OkHttpClient创建方式有两种,我们看看两种方式有什么区别。
第一种直接使用默认构造函数,内部依然是使用建造者模式
第二种使用建造者模式
两种方式最终都是调用构造函数OkHttpClient(builder:Builder),由参数builder负责所有的参数配置工作。
当您创建单个OkHttpClient实例并将其用于所有 HTTP 调用时,OkHttp 性能最佳。 这是因为每个OkHttpClient都拥有自己的连接池和线程池,重用连接和线程可减少延迟并节省内存。 相反,为每个请求创建一个客户端会浪费空闲池上的资源。
Request同样使用建造者模式来创建,这里贴上部分重要源码,很简单就不细说了。
OkHttp发起网络请求分为同步请求和异步请求两种方式,我们只分析异步请求流程,因为只要理解了异步请求过程,基本上也就明白同步请求是怎么一回事了。
RealCall是连接应用层与网络层的桥梁,负责处理连接、请求、响应和数据流。
Dispatcher维护着一套异步任务执行策略,分析策略之前先介绍几个重要概念:
client.dispatcher.enqueue(AsyncCall(responseCallback)) 执行步骤为:
AsyncCall实现了Runnable接口,因此一旦被线程池中的线程处理就会调用它的run()方法:
话休絮烦,我们开始分析拦截器责任链:
责任链执行流程:首先获取当前拦截器interceptor,并且调用interceptor.intercept(next)执行拦截器操作。这里的next表示的是index+1后的责任链对象,拦截器的intercept()方法内部会调用next.proceed(request)方法再次进入到责任链,由于此时index已经加1,所以处理的是下一个拦截器。
如此循环往复,直到处理完责任链上最后一个拦截器为止。
注意除最后一个拦截器CallServerInterceptor不会调用chain.proceed(request)方法之外,其他拦截器都应该至少调用一次chain.proceed(request)方法。
为了验证上面的结论,我们进入到RetryAndFollowUpInterceptor的intercept()方法一探究竟:
可以看到注释1处重新进入责任链处理下一个拦截器。
有兴趣可以自行查看最后一个拦截器CallServerInterceptor源码,此处只给出本人阅读源码后得出的结论:
以上就是拦截器责任链的工作流程,我们再通过流程图仔细感受一下。
分析完拦截器责任链,我们继续分析AsyncCall#run()方法:
我们看到,如果()方法成功获得服务端返回的数据,则调用responseCallback.onResponse(this@RealCall, response)方法完成异步回调;如果服务端数据获取失败(请求异常),则调用responseCallback.onFailure(this@RealCall, canceledException)方法完成异步回调
需要注意的是,responseCallback回调是在子线程中完成的,所以如果想把数据显示到UI上,需要切换回主线程进行UI操作。
OkHttp发起网络请求全过程:
【知识点】OkHttp 原理 8 连问
D. 网络请求框架-OkHttp原理解析
okhttp是square公司贡献的一个处理网络请求的开源框架,是目前Android开发使用最广泛的一个网络框架,从Android4.4开始,httpURLconnection的底层实现采用的就是okhttp。内部实现就是利用java基础,对socket进行封装,实现http通信。最重要的两个关键点就是分发器和5个拦截器。
分发器 就是内部维护队列和线程池,完成请求分配,总结就是用于对异步任务加入队列管理,然后判断条件,控制数量,加入线程池执行异步请求任务。
五个默认拦截器 就是利用责任链模式对网络请求进行层层处理,完成整个请求过程,简单总结如下。
1.桥接拦截器对用户发出的请求添加缺少的请求配置字段,比如keep-alive等
2.缓存拦截器就是查询有没有符合判断条件的已缓存的网络请求,执行复用,直接返回response
3.连接拦截器就是创建请求,加入连接器 或者访问连接池,根据条件判断,是否能怼已创建的tcp请求进行复用
4.请求服务器拦截器就是对scoket进行操作,请求网络访问服务器,返回response,
5.重试和重定向拦截器就是对返回的response进行code判断,决定是否要重试或者重定向操作。
1.支持http2.0版本,并且允许对同一主机的所有请求共享一个套接字
2.即使不是http2.0版本,通过连接池,减少请求延迟
3.默认使用Gzip 压缩数据
4.响应缓存,避免重复请求网络
最简单的http请求案例
1.利用建造者模式构建okHttpClient实例对象,构建过程中可以动态配置参数,请求时间,响应时间,缓存信息等。
2.创建Request对象,设置请求方式,链接地址,参数等信息。
3.把request对象,传给client,通过newCall函数,得到RealCall对象。
4.RealCall 分为同步和异步执行
5.同步执行时,分发器只是做个记录,把请求任务加到队列中,然后直接通过拦截器访问服务器,返回response。
6.异步执行
6.1先对异步任务进一步封装,把任务放到AsyncCall对象中
2.分发器 把 封装后的异步任务 添加到等待运行的队列中
7. 通过拦截器,获取response
okhttp 默认提供5个拦截器 重试重定向拦截器,桥接拦截器,缓存拦截器,连接拦截器,访问服务器拦截器。还可以自定义拦截器。
自定义拦截器分为应用拦截器(通过addInterceptor 添加)和网络拦截器(通过addNetworkInterceptor拦截)
拦截器采用责任链的设计默认,让请求者和处理者解耦,最终请求从前往后,响应从后往前。
首先先判断用户是否取消了请求,如果没有取消,就把请求交个桥接拦截器。
在获得响应结果response的时候根据响应码,判断是否需要重试或者重定向, 重试不限制次数,重定向最多20次 ,如果需要重试或者重定向,那么会再一次重新执行所有拦截器。
有如下几种情况不会重试:IO异常,线路异常,配置client实例时配置不允许重试,协议异常,证书异常等等。
先获取用户发送的请求,判断条件用户是否已经配置过请求头字段,若用户没有配置,则将http协议必备的请求头字段补齐,比如Content-Type,Content-Length等,然后交给下一个拦截器。
在获得响应结果response之后,调用保存cookie的接口(也可以在配置client的时候,设置cookjar进行cookie回调数据),并且解析gzip数据
获取结果之后,对cookie进行保存,对返回的数据进行gzip解压
就是根据缓存策略从缓存中查找是否有合适的缓存response,如果有合适的缓存,直接返回给请求任务,不在继续执行后面的拦截器。
获得响应结果response后,根据条件判断,决定是否要缓存。
维护一个连接池,负责对连接的服务。在把请求交给下一个拦截器之前。会先在连接池中找到一个合适的连接(满足适配条件相同,并且没有正在被使用)或者新建一个连接,并且接入连接池,获得对应的socket流,把请求交给下一个拦截器。获得response结果后不会进行额外的处理。
连接池, 也称之为对象池,主要用来存放request请求连接,内部维护了一个LinkedQueue队列用来存放请求。在添加新的请求对象时,都会执行一个周期性任务,用以对连接池进行清理操作。
1.队列长度超过5,清理最近未被使用连接,LRE算法
2.存储的连接,5分钟未被复用,清理
拿到上一个拦截器返回的请求,真正的与服务器进行通信,向服务器发送数据,解析读取响应的数据,返回给上一个拦截器。
1.创建request =>OkHttpClient=>RealCall()
2.同步执行 ,分发器添加同步任务,执行拦截器,访问服务器,返回reponse,触发异步分发流程。
3.异步执行 ,封装任务= >AsyncCall ,实现runnable接口。添加任务到异步任务等待队列,执行分发任务,判断异步任务是否能加入正在执行的异步任务队列,满足两个条件
同时执行的异步任务数量不得大于64个
对同一个主机的访问任务,最多不得大于5个
4.加入正在执行的异步任务队列,通过线程池执行任务,经过5个默认拦截器访问服务器,返回response,执行异步任务分发。
分发器工作 分为同步任务和异步任务两种
同步任务 就是把任务加入同步任务队列,加个标记,执行结束之后,触发异步任务的分发操作。
异步任务 先封装任务到asyncCall对象,实现了runnable接口。把任务加入等待执行队列,执行分发操作。
先遍历等待任务队列,判断是否符合加入正在运行的异步任务队列,要同时满足两个条件。
同时执行的异步任务数量不得大于64个
对同一个主机的访问任务,最多不得大于5个
当满足条件后,从等待队列中删除任务,把任务加入正在执行的队列中,通过自定义的线程池,执行任务,任务执行结束后,再次执行分发操作。
拦截器采用了责任链设计默认,让请求者和执行者解耦,请求者只需要将请求发给责任链即可,无需关心请求过程和细节。okHttp 默认有5个拦截器,重试重定向拦截器,桥接拦截器,缓存拦截器,连接拦截器,请求服务拦截器。工作细节参考上面拦截器原理分析部分
1.位置的关系,应用拦截器 放在责任链最顶端,网络拦截器放在责任链倒数第二的位置。所以应用拦截器 最先拦截,最后响应,网络拦截器 倒数第二拦截,第二响应。如果打印请求日志的情况,应用拦截器打印的是用户请求信息,经过重试重定向,桥接,缓存,链接 等拦截器的层层包装,网络拦截器打印的是实际请求的信息。
2.应用拦截器一定会被执行,网络拦截器不一定被执行。
利用连接池,缓存所有的有效连接对象。
清理机制:垃圾连接
1.超过5分钟没有用过的链接
2.超过5个闲置链接后,从最久闲置的链接开始执行清理(LRU)