一.网络通信概念理解
1.http协议概述
当我们在自己电脑的web浏览器地址栏敲入网址url,点击enter,web会呈现一个返回结果(界面)。我们可以把自己的电脑理解为客户端,返回的结果(资源)是从服务器获取得来的,我们把服务器理解为服务端。则这是一个客户端向服务端发送请求并获得响应的过程。
web使用一种称为http的协议作为规范,完成从客户端到服务端的一系列的运作流程。可以说,web是建立在http协议上通信的。
2.tcp/ip协议
通常我们使用的网络是建立在tcp/ip协议上运作的,而http属于它内部的一个子集。
想一想两个计算机网络设备之间相互通信需要约定哪些事情,也就可以大概猜出来tcp/ip
协议干了哪些事情,下面大致描述出来
a.如何找到(探测)通信目标
b.由哪一边先发起通信
c.用什么语言进行通信
d.如何结束通信
等
tcp/ip 协议是分层管理的,给一张图先
逐一介绍下各层对应的功能
application layer:应用层,总的来说就是使用http协议的客户端,比如你电脑上安装的谷歌浏览器
transport layer:传输层,这一层是为已经处于网络连接状态的计算机提供数据传输。那么不禁要问,根据什么协议传输数据呢,协议有tcp和udp两种。
internet layer:网络层,网络层就是选择一条路线(可能中间经过多个其他设备)把数据包传送到目的计算机
link layer:链路层,用来处理连接网络的硬件部分
我们按照客户端向服务端发送请求的这一过程,来分析一下整个流程的运作。
我们首先在自己的电脑上谷歌浏览器键入一个网址,enter,则发出了一个http请求。那么这个http请求作为一个数据(bean),可以称之为 http请求报文。
那么传输层(假设使用tcp协议)收到来自应用层的http请求报文,按照tcp协议把报文分割,并打上标记序号和端口号,转发给网络层
现在到了网络层(ip协议),只需在报文中添加目的计算机的mac地址转发给链路层,这样,发送网络请求的工作就做完了。
----------------------------------------我是分割线---------------------------------------------
那么传输层使用的tcp协议的真正面目与作用是什么呢
客户端向服务端发送请求的http请求报文体积不大,当服务端返回结果的时候经过传输层数据体检可就大了。如何可靠的把请求结果传回客户端呢
tcp的想法是把大数据进行分割传输更容易,同时tcp也要拥有确认数据最终是否送达到对方的功能。
备注:大块数据被tcp分割成很多数据包,数据包的单位是segment(报文段)
如何把大块数据分割成segment就不讲了,有这个概念就行。很明显,我们要讲如何确定数据包是否成功发送到对方了呢、简单一句:通过三次握手
就是说用tcp协议把数据包发出去后,tcp不会对传送后的情况置之不理,它一定会向对方确认是否成功送达。
备注:握手中使用的两个标志:SYN ACK,三次握手不是人类操作的具体的http请求,而是一种默认的建立连接的过程,tcp连接成功后,才是具体的http请求响应操作
过程简略如下:
step1 :client segments1+syn-------->server //客户端发送一个数据
备注:第一次握手:建立连接时,客户端发送syn包到服务器,并进入SYN_SENT状态,等待服务器确认。
step2 :server segments2+syn+ack-------->client //服务端收到了
备注:第二次握手:服务器收到syn包,必须确认客户的SYN,同时自己也发送一个SYN包,即SYN+ACK包,此时服务器进入SYN_RECV状态。
step3 :client segments3+ack-------->server //一切ok,结束握手
备注:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK,此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
若在三次握手过程中某个阶段中断了,tcp协议再次以相同的顺序发送相同的数据包。
3.简单的http协议
http协议规定,请求必须是从客户端发起,服务端响应请求并返回。换句话说,肯定是先从客户端建立起通信的。
在http协议初始版本中,每进行一次http通信就要断开一次tcp连接。也就是这样,
三次握手建立tcp连接(tcp连接成功)--->http请求/相应过程---->断开tcp连接。
那么上述过程存在什么问题呢?
当网页中存在大量图片,当请求整个网页的时候,肯定需要发送很多个不同的http请求,那么就需要重复tcp连接与tcp断开很多次。这是不划算的。
所有http/1.1版本中有了持久连接的概念。也就是说两个设备之间的tcp连接一定时间内一直有效,在这个有效期内,把多个http请求响应工作做完就好,这多个http请求响应一只使用这个一定时间内有效的tcp连接。
同时,不同请求可以同时发出,不管先发出的请求有没有已经收到响应。
----------------------------------------我是分割线---------------------------------------------
4.http报文内的http信息
用于http协议交互的信息被称为http报文。下图是报文的结构与格式组成。
http在传输数据的过程中可以按照数据原貌进行传输,也可以对数据编码后进行传输。编码的唯一好处是能够提升传输速率。
上面一段话只是笼统的说http在传输的数据,那么数据又可以分为
message (报文)
报文是http通信的基本单位,字节流
entity(实体)
请求或响应中的有效数据载荷,由 实体主体+实体首部 组成。
举个例子,我们正在电脑上写一封邮件还没写完,很明显,这封邮件是要被发送到一个地方去的。现在我们想在这封邮件中添加一个较大的附件,为了使这封邮件容量变小,我们会用zip压缩文件,把压缩后的文件作为附件添加到邮件中。
http协议中也有类似的一个操作,暂且把这个操作叫做-----内容编码
内容编码指明的应用在实体内容上编码格式,内容编码后的实体由客户端接收并解码,常用的内容编码有这几种:
gzip(GNU zip)
compress等
----------------------------------------我是分割线---------------------------------------------
分割发送的分块传输编码
在http通信过程中,请求的编码实体资源尚未全部传输完成之前,浏览器无法显示请求结果页面。在传输大容量数据时,通过把数据分割成多块,让浏览器逐步显示完全的页面。
这种把实体主体分块的功能称为 分块传输编码
----------------------------------------我是分割线---------------------------------------------
多部分对象集合
http协议中也采用了多部分对象集合,http报文主体中可能含有多种类型实体。可以在http报文中加入content-type首部标明实体类型。
二:okhttp框架的介绍
对于http交互,android为我们提供了什么方式呢?
HttpURLConnection 和 Apache HTTP Client,为什么还要使用okhttp呢?那么应该okhttp可以让你的应用更快的运行 网络通信更节省流量
网络上的文章都介绍okhttp是一个高效的http库,而且支持SPDY。不过我真的不知道spdy是什么。那么通过一小节介绍一下SPDY。
okhttp的个主要优点如下:
2.1:SPDY(speedy,可以理解为更快)
SPDY是什么?
备注:不探讨spdy如何修改http的细节
spdy 是application layer层的通讯协议,用以替换http,但是沿用了http的语义,SPDY 协议旨在通过压缩、多路复用和优先级来缩短网页的加载时间和提高安全性。
http协议的不足:
单路连接,请求低效
http只允许客户端发起请求
http头冗余
spdy协议的优点:
SPDY 规定在一个 SPDY 连接内可以有无限个并行请求,即允许多个并发 HTTP 请求共用一个 TCP会话。
服务器可以主动向客户端发起通信向客户端推送数据,这种预加载可以使用户一直保持一个快 速的网络。
舍弃掉了不必要的头信息,经过压缩之后可以节省多余数据传输所带来的等待时间和带宽
Google 认为 Web 未来的发展方向必定是安全的网络连接,全部请求 SSL 加密后,信息传输更加安全。
备注:SPDY 的实现需要浏览器客户端和 Web 服务器同时支持。
2.2 okhttp 的类图
原图很大,下载下来应该能看清:
那么okhttp的总体设计是怎么样的呢?想一想再怎么设计也是为了完成网络请求,okhttp到底在网络请求的过程中做了哪些封装与优化呢,先看看okhttp总体的架构设计图吧
现在看着个图可能对这个图不可能一下子理解的完全准确,不过可以大概描述一下上图的流程
和一般的异步处理框架类似,网络请求request都存储在一个队列中,一个dispatcher(其实是一个线程)不断的从request队列中取出队列,根据是否有缓存这个判定条件,通过缓存数据获取接口或者网络数据获取接口来获取请求的响应数据,获取数据的实质过程可以是异步也可以是同步的。更加具体的流程可看下图
那就按照这个流程图看看细节吧,okhttp库代码量还是挺大的,这里化繁为简的介绍
OkhttpClient这个类有个内部类Builder,可以通过Builder配置okhttpclient获得一个配置好的OkhttpClient实例。那么这个okhttpclient实例完成了哪些操作呢
上图是okhttpclient的思维脑图,可以先看看了解下okhttpclient这个角色承担的功能。对我而言上图的重点是变量分支和发起请求分支。
下面看下发起请求分支中的 newCall()方法。其实okhttpclient中的Call类是对Request类的封装,所以新建一个Call的时候肯定要传入一个配置好的Request,而一个request从具体流程图中可以看出包括url,method,header,body四个部分(其实request还有一个tag成员变量)。
当我们在使用okhttp库时,一定是先获取okhttpclient实例,然后新建一个request,调用okhttpclient的newCall()方法。其实Call类只是一个接口,newCall方法调用的是实现了Call接口的RealCall类的newRealcall方法,可以看出newRealcall方法返回的是Realcall,每个realcall有一个listener。
Realcall创建出来肯定要被分发出来,但是要经过okhttp的拦截器机制。拦截器机制在okhttp3之前是在HttpEngine这个类中的,okhttp3中没有了HttpEngine这个类,取而代之的是5个Interceptor类。如下图:可以看出拦截器分为两大类 应用拦截器和网络拦截器,okhttp自带的拦截器有5个。
这5个拦截器的功能分别是
RetryAndFollowUpInterceptor,重试那些失败或者redirect的请求。
BridgeInterceptor,请求之前对响应头做了一些检查,并添加一些头,然后在请求之后对响应做一些处理(gzip解压or设置cookie)。
CacheInterceptor,根据用户是否有设置cache,如果有的话,则从用户的cache中获取当前请求的缓存。
ConnectInterceptor,复用连接池中的连接,如果没有就与服务器建立新的socket连接。
CallServerInterceptor,负责发送请求和获取响应。
看下Interceptor接口的源码:
可以看到interceptor接口中有一个接口Chain,chain的preceed方法传入request得到响应response。得到response就可以修改response或者根据response执行其它逻辑。
chain的request方法可以得到Request,这样就可以修改request。
下图是在Interceptor Chain中的数据流:
2.3 http客户端向服务器发送报文
step1.从url中解析出服务器的ip地址和端口号
step2.在客户端和服务器端建立tcp/ip连接
step3.开始传输http报文
从文章开始的第一节可以知道http协议是application layer层的协议,位于transport layer层tcp协议的上层。
如果你使用okhttp请求一个URL,具体的工作如下:
Step1.框架使用URL和配置好的OkHttpClient创建一个address。此地址指定我们将如何连接到网络服务器。
看下address源码
Step2.框架通过address从连接池中取回一个连接。
如果没有在池中找到连接,ok会选择一个route尝试连接。这通常意味着使用一个DNS请求, 以获取服务器的IP地址。如果需要,ok还会选择一个TLS版本和代理服务器。
如果获取到一个新的route,它会与服务器建立一个直接的socket连接、使用TLS安全通道(基于HTTP代理的HTTPS),或直接TLS连接。它的TLS握手是必要的。
Step3.开始发送HTTP请求并读取响应。
如果有连接出现问题,OkHttp将选择另一条route,然后再试一次。这样的好处是当服务器地址的一个子集不可达时,OkHttp能够自动恢复。而且当连接池过期或者TLS版本不受支持时,这种方式非常有用。
一旦响应已经被接收到,该连接将被返回到池中,以便它可以在将来的请求中被重用。连接在池中闲置一段时间后,它会被赶出。
------------------------------------------------我是分割线------------------------------------------------------
Step1和Step2的步骤都在ConnectInterceptor中执行。看下源码
connectinterceptor的功能注释上写的是:开启与目标服务器的connection(连接),并执行下一个拦截器。不妨看下connection的源码,也可以看connection接口的实现类RealConnection的源码
接口connection的功能注释是:
The sockets and streams of an HTTP, HTTPS, or HTTPS+HTTP/2 connection. May be used for multiple HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.
我翻译的是:http连接的套接字和流,可用于多个http请求响应的交互,连接可能直达服务器或者经过代理服务器。
可以看到realconnection中有ConnectionPool,其实是因为tcp连接断开操作耗时,为了加快响应速度,用了tcp连接池。如果pool中有与你想要连接的服务器的可用连接,则直接用,没有的话才去连接服务器。
不如继续看下connectionPool的源码
connectionPool的功能注释翻译过来是:
管理http和http2连接的重用
可以看到connectionPool中有一个线程池用于清理过期的连接,但在connectionPool中最多只能运行一个线程。executor也运行被gc回收。
在图connectinterceptor中可以看到代码
RealConnection connection = streamAllocation.connection();
执行这行代码获得到了connection,至于这个connection是从pool中获取还是重新连接服务器获取,肯定是在赋值符号右侧的connection方法中执行了具体逻辑。不过我们还是先看下connection的拥有者streamAllocation类。
源码中给streamAllocation这个类的注释是:
这个类协调三个实体之间的关系,这三个实体是connections,calls,streams。
connections---physical socket connections to remote servers
calls---a logical sequence of streams, typically an initial request and its follow up requests.
streams---logical HTTP request/response pairs that are layered on connections.
我翻译过来就是:
connections--通过套接字与远程服务器的连接
calls----流的逻辑序列,通常是初始请求和该请求的后续请求
streams---基于连接的http请求响应对
其实okhttp把socket的io(发送request和接收response)封装到了httpstream中,其实这个底层的io操作又设计到支撑okhttp库的okio库,先看下httpstream的构造函数,再分析okio的应用场景。
/*************************************************okio**********************************************/
首先看下okio为okttp做了什么,给一张图先有个概念
上面这张图其实表达的是网络io的流程图。
okio库的主要功能都被封装在ByteString和Buffer这两个类中,整个库也是围绕这两个类展开。
ByteString代表一个不可变的 字节序列。对于字符数据来说,String是非常基础的,但在二进制数据的处理中,则没有与之对应的存在,ByteString应运而生。它为我们提供了对串操作所需要的各种 API,例如子串、判等、查找等,也能把二进制数据编解码为十六进制(hex),base64 和 UTF-8 格式
ByteString的源码注释是:
这个类提供了不受信任的输入流和输出流,并对底层字节数组进行原始访问。看下ByteString的源码:
对于ByteString,可以看一个png格式图片解码的例子
上图第一行代码有一个ByteString的decodeHex方法,看下方法实现
decodeHex传入的参数是字符串,字符串中的每个字符都是一个16进制字符,也就是从0-f。
decodeHex方法得到的类型是ByteString,字节串,在decodeHex方法中可以看到新建的byte[]数组的长度是hex String 长度的一半,可知,hex串的每两个字符组成用一个byte表示,比如od可以表示为13,一个byte也就是00001101。
先不看Buffer这个类,先看Sink和Source这两个类。
Okio 吸收了java.io一个非常优雅的设计:流(stream),流可以一层一层套起来,不断扩充能力,最终完成像加密和压缩这样复杂的操作。这其实也是装饰模式。
Okio 有自己的流类型,那就是Source和Sink,它们和InputStream与OutputStream类似,前者为输入流,后者为输出流。
但它们还有一些新特性:
超时机制,所有的流都有超时机制;
Source和Sink的 API 非常简洁,为了应对更复杂的需求,Okio 还提供了BufferedSource和 BufferedSink接口,便于使用(按照任意类型进行读写,BufferedSource 还能进行查找和判 等);
不再区分字节流和字符流,它们都是数据,可以按照任意类型去读写;
便于测试,Buffer同时实现了BufferedSource和BufferedSink接口,便于测试;
Source和InputStream互相操作,我们可以把它们等同对待,同理Sink和OutputStream也可以等同对待。
---------------------------------------------------------------------------------------------------------------------
刚才没有讲述的Buffer类实现了BufferedSource和BufferedSink接口,BufferedSource和BufferedSink接口分别继承于接口Source和Sink。
Buffer类是可变 字节序列,但它像ArrayList一样,封装的很好。我们只有从Buffer的头部读取数据,从尾部添加数据。
继续看上图pgn解码那张图,decodePng方法体中的第一行代码就是:
BufferedSource pngSource=Okio.buffer(Okio.source(in));
看下Okio类中的source方法源码:
查看上述代码可知,Okio的source方法最终返回了一个匿名的实现了Source接口的实现类。
其实source方法就是把inputStream读取到Buffer的segment中,segment是Buffer中一种提高性能的数据结构,这里暂时不分析。
BufferedSource接口继承自Source接口,RealBufferedSource是其实现类,RealBufferedSource是个装饰类,内部管理Source对象来扩展Source的功能,通顺拥有Source读取数据的Buffer对象。
读写的流程可以归结为如下
InputStream-->Source-->BufferedSource-->Buffer-->segment-->Buffer-->Sink-->BufferedSink-->OutputStream
/*************************************************okio**********************************************/
把图再copy一遍
connectinterceptor
RealConnection connection = streamAllocation.connection()这行代码的上一行代码执行了
streamAllocation类的newStream方法,返回了一个实现了HttpCodec的匿名类,HttpCodec的实现类是Http1Codec。
在网络请求过程中,首先创建一个StreamAllocation的对象,然后调用其newStream()方法,查找一个可用连接,要么复用连接,要么新建连接,复用连接则根据address从连接池中查找,新建连接则是根据address查找一个Route对象建立连接,建立连接以后会将该连接添加到连接池中,同时连接池的清理任务停止的情况下,添加新的连接进去会触发开启清理任务。这是建立连接和管理连接的整个过程,当拥有连接以后,StreamAllocation就会在连接上建立一个流对象,该流持有connection的输入输出流,也就是socket的输入输出流,通过它们最终完成数据通信的过程,下面分析流对象Http1Codec,以及数据通信的过程。
这里首先简单介绍一下流对象,在okhttp中,流对象对应着HttpCodec, 它有对应的两个子类, Http1Codec和Http2Codec, 分别对应Http1.1协议以及Http2.0协议,本文主要学习前者。在Http1Codec中主要包括两个重要的属性,即source和sink,它们分别封装了socket的输入和输出,CallServerInterceptor正是利用HttpCodec提供的I/O操作完成网络通信。
/***********************************************************************************/
Http1Codec的功能注释是:
可用于发送HTTP / 1.1消息的套接字连接。这个类严格执行以下生命周期:
发送请求头-->打开一个sink为了写入请求体-->写入然后关闭sink-->读响应头-->
打开一个source来读响应体-->读出然后关闭source
/***********************************************************************************/
下面来看CallServerInterceptor的源码,这个拦截器的功能注释是
这是链中的最后一个拦截器。它对服务器进行网络调用。
对于拦截器,依然是学习它的intercept方法.我们提取intercept方法中对应Http1Codec调用生命周期的代码来看。
//1. 向socket中写入请求header信息
httpCodec.writeRequestHeaders(request);
//2. 向socket中写入请求body信息
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
//3. 完成网络请求的写入
httpCodec.finishRequest();
//4. 读取网络响应header信息
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
//5. 读取网络响应的body信息
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
因为要严格执行以上生命周期调用,所以Http1Codec使用了状态模式。Http1Codec类中维护了几个状态常量,根据类的状态执行逻辑。
我们知道在okhttp中关于连接和流,有三个重要的类,即RealConnection, StreamAllocation和Http1Codec,StreamAllocation作为连接和流的桥梁,承担资源的回收和清理工作。