OkHttp系列文章如下
本文主要是综述与常识介绍
OkHttp是一个高效的Http客户端,有如下的特点:
- 支持HTTP2/SPDY黑科技
- socket自动选择最好路线,并支持自动重连
- 拥有自动维护的socket连接池,减少握手次数
- 拥有队列线程池,轻松写并发
- 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)
- 基于Headers的缓存策略
本文基于okhttp3
源码进行分析,逻辑错误或者不足请指出!
建议使用Idea作为分析工具。
主要对象
- Connections: 对JDK中的socket进行了引用计数封装,用来控制socket连接
- Streams: 维护HTTP的流,用来对Requset/Response进行IO操作
- Calls: HTTP请求任务封装
- StreamAllocation: 用来控制
Connections
/Streams
的资源分配与释放
工作流程的概述
当我们用OkHttpClient.newCall(request)
进行execute/enenqueue
时,实际是将请求Call
放到了Dispatcher
中,okhttp使用Dispatcher进行线程分发,它有两种方法,一个是普通的同步单线程;另一种是使用了队列进行并发任务的分发(Dispatch)与回调,我们下面主要分析第二种,也就是队列这种情况,这也是okhttp能够竞争过其它库的核心功能之一
1. Dispatcher的结构
Dispatcher维护了如下变量,用于控制并发的请求
- maxRequests = 64: 最大并发请求数为64
- maxRequestsPerHost = 5: 每个主机最大请求数为5
- Dispatcher: 分发者,也就是生产者(默认在主线程)
- AsyncCall: 队列中需要处理的Runnable(包装了异步回调接口)
- ExecutorService:消费者池(也就是线程池)
- Deque<readyAsyncCalls>:缓存(用数组实现,可自动扩容,无大小限制)
- Deque<runningAsyncCalls>:正在运行的任务,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存
根据生产者消费者模型的模型理论,当入队(enqueue)请求时,如果满足(runningRequests<64 && runningRequestsPerHost<5)
,那么就直接把AsyncCall
直接加到runningCalls
的队列中,并在线程池中执行。如果消费者缓存满了,就放入readyAsyncCalls
进行缓存等待。
当任务执行完成后,调用finished的promoteCalls()
函数,手动移动缓存区(可以看出这里是主动清理的,因此不会发生死锁)
本部分详细版在OkHttp3源码分析[任务队列]
Socket管理(StreamAllocation)
经过上一步的分配,我们现在需要进行连接了。我们目前有封装好的Request,而进行HTTP连接需要进行Socket握手,Socket握手的前提是根据域名或代理确定Socket的ip与端口。这个环节主要讲了http的握手过程与连接池的管理,分析的对象主要是StreamAllocation
1. 选择路线与自动重连(RouteSelector)
此步骤用于获取socket的ip与端口,各位请欣赏源码中next()
的迷之缩进与递归,代码进行了如下事情:
如果Proxy
为null
:
- 在构造函数中设置代理为
Proxy.NO_PROXY
- 如果缓存中的
lastInetSocketAddress
为空,就通过DNS(默认是Dns.SYSTEM
,包装了jdk自带的lookup函数)查询,并保存结果,注意结果是数组,即一个域名有多个IP,这就是自动重连的来源 - 如果还没有查询到就递归调用next查询,直到查到为止
- 一切next都没有枚举到,抛出
NoSuchElementException
,退出(这个几乎见不到)
如果Proxy
为HTTP
:
- 设置socket的ip为代理地址的ip
- 设置socket的端口为代理地址的端口
- 一切next都没有枚举到,抛出
NoSuchElementException
,退出
- HTTP代理是不安全的,本文附录有介绍
- HTTP代理会帮你在远程服务器进行DNS查询
- 至于socket代理这里就不分析了,它已经不属于应用层了
2. 连接socket链路(RealConnection)
当地址,端口准备好了,就可以进行TCP连接了(也就是我们常说的TCP三次握手),步骤如下:
- 如果连接池中已经存在连接,就从中取出(get)RealConnection,如果没有命中就进入下一步
- 根据选择的路线(Route),调用
Platform.get().connectSocket
选择当前平台Runtime下最好的socket库进行握手 - 将建立成功的
RealConnection
放入(put)连接池缓存 - 如果存在TLS,就根据SSL版本与证书进行安全握手
- 构造HttpStream并维护刚刚的socket连接,管道建立完成
关于
Platform
,DNS
,Proxy
详细请看附录
3. 释放socket链路(release)
如果不再需要(比如通信完成,连接失败等)此链路后,释放连接(也就是TCP断开的握手)
- 尝试从缓存的连接池中删除(remove)
- 如果没有命中缓存,就直接调用jdk的socket关闭
本部分详细版见: OkHttp3源码分析[复用连接池]
HTTP请求序列化/反序列化
本段主要分析从拼装HTTP套接字到读取的步骤,用垠神的话说,就是实现了一个Parser。分析的对象是HttpStream
接口,在HTTP/1.1下是Http1xStream
实现的。
1. 获得HTTP流(httpStream)
以下为无缓存,无多次302跳转,网络良好,HTTP/1.1下的GET
访问实例分析。
我们已经在上文的RealConnection
通过connectSocket()
构造HttpStream
对象并建立套接字连接(完成三次握手)
httpStream = connect();
在connect()
有非常重要的一步,它通过okio库与远程socket建立了I/O连接,为了更好的理解,我们可以把它看成管道
//source 用于获取response
source = Okio.buffer(Okio.source(rawSocket));
//sink 用于write buffer 到server
sink = Okio.buffer(Okio.sink(rawSocket));
Okhttp的I/O使用的是Okio库,它是java中最好用的I/O API,本人曾经写NFC对这个用的就非常顺手。
Buffer
: Buffer是可变字节,类似于byte[]
,相当于传输介质source
: source是okio库中的输入组件,类似于inputstream,经常在下载中用到。它的重要方法是read(Buffer sink, long byteCount)
,从流中读取数据。Sink
: sink是okio库中的io输出组件,类似于outputstream,经常用于写到file/Socket,它的最重要方法是void write(Buffer source, long byteCount)
,写数据到Buffer
中如果把连接看成管道,
->
为管道的方向,如下图,这里借鉴了go语言的描述
Sink -> Socket/File Source <- Socket/File
2. 拼装Raw请求与Headers(writeRequestHeaders)
我们通过Request.Builder
构建了简陋的请求后,可能需要进行一些修饰,这时需要使用Interceptors
对Request
进行进一步的拼装了。
拦截器是okhttp中强大的流程装置,它可以用来监控log,修改请求,修改结果,甚至是对用户透明的GZIP压缩。类似于脚本语言中的map操作。在okhttp中,内部维护了一个Interceptors
的List,通过InterceptorChain
进行多次拦截修改操作。
请求的代码如下,详细代码在这里,源代码中是自增递归(recursive)调用Chain.process()
,直到interceptors().size()
中的拦截器全部调用完。这里代码维护性估计看着头大,大神们以后可能把它改成for等更简单的循环,主要做了两件事:
- 递归调用Interceptors,依次入栈对response进行处理
- 当全部递归出栈完成后,移交给网络模块(getResponse)
if (index < client.interceptors().size()) {
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
Interceptor interceptor = client.interceptors().get(index);
//递归调用Chain.process()
Response interceptedResponse = interceptor.intercept(chain);
if (interceptedResponse == null) {
throw new NullPointerException("application interceptor " + interceptor
+ " returned null");
}
return interceptedResponse;
}
// No more interceptors. Do HTTP.
return getResponse(request, forWebSocket);
}
接下来是正式的网络请求getResponse()
,此步骤通过http协议规范
将对象中的数据信息
序列化为Raw文本
:
- 在okhttp中,通过
RequestLine
,Requst
,HttpEngine
,Header
等参数进行序列化操作,也就是拼装参数为socketRaw数据。拼装方法也比较暴力,直接按照RFC协议要求的格式进行concat输出就实现了 - 通过sink写入
write
到socket连接。
具体代码在这里。
1.3. 获得响应(readResponseHeaders/Body)
此步骤根据获取到的Socket纯文本
,解析为Response对象
,我们可以看成是一个反序列化(通过http协议将Raw文本转成对象)的过程:
拦截器的设计:
自定义网络拦截器
请求进行递归入栈- 在
自定义网络拦截器
的intercept
中,调用NetworkInterceptorChain
的proceed(request),进行真正的网络请求(readNetworkResponse) - 接自定义请求递归出栈
网络读取(readNetworkResponse)分析:
伪代码如下:
(RawData <- RemoteChannel(www.xx.com, 80))//读取远程的Raw
map(func NetworkInterceptorChains())//预处理
//这里的source引用了HttpEngine,并重写了read方法
.map(func getTransferStream(){})
//根据source拼装body对象
.map(func RealResponseBody(){})
接下来进行释放socket连接,上文已经介绍过了。现在我们就获得到response
对象,可以进行进一步的Gson等操作了。
附录
以下为一些计算机常识
1. Proxy
代理,也就是有个中间服务器帮助你访问不存在的网站,okhttp中使用jdk自带的代理
You ---- Proxy ----- Server
HTTP代理的本质是改Header信息,当你访问HTTP/HTTPS服务时,本质是明文向跳板发送如下raw,远程服务器帮你完成dns与请求操作,比如HTTPS请求源码就详细的解释了发送的内容是非加密的,下面是我实际抓包的内容
//HTTP 请求
GET HTTP://www.qq.com HTTP/1.1
//HTTPS 请求
CONNECT github.com:443 HTTP/1.1
上面的抓包过程,廉价的民用上网行为管理交换机就可以把你记录的一清二楚,所以慎用HTTP代理或者尽量使用HTTPS代理,它是“不安全”的。
2. DNS
DNS也就是域名到ip的映射(mapping
)操作,用户向DNS服务器的53端口发送udp包后,会返回域名对应的地址,当然发送udp的细节对用户是透明的,用户直接调用jdk就可以了。我们先试下Unix下的查询
$ host baidu.com
baidu.com has address 111.13.101.208
baidu.com has address 123.125.114.144
.....
在OkHttp中,提供了DNS接口,默认是使用Dns.SYSTEM
,它包装了java原生socket包中的InetAddress.getAllByName(hostname)
方法。
3. Platform
OkHttp的最底层是Socket,而不是URLConnection,它通过Platform
的Class.forName()
反射获得当前Runtime使用的socket库,调用栈如下(了解即可)
okhttp//实现HTTP协议
framwork//JRE,实现JDK中Socket封装
jvm//JDK的实现,本质对libc标准库的native封装
bionic//android下的libc标准库
systemcall//用户态切换入内核
kernel//实现下协议栈(L4,L3)与网络驱动(一般是L2,L1)
如果你想用蓝牙硬件中Socket的进行HTTP协议开发,尝试重写这个类。
另外,再说一句废话,自从Android4.4以来,URLConnection在fram的实现也是使用了okhttp
OkHttp支持非常多平台下的Socket库实现,包括
Android, JettyBootPlatform
等都是支持的,具体的平台支持可以看这里
4. 如何调试HTTP发送的内容
如果需要对OkHttp进行调试,可以看
综述完成,如果需要更深入了解,可以按照目录接着看下去