之前使用Retrofit的时候把里面的源码过了一遍,retrofit的核心在于帮助使用者构建构建一个个请求,避免我们每次调用网络的时候都需要重新的去创建请求,其底层的网络库的实现采用的是okhttp, 下面是自己看了源码之后的学习笔记(目前还比较凌乱,后面写完了会重新整理)
个人的学习源码的习惯从一个非常简单的事例开始,然后自己跟着这个示例一步步的调试源码,看源码里面到底是怎么一步步实现的的,下面是非常简单的一个例子,使用okhttp 请求访问百度。
|
|
Okhttp的核心设计
okhttp的核心设计在于将一个请求的不同阶段分开实现通过 Interceptor 来实现,通过这样的分层,每个层级专注于自己的工作,充分实现了解耦,比如针对是http1还是http2,BridgeInterceptor 只负责添加一些基本的请求头,而不用管TCP 连接到底是复用还是新建的,任何TCP链接实现层的变动都不会影响 BridgeInterceptor的实现。
在默认构建的OkhttpClient中,添加了四个Interceptor ,
BridgeInterceptor 主要用于构建http 请求,在拿到用户的请求之后,会自己添加一些基本的头信息
CacheInterceptor 主要负责应用缓存,通过缓存处理请求和根据响应头内容缓存响应
ConnectInterceptor okhttp中的重点,找到一个可用的链接来发送请求,其中的逻辑比较复杂也是重点学习和分析的对象。
CallServerInterceptor 最终完成与server的交互,向server写入请求和读取请求。
在发送请求的过程中,请求发送的过程中从上往下调用,一步步将请求发送到server,在处理响应的过程中,从下往上调用,一步步将响应交给用户。
Client的构建
在Ok中一个非常重要的对象就是Client,在这个对象当中我们根据自己的需要来定制一些对请求和响应的处理,监听请求的各个阶段,代理的设置,缓存的处理,请求协议的处理,以及在OK中非常重要的拦截器的配置等等,这些实际上都是在Client构建的过程中完成的在示例中我们简单的创建了一个请求事件的监听者,监听请求刚开始发送的事件。
在这个Client对象中,有几个比较重要的成员需要先说一下
- Dispatcher 在文档的注释中,这个类的说明是 当异步请求执行时的策略, 也就是异步请求是怎么被调度的,其中是采用ExecutorService 在实现每一个call的调用。
- Interceptor ,拦截器,在Client当中为我们默认的添加了一列的拦截器,在拦截器中我们可以对发出去的请求和接收到的响应做修改,这个和 Java web当中的filter很像,都是在中间环节去请求和响应做一些处理。在Interceptor 中还有一个子接口,Chain, 这个接口中定义一个十分重要的方法,
|
|
即处理请求返回响应,同时这个接口也可以间接的说明这些拦截器构成了一个链 来处理这些请求。
请求的构建
一个Http Request 的构建
ok中请求的构建使用过建造者模式创造出来的,非常的简单,其中包括了构建一个请求的必不可少的要素,包括方法,请求地址 , 请求的头部,请求的body. 不指定的方法时候默认使用 GET 方法 , 同时头部默认是一个空的集合,既然默认是GET方法,那么body也就为空了,除此之外每个请求之中还有一个为object类型的tag.
创建一个请求的调用
在ok中,使用call这个接口来表示一个请求/响应 的调用,每个请求只能够执行一次,其中有两个关于执行请求的方法。
|
|
execute 方法表示同步执行这个请求,在执行了之后,就会返回这个响应,而enqueue则是异步的执行请求,在请求执行完成之后会调用传入的callback 对象。
在上面的示例代码中,我们直接调用execute方法,让这个方法同步执行,返回一个响应。
先看 client.newCall(request) 这个方法
|
|
方法十分简单,返回了一个RealCall 对象,而这个RealCall对象实现了Call这个接口,表示了这是一个请求调用,从上面的这一部分,可以说就完成了一个请求的对象的构建,接下来就是将我们的请求对象发送出去了。
请求的发送
1. TCP链接
因为Http 是基于TCP协议的,所以一个HTTP请求的构建必须建立在一个TCP链接之上,而okhttp之所以这么受欢迎就是因为这个库在TCP链接的使用上做了比较大的优化, 在okhttp中,使用 Connection 来表示一个链接,一个链接上有多个数据流,在http 1版本中可以一个连接上只能有一个数据流,在http2中可以实现多路复用,而实现了一个链接上有多个数据流同时传输.(其实这里的数据流可以理解为一个http请求)。
在okttp中,为了实现一个一个数据流去复用一个connection ,数据流和链接被分离开来。
在实际的请求当中,StreamAllocation 扮演了一个重要的角色,在okhttp的文档当中,这个类将三个重要的实体关联了起来
- 连接,代表了和远程服务器之间的物理连接
- Stream,表示在连接上之上的http请求和响应对
- Calls: 表示一个逻辑序列的Stream调用
这个对象的创建时在 RetryAndFollowUpInterceptor 这个拦截器的 intercept 方法当中,实际上每次请求调用的时候都会首先执行 RetryAndFollowUpInterceptor 的 intercept 方法,
|
|
在构建这个对象的时候需要一个ConnectionPool ,一个Address 对象,请求调用和,事件监听对象,以及一个object.
接下来我们来看一个请求的连接是如何构成的,在http中定义了一系列额拦截器,每个拦截器都在构建发送http请求的过程中发挥了作用,其中 网络连接的建立请求是通过 ConnectInterceptor 这个拦截器完成的,其中的 intercept 方法实现如下
|
|
在方法的第三行拿到的streamAllocation对象就是我们在 RetryAndFollowUpInterceptor 中创建的 StreamAllocation 对象,然后获取了一个HttpCodec 对象。 在okhttp中,实现了对http 1/2 两个版本的支持。而两种实现都是通过对HttpCodec这个接口实现实现。在文档中,对这个类只有一句话,
Encodes HTTP requests and decodes HTTP responses.
先看这个HttpCodec是怎么获取的,进入到StreamAlloaction中的newStream 方法当中,核心代码如下
|
|
在方法当中核心是构建了一个Connection 对象(代表了到远程服务器的一个链接),然后在这个对象上构建了HttpCodec 方法,继续看 关键的findHealthyConnection方法,
|
|
其实这个方法的注释也说得比较清楚了,从代码中也可以看到在一个死循环中寻找一个可用的Connection ,核心代码是 findConnection 这个方法的执行。
从这个方法的注释当中可以看出来这个方法到底是怎么工作的
- 如果当前已经有一个链接了,那么直接复用这个链接。
- 如果没有的话从连接池里面拿一个链接。
- 如果连接池里面没有的话,那么新建一个链接。
第一个步骤略过, 接下来,看下这三个步骤是如何构执行的。
从连接池里面找到链接。
|
|
Internal.instance 这个对象是在构建 OkHttpClient 这个Class对象的时候创建,放在OkHttpClient的静态代码块中,目前来这个类中没有什么有价值的代码,所有方法的实现都是直接调用了传入参数的方法,相当于只做了已成封装,看get方法的调用
|
|
直接返回了pool调用的结果,进入ConnectionPool 的 get方法,
|
|
方法中把当前已有的connections 遍历了一遍,然后筛选出一个可用的链接,进入到isEligible 这个方法当中
这个方法比较长,这里就不贴代码了,说一下里面的关键步骤
- 首先判断当前连接上还能不能接受新的Stream(超过最大的限制,或者 noNewStreams被设定为true)
- 如果当前连接的route 的地址和请求的Address不匹配直接返回false,在okhttp 中,Address的比较考虑了很多的因素,这里就不展开了,同时在比的过程并没有对比host这个关键的部分。
- 如果这个时候请求的host相等,那么认为当前的连接是可以发送这个请求的
- 到这里为止,请求的地址在先有的连接中并没有找到,但是这个时候任然可以将请求合并,在源码的注释当中也给出了这个理论的说明。
- 首先这个链接上实现的必须是http2 协议
- 先有链接的ip和请求的ip必须是同一个ip,因为这个限制,所以必须有了DNS的信息之后我们才能知道ip信息,同时这也要求这个连接是没有代理的,因为在经过代理之后是不知道源服务器的地址的。
- 如果是https请求,证书锁必须匹配主机,这个实际上是指需要支持通配符证书。
如果当前的连接是可以用的,进入到streamAllocation 的 acquire方法当中,而这个方法将streamAllocation的connection 对象指向了刚才获取的连接。
如果当前的连接池当中没有找到一个合适的连接,返回null.
这个时候方法的调用回到 findConnection 当中,
|
|
这个时候如果当前的Connection 不是null ,说明ConnectionPool的get方法找到了可用的连接,那么这个时候可以直接返回,否则继续往下走。
在创建一个新的连接的时候,有几个事情是需要决定的,首先连接到一个域名的时候,DNS可能返回了多个指向这个地址的IP ,同时这个地址还有使用了代理,所以这个时候需要选择一个连接的地址,接下来的逻辑就是在选择连接对象。
在选择连接对象中,Route这个对象核心,所有的逻辑都是为围绕这儿类来进行的,这里就不详细说里面的代码了。
在获取到了具体的Route之后,在从连接池里面寻找有没有可用的连接。继续调用ConnectionPool的get方法。
如果找到了就直接返回,这个可以复用的连接。
如果这个时候还没有找到可以复用的连接,那么这个时候直接先创建一个RealConnection 对象。
这个时候只是表示连接的对象被创建了,需要开始进行connect操作。
|
|
这个里面就是具体的连接TCP连接操作了,先看第一部分
|
|
首先在这个方法的前面需要获取对应Address的请求ConnectionSpec,ConnectionSpec指定了在socket之上的HTTP流的配置,比如在使用https的时候,使用的TLS版本和cipher suites(密码套件),这个ConnectionSpec列表是在构建okhttpClient的时候使用的,默认的情况下它包含了两个配置项,ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTET,这两个对象分别表示了TLS的1.0 - 1.3的四个版本和未加密的明文。
ConnectionSpecSelector这个类主要负责connection spec 的降级机制,
接下来的这部分代码就是在作请求配置的验证,如果这个不是一个https的请求,但是这个请求当中又不包含ConnectionSpec.CLEARTEXT,这个可以发送明文请求的配置,那么这个时候抛出一个Runtime异常。下一步调用的isCleartextTrafficPermitted一直都是返回true,那么这里实际上需要根据的自己的需要来配置。
在完成了http请求配置的验证之后,下面的代码开始建立一个socket连接
|
|
首先是requiresTunnel的理解,当使用https通过http代理服务时,http代理会认为这是一个安全https链接,将会放行https封包,所以我们可以使用https来欺骗http代理服务器,而我们自行构建的https封包中则可以封装我们自定义协议的真实数据(因为https是二进制的)。这一切让http代理看起来就像是我们在进行安全http连接一样。
简单的说一下connectTunnel中的步骤
1) 首先创建一个连接到代理服务器的Request 对象。
2)创建并且建立到代理服务器的socket 请求(调用connectSocket方法)
3)createTunnel 向代理服务器发送一个Connect请求,然后对响应进行判断,如果成功的和目标服务器建立了连接,那么这个时候connect回返回200,还有一种可能是代理服务器反悔了401,即未授权。
然后,如果这个时候是不需要http 隧道的话,就可以调用connectSocket方法直接创建一个的socket连接了。
再接下来建立协议进入 establishProtocol 方法,这个里面主要包含了https 和 http2 协议的相关设定。
当这个这个请求是一个非https的请求的时候,那么一旦socket连接建立完成,那就可以直接返回了,如果是一个https的请求,那么这个时候就要开始TLS的握手过程了。这个握手的过程在RealConnection 的connectTls 方法当中.
方法内存首先开始配置socket的密码套件,TLS版本和TLS扩展,关于TLS扩展可以查看博客
调用sslsocket.startHandShake 开始握手(博客),具体握手流程如下
a) 客户端发送被称为ClientHello的信息给服务器端,其中包括了TLS的版本,支持的加密算法和一个随机生成的字符串。
b) 服务器端接收到客户端发送的ClientHello之后,也向客户端发送一个被称为ServerHello的信息,其中包括了选择的加密算法,一个随机字符串,服务器端的证书,还有选择的TLS版本,如果客户端支持的版本和服务器支持的版本不一致则关闭加密通信。
c)客户端收到服务器的消息之后开始对服务器端的消息进行验证,包括证书中的域名和实际的域名是否匹配,证书是否过期和证书是不是由可靠机构办法,否则向用户给出警告,由用户决定是否继续进行。完成校验之后,服务器端回复如下信息:由服务器公钥加密的随机串(保证安全); 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送;客户端握手结束通知,表示客户端的握手已经结束了,这一项内存还是前面所有消息的hashcode ,给服务器端验证。
4)服务器端接收到客户端消息,向客户端发送消息编码改变的通知,随后的消息都将采用约定的秘钥和加密方法进行,同时还有服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
从sslSocketSession中创建一个Handshake对象,这个对象里面包括了这次握手的基本信息,TLS版本,加密套件,本地发送的证书,和从服务器端接收到的证书。
然后调用HostnameVerifier 的verify 验证主机地址是否在证书的接收范围之内,如果不在的话会抛出一个SSLPeerUnverifiedException,https连接验证失败。在okhttp中,HostnameVerifier中返回的是 OkHostnameVerifier的一个实例。
接下来是HPKP的验证,这种验证也是一种防止中间人攻击的验证(博客),如果验证是失败同样会抛出一个SSLPeerUnverifiedException异常。
如果以上步骤都没有出现问题,那么这一次的handshake就完成了。
那么一直走到这里,一个socket链接的建立就算是完成了,其中包含了很多实现的细节和各种需要考虑的情况。
这个时候一步步返回的话最终回到了StreamAllocation的newStream 方法当中,
|
|
在找到了resultConnection 之后,通过RealConnection的 newCodec 方法来构建了一个HttpCodec,在这个方法里面判断了一个下当前的协议的版本,http1 还是http2,然后返回了不同的实现。
再接着从调用栈里面向上返回到了ConnectInterceptorConnectInterceptor 的intercept方法。
|
|
获取了connection 之后,最终使用Chain来调用下一个拦截器。
接下需要执行的是execute这个方法,在RealCall的实现中,这个方法的代码十分简短,但是其中的调用链却是非常的复杂,先看代码
|
|
在上面已经说过了,一个调用只能执行一次,所以在这个方法前面,首先就是判断是否已经执行过。
captureCallStackTrace(); 可以暂时先略过
接下来就是事件监听者的调用了,在第一部分中我们说明了,可以监听这个请求的各个阶段,在这里这个请求就正式开始调用了,所以调用了 callStart 这个方法。
在第一部分中说过,请求的调度是通过dispatcher这个部分来完成,但是在Dispatcher 的文档中说了这个对象是异步请求的调度策略,所以对于同步请求来说,只是这个对象放到了一个 Deque (双向队列)集合当中,真正的执行还不在这一步。
|
|
接下来就是 getResponseWithInterceptorChain 这个方法,看到这个方法名字就知道正如第一部分所说义,所有的请求都是在这个方法中完成的,先看源码。
|
|
在这个方法里面通过配置了一系列的拦截器,通过这些拦截器最终完成了一个请求,再看这些拦截器之前,先看下Chain是怎么调用这系列的拦截器的。
Chain 这个接口的真正实现类是 RealInterceptorChain, 其中核心的实现方法如下。
|
|
其中方法的核心就在于方法的处理过程中又创建了一个拦截器来处理请求,这个时候讲index加了1 ,所以这个时候请求是直接传递了下个拦截器处理。
这里找个拦截器的代码看下,在RetryAndFollowUpInterceptor 的intercept方法中,
|
|
可以看到其中对请求的处理还是调用了Chain的 proceed方法,通过这样一个链式的调用,最终完成了对请求链的调用。
回到 RealCall 中的getResponseWithInterceptorChain方法,那么实际上最先处理请求的反而是 CallServerInterceptor 这个拦截器了。
|
|
接下来我们就来分析这一个个的拦截器,来看一个请求到底是怎么完成的。
按照刚才我们说的最后添加的拦截器反而是最后执行的,那么这个时候应该是 CallServerInterceptor这个拦截器。
这个时候方法的执行我们分几部分执行
请求头的写入
|
|
这里使用前文提到的HttpCodec 完成http请求的编码,非常简单就是对http请求的编码和对响应的解码,其中包含的方法可以去看其中的方法,这里就不贴出来了,跟上面代码相关的是 writeRequestHeaders 这个方法,表示对请求头部的写入。
在代码的分析中只分析Http1Codec.
|
|
先获取请求行,然后在写具体的请求头部.
请求体的写入
部分请求方法需要携带请求体,那么在写完请求头部之后,就开始队请求体的处理。
|
|
请求的读取
在完成了请求发送之后就是请求的读取了,在读取的时候如果发现100 这种临时响应,则需要再次读取一个响应。
|
|
到这里就已经完成了从数据发送到响应读取的过程。
首先是添加 client.interceptors(),在示例代码中,并没有配置拦截器,所以这个集合当中什么都没有,但是在项目中,可以通过自己自定义的拦截器来实现对数据的一些个性化处理,比如在开发阶段通过自定义的拦截来实现对请求和响应的日志输出。
接下来是 retryAndFollowUpInterceptor 这个拦截器,这个类的定义如下,
|
|
这个方法是用在在错误处理和重定向当中发挥作用的,大概的看一下其中interecept方法的实现
|
|
上面的代码主要分为了两个部分,首先判断请求有没有出现错误,并且判断这个错误是不是可以恢复的,其中细节可以看一下,如果是可以恢复的,那么就继续请求的发送处理,如果是不能处理的那么直接抛出异常。
如果请求没有出现错误,那么接下来会对响应进行处理,判断是否需要重定向等等一系列操作,在 followUpRequest 这个方法当中对各种请求码都进行了处理,返回了一个接下来需要进行的请求。
下面是抽取的其中一段代码,这一段对3xx一系列的请求进行了处理
|
|
由于在RetryAndFollowUpInterceptor 这个类中,对于请求的处理包含在一个whlie循环里面,所以不断的重复重复,直到得到一个正确的响应或者超过了最大的重定向请求次数,或者其他的条件不满足之后才会退出循环。