[读书笔记]Android进阶之光-网络编程

187 阅读23分钟

网络分层

TCP的三次握手与四次挥手

TCP有6种标示:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急)

第一次握手:

客户端向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时随机生成初始序列号 seq=x,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状

态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。这个三次握手中的开始。表示客户端想要和服务端建立连接。

第二次握手:

TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己随机初始化一个序列号 seq=y,此

时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。这个报文带有SYN(建立连接)和ACK(确认)标志,询问客户端是否准备好。

第三次握手:

TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。

TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。这里客户端表示我已经准备好。

思考:为什么要三次握手呢,有人说两次握手就好了?

举例:已失效的连接请求报文段。

client发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,直到某个时间才到达server,本来这已经是一个失效

的报文,但是server端接收到这个请求报文后,还是会想client发出确认的报文,表示同意连接。假如不采用三次握手,那么只要server发出确认,新的建立就连接了,但其实这个

请求是失效的请求,client是不会理睬server的确认信息,也不会向服务端发送确认的请求,但是server认为新的连接已经建立起来了,并一直等待client发来数据,这样,server的

很多资源就没白白浪费掉了,采用三次握手就是为了防止这种情况的发生,server会因为收不到确认的报文,就知道client并没有建立连接。这就是三次握手的作用。

四次挥手

第一次挥手:

TCP发送一个FIN(结束),用来关闭客户到服务端的连接。

客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),

此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

第二次挥手:****

服务端收到这个FIN,他发回一个**ACK(确认),**确认收到序号为收到序号+1,和SYN一样,一个FIN将占用一个序号。

服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器

通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)

第三次挥手:****

服务端发送一个**FIN(结束)**到客户端,服务端关闭客户端的连接。

服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,

此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

第四次挥手:****

客户端发送**ACK(确认)**报文确认,并将确认的序号+1,这样关闭完成。

客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时

TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

思考:那么为什么是4次挥手呢?

为了确保数据能够完成传输。

关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也

即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

可能有人会有疑问,tcp我握手的时候为何ACK(确认)和SYN(建立连接)是一起发送。挥手的时候为什么是分开的时候发送呢.

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到

FIN报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能

发送FIN报文,因此不能一起发送。故需要四步挥手

思考:客户端突然挂掉了怎么办?

正常连接时,客户端突然挂掉了,如果没有措施处理这种情况,那么就会出现客户端和服务器端出现长时期的空闲。解决办法是在服务器端设置保活计时器,每当服务器收到

客户端的消息,就将计时器复位。超时时间通常设置为2小时。若服务器超过2小时没收到客户的信息,他就发送探测报文段。若发送了10个探测报文段,每一个相隔75秒,

还没有响应就认为客户端出了故障,因而终止该连接。

TCP和UDP的区别?

  1、基于连接与无连接;UDP是无连接的,即发送数据之前不需要建立连接

2、TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付

,即不保证可靠交付Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。

5、TCP对系统资源要求较多,UDP对系统资源要求较少。

1. HTTP的8大请求方法

GET:请求获取Request-URI所标识的资源。

POST:在Request-URI所标识的资源后附加新的数据。

HEAD:请求获取由Request-URL所标识的资源的响应消息报头。

PUT:请求服务器存储一个资源,并用Request-URI作为其标识。

DELETE:请求服务器删除Request-URL所标识的资源。

TRACE: 请求服务器回送收到的请求信息,主要用于测试或诊断。

CONNECT: HTTP 1.1协议中预留给能够将连接改为管道方式的代理服务器。

OPTIONS: 请求查询服务器的性能, 或者查询与资源相关的选项和需求。

HTTP的响应报文

1. code

  • 100-199 指示信息,收到请求,需要请求者继续执行操作。
  • 200-299 请求成功,请求已被成功接收并处理。
  • 300-299  重定向,要完成请求必须进行更进一步的操作。
  • 400-499  客户端错误,请求有语法错误或者请求无法实现。
  • 500-599  服务器错误,服务器不能实现合法的请求。

