OkHttp3源码分析[综述]

144
作者 BlackSwift
2016.01.06 19:10* 字数 2931

OkHttp系列文章如下

本文主要是综述与常识介绍


OkHttp是一个高效的Http客户端,有如下的特点:

  1. 支持HTTP2/SPDY黑科技
  2. socket自动选择最好路线,并支持自动重连
  3. 拥有自动维护的socket连接池,减少握手次数
  4. 拥有队列线程池,轻松写并发
  5. 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)
  6. 基于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进行缓存等待。

当任务执行完成后,调用finishedpromoteCalls()函数,手动移动缓存区(可以看出这里是主动清理的,因此不会发生死锁)


okhttp dispatcher

本部分详细版在OkHttp3源码分析[任务队列]

Socket管理(StreamAllocation)

经过上一步的分配,我们现在需要进行连接了。我们目前有封装好的Request,而进行HTTP连接需要进行Socket握手,Socket握手的前提是根据域名或代理确定Socket的ip与端口。这个环节主要讲了http的握手过程与连接池的管理,分析的对象主要是StreamAllocation

1. 选择路线与自动重连(RouteSelector)

此步骤用于获取socket的ip与端口,各位请欣赏源码next()的迷之缩进与递归,代码进行了如下事情:

如果Proxynull:

  1. 在构造函数中设置代理Proxy.NO_PROXY
  2. 如果缓存中的lastInetSocketAddress为空,就通过DNS(默认是Dns.SYSTEM,包装了jdk自带的lookup函数)查询,并保存结果,注意结果是数组,即一个域名有多个IP,这就是自动重连的来源
  3. 如果还没有查询到就递归调用next查询,直到查到为止
  4. 一切next都没有枚举到,抛出NoSuchElementException,退出(这个几乎见不到)

如果ProxyHTTP:

  1. 设置socket的ip为代理地址的ip
  2. 设置socket的端口为代理地址的端口
  3. 一切next都没有枚举到,抛出NoSuchElementException,退出
  1. HTTP代理是不安全的,本文附录有介绍
  2. HTTP代理会帮你在远程服务器进行DNS查询
  3. 至于socket代理这里就不分析了,它已经不属于应用层了

2. 连接socket链路(RealConnection)

当地址,端口准备好了,就可以进行TCP连接了(也就是我们常说的TCP三次握手),步骤如下:

  1. 如果连接池中已经存在连接,就从中取出(get)RealConnection,如果没有命中就进入下一步
  2. 根据选择的路线(Route),调用Platform.get().connectSocket选择当前平台Runtime下最好的socket库进行握手
  3. 将建立成功的RealConnection放入(put)连接池缓存
  4. 如果存在TLS,就根据SSL版本与证书进行安全握手
  5. 构造HttpStream并维护刚刚的socket连接,管道建立完成

关于Platform,DNS,Proxy详细请看附录

3. 释放socket链路(release)

如果不再需要(比如通信完成,连接失败等)此链路后,释放连接(也就是TCP断开的握手)

  1. 尝试从缓存的连接池中删除(remove)
  2. 如果没有命中缓存,就直接调用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构建了简陋的请求后,可能需要进行一些修饰,这时需要使用InterceptorsRequest进行进一步的拼装了。

拦截器是okhttp中强大的流程装置,它可以用来监控log,修改请求,修改结果,甚至是对用户透明的GZIP压缩。类似于脚本语言中的map操作。在okhttp中,内部维护了一个Interceptors的List,通过InterceptorChain进行多次拦截修改操作。


interceptors

请求的代码如下,详细代码在这里,源代码中是自增递归(recursive)调用Chain.process(),直到interceptors().size()中的拦截器全部调用完。这里代码维护性估计看着头大,大神们以后可能把它改成for等更简单的循环,主要做了两件事:

  1. 递归调用Interceptors,依次入栈对response进行处理
  2. 当全部递归出栈完成后,移交给网络模块(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文本

  1. 在okhttp中,通过RequestLineRequstHttpEngineHeader等参数进行序列化操作,也就是拼装参数为socketRaw数据。拼装方法也比较暴力,直接按照RFC协议要求的格式进行concat输出就实现了
  2. 通过sink写入write到socket连接。

具体代码在这里

1.3. 获得响应(readResponseHeaders/Body)

此步骤根据获取到的Socket纯文本,解析为Response对象,我们可以看成是一个反序列化(通过http协议将Raw文本转成对象)的过程:

拦截器的设计:

  1. 自定义网络拦截器请求进行递归入栈
  2. 自定义网络拦截器intercept中,调用NetworkInterceptorChain的proceed(request),进行真正的网络请求(readNetworkResponse)
  3. 接自定义请求递归出栈

网络读取(readNetworkResponse)分析:

  1. 读取Raw的第一行,并反序列化StatusLine对象
  2. Transfer-Encoding: chunked的模式传输并组装Body

伪代码如下:

(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)方法。

参考:DNSPod中HTTP DNS的实现

3. Platform

OkHttp的最底层是Socket,而不是URLConnection,它通过PlatformClass.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进行调试,可以看

  1. 抓包方法
  2. okhttp-logging-interceptor

综述完成,如果需要更深入了解,可以按照目录接着看下去

Refference

  1. Socket sample in C and Java
  2. https://imququ.com/post/optimize-tls-handshake.html
  3. http://www.williamlong.info/archives/2210.html
  4. http://www.cnblogs.com/zemliu/p/4263048.html
  5. http://www.cnblogs.com/ct2011/p/3997368.html
  6. 架构设计:生产者/消费者模式
框架源码[APP]