Android网络编程学习(2)-http header

前言

HTTP 协议在网络知识中占据了重要的地位,HTTP 协议最基础的就是请求和响应的报文头(Header),大多数 Http 协议的使用方式,都是依赖设置不同的 HTTP 请求/响应 的 Header 来实现的

http缓存

在 HTTP 协议中,天然就有对缓存的支持;HTTP 缓存主要是通过请求和响应报文头中的对应 Header 信息,来控制缓存的策略。
这里主要涉及两个 Header:
Cache-Control:设定缓存策略,是否使用缓存,超时时间是多少。
ETag:当前返回数据的验证令牌,可能是 Hash 值也可能是其他指纹,主要用于在下次请求的时候携带上,让服务端依此判断当前数据是否有更改。

http缓存示意图1.png

服务端在返回响应数据的时候,会在报文头中,增加用于描述当前响应的内容类型、数据长度、缓存策略(Cache-Control)、验证令牌(ETag)等信息。
例如上图就表示了一次请求响应的事务,大概客户端请求一个文件的时候,服务端返回了一个 200 的状态码,表示响应正常,响应的数据长度为 1024 个字节,建议客户端将此资源缓存最多 120 秒,并且提供了一个指纹令牌(“cxmyDev123”),用来作为当前数据的唯一标识。

ETag:数据令牌
假如没有数据令牌的情况下,大概步骤应该是这样的:

  1. 客户端会首先找到本地缓存,然后发现它已经失效,无法再次使用。
  2. 客户端再次向服务端发出新的请求,并获取完整的数据再次进行缓存。之后再刷新该缓存的超时时间。
    但是这是一件效率非常低的事情,服务端并无法确定所持有的源资源什么时候会失效,所以提供的 max-age 值,只是一个参考值,是需要取平衡的,太短会导致请求频繁,太长又会导致无法及时刷新客户端资源。而此时再次请求的时候,是存在一定的概率,客户端缓存的数据和服务端上持有的数据是一致的,我们就不需要再次对此数据资源进行二次缓存,直接使用客户端之前缓存的数据即可,同时还需要刷新缓存超时时间。
    这正是数据验证令牌(ETag)想要解决的问题,服务端生成并返回的这个数据指纹令牌,通常就是返回数据的 Hash 值或者其他数据指纹,客户端无需关心它的生成规则,只需要知道它是当前数据的一个唯一标识。


    http缓存示意图2.png

    客户端需要在下次请求时将其通过 If-None-Match 这个请求报文头,将此验证令牌发送至服务端,如果数据令牌指纹和服务端当前的数据一致,则标识资源未发生新的变化。就会返回一个 304 的状态码,表示可以继续使用客户端本地缓存的数据,并刷新超时时间。注意当响应码为 304 的时候,它是不包含数据内容的。
    通常此缓存操作对我们都是透明的,它是浏览器和开源网络库的基本实现,我们无需自己去判断 max-age 和 ETag 的值,这一步我们只需要确定服务端对此有支持即可

http Cache-Control更多配置

Cache-Control 还有一些更灵活的配置,用来对缓存做一些更细致的操作。

  1. “no-cache” 和 “no-store”
    这两个参数都表示每一次请求,都需要真实的发送一个网络请求。
    它们之间的区别在于,“no-cache”并不是真的不缓存数据,它只是要求每次都确认资源是否过期,也就是它会利用数据令牌 ETag 来一定程度的减小传输的流量。
    而 “no-store” 完全是要求客户端,每次都重新请求数据并下载最新的数据,不做任何缓存处理。这种不缓存的策略,也包括中间连接的代理、网关 等中间传输的通道,也一并不对数据进行缓存,每次都从源服务器上获取数据。
  2. “public” 和 “private”
    “public” 是一种默认的策略,表示当前缓存是开放的,任何请求响应的中间环节,都可以对其进行缓存,如果我们不显式指定,则当前为 “public” 缓存。
    与之相对的 “private”,则表示当前响应是针对单个用户的,并非通用数据,因此不建议任何中间缓存对其进行缓存。例如:浏览器就是一个比较私人的缓存源,它会缓存 “private” 的缓存,而 CDN 则不会