2.常见状态码

  • 200 OK:客户端请求成功。
  • 400 Bad Request:客户端有语法错误,服务器无法理解。
  • 401 Unauthorized:请求未经授权
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务。
  • 500 Internal Server Error:服务器内部错误,无法完成请求。
  • 503 Server Unavailable:服务器当前不能处理客户端请求,一段时间后可处理。

HTTP的消息报头

1. 通用报头

1. Date:表示消息产生的日期和时间。

2. Connection:允许发送指定连接的选项。keep-alive,close

3. Cache-Control

  • no-cache:如果是客户端的话,说明客户端不会接收缓存过的响应,要请求最新的内容。而服务器端则表示缓存服务器不能对相应的资源进行缓存。

  • no-store:表示缓存不能在本地存储。

  • max-age:该参数后方会被赋值上相应的秒数,在请求头中表示如果缓存时间没有超过这个值就返回给我。而在响应头中时,则表示资源在缓存服务器中缓存的最大时间。

  • only-if-cached:表示客户端仅仅请求缓存服务器上的内容,如果缓存服务器上没有请求的内容,那么返回504 Gateway Timeout。

  • must-revalidata:表示缓存服务器在返回资源时,必须向资源服务器确认其缓存的有效性。

2. 请求报头

1.  Host : 请求的主机名。

2.  User-Agent:发送请求的浏览器类型,默认是火狐,有历史原因。

3.  Accept: 该字段可通知服务器用户代理能够处理的媒体类型以及该媒体类型对应的优先级。媒体类型可使用“type/subtype”这种形式来指定,分号后边紧跟着的是该类型的优先级。

4.  Accept-Encoding:

该字段用来告知服务器,客户端这边可支持的内容编码以及相应内容编码的优先级, 下方就是Accept-Encoding的用法。gzip表示由文件压缩程序gzip(GNU zip)生成的编码格式。compress表示UNIX文件压缩程序compress生成的编码格式。deflate表示组合使用zlib格式以及有deflate压缩算法生成的编码格式。identity表示不执行压缩或者使用一致的默认编码格式。

5.  Accept-Language

该字段用来告知服务器,客户端可处理的自然语言集,以及对应语言集的优先级

6.  Connection: 该字段可以控制不转发给代理服务器的首部字段以及管理持久连接,下方这个响应报文头中的Connection就是用来管理持久连接的,其参数为keep-alive,就是保持持久连接的意思。可以使用close参数将其关闭。

7.   Transfer-Encoding: 该字段表示报文在传输过程中采用的编码方式,在HTTP/1.1的报文传输过程中仅对分块编码有效。chunked.

8.  **Authorization:**用来告知服务器用户端的认证信息。

3. 响应报头

1. Location:

Location字段一般与重定向结合着使用。下方是我访问“www.baidu.com/hello”这个连接的…

2. Server:

该响应字段表明了服务器端使用的服务器型号,下方是博客园某张图片的响应头,使用的Web服务器是Tengine, Tengin是淘宝发起的Web服务器项目,是基于Nginx的。

4. 实体报头

1. Content-Type:

一般是指网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件

Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something

2. Content-Length:

该字段用来指定报文实体的字节长度。

3. Content-Language:

该字段表示报文实体使用的自然语言,使用方式如下所示:

Content-Language: zh-CN

4. Content-Encoding:

该字段用来说明报文实体的编码方式,下方这段报文头中的Content-Encoding的参数为gzip,说明是使用gzip对报文实体进行压缩的。

5. Last-Modified:最后修改时间

6. Expires: 过期时间

7. Allow:

该字段用于服务器通知客户端服务器这边所支持的所有请求方法(GET、POST等)。如果服务器找不到客户端请求中所提到的方法的话,就会返回405 Method Not Allowed,于此同时还会把所有能支持的HTTP方法写入到首部字段Allow后返回。

Allow : GET, POST, HEAD, PUT, DELETE 

Https为什么是安全的?

1、http为什么不安全?

http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。

可以把http通信比喻成寄送信件一样,A给B寄信,信件在寄送过程中,会经过很多的邮递员之手,他们可以拆开信读取里面的内容(因为http是明文传输的)。A的信件里面的任何内容(包括各类账号和密码)都会被轻易窃取。除此之外,邮递员们还可以伪造或者修改信件的内容,导致B接收到的信件内容是假的。

比如常见的,在http通信过程中,“中间人”将广告链接嵌入到服务器发给用户的http报文里,导致用户界面出现很多不良链接; 或者是修改用户的请求头URL,导致用户的请求被劫持到另外一个网站,用户的请求永远到不了真正的服务器。这些都会导致用户得不到正确的服务,甚至是损失惨重。

2、https如何保证安全?

要解决http带来的问题,就要引入加密以及身份验证机制。

如果Server(以后简称服务器)给Client(以后简称 客户端)的消息是密文的,只有服务器和客户端才能读懂,就可以保证数据的保密性。同时,在交换数据之前,验证一下对方的合法身份,就可以保证通信双方的安全。那么,问题来了,服务器把数据加密后,客户端如何读懂这些数据呢?这时服务器必须要把加密的密钥(对称密钥,后面会详细说明)告诉客户端,客户端才能利用对称密钥解开密文的内容。但是,服务器如果将这个对称密钥以明文的方式给客户端,还是会被中间人截获,中间人也会知道对称密钥,依然无法保证通信的保密性。但是,如果服务器以密文的方式将对称密钥发给客户端,客户端又如何解开这个密文,得到其中的对称密钥呢?

说到这里,大家是不是有点儿糊涂了?一会儿密钥,一会儿对称密钥,都有点儿被搞晕的节奏。在这里,提前给大家普及一下,这里的密钥,指的是非对称加解密的密钥,是用于TLS握手阶段的; 对称密钥,指的是对称加解密的密钥,是用于后续传输数据加解密的。下面将详细说明。

这时,我们引入了非对称加解密的概念。在非对称加解密算法里,公钥加密的数据,有且只有唯一的私钥才能够解密,所以服务器只要把公钥发给客户端,客户端就可以用这个公钥来加密进行数据传输的对称密钥。客户端利用公钥将对称密钥发给服务器时,即使中间人截取了信息,也无法解密,**因为私钥只部署在服务器,其他任何人都没有私钥,因此,只有服务器才能够解密。**服务器拿到客户端的信息并用私钥解密之后,就可以拿到加解密数据用的对称密钥,通过这个对称密钥来进行后续通信的数据加解密。除此之外,非对称加密可以很好的管理对称密钥,保证每次数据加密的对称密钥都是不相同的,这样子的话,即使客户端病毒拉取到通信缓存信息,也无法窃取正常通信内容。

但是这样似乎还不够,如果通信过程中,在三次握手或者客户端发起HTTP请求过程中,客户端的请求被中间人劫持,那么中间人就可以伪装成“假冒客户端”和服务器通信;中间人又可以伪装成“假冒服务器”和客户端通信。接下来,我们详细阐述中间人获取对称密钥的过程:

中间人在收到服务器发送给客户端的公钥(这里是“正确的公钥”)后,并没有发给客户端,而是中间人将自己的公钥(这里中间人也会有一对公钥和私钥,这里称呼为“伪造公钥”)发给客户端。之后,客户端把对称密钥用这个“伪造公钥”加密后,发送过程中经过了中间人,中间人就可以用自己的私钥解密数据并拿到对称密钥,此时中间人再把对称密钥用“正确的公钥”加密发回给服务器。此时,客户端、中间人、服务器都拥有了一样的对称密钥,后续客户端和服务器的所有加密数据,中间人都可以通过对称密钥解密出来。

为了解决此问题,我们引入了数字证书的概念。服务器首先生成公私钥,将公钥提供给相关机构(CA),CA将公钥放入数字证书并将数字证书颁布给服务器,此时服务器就不是简单的把公钥给客户端,而是给客户端一个数字证书,数字证书中加入了一些数字签名的机制,保证了数字证书一定是服务器给客户端的。中间人发送的伪造证书,不能够获得CA的认证,此时,客户端和服务器就知道通信被劫持了。

所以综合以上三点:非对称加密算法(公钥和私钥)交换对称密钥+数字证书验证身份(验证公钥是否是伪造的)+利用对称密钥加解密后续传输的数据=安全

SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。

解析Volley

Volley的使用步骤

  1. 创建一个RequestQueue对象。
  2. 创建一个Request对象。
  3. 将Request对象添加到RequestQueue里面

在newRequestQueue方法中创建了一个Cache一个newWork,然后用这两个创建了一个RequestQueue,随后启动这个请求队列,这个队列启动的是5个线程(默认是1个处理走缓存的请求的线程,4个处理网络请求的线程,其实这里可以自己改,走缓存的请求个人觉得1个就足够,处理网络请求的线程数可以根据cpu核数等因素自己来确定一个合理值),在5个线程中主要的工作就是在自己的while循环中不断的读取队列中的内容(1个走缓存的请求队列,之后都称为A,1个走网络的请求队列,之后都称为B),也就是1个线程不断从A中读数据,4个线程不断从B中读数据。A、B中没有数据的时候,也就是take拿到的数据为空时,take就会阻塞所在的线程,take不为空的话,就进行各自的处理,一个走缓存,一个走网络。 

add和put的区别,一个回阻塞,一个会抛异常。

为什么使用工作线程任务队列而不用线程池?

不断的创建新的线程销毁线程是很好资源的,所以对于频繁的网络请求就需要复用已有工作线程,这样做同时也可以避免出现同一时间出现大量的线程的情况。就这两点来看,其实是也可以使用线程池的,都能达到要求。但是我们可以先看看Volley的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。从这个目标出发工作线程任务队列适合处理大量耗时较短的任务,并且我个人觉得如果使用线程池的话编写的难度上比使用线程任务队列大,而且就最终的性能上我不觉得会有什么差别,可能就一点:灵活性上来说线程池更加优越,总的来说就是两个能用同样的性能达到同样的效果,当然是选择更容易使用的。

解析OkHttp

1.1、整体流程

(1)、当我们通过OkhttpClient创建一个Call,并发起同步或异步请求时;
(2)、okhttp会通过Dispatcher对我们所有的RealCall(Call的具体实现类)进行统一管理,并通过execute()及enqueue()方法对同步或异步请求进行处理;
(3)、execute()及enqueue()这两个方法会最终调用RealCall中的getResponseWithInterceptorChain()方法,从拦截器链中获取返回结果;
(4)、拦截器链中,依次通过RetryAndFollowUpInterceptor(重定向拦截器)、BridgeInterceptor(桥接拦截器)、CacheInterceptor(缓存拦截器)、ConnectInterceptor(连接拦截器)、CallServerInterceptor(网络拦截器)对请求依次处理,与服务的建立连接后,获取返回数据,再经过上述拦截器依次处理后,最后将结果返回给调用方。
提供两张图便于理解和记忆:

maxRequests = 64: 最大并发请求数为64 

maxRequestsPerHost = 5: 每个主机最大请求数为5

okhttp每次请求的最大字节是2048.

ExecutorService 线程池的使用
这里有个Dispatcher,顾名思义它就是专门分发和执行请求的,看它的enqueue方法:

解析Retrofit

到Retrofit源码里看create函数,是一个动态代理。由于动态代理是在运行时动态生成的代理类。代理类生成的是一个INetApiService接口的实例对象,该对象的getBizInfo函数返回的是接口中定义的Call网络工作对象,这也体现了Retrofit的核心价值,生成接口定义的Call网络工作对象。

这个Call网络工作对象是在InvocationHandler中实现的,也就是在Retrofit.create函数中,由InvocationHandler实现的。

这样我们就明白了,Retrofit使用动态代理,其实是为了开发者在写代码时方便调用,而真正负责生产Call网络工作对象的,还是Retrofit.create函数中定义的这个InvocationHandler,这个InvocationHandler的代码我们再贴一遍:

ServiceMethod能让我们准确解析到INetApiService中定义的函数,为最后的适配转换提供转换目标,详细分析我们后面再说,先看适配转换的过程。

我们看到,Retrofit内部默认使用OkHttpCall对象去处理网络请求,但是返回的网络工作对象是经过适配器转换的,转换成接口定义的那种Call网络工作对象。

在adapt函数中,适配器会把Retrofit中用来访问网络的OkHttpCall,转换为一个ExecutorCallbackCall(继承了INetApiService接口里要求返回的网络工作对象retrofit2.Call),

这个CallAdapter的转换就比较明显了,把retrofit2.Call对象通过适配器转换为了一个实为Observable<?>的Object对象。

至此,我们可以理解Retrofit根据接口定义动态生产Call网络请求工作对象的原理了,其实就是通过适配器把retrofit2.Call对象转换为目标对象。

至于适配器转换过程中,如何实现的对象转换,就可以根据需求来自由实现了,比如利用静态代理等,如有必要,我们可以自行开发扩展,Retrofit框架并不限制我们对于适配器的实现方式。

Retrofit的整体工作流程,就是Retrofit用动态代理生成Call网络请求对象,在这个过程中,用适配器把Retrofit底层的retrofit2.Call对象转换为INetApiService中定义的Call网络请求对象(如Flowable)。

问题是,Retrofit具体是如何知道了INetApiService中定义的Call网络请求对象,如何实现网络请求,以及如何执行的数据转换呢?

首先,根据INetApiService中定义的函数,解析函数,得到函数的具体定义,并生成对应的ServiceMethod。
然后,根据这个ServiceMethod,实现一个OkHttpCall的Call对象,负责在Retrofit底层实现网络访问。
其中,在网络访问返回了网络数据时,根据ServiceMethod实现数据转换。
最后,利用上一小节中匹配的适配器,把OkHttpCall对象转换为INetApiService要求的Call网络请求对象。

loadServiceMethod其实就是加载ServiceMethod,它是对应我们接口中定义好的一个网络请求方法

ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

retrofit的核心思想是将 http 请求过程抽象成了一个对象 ServiceMethod。

这个对象的构造的时候,会通过 java 反射的方式传入一个 method 对象,而这个对象就是我们在 interface 中定义的请求方法

通过 method 的 returnType 构造出 createCallAdapter 和 responseConverter,然后自动的完成从服务器的返回结果,到程序用的 model 类实例的转换。

getAlbums() 就是我们的 method 方法。根据这个 method 方法, 生成 ServiceMethod 对象,返回的结果是 serviceMethod.callAdapter.adapt(okHttpCall); 

在radioCall.execute() 的内部,我们会把调用 delegates.execute() 来实际执行网络请求。这个 delegates 的类型就是 retrofit.OkHttpCall。然后,发起请求,等待服务器返回结果,并对结果进行处理。尚学堂•百战程序员陈老师指出,此时的结果还是 rawResponse,即都是 json 字符串,还不是可以直接使用的 java model 对象。这个时候,我们就需要 responseConverter 来帮我们进行转换了。

首先我们通过我们 create 出来的 retrofit 实例来调用接口方法。所有的 interface 方法都会在 java 动态代理机制的作用下,调用一个匿名类 new InvocationHandler 中的 invoke。在 invoke 中,会根据我们想调用的方法 method 构造出一个 serviceMethod,然后调用 serviceMethod.callAdapter.adapt(okHttpCall) 作为返回结果。

构造 serviceMethod 的时候,会根据 interface 中 method 的的返回类型,构造出 converter 和 callAdapter。其中, converter 一般使用 gson converter。gson converter 可以自动将服务器返回的 json 数据转化成 java 中的 model 类的实例。callAdapter 绝大多数的实现方式是在构造函数中接收一个 okHttpCall 实例,然后将 enqueue 和 execute 委托给这个 okHttpCall 实例来执行。okHttpCall 在获取到服务器数据之后,会利用 serviceMethod.toResponse(body) 来对数据进行转化。其中,转化的时候便利用了 converter。数据转化完成后,封装成 Response ,传递给调用方。其中 R 就是我们的数据类。