一:概念
okhttp框架大家都很熟悉,是很常用的网络框架。okhttp可以理解为一个HTTP层面的框架,它的工作原理简单来说就是,先利用socket建立了与服务器的TCP连接,建立连接之后,在根据具体的需求,将符合HTTP协议的请求报文拼接好,进而通过刚才的连接传递到服务器,然后再读取服务器的响应。同时除了刚才基本HTTP的使用,okhttp提供了线程池,以此来执行具体的异步请求。
现在,我们对okhttp框架有了一个大概的认识。下一步就是分析它的工作原理,在分析其原理之前,先大概的分析一下HTTP协议,这是理解okhttp工作过程的前提。
HTTP协议,也就是超文本传输协议。它本质上也就是一个协议,而这个协议的主要目的是在网络中,让不同的计算机设备之间进行通信。HTTP协议针对通信的过程,提出了自己的规范和标准,然后通信的设备之间只要遵守这些限制条件,发送符合标准的格式的数据,就可以正常的通信。只不过它本质上只是一个协议,并不是强制执行的约束。
在真实的通信中,HTTP协议并不是一个孤立的协议,HTTP协议会依赖于很多协议,同时也有很多协议依赖于HTTP。HTTP本身会依赖于IP协议实现寻址和路由,依赖TCP协议来实现数据可靠传输,依赖DNS协议来实现域名的解析。如果是HTTPs的话,还会依赖TLS建立一个安全的传输通道。当然,也有一些协议依赖于HTTP,比如websoket。
刚才的IP/TCP可以理解为一个协议栈,而且还是一个有层次的协议栈,也就是我们常说的四层模型,具体见下图(图来自于极客教程):
刚才提到了,这是一个有层次的协议栈,类似于分而治之的思想。将真实复杂的通信世界,拆分成了四层,每层都有自己的职责,互不影响。每层的职责为:
一层:链路层,依靠mac地址来定位设备,网卡在这一层,数据单位是帧。
二层:网际层,ip协议工作的地方,通过ip协议进行寻址和路由,数据单位是包
三层:传输层,顾名思义,就是把需要传输的数据传输给接收方。并且只负责传输,不会 考虑数据的格式。用到的协议一个是TCP协议,是连续的字节流,顺序发,顺序 收,进行可靠的数据传输。另一个是UDP协议,分散的数据包,顺序发,乱序收。 这两个协议的数据单位是段。
四层:应用层,HTTP协议在这一层,除此之外,还有ssh,smtp等等。Http协议的数据单 位是报文。
TCP协议,也就是传输控制协议,HTTP依赖于它进行可靠的传输工作。TCP传输的数据单位可以理解为段,每一段数据除了传输的数据之外,还有TCP的头。头里面主要包括发送方端口号,接收方端口号,序列号和标志位等等。TCP会先通过三次握手来建立连接,然后才会传输数据。比如有两台主机A和B,然后A向B建立TCP连接。首先,A会向B发送一个标志位为SYN的数据,并同时还传递一个初始序列号,TCP能实现数据可靠的连续传输的关键就是序列号,这是第一次握手。B接收到了之后,会把A的序列化+1,然后回传给A确认,标志位为ACK。TCP中一般发送ACK确认消息的时候,都会附带一个ACK NUMBER,回传给发送给,用于序列号的确认。同时,还会传输一个SYN,并携带B自己的初始序列号,这是第二次握手。等A接收到B的回传之后,确认自己的初始序列号没问题,就会把ACK标志位和B的初始序列号,一块回传给B,B收到了之后,验证自己的序列号没问题,连接就此建立,这是第三次握手。
由此可知,TCP的三次握手真正目的是为了实现各自初始序列号的交换。只要A和B都确定对方接收到了自己的初始序列号,那么后续就可以进行可靠的数据传输。序列号是TCP能够实现可靠传输的关键,在HTTP通信中,客户端或者服务端,每接收到报文之后,总会回复给对方一个ACK,进行序列号确认。如果ACK没有成功发送到对方,那么TCP不会重发ACK。因为刚才的发送方在迟迟没有接收到ACK的时候,会消息重发,直到接收到确认消息为止。必要的时候会根据序列号进行消息的重组,从而保证传输的是连续的字节流。
总结来说,TCP能实现可靠传输的关键就是序列号。每次发送方发送数据的时候,头里面会有序列号,然后发送的每个字节的数据都有编号。每发送一段数据会等接收方的ACK确认消息,根据这个消息里面回传的序列号,就可以确认刚才发的消息都被接收方接收到了,就可以继续发送下一段的数据。如果迟迟没有接收到ACK,那么就会重发刚才的消息,避免消息的丢失,从而保证TCP以连续的字节流的方式来发送数据。
ACK,SYN都是TCP头数据里面的标志位,除了这两个,还有一个FIN,也就是Finish的意思,是在断开连接的时候使用。TCP断开连接的时候需要四次挥手,具体可以看下图:
这张图是笔者从自己在极客时间上买的课程里拷贝的。假如有两个主机A和B,要断开连接。A会先给B发送一个Fin,B接收到了之后会回复一个ACK。但A发送这个Fin只能代表A没有消息要发送给B了,而TCP是双工的,B可能还会有消息发送给A,这时A可能正在接收数据。所以B发送的Fin和ACK是分开的,在确认自己也没有数据发送的前提下,在发送Fin,进而A给B回传ACK,然后连接安全断开。简单来说,由于TCP是双工的,只有在双方都各自确认了都不需要再发送数据了,那么连接才会断开,所以TCP连接的断开采用的是四次挥手。
TCP的传输是个很复杂的问题,笔者自己对这方面的研究也不是特别深入,所以就从自己的角度,大概笼统的介绍了一下。只能做到大概的把过程理清,还做不到很精细。有错误的地方,也欢迎批评指正。还有一个很常见的问题就是:为什么是三次握手,而不是两次或者四次。大家可以看这里。简单来说,就是两次不够,四次多余。因为TCP连接一方面要实现数据的可靠传输,同时还要保证传输效率。
在依赖TCP建立了可靠连接之后,HTTP就会在这上面,封装报文,然后依靠TCP进行传输。在TCP的基础上,HTTP也带来了很多灵活的功能,包括连接控制,内容协商等,后面会在Okhttp的分析中穿插涉及到。
HTTP协议,包括报文和请求方式两部分。请求方式是一种动作,代表客户端想对服务端的资源执行某项操作。标准的HTTP请求有GET,POST,HEAD,PUT,DELETE等等,常用的也就是GET和POST。像个人所在的公司,全是POST请求,没有GET请求。接下来,我们就沿着HTTP请求的过程,根据请求方式来区分,来逐步的分析okhttp的原理。
二:POST请求
POST请求,是指将部分数据传输到服务器,并且数据放在消息体中。HTTP请求报文的格式从上到下可以分为:请求行,头部字段和实体数据。请求行包括请求方式,路径和HTTP协议版本号。我们这次针对OkHttp原理的分析,暂时只关注HTTP1.1版本的实现。现在最新的是HTTP2,只不过笔者对HTTP2还没有深入的研究,只是大概的直到相比HTTP1.1,HTTP2多了多路复用,内容二进制和双工等新特性。
请求行结束了之后会接一个CRLF,也就是回车换行,\r\n。请求行下面就是头部字段集合,有HTTP规定的,也可以自定义。头部字段的名称不可以有空格和下划线,否则会返回400-Bad Request。笔者工作的时候也遇到了这个状态码,当时是因为请求数据使用了Gzip压缩,但是服务器不支持,所以报了这个错。每一个Head结束了之后也要跟一个CRLF,并且头字段和实体数据之间会有一个空行。
POST请求是向服务器提交数据,但是数据的格式确实多种多样,比如String,Json,xml甚至文件等等。这个地方官网给了几个例子,那么我们就沿着这几个例子逐一分析。首先来看,通过POST请求,向服务器提交Sting,代码如下:
上述代码截图来自官网。首先,第一个类型是Request,代表的是HTTP请求报文的相关信息。根据前面的分析,我们可以猜测,一个Reuqest对象,应该包含请求方式,URL,头部字段集合和实体数据,而这些都被定义成了Request 的字段:
这里需要重点注意的是实体数据,对应类型为RequestBody,是一个抽象类:
这个类看起来也很简单,包括三个抽象函数。第一个是contentType(),对应的是数据类型,这就涉及到了HTTP的内容协商。
当遵循Http发送数据的时候,HTTP属于应用层协议,发送的数据也是多种多样,既可以发文本,也可以发图片,音视频等等。所以发送方就要告诉接收方对应的数据类型,这样接收方才能正确解析。HTTP的内容协商主要数据类型,压缩编码,语言类型和字符编码。
首先来看数据类型,也就是实体数据的类型。HTTP使用的是从邮件中产生的MIME,也就是多用途互联网邮件扩展。MimeType分为大类型和小类型,具体格式为type/subtype。常用的有:
text:文本格式,text/html text/plain text/css
image:图片 image/png image/jpeg image/gif
audio:音频 audio/mp3
video:视频 video/mp4
application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释, 像application/json,application/xml,application/javascript以及对应表单提交 的application/x-www-form-urlencoded。
以上就是常用的数据类型,对应的字段是content-type,这是一个实体字段,请求报文和响应报文里都可以用。还有一个请求字段Accept,用于请求报文中,来告诉服务器自己可以接收哪几种类型。可以有多个,用逗号分开。
下一个是压缩类型,因为传输的数据,为了提高效率,在传输的时候可能会被压缩,像gzip,br和deflate等。对应的实体字段是Content-Encoding,这个字段可以没有,代表数据没有被压缩。请求字段是Accept-Encoding,用于请求报文。也可以没有,代表客户端不支持压缩。
最后就是语言类型和编码,语言类型对应的请求字段是Accept-Language,实体字段是Content-Language。字符编码的请求字段是Accept-Charset,没有对应的实体字段。一般是出现在Content-Type中,类似于这样:Content-Type: text/html; charset=utf-8。不过在实际开发中,客户端支持的字符编码比较多,所以请求报文里就不会有Accept-Charset字段。同时,语言类型可以从字符编码中推断出来,响应报文中就不会有Content-Language。
以上就是HTTP内容协商的一些细节,回来继续看RequestBody。这个例子中使用的类型是text/x-markdown; charset=utf-8,对应的数据类型是MediaType。确定了数据类型之后,例子中是通过RequestBody的静态函数create来生成了一个对象。
如果数据类型里没有指定charset的话,Okhttp会默认使用utf-8。接下来调用的是bytes版本的create():
通过字符串确认了contentLength()的返回值,这个返回值会确认请求头Content-Length的值,代表实体数据的长度。最后一个函数是writeTo,当通过POST向服务器提交数据的时候,本质上是依赖于底层的TCP连接,然后拿到服务器的OutputStream,通过IO流的方式,把数据写入到服务器中。只不过OKHttp依赖的是okio,笔者还没有对okio进行深入的了解,所以这个地方只能简单的介绍。writeTo里面的BufferedSink,是Sink的子类。Sink就相当于OutputStream,它里面的函数有:
重点是write函数,其中的Source就类似于InputStream。这个函数的意思就是把Source中的数据读取出来,然后写入到当前Sink中。Sink和Source都有对应的Buffer版本,进行了功能的扩展,一方面提供了Buffer,另一方面提供了更灵活的函数,比如BufferedSink中的write(byte[] source),writeUtf8(String string)等等。这个函数里面的BufferedSink,对应的就是服务器Socket的OutputStream,具体的过程后面可以看到。
那这样,实体数据RequestBody就确认好了。确定了数据类型,长度和写入服务器的内容。调用post()函数来确定请求方式是POST,默认是GET。
至此,代表请求报文的Request就已经确定好了,接下来就要发送请求。Okhttp首先会把这个请求封装成Call,可以把Call理解为代表某个网络请求的任务。Call是一个接口,它里面的函数包括:
Call有点类似于JDK中的Future,代表要执行的任务,也可以对这个任务进行监听。Call的函数比较简单,一看名字就明白。需要注意的是,同一个Call代表的任务最多只能执行一次,否砸会抛出异常。Call有个内部类Factory,用于构造Call:
okhttp通过OkHttpClient的newCall函数,来讲一个Request封装为一个Call任务:
它内部返回的是Call的实现类,RealCall:
这里说一下OkhttpClient,OkhttpClient可以理解为整个Okhttp框架的工具类。通过okhttp来进行网络请求,每个请求都会封装成一个Call任务。OkhttpClient的主要目的包括将请求封装为Call,为所有的请求提供通用的配置和监听,并负责具体任务的执行调度。
Okhttpclient在newCall的时候,还会给Call提供一个EventListener。EventListener是对每个具体网络请求的监听,一个完整的HTTP请求过程,可以拆分成很多小步骤,比如建立连接,发送请求头,发送请求实体,读取响应头等等。这些关键的事件,都可以在EventListener被监听到,Okhttp会在关键的步骤调用相应的监听函数。
那现在我们拿到了代表任务的RealCall对象,这个任务既可以同步执行,也可以异步执行。首先来看同步执行,调用的是execute()函数:
首先检查当前Call有没有被执行过,如果已经执行过的话,抛出IllegalStateException。然后会调用Timeout的enter函数,Timeout是okio里面的一个类,用来监听某个操作是否超时。这个类笔者没有深入研究,只大概介绍一下它在Okhttp中的作用。就是给每个Call任务指定一个超时时间,如果超时时间还没有执行完毕的话,那么就会被取消,这个超时时间设置在OkhttpClient中的callTimeout,默认是0,也就是没有设置超时,所以这个类我们可以暂时忽略。
接下来,会调用EventListener中的callStart函数,来表明当前这个任务马上就要开始执行了,继续调用Dispatcher 的execute函数。Dispatcher是okhttp中的调度器,一方面安排任务在哪个线程里执行,如果是异步任务的话,它内部使用过一个类似于缓存线程池的线程池来执行:
另一方面,Dispatcher对所有执行的同步和异步任务进行保存,进行监听和批量管理,比如取消。Dispatcher默认对执行的请求数量,有一个最大64的数量限制。
由于当前是同步请求,所以Dispatcher的execute只是把任务存到集合中:
然后继续调用RealCall的getResponseWithInterceptorChain函数:
这个时候就涉及到了okhttp的核心概念:拦截器。在okhttp中,将一个完整的Http请求拆分成了好几个步骤,然后每个步骤都通过拦截器来完成。这种思想一方面类似于TCP/IP的分层思想,将HTTP请求拆分成了好几个层,每一层只负责自己的逻辑,互不干扰。另一方面类似于AOP,一个HTTP请求的完整路径,会经过一系列拦截器的渗透和过滤。拦截器可以分为四种,有先后顺序,依次为:应用拦截器,okhttp核心拦截器,网络拦截器以及最后的call server拦截器。所谓的call server也就是真正的向服务器发送请求,按照HTTP协议的规范,顺序的向服务器发送请求头和请求实体,然后在顺序的读取响应头和响应实体。这些拦截器会形成一个拦截器链,对应的类型为Interceptor.Chain。
从概念上将,任意一个请求都会对应一个拦截器链,然后这个链包括了该请求的所有拦截器。按照刚才的顺序,每个拦截器做完自己的工作之后,就会调用Chain的proceed函数,将这个过程向后推动,继续调用后面的拦截器。
刚才是从理论的角度分析了okhttp的工作原理,接下来从代码的角度,继续看它的工作过程,继续看getResponseWithInterceptorChain():
首先通过OkhttpClient的interceptors()来获得配置的所有应用拦截器,接下来就是okhttp自己的核心拦截器,再是OkhttpClient配置的网络拦截器,最后就是CallServer拦截器。这里我们假设OkhttpClient配置的应用和网络拦截器都为null,就单纯的分析Okhttp自己拦截器的工作过程。
确定好了需要的拦截器之后,接下来就构造包含这些拦截器的chain,然后就调用了Chain的proceed()函数:
proceed翻译过来是继续做,继续做某件事的意思。Chain的工作原理是这样的:首先在okhttp中,一个HTTP请求对应一个Request,一个Request又会对应一个Call。当调用Call的execute()来同步执行HTTP请求的时候,这个请求所需要的所有拦截器已经定义好了。这些拦截器会形成一个拦截器链Chain,而Chain的proceed()函数的目的就是将拦截器链里面的拦截器依次调用,挨个的调用每个拦截器的intercept()函数,从而执行每个拦截器的逻辑。在这过程中,会产生很多个Chain类型的临时对象,具体类型为RealInterceptorChain。这些RealInterceptorChain对象对应同一个Reqeust,对应的拦截器链也相同。但每个RealInterceptorChain对象都有一个index,对应拦截器链中某个具体的拦截器。当RealInterceptorChain的proceed()函数被调用的时候,它会先生成一个对应下个index的RealInterceptorChain对象,然后将这个新的RealInterceptorChain对象传入当前拦截器的intercept(Chain)函数中。当前的拦截器的intercept()一方面完成自己的逻辑,而另一方面就会继续调用它接收到的Chain的proceed。而此时这个Chain已经对应下一个index了,这样就会轮动起来,把所有的拦截器都调用一遍。除此之外,proceed()还会对状态进行校验,排除一些非法的错误。
刚开始Chain对应的第一个拦截器是RetryAndFollowUpInterceptor,这个拦截器的作用是在网络请求失败或者需要重定向的时候,进行重试的操作。而这些操作,都需要Http响应了之后才能继续,所以它在调用Chain的proceed之前,并没有做太多的处理,只是生成了一个StreamAllocation对象。
StreamAllocation涉及到了与服务器TCP连接的底层实现,这部分笔者暂时还没有深入研究,所以这里只给出一些理论上的总结。okhttp框架的实现停留在了HTTP层面,至于HTTP下面的TCP连接这些东西,它也是通过JDK中的Socket来实现的。在OKHttp中,与服务器连接会分为几个层级的考虑,先是url,然后会根据url和一些必要的配置,像端口号,http协议,https相关的证书和算法等等,这也是url欠缺的地方。会把这些东西封装为一个Address,这是OKHttp自己提出的概念。然后会根据Address来去ConnectionPool里面查找有没有可以复用的Connection,属于同一个Address的urls会复用同一个Connection,从而降低了延迟,提高了吞吐量。如果没有可以复用的,那么会根据URL,在找到一个Route,而ROUTE就可以理解为目标服务器的ip地址,会经过dns解析,然后建立一个新连接。等这次http响应回来了之后,就会把这个Connection返回到connectionPool中,其中的连接空闲了一段时间之后,会被回收销毁。
上面的Connection会对应Okhttp中的Connection接口,实现类为RealConnection可以理解为TCP连接的实现。在Connection上还有一个stream的概念,对应的是HttpCodec接口。这个接口的目的就是在连接建立了之后,按照HTTP协议的要求,陆续的发送请求头,发送请求实体,读取响应头,读取响应实体等等。在Http1.1协议下,对应的类型为Http1Codec。HttpCodec的函数如下,通过这些函数,可以更好的理解HttpCodec的作用。就是在连接建立后,通过IO的方式进行HTTP数据的发送读取。
对于Connection和ConnectionPool的实现,笔者还没有深入的研究,以后再来补充吧。而StreamAllocation可以理解为对刚才connection,stream这些概念的管理,安排和调用。RetryAndFollowUpInterceptor在生成了StreamAllocation之后,就继续调用chain的proceed函数了。注意的时候,这个时候的chain对应的index已经变成了1,因为在之前chain的proceed调用的时候就已经将index+1了:
那么在RetryAndFollowUpInterceptor工作完了之后,下一个拦截器就是BridgeInterceptor。这个拦截器的作用是按需添加需要的头,并根据情况来决定要不要解压缩响应实体数据。接下来看代码:
在请求报文里有实体数据的前提下,根据RequestBody里的contentType()来添加Content-Type头,这个函数我们前面分析过。下一个会看contentlength,如果RequestBody的contentLength返回的不是-1(默认返回-1),那么就说明这个请求报文的实体数据是定长的,是有长度的,就会添加Content-Length。如果返回的是-1,那么就会添加Transfer-Encoding头,也就是流式传输。
对于请求报文来说,Transfer-Encoding和Content-Length是互斥的。实体数据要么是定长的,要么不是定长的。不定长的数据往往是动态生成的,比如Gzip压缩的数据。压缩之前根本不知道长度,所以只能通过流式传输的方式。chunked传输的实体数据格式类似于下面这样:
(图片来自极客)在会配置通用的host和connection。继续看:
接下来会在没有配置Accept-Encoding头并且不是范围请求的话,设置Accept-Encoding:gzip,表明当前客户端支持Gzip压缩。如果OKhttp配置了gzip,那么就会自动的帮我们解压缩响应报文里的实体数据。
范围请求的意思是请求部分资源,主要用于多线程下载中,可以加快下载速度。一般流程是先发送一个Head请求,来验证服务器是否支持范围请求。如果服务器支持,那么就返回Accept-Ranges: bytes;如果不支持的话,就返回Accept-Ranges: none,或者直接不返回这个头。如果服务器支持范围请求的话,那么请求头里就要添加Range来指明自己的请求范围。格式为bytes=x-y,x和y分别是起点和终点,并且x或y也可以省略,常见的用法这里就直接从专栏里拷贝过来了:
当服务器接收到范围请求后,会进行处理,相应的处理逻辑这里也直接拷贝了:
当okhttp告诉服务器自己支持gzip压缩之后,如果服务器返回的是压缩后的数据,那么okhttp会自动解压缩,这个地方会涉及到ResponseBody,先放一放,会面在分析。那对于BridgeInterceptor,请求方面的细节分为完了。它会继续通过chain的proceed函数继续传导到下个拦截器。下一个拦截器是CacheInterceptor,涉及到的是Http缓存。这个地方也放一放,后续再单独分析吧。
CacheInterceptor之后就该是ConnectInterceptor,这个拦截器从名字上也能明白,就是建立与服务器的连接:
建立服务器的连接工作交给了StreamAllocation,这个类我们前面分析过,实现了对connection,stream的管理。这个地方涉及到了ConnectionPool,笔者暂时还没有深入的研究,所以这里只分析一些关键的地方。
首先调用StreamAllocation的newStream函数,得到一个HttpCodec的对象。这个类的作用前面也分析过,就是在TCP连接之上,按照HTTP的规则发送HTTP格式的数据。
newStream中先是通过findHealthyConnection得到一个RealConnection,再通过RealConnection的newCodec得到HttpCodec对象。那继续看findHealthyConnection:
findHealthyConnection内部又调用了findConnection,这个函数内部逻辑比较复杂,核心目的就是先得到一个Connection,可能是新建的,可能是来自与ConnectionPool。如果是新建的话,就会调用它的connect()函数:
继续来看connnect()的逻辑:
它内部先处理一些host,url,address,route的逻辑,在不考虑代理的前提下,会调用connectSocket函数:
这个函数里的逻辑大家就很熟悉了,创建socket对象,然后通过PlatForm的connectSocket()函数调用socket的connect(),建立与服务器的连接:
连接建立好了之后,会拿到Socket的OutputStream和InputStream,并利用Okio封装成对应的Sink和Source。当RealConnection创建好了之后,会通过newCodec来创建HttpCodec对象:
在只考虑Http1.1的前提下,拿到的就是Http1Codec。那么至此与服务器的连接已经建立好了,那下一步就要按照HTTP的规则,来实现客户端和服务器之间数据的传输了,这部分工作交给了最后一个拦截器CallServiceInterceptor,我们一点一点的来看:
首先,是向服务器传输请求头,并调用相关的EventListener事件,writeRequestHeaders具体的实现在Http1Codec里:
里面先是封装请求行,再调用writeRequest函数。请求行比较简单,就包括请求方式,url和Http协议版本号,这里是HTTP/1.1。直接来看writeRequest():
其中的sink就是对Socket的OutputStream的封装,具体的实现也很容易理解,就是对请求头的封装,最后再加一个空行,通过IO的方式将数据传输到服务端。继续向下看:
在请求报文有实体数据的情况下,会先处理请求头里携带Expect:100-continue的情况。这个头在笔者工作中没有遇到过,网上查了一下,它的意思是当客户端要给服务器发送的数据较大的时,可以先发送这个头向服务器询问一下,看看服务器是否愿意接收。如果愿意的话,再继续发送实体数据。这个细节,笔者只是上网查了一下概念理论,并没有相关的经验,所以暂时略过。
传输完了请求头,下一步就要传输实体数据:
在实体数据的传输前后,都会分别调用相应的EventListener函数。然后核心就是要拿到服务器的OutputStream,从而写入当前请求报文的实体数据。那这个时候根据实体数据的长度进行区分,就会有两种情况:定长的和chunked的,分别对应Okhttp的FixedLengthSink和ChunkedSink。我们先来看chunked传输方式:
如果请求头里面携带Transfer-Encoding:chunked,那么就会生成一个ChunkedSink,再次利用Okio的buffer函数返回一个BufferSink,最后以这个BufferSink为参数,调用RequestBody的writeTo,把RequestBody里的数据写入到这个BufferSink中。再来看一下ChunkedSink的实现:
chunked数据传输的数据格式前面我们分析过了,所以现在来看ChunkedSink的write函数就很一目了然了。每一段的数据都包含长度和数据,由于ChunkedSink是Http1Codec的内部类,它的sink变量也就是Http1Codec中的sink,对应的是Socket的OutputStream。同时,在ChunkedSink被close的时候,还会在写入"0\r\n\r\n",从而表明chunked传输的结束。ChunkedSink帮我们实现了传输的数据格式符合chunked传输的要求,至于如何把RequestBody里的数据写入到ChunkedSink里去,这就是开发人员的工作了。像最开始的通过post像服务器传递一个字符串,它的实现为:
再来说一下chunked传输,它主要适用于传输不定长的数据,流式传输。可能是动态生成的数据,也可能是gzip压缩的数据。像笔者的项目,有的接口就会添加一个实现了Gzip的拦截器,把实体数据压缩传输,是边压缩边传递。只不过我们传输的只是压缩之后的数据,至于符合chunked传输的规范,补齐相应的长度,就是okhttp帮我们做的事情了。
那现在请求报文的头和实体数据都处理完毕了,紧接着会调用HttpCodec的finishReqeust进行flush操作:
flush完了之后,就要开始读取响应报文了。首先是读取响应头,这部分逻辑由readResponseHeaders来负责:
首先是读取状态行,状态行中包括Http协议版本号,状态码和状态码的描述信息。然后通过readHeaders来读取头:
逻辑也简单,一行一行的读取。响应头处理完了之后,下一步就是响应报文的body。这里略过100的状态码和websocket的情况,body通过HttpCodec的openResponseBody来实现:
这里先来分析一下ResponseBody,它代表的是响应报文里的body,和RequestBody一样,是个抽象类,只不过比RequestBody要复杂一些。里面的函数有:
其中contentType和contentLength的意思和RequestBody是一样的,核心函数是其中的Source。前面我们提到Source类似于Jdk中的InputStream,那这个地方的Source就是对服务器Socket的InputStream的封装,可以读取服务器的响应数据。有了Source之后,就可以对服务器的响应数据进行读取。可以一次性读取,对应的是string()和bytes(),一次性读取到String和byte数组中。并且不需要我们close,函数内部自动close了,以string()为例:
如果数据较大,那么就不能一下子全部读取,就只能通过IO流的方式读取,ResponseBody里面提供了byteStream()和charStream(),当然了按照okio的方式,直接使用Source也可以。那分析完了ResponseBody,回到HttpCodec的openResponseBody中:
通过代码可以发现,返回的是RealResponseBody,而这个类也很简单,
大家一看就不明白,区别的地方在于里面的Source。响应报文里的body,可能是定长的,也可能是chunked传输的,所以他也分为了FixedLengthSource和ChunkedSource。如果判断出当前的响应报文里没有body,那么就返回一个长度==0的FixedLengthSource。如果返回的响应头里携带了Transfer-Encoding:chunked,那么就生成一个ChunkedSource:
ChunkedSource里的核心逻辑会帮我们去掉数据中的长度,读取的都是真实的数据块。并在读取到最后长度==0的时候,会让read()返回-1,从而标志着响应报文body的结束。这样我们读取都是真实的数据,读到了之后,再度拼接组合就好了。相比较而言,FixedLengthSource里的逻辑更简单明了一些,这个就不赘述了。那现在,通过HttpCodec的openResponseBody函数,我们就得到了一个RealResponseBody对象,包括了响应报文body的长度,类型和Source,至于如何读取这个Source,那就是我们开发人员自己的职责了。继续回到CallServierInteceptor中:
接下来是对connection和状态码的判断,其中204和205代表了请求已经被成功执行,但是不可以有body。205除此之外,还有重置内容的意思。所以如果这个时候ResponseBody的contentlength>0的话,就会报错。
那至此,一个HTTP请求已经基本执行完毕了,从最开始的RetryAndFollowUpInterceptor一直到最后的CallServerInteceptor,最终拿到了这次请求的Response和ResponseBody,只不过ResponseBody封装的是一个Source,只是一个InputStream,里面的内容还需要我们自己读取。有了Response之后,就会沿着相反的顺序,在一个一个的经过刚才的拦截器。在CallServerInteceptor之前是ConnectInterceptor,它没有做额外的处理。接下来就是CacheInterceptor,这个我们后面单独在分析。在接下来就是BridgeInterceptor了,它内部重点做的是gzip的解压缩处理:
在返回的响应头里有Content-Encoding:gzip的情况下,都会通过GzipSource来做解压缩处理,其中的逻辑现在大家一看也就明白了。那最后一个拦截器就是RetryAndFollowUpInterceptor,这个拦截器有两个作用,一是尝试从失败的网络请求中失败过来,二是对请求重定向进行处理。这部分暂时先不深入的研究,只是理清大概的流程。
如果在网络请求的过程中,出现了问题,抛出了异常,那么就会尝试通过recover()函数,从异常的状况中恢复过来。
接下来还会调用followup,这个函数的主要目的是帮我么自动处理一些通用的状况,比如请求重定向,添加一些认证的头或者请求超时,出现了任意情况,都会生成一个新的Request。然后这部分代码运行在一个while循环中,会自动的通过新Request再度发起请求。
其中HTTP_MOVED_PERM,HTTP_MOVED_TEMP都是定义在HttpUrlConnection中的和重定向相关的状态码,对于这些情况,okhttp会自动从Location字段中获取新地址,然后重新发起请求。
至此,通过POST方式向服务器发送一个String,并且是同步调用的流程我们就分析完了。通过这个分析,我们可以意识到okhttp是一个HTTP层面的框架,所有的东西都是围绕HTTP层面来展开的。它从始至终只关注HTTP相关的东西,不会关注HTTP语义和JAVA对象的转换,更不会关注项目中的业务类型。它的内部也是依赖于Socket与服务器建立的TCP连接,在此基础之上,遵循HTTP的规范,进行了封装。
三:异步POST请求
刚才针对okhttp的分析是建立在同步的基础上,接下来在看异步的处理方式,对应的是Call中的enqueue()函数,
异步请求就要依赖于CallBack的回调,这个回调接口也比较简单,
然后调用了EventListener的callStart()函数,最后以刚才的Callback封装成了一个AsyncCall,进而调用了Dispatcher的enqueue函数:
针对异步的Call,Dispatcher内部维护了两个数据结构,一个存储的是准备要执行的AsyncCall,另一个存储的是正在执行的AsyncCall:
enqueue()先把AsyncCall存储到readyAsyncCalls中,接下来调用了promoteAndExecute()函数:
这个函数的主要目的就是为了把AsyncCall从readyAsyncCalls转移到runningAsyncCalls中,并通过Dispatcher内部的executorService来执行。这个地方任务的数量会受到Dispatcher的限制,同时执行的任务数量不可以超过maxRequests,默认64;每个Host的任务数量不可以超过maxRequestsPerHost,默认5。在满足数量限制之后,会把它添加到runningAsyncCalls中。然后调用executeOn()函数:
内部的逻辑就是调用ExecutorService的execute来执行这个任务,如果被拒绝了,那么就会调用EventListener的callFailed和Callback的onFailure函数。关于线程池,有兴趣的朋友可以看笔者的这篇文章。AsyncCall本身也是一个Runnable,它是NamedRunnable的子类:
它的run()函数里面调用了自己的execute()函数,这是个抽象函数,AsyncCall有具体的实现:
它里面的核心逻辑是getResponseWithInterceptorChain函数,这个在前面的同步调用的时候已经分析过了,所以Call的同步调用和异步调用逻辑很类似。根据请求的成功与否,调用相应的回调函数。一个Call不管是同步也好异步也好,不管成功也好失败也好,最后都会调用Dispatcher的finished函数:
finished里面会把Call从相应的集合里面删除,从而避免内存泄漏。同时在Dispatcher空闲的情况下,执行一个预设好的idleCallback,有点类似于Handler里面的逻辑。
四:POST其他数据格式
前面以传递String方式为例,分析了okhttp同步执行和异步执行的原理。在实际开发中,POST除了可以传输简单的字符串之外,还可以传输很多种类型的数据,这里就以官网的例子进行分析。其实这几种方式只有RequestBody这个地方不一样,像数据类型,写入数据的逻辑等等,其他的流程是完全一致的。首先来看一下上传文件,代码示例为:
这里也是通过RequestBody的create函数生成了一个RequestBody:
它内部的处理逻辑也很简单,将File封装成了一个Source,Source里面包裹了基于这个File的InputStream。然后通过Sink的writeAll函数,把File里面的全部数据写入到了Sink中。很显然这只是一个示例代码,传的是特别小的文件。如果传输的是大文件,就不能这么搞了。
下一个类型就是表单提交,示例代码为:
对应的RequestBody是FormBody,那么只要是RequestBody,我们就会关注三个方面。首先是数据类型,我们直到表单提交对应的Content-typ是application/x-www-form-urlencoded,这个在FormBody中已经定义好了:
表单提交的数据是通过add函数添加的,并且会经过URL编码:
那现在表单提交的数据和数据类型都准备好了,还需要长度和写入的逻辑:
这俩函数都依赖了writeOrCountBytes,writeOrCountBytes函数有两个目的,要么计算长度,要么把数据写入服务器:
当使用这个函数计算长度的时候,参数sink==null而countBytes==true。这种情况下,函数内部会新建一个Buffer对象,把刚才通过add添加的数据写入到这个Buffer中,格式为name:value,写完了之后,Buffer的长度就是数据的长度。如果是为了写入服务器,那么sink!=null,countBytes==false,这个时候就直接往sink里面写数据,这个Sink就是对Socket的OutputStream的封装。
那POST上传数据还有一种格式为multipart/form-data,也就是常说的表单上传文件。multipart,翻译过来是多部分的意思。通俗点说,就是请求报文的实体数据是分段的,由多段数据组成。每段数据的Content-Type可以不一样,每段数据都有自己的Content-Disposition,Content-Type,这里以为例,个人截取了上传头像的请求报文,格式如下:
Content-Type除了指定类型之外,还会额外的指定一个分隔符,用于多段数据的划分。
multipart/form-data这种类型的数据也是被Okhttp支持的,示例代码为:
这种格式的数据对应的是MultipartBody,MultipartBody从概念上可以理解为多个RequestBody的结合,从代码上看,它是多个Part的组合:
Part是MultipartBody的内部类,它里面包括两部分Headers和RequestBody,分别对应每段数据的类型和数据。添加数据的时候,会调用Builder 的addFormDataPart():
无论是传简单的value也好,还是文件也好,都会通过Part的createFormData来封装成一个Part:
这个函数的主要目的就是封装Content-Disposition头,格式也比较固定,然后继续调用create()函数:
这里面主要是对Headers进行了合法性校验,值得注意的是每段数据里是允许有Content-Type和Content-Length的,只不过不是在这里配置,是在writeTo的时候才会写入。那剩下的就是长度和写入逻辑,这个地方和前面的FormBody一样,交给了writeOrCountBytes来负责:
这里会按照multipart的逻辑,将数据写入到Sink或者新建的Buffer中。首先对parts遍历,每一个part先写入分隔符,再写入每个Part里面的Headers,再根据RequestBody写入Content-Type和Content-Length,如果是计数,就依据刚才得到的contentlength进行累加。如果是writeTo的话,就直接调用RequestBody的writeto。parts循环完了之后,最后还会在写入一个分隔符,这样就完成了计算长度和写入的逻辑。
到现在为止,我们就分析了通过POST方式来向服务器传输String,表单提交和multipart数据,但实际开发中使用更多的传json,对应的类型为application/json。
五:GET请求
前面分析了POST请求的细节,在此基础上看GET请求就很简单了。GET请求主要用于查询,也不会向服务器传递body。所以它的Request只需要生命了URL和GET的请求方式就可以了,而Builder的默认请求方式就是GET。示例代码为:
okhttp支持所有的标准的HTTP请求方式,并提高了相应的get(),post(),head(),delete()和put()等函数:
这些函数,最终都会调用method()来制定请求方式和body,这个地方okhttp做了校验,不允许GET和HEAD请求里面有body,避免了不规范的使用:
相应的判断逻辑放在了HttpMethod中,逻辑也很简单,一看就明白:
由于GET请求里没有body,所以在CallServierInterceptor中会跳过body,只传输head:
在没有body的情况下,就直接调用finishRequest()进行flush操作,把头部字段传输到服务器了。至于其他的逻辑,就和前面的POST请求没有区别了。
五:取消请求
取消请求的操作被定义在了Call的cancel函数中,直接来看RealCall里面的cancel()的逻辑:
RealCall又是调用的第一个拦截器RetryAndFollowUpInterceptor的cancel()函数:
RetryAndFollowUpInterceptor又调用了StreamAllocation的cancel()函数:
而StreamAllocation又会调用HttpCodec或者RealConnection的cancel,即便调用的是HttpCodec的cancel,它也会调用RealConnection的cancel,所以直接看RealConnection就好:
那这个地方就简单了,直接close了Socket。当Socket被关闭的时候,会抛出SocketException或者IOException,而SocketException本身也是IOException的子类。okhttp内部也会在取消的时候抛出IOException,比如建立连接的时候:
同时,第一个拦截器RetryAndFollowUpInterceptor相当于对整个Http请求的总控,它一方面会对chain的proceed函数进行try/catch,另一方面会通过循环来包裹这段代码:
一旦chain的proceed执行过程中,抛出了IOException,那么就会被捕获到,然后在下次循环的时候进行检查,如果被取消了,就会抛出IOException("Canceled")。那最后回到RealCall中,同步调用的话,这个异常会被直接抛出:
异步请求的话,就会调用对应的Callback的onFailure()函数:
至此,OkHttp框架的原理我们就分析完了,其中略过了HTTP缓存和HTTPS的内容,这个后续会在单独的开篇章补充。其实当OkHttp的原理分析完后,会感觉OkHttp的源码还是很容易理解的。主要是因为OkHtttp代码设计的很出色,模块划分也很清晰。很清晰的对HTTP请求的各个环节进行了拆解,我们看源码的时候,能够很容易和相应的HTTP细节对照起来,这也应该是值得我们学习的一个地方。