废弃和更新缓存的响应

缓存的策略,一旦确定并下发到客户端,服务端就失去了对齐的控制权。也就是说,如果我们设定了 max-age,在此资源有效期超时之前,哪怕服务端的源资源已经被替换修改,我们也没有一个合适的时机去通知客户端更新新的响应数据。

那么有没有什么好的策略去标记资源废弃?同时又能友好的利用缓存策略。

在互联网上,所有服务上的资源,都有一个对应的 URL(统一资源定位符),它可以明确说明如何从一个精确且固定的位置获取资源。而 HTTP 缓存,也是依赖于 URL 的,注意 URL 是大小写敏感的,同一个 URL 表示同一个请求响应,依此来判断缓存和后续缓存的复用。

所以我们是可以在 URL 上做文章的。(数据变更,不再用缓存的情况,用url改变的方式进行解决)

浏览器的废弃策略

前面提到,浏览器是天然支持 HTTP 缓存的,对于浏览器来说,它所面对的就是一个个 HTML 页面,页面内会包含一些 CSS、Image、JavaScript、JSON 资源和数据。

针对不同的资源和数据,我们可以在其 URL 上,增加数据令牌指纹,当资源变动的时候,同时也去刷新改指纹令牌。

浏览器缓存变更

到这里就很好理解了:

  • HTML 页面,使用 no-cache,强制每次都向源服务器确认数据。
  • CSS文件通常变动的频率非常低,所以可以允许中间层缓存,并且缓存时间为一年不过期。
  • JavaScript内有业务逻辑,可以设定为只允许客户终端缓存。
  • getUserInfo,是为个人用户数据相关,这里推荐可缓存,但是需要每次向服务器重新确认。

App 接口的缓存策略

在 App 中使用的接口,其实和网页又不一样,HTML 网页的结构类似一个树形结构,先通过获取 .html 文件获取其内所有资源的表,然后依次根据缓存策略进行访问。

但是在 App 中,和服务器的交互都是通过数据接口来实现的,就不存在最开始获取一个类似 HTML 文件这样的树形接口,每个接口都是一个个“孤岛”,可以单独存在。我们就无法提前知道某个接口的响应数据已经过期,同时也无法修改 URL 上携带的数据指纹令牌。

但是其实我们是可以通过 App 和设备的一些固有信息,作为 URL 的参数传递,以此来刷新数据。

app接口缓存变更

http缓存小结

到这里我们基本上把 HTTP 的缓存所有相关的内容都讲了一遍,这里简单总结一下。
HTTP 缓存依赖 URL 做唯一标识,不同的 URL 使用不同的缓存。
Cache-Control 可以控制缓存策略,共有或者私有、缓存超时时长等。
通过 ETag 来标记数据指纹令牌,以此来确定响应数据是否更新。
应该为每个响应资源提供对应的缓存策略。
如果需要废弃之前的缓存,可以利用修改请求 URL 的方式,将数据指纹令牌追加在 URL 之后,以此来更新数据。

http-压缩编码(内容编码)

其实缓存的主要起因就是为了减少网络请求次数,来达到快速响应的目的。而除了减少网络请求之外,其实我们还可以通过对实体内容,进行编码压缩的方式,减少传输的内容大小,从而加快响应的速度

压缩编码

说到压缩编码,就先简单聊聊压缩算法,对于压缩算法而言,分为两类:
1.无损压缩算法
2.有损压缩算法
从名称上就可以理解,无损压缩意味着它是可以被还原的,通常被应用在文本,而有损压缩会对原始数据进行修改,以加大压缩率的目的,对文件进行有损失的压缩,这是一种不可逆的操作,通常一些对质量要求不高的图片和视频上,虽然压缩以后可能会导致文件模糊,但是勉强还可以看。
而在 HTTP 协议中,通常我们只会对文本内容,进行压缩编码。一个主要的原因在于,压缩本身是会消耗服务器资源的,而文件比多媒体文件轻便了很多。并且多媒体文件多数情况下,本身就已经是高度压缩的二进制格式,再次进行压缩的意义也不大。

设计一个“压缩协议”

前面提到,HTTP 协议是一种松散的 “协商协议”,需要客户端和服务端双端配合,才可以生效。而压缩算法有很多种,到底应该选择哪一种,也是需要双方协商的。
如果我们尝试设计一下这个 HTTP 的 “压缩协议”,主要需要关注这两点。

  1. 通知服务端,客户端支持的压缩算法
    一个 HTTP 事务,总是由客户端发起请求,而服务端将响应返回。那么客户端就要在发起请求的时候,率先告知服务端,当前客户端支持的压缩算法。
    通常客户端会支持多种压缩算法,为了让服务端有选择的空间,应该允许传递多个支持的压缩算法。既然有多选的空间,那么就一定要有优先级的概念。
    类似于我们在市场上交易,我接受人民币、美元、比特币的交易,但是因为我使用人民币更方便,所以我需要指明交易方,如果方便的话最好通过人民币交易。

  2. 服务端选择支持的压缩算法压缩内容
    服务端接受到客户端的请求后,辨识出客户端支持的压缩算法,现在当前环境最优的一种压缩算法对响应内容体进行压缩,然后将压缩后的内容返回。
    为了让客户端接收到响应后,能明确知道服务端使用的压缩算法,还需要在响应中明确指明,当前的响应实体的数据使用的压缩算法(当然也可以不压缩)。
    2.4 HTTP 的“压缩协议”
    前面我们自己设计的两个条件,都是基于 HTTP 报文中的报文头来实现的。接下来我们看看 HTTP 协议中,是如何设计“压缩协议”的。

  3. 请求头中的 Accept-Encoding
    客户端为了告知服务端当前支持的压缩编码,可以在请求头中,增加 Accept-Encoding 这个头部字段,用来指定当前客户端支持的压缩编码,如果有多个可以使用逗号 , 进行分割。
    为了满足优先级,其实是可以通过 , 分割的顺序来指定的。HTTP 协议中,还可以使用 Q 值来说明编码的优先级,Q 值的取值范围是 0.0 ~ 1.0。0.0 表示客户端不想接受此编码,而 1.0 则表示希望使用此编码,不过通常我们不需要明确的指定它,大家了解一下即可。

  4. 响应头中的 content-encoding
    服务端为了在响应报文里体现当前对内容压缩使用的编码格式,会在响应头中使用 Content-Encoding 标记,它是一个明确值,所以只可能有一个。
    编码的目的就是为了压缩,所以当服务端选择压缩内容实体的时候,同时还会修改 Content-Length 来明确表示当前实体被编码压缩后的长度。
    发两张压缩前和压缩后的流程图,就清晰了


    http无压缩.png

    http内容压缩编码.png

HTTP 的编码类型(压缩方式)

HTTP 定义了一些标准的内容编码类型,并且可以扩展更多的编码类型。由互联网号码分配机构(IANA)对各种编码进行标准化,它给每个内容编码算法分配一个唯一的代号。
Content-Encoding 就是用这些标准化的代号来说明编码使用的算法。
比较常用的算法有:
1.gzip:表明实体采用 GNU zip 编码。
2.compress:表明实体采用 Unix 的文件压缩程序。
3.deflate:表明使用是用 zlib 的格式压缩的。
4.br:表明实体使用 Brotli 算法的压缩格式。
5.identity:表明没有对实体进行编码,为默认值。
在这些算法中,除了 identity 之外,都是无损压缩,他们都是需要可还原成原始的文本内容的。gzip 通常是效率最高的,使用最广泛的。
但是 gzip 对媒体文件的压缩效果相对较差,本身 JPG/PNG 这类文件已经是一种高度压缩的二进制文件,开启 gzip 效果甚微还会浪费大量 CPU 资源。
浏览器的默认实现中,这些压缩编码通常只会作用在文本内容上,就是 Content-Type 为 text/Xxx 的请求上,而对于一些媒体文件,则不会使用这种方式对其进行压缩。

再总结几个关键点:

  1. 请求头中,通过 Accept-Encoding 来指定客户端支持的内容编码格式。
  2. 服务端选择一个支持的内容编码去压缩原始响应内容实体。
  3. 修改响应头,增加 Content-Encoding 用于指定使用的编码方式,并且修改 Content-Length 来表明压缩后的内容大小。
  4. 内容压缩的算法有很多,但是 gzip 是最常用的。
  5. 内容压缩算法,都是基于无损压缩,最终都需要在客户端将内容还原。

http-内容编码总结

一个报文通常会包含报文头部和报文实体,而本文介绍的 HTTP 压缩编码,主要是针对报文实体内容中,文本内容的压缩编码,并未涉及到报文头部的压缩。主要是因为在 HTTP/1中,报文头部始终是以 ASCII 文本传输,没有经过任何压缩,而在 HTTP/2 中才对其实现了解决方案,所以 HTTP 的编码压缩只是针对报文实体的,这句话并不全对,这个有机会以后再说。
除了内容编码之外,HTTP 还有传输编码,这个同样也是有机会再说。
在本文中,说明了 HTTP 对报文实体内容的压缩策略和方法,希望对你有帮助

http-传输编码

什么是传输编码?

传输编码在 HTTP 的报文头中,使用 Transfer-Encoding 首部进行标记,它就是指明当前使用的传输编码。
Transfer-Encoding 会改变报文的格式和传输的方式,使用它不但不会减少内容传输的大小,甚至还有可能会使传输变大,看似是一个不环保的做法,但是其实是为了解决一些特殊问题。
简单来说,传输编码必须配合持久连接去使用,为了在一个持久连接中,将数据分块传输,并标记传输结束而设计的,后面会详细讲解。
在早年间的设计里,和内容编码使用 Accept-Encoding 来标记客户端接收的压缩编码类型一样,传输编码还需要配合 TE 这个请求报文头来使用,用于指定支持的传输编码。但是在最新的 HTTP/1.1 协议规范中,只定义了一种传输编码:分块编码(chunked),所以并不需要再依赖 TE 这个头部。
这些细节,后面都会讲到。既然传输编码和持久连接是息息相关的,那我们就先来了解一下什么是持久连接

持久连接(Persistent Connection)

持久连接通俗来讲,就是长连接,英文叫 Persistent Connection,其实按字面意思理解就好了。
在早期的 HTTP 协议中,传输数据的顺序大致分为发起请求、建立连接、传输数据、关闭连接等步骤,而持久连接,就是去掉关闭连接这个步骤,让客户端和服务端可以继续通过此次连接传输内容。
这其实也是为了提高传输效率,我们知道 HTTP 协议是建立在 TCP 协议之上的,自然有 TCP 一样的三次握手、慢启动等特性,这样每一次连接其实都是一次宝贵的资源。为了尽可能的提高 HTTP 的性能,使用持久连接就显得很重要了。为此在 HTTP 协议中,就引入了相关的机制。
在早期的 HTTP/1.0 协议中并没有持久连接,持久连接的概念是在后期才引入的,当时是通过 Connection:Keep-Alive 这个头部来标记实现,用于通知客户端或服务端相对的另一端,在发送完数据之后,不要断开 TCP 连接,之后还需要再次使用。
而在 HTTP/1.1 协议中,发现持久连接的重要性了,它规定所有的连接必须都是持久的,除非显式的在报文头里,通过 Connection:close 这个首部,指定在传输结束之后会关闭此连接。
实际上在 HTTP/1.1 中Connect 这个头部已经没有 Keep-Alive 这个取值了,由于历史原因,很多客户端和服务端,依然保留了这个报文头。
长连接带来了另外一个问题,如何判定当前数据发送完成。(而从HTTP/1.1起,默认使用长连接)

在早期不支持持久连接的时候,其实是可以依靠连接断开来判定当前传输已经结束,大部分浏览器也是这么干的,但这并不是规范的操作。应该使用 Content-Length 这个头部,来指定当前传输的实体内容长度。
下面举个例子,在保持持久连接的情况下,依赖 Content-Length 来确定数据发送完毕,Content-Length 在这里起到了一个响应实体已经发送结束的判断依据。这样的情况下,我们就要求 Content-Length 必须和内容实体的长度一致,如果不一致,就会出现各种问题

Content-Length决定传输完成的缺陷.png

如上图所示,如果 Content-Length 小于内容实体的长度,则会截断,反之则无法判定当前响应已经结束,会将请求持续挂起造成 Padding 状态。

理想情况下,我们在响应一个请求的时候,就需要知道它的内容实体的大小。但是在实际应用中,有些时候内容实体的长度并没有那么容易获得。例如内容实体来自网络文件、或者是动态生成的。这个时候如果依然想要提前获取到内容实体的长度,只能开一个足够大的 Buffer,等内容全部缓存好了再计算。

但这并不是一个好的方案,全部缓存到 Buffer 里,第一会消耗更多的内存,第二也会更耗时,让客户端等待过久。

此时就需要一个新的机制,不依赖 Content-Length 的值,来判定当前内容实体是否传输完成,此时就需要 Transfer-Encoding 这个头部来判定

Transfer-Encoding:chunked

前面也提到,Transfer-Encoding 在最新的 HTTP/1.1 协议里,就只有 chunked 这个参数,标识当前为分块编码传输。
分块编码传输既然只有一个可选的参数,我们就只需要指定它为 Transfer-Encoding:chunked,后续我们就可以将内容实体包装一个个块进行传输。
分块传输的规则:

  1. 每个分块包含一个 16 进制的数据长度值和真实数据。
  2. 数据长度值独占一行,和真实数据通过 CRLF(\r\n) 分割。
  3. 数据长度值,不计算真实数据末尾的 CRLF,只计算当前传输块的数据长度。
  4. 最后通过一个数据长度值为 0 的分块,来标记当前内容实体传输结束。


    传输编码-分块传输.png

在这个例子中,首先在响应头部里标记了 Transfer-Encoding: chunked,后续先传递了第一个分块 “0123456780”,长度为 b(11 的十六进制),之后分别传输了 “Hello CxmyDev” 和 “123”,最后以一个长度为 0 的分块标记当前响应结束

chunked 的拖挂

当我们使用 chunked 进行分块编码传输的时候,传输结束之后,还有机会在分块报文的末尾,再追加一段数据,此数据称为拖挂(Trailer)。
拖挂的数据,可以是服务端在末尾需要传递的数据,客户端其实是可以忽略并丢弃拖挂的内容的,这就需要双方协商好传输的内容了。
在拖挂中可以包含附带的首部字段,除了 Transfer-Encoding、Trailer 以及 Content-Length 首部之外,其他 HTTP 首部都可以作为拖挂发送。
一般我们会使用拖挂来传递一些在响应报文开始的时候,无法确定的某些值,例如:Content-MD5 首部就是一个常见的在拖挂中追加发送的首部。和长度一样,对于需要分块编码传输的内容实体,在开始响应的时候,我们也很难算出它的 MD5 值。


传输编码-拖挂.png

注意这里在头部增加了 Trailder,用以指定末尾还会传递一个 Content-MD5 的拖挂首部,如果有多个拖挂的数据,可以使用逗号进行分割。

内容编码和传输编码结合

内容编码和传输编码一般都是配合使用的。我们会先使用内容编码,将内容实体进行压缩,然后再通过传输编码分块发送出去。客户端接收到分块的数据,再将数据进行重新整合,还原成最初的数据。

内容编码-传输编码结合.png

传输编码小结

我们对传输编码应该有一定的了解了。这里简单总结一下:

  1. 传输编码使用 Transfer-Encoding 首部进行标记,在最新的 HTTP/1.1 协议里,它只有 chunked 这一个取值,表示分块编码。

  2. 传输编码主要是为了解决持久连接里将数据分块传输之后,判定内容实体传输结束。

  3. 分块的格式:数据长度(16进制)+ 分块数据。

  4. 如果还有额外的数据,可以在结束之后,使用 Trailer 进行拖挂传输额外的数据。

  5. 传输编码通常会配合内容编码一起使用。

此外,传输编码应该是所有 HTTP/1.1 的标准实现,应该都有支持,如果收到无法理解的经过传输编码的报文,应该直接返回 501 Unimplemented 这个状态码来回复即可

http-cookie

本质上cookie是对http的补充,将一些信息不放在报文内容中,而是放在特定的报文头里;本身 HTTP 就是一个无状态的协议,但是有时候我们又有需要增加状态的需求,这个时候延伸出来了 Cookie,利用 Cookie 可以让传输的时候保持一些状态信息

什么是 Cookie?

先明确一点,Cookie 就是为了解决 HTTP 协议无状态的问题,接下来举个例子说明。
早年间医院对患者的病例还没有在线建档的时候,都需要患者在就医之前,办理一个病历的小册子,医生会在病历中写上此次就医的情况,什么时间、有什么表现的反映、诊断是什么病、开了一些什么药等等。如果下次又生病了,有病历的情况下,都会要求患者再把病历带上,这样医生就能通过病历了解到之前的情况。
在 Cookie 的实现上,也是这样的。服务端(医生)在收到客户端(患者)请求的时候,将一些用户标识信息加入到 Cookie (病例)中,随着响应返回给客户端,客户端将 Cookie 中的信息存储在本地,下次再请求此服务器的时候,再将 Cookie 中携带的数据原样传输给服务端,此时服务端就能通过 Cookie 中的用户标识,识别出这是之前请求过的某个用户。
在这个例子中,服务端就是医生的角色、客户端是患者的角色、Cookie 就是病历。
Netscape 官方文档中的定义为:Cookie 是指在 HTTP 协议下,服务器或脚本可以维护客户端计算机上信息的一种方式 。通俗地说,Cookie 是一种能够让网站 Web 服务器把少量数据储存到客户端的硬盘或内存里,或是从客户端的硬盘里读取数据的一种技术。 Cookie 文件则是指在浏览某个网站时,由 Web 服务器的 CGI 脚本创建的存储在浏览器客户端计算机上的一个小文本文件。

一个完整的 Cookie 传输流程

HTTP 协议中的规则,都是通过在请求头和响应头中写入输入来实现,Cookie 也是这样的。
服务端通过 Set-Cookie 这个响应头来向客户端中写入 Cookie 信息,而客户端读取 Set-Cookie 这个响应头中的信息存储起来,在下次请求的时候取出来,再通过 Cookie 这个请求头,将 Cookie 的数据传输给服务端。


Cookie传输流程.png

在响应头(Response Header)中,使用 Set-Cookie 传递不同的 Cookie 数据,多个数据可以分开成多个 Set-Cookie 头。
在请求头中(Request Header)中,使用 Cookie 这个请求头传递 Cookie 数据,不同的数据通过 ;分割。

Cookie 的配置参数

到现在我们已经介绍了两个 Cookie 配置的信息,Domain 和 Expires/Max-Age,分别用来配置域名和过期策略。
这些都很好理解,毕竟浏览器是开放的,它会访问很多不同的网址,如果每个请求都将所有的 Cookie 信息都传递过去,基本上是不现实的。而这些配置参数,就是对 Cookie 增加一些附加的设置,进行一些简单的限制和过滤,在减少传输量的同时也保证了安全。
Domain 这个参数可以限制只在此域名下的请求,才传递该 Cookie,其他的不传递。
Cookie 其实还支持其他的一些参数配置,打开 Chrome 的调试模式,在 Application 中就可以看到当前页面的 Cookie 信息。
下面以一篇微信文章页面所存储的 Cookie 为例。

Cookie携带信息

Name:Value :Cookie 存储的数据就是一个 Key-Value 的键值对,所以这两个参数没什么争议,就是数据的 Key 和 Value。
Domain:Cookie 的域,限制请求头传输的域。
Path:域中与 Cookie 相关的路径前缀。
Expires/Max-Age:过期时间或者超时间隔。
http:此属性为 True,表示只会在 HTTP 请求头中携带此 Cookie 信息,而无法通过 document.cookie 来访问此 Cookie。
Secure:安全,是否只有在使用 SSL 连接时才发送这个 Cookie。

Cookie总结

HTTP 中的 Cookie 知识点,基本上都已经讲解清楚了,我们再次总结一下关键知识点。

  1. Cookie 主要是为了解决 HTTP 协议无状态的问题。
  2. 服务端通过 Set-Cookie 响应头来向客户端设置 Cookie。
  3. 客户端通过 Cookie 请求头向服务端发送之前存储的 Cookie 数据。
  4. Cookie 依据过期时间进行区分,将类型分为:临时 Cookie 和 持久 Cookie。
  5. Cookie 可以通过配置不同的参数,进行限制,例如过期时间、支持的域名、是否安全(secure)等。
  6. Cookie 不支持跨域,跨域还需要其他的方式绕开来实现。
  7. Cookie 只能做到相对的安全,任何事情没有绝对的安全。

http-范围请求

HTTP 的范围请求。范围请求主要是针对较大的文件的请求或者上传,可以仅操作它的某一段

一个比较常见的场景,就是断点续传/下载,在网络情况不好的时候,可以在断开连接以后,仅继续获取部分内容。例如在网上下载软件,已经下载了 95% 了,此时网络断了,如果不支持范围请求,那就只有被迫重头开始下载。但是如果有范围请求的加持,就只需要下载最后 5% 的资源,避免重新下载。
另一个场景就是多线程下载,对大型文件,开启多个线程,每个线程下载其中的某一段,最后下载完成之后,在本地拼接成一个完整的文件,可以更有效的利用资源

是否支持范围请求

HTTP 本身是一种无状态的“松散”协议,而在经历了很多版本的迭代之后,只在 HTTP/1.1(RFC2616) 之上,才支持范围请求。所以如果客户端或者服务端两端的某一端低于 HTTP/1.1,我们就不应该使用范围请求的功能。
而在 HTTP/1.1 中,很明确的声明了一个响应头部 Access-Ranges 来标记是否支持范围请求,它只有一个可选参数 bytes


Accept-Ranges是否支持范围请求.png

范围请求

如果已经确定双端都支持范围请求,我们就可以在请求资源的时候使用它。

所有的文件最终都是存储在磁盘或者内存中的字节,对于待操作的文件可以将其以字节为单位分割。这样只需要 HTTP 支持请求该文件从 n 到 n+x 这个范围内的资源,就可以实现范围请求了。

HTTP/1.1 中定义了一个 Ranges 的请求头,来指定请求实体的范围。它的范围取值是在 0 - Content-Length 之间,使用 - 分割。

例如已经下载了 1000 bytes 的资源内容,想接着继续下载之后的资源内容,只要在 HTTP 请求头部,增加 Ranges:bytes=1000- 就可以了

HTTP 协议是一种双边协商的协议,既然请求头部已经确定是使用 Ranges 了,还有响应头部中,也需要使用 Content-Ragne 这个响应头来标记响应的实体内容范围。

Content-Range 的格式也很清晰,首先标记它的单位是 bytes 然后标记当前传递的内容实体范围和总长度。

Content-Range: bytes 100-999/1000

在这个例子中,会传递 100 ~ 999 范围的内容实体,而该资源文件的总大小是 1000 bytes。并且此时的 HTTP 响应状态码为 206 Partial Content 。

HTTP 206 Partial Content 成功状态响应代码表示请求已成功,并且主体包含所请求的数据区间,该数据区间是在请求的 Range 首部指定的。

所以一个正常的流程(范围请求流程)应该如下图所示:


范围请求.png

注意这里的每个 HTTP 事务中的响应头里,都是会包含 Content-Length 的,只是它包含的是当前范围请求响应的内容实体长度,而非此资源完整的长度。
到这里基本上算是讲清楚 HTTP 范围请求的正确流程了,接下来看看一些特殊的情况。

资源变化

当我们在一些下载工具中,下载大尺寸资源的时候,偶尔中间暂停过再重新下载,可能会遇见它又重头开始下载的情况。

这看似是 HTTP 的范围请求失效了,但是实际上并不一定如此,很可能是因为请求的资源,在请求的这个过程中,发生了改变。

假如你下载的过程中,下载的源资源文件发生了变化,但是 URL 没有改变,此时文件长度可能已经变化了(这是非常容易发现的),极端情况下就算没有长度没有变化,你再继续下载,很可能最终下载完成之后,无法将下载的内容拼接成我们需要的文件。

资源变化.png

如果我们需要从服务器上下载某个资源,一定要预防此资源可能发生的变动。在之前讲HTTP 缓存的时候讲到,在 HTTP 协议中,可以通过 ETag 或者 Last-Modified 来标识当前资源是否变化。

  • ETag:当前文件的一个验证令牌指纹,用于标识文件的唯一性。
  • Last-Modified:标记当前文件最后被修改的时间。

在 HTTP 的范围请求中,也可以使用这两个字段来区分分段请求的资源,是否有修改过,只需要在请求头中,将它放在 If-Range 这个请求报文头中即可。If-Range 使用 ETag 或者 Last-Modified 两个参数任意一个,原样填入即可。

通过令牌指纹判断资源是否变化.png

范围请求+令牌指纹

此时,如果两次操作的都是同一个资源文件,就会继续返回 206 状态码,开始后续的操作,反之则会返回 200 状态码,表示文件发生改变,要从头下载。

需要注意的是 If-Range 需要和 Range 配合起来使用,否则会被服务端忽略。

再额外提一点,如果客户端请求报文头中,对 Range 填入的范围错误,会返回 416 状态码。

HTTP 416 Range Not Satisfiable 错误状态码意味着服务器无法处理所请求的数据区间。最常见的情况是所请求的数据区间不在文件范围之内,也就是说,Range首部的值,虽然从语法上来说是没问题的,但是从语义上来说却没有意义。

范围请求小结

到这里我们就已经把 HTTP 范围请求的整个流程都说明清楚了。
再重新整理一下关键点:

  1. HTTP 范围请求,需要 HTTP/1.1 及之上支持,如果双端某一段低于此版本,则认为不支持。
  2. 通过响应头中的 Accept-Ranges 来确定是否支持范围请求。
  3. 通过在请求头中添加 Range 这个请求头,来指定请求的内容实体的字节范围。
  4. 在响应头中,通过 Content-Range 来标识当前返回的内容实体范围,并使用 Content-Length 来标识当前返回的内容实体范围长度。
  5. 在请求过程中,可以通过 If-Range 来区分资源文件是否变动,它的值来自 ETag 或者 Last-Modifled。如果资源文件有改动,会重新走下载流程。
    再配一张流程图,就更清晰了


    范围请求小结.png

总结

1.http缓存-减小http连接次数
响应报文:Cache-Control(用于控制客户端缓存) ETag(指纹令牌)
请求报文:If-None-Match
废弃和更新缓存:通过更新url
2.http内容编码-对内容报文进行压缩
响应报文:content-encoding 响应报文中用的压缩格式 ;同时还会修改 Content-Length 来明确表示当前实体被编码压缩后的长度
请求报文:Accept-Encoding 告诉服务端,客户端可以支持压缩方式
3.http传输编码-对长连接的传输内容进行分块传输(长连接-http1.1默认开启 ,除非报头里有Connection:close)
响应报文:Transfer-Encoding:chunked用于指定分块传输(传输格式 长度\r\n 传输内容)
Trailer 拖挂 还有机会在分块报文的末尾,再追加一段数据,此数据称为拖挂(Trailer)
请求报文:
4.http Cookie-对http传输内容的一些补充,将客户端的一些信息放到报文头部进行传递
响应报文:Set-Cookie
请求报文:Cookie
5.http范围请求-断点续传,多线程下载等等
响应报文:Access-Ranges 服务端表示是否支持范围请求 Content-Ragne 使用范围请求,这个响应头来标记响应的实体内容范围
请求报文:Range:使用范围请求(请求哪段范围)
服务端数据是否已经更新了,更新之后需要从头下载
响应报文:ETag 指纹令牌
请求报文:If-Rangle(配合Range使用)

6.okhttp 添加报头 Request.Builder.addHeader

参考:
https://www.cnblogs.com/plokmju/p/htt_cache_cxmy.html
https://www.cnblogs.com/plokmju/p/http_gzip.html
https://www.cnblogs.com/plokmju/p/http_code.html
https://www.cnblogs.com/plokmju/p/http_cookie.html
https://www.cnblogs.com/plokmju/p/http_range.html
常见http报文

你可能感兴趣的:(Android网络编程学习(2)-http header)