Android开发之漫漫长途 XIX——HTTP
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!
前言
在开始Android并发系列文章之前先插入一些文章,后续Android并发系列文章会按照计划发布。本篇文章是来说说HTTP那些事。
HTTP简介
Web 使用一种名为 HTTP ( HyperText Transfer Protocol ,超文本传输协议的协议作为规范,完成从客户端到服务器端等一系列运作流程。本文探讨的是HTTP/1.1版本。这仍然是大多数网站采用的HTTP协议
我们先看一下下面这张广为流传的图。
那些与HTTP协议密切相关的协议
IP
IP 协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件。其中两个重要的条件是 IP 地址和 MAC地址( Media Access Control Address )。IP 地址指明了节点被分配到的地址, MAC 地址是指网卡所属的固定地址。 IP 地址可以和 MAC 地址进行配对。 IP 地址可变换,但 MAC地址基本上不会更改。IP使用 ARP 协议凭借 MAC 地址进行通信
TCP(确保可靠性的协议)
按层次分, TCP 位于传输层,提供可靠的字节流服务。所谓的字节流服务( Byte Stream Service )是指,为了方便传输,将大块数据分割成以报文段( segment )为单位的数据包进行管理。而可靠的传输服务是指,能够把数据准确可靠地传给对方。
TCP连接的3次握手
为了准确无误地将数据送达目标处, TCP 协议采用了三次握手( three-way handshaking )策略。
握手过程中使用了 TCP 的标志( flag ) —— SYN ( synchronize ) 和ACK ( acknowledgement )。发送端首先发送个带 SYN 标志的数据包给对方。接收端收到后,回传一个带有 SYN/ACK 标志的数据包以示传达确认信息。最后,发送端再回传一个带 ACK 标志的数据包,代表 “ 握手 ” 结束。若在握手过程中某个阶段莫名中断, TCP 协议会再次以相同的顺序发送相同的数据包。
整个过程如下图所示
为什么需要3次握手
为什么需要3次握手,如果面试中问到了TCP相关知识,那么这个问题也几乎是必问的,为什么是3次,而不是1次,2次或者4次,5次??
首先我们知道不管是客户端或者服务端发送数据都会消耗网络流量,还会造成许多不必要的通信开销,浪费资源,所以我们在保证TCP可靠性的前提下所经历的通信次数自然越少越好。
TCP的可靠性含义我们上面已经说了,那我们就从3次握手分析,如果只有1次握手,客户端只向服务端发送数据,那么就谈不上可靠性了,因为服务端都没有回复,那么我们来看只有2次握手行不行,如果只有2次握手,客户端只向服务端发送数据,服务器也向客户端回复我收到数据了,但是考虑此时如果发送数据的过程中数据丢失了,服务端认为连接建立了(数据我已经发出去了),可是客户端没有收到数据,客户端认为连接没有建立,就会重复请求,而这对与服务端来说就又是一个全新的连接。一言以蔽之:第三次握手是为了防止:如果客户端迟迟没有收到服务器返回确认报文,这时会放弃连接,重新启动一条连接请求,但问题是:服务器不知道客户端没有收到,所以他会收到两个连接,浪费连接开销。如果每次都是这样,就会浪费多个连接开销。
HTTP详解
HTTP的报文结构
用于 HTTP 协议交互的信息被称为 HTTP 报文。请求端(客户端)的HTTP 报文叫做请求报文,响应端(服务器端)的叫做响应报文。HTTP 报文本身是由多行(用 CR+LF 作换行符)数据构成的字符串文本。HTTP 报文大致可分为报文首部和报文主体两块。两者由最初出现的空行( CR+LF )来划分。通常,并不一定要有报文主体。
HTTP的请求以及响应报文结构
HTTP的报文头部
下面的是请求某网站时,请求报文以及响应报文的首部信息。
请求报文首部
Host: hackr.jp
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
响应报文首部
HTTP/1.1 200 OK
Date: Thu, 23 Aug 2018 08:17:43 GMT
Server: Apache
Last-Modified: Tue, 08 Jan 2013 08:53:29 GMT
ETag: "25e-4d2c3145df440-gzip"
Accept-Ranges: bytes
Vary: Accept-Encoding,User-Agent
Content-Encoding: gzip
Content-Length: 379
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html
由上可知HTTP 首部字段是由首部字段名和字段值构成的,中间用冒号 “:” 分隔。
HTTP的报文头部类型
HTTP 首部字段根据实际用途被分为以下 4 种类型。
通用首部字段( General Header Fields )请求报文和响应报文两方都会使用的首部。
| 首部字段名 | 说明
| ————- |:————-:
| Cache-Control|控制缓存的行为
| Connection|逐跳首部、连接的管理
| Date|创建报文的日期时间
| Pragma|报文指令
| Trailer|报文末端的首部一览
| Transfer-Encoding|指定报文主体的传输编码方式
| Upgrade|升级为其他协议
| Via|代理服务器的相关信息
| Warning|错误通知
请求首部字段( Request Header Fields )从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。
| 首部字段名 | 说明
| ————- |:————-:
| Accept| 用户代理可处理的媒体类型
| Accept-Charset| 优先的字符集
| Accept-Encoding| 优先的内容编码
| Accept-Language| 优先的语言(自然语言)
| Authorization| Web 认证信息
| Expect| 期待服务器的特定行为
| From| 用户的电子邮箱地址
| Host| 请求资源所在服务器
| If-Match| 比较实体标记( ETag )
| If-Modified-Since| 比较资源的更新时间
| If-None-Match| 比较实体标记(与 If-Match 相反)
| If-Range| 资源未更新时发送实体 Byte 的范围请求
| If-Unmodified-Since| 比较资源的更新时间(与 If-Modified-Since 相反)
| Max-Forwards| 最大传输逐跳数
| Proxy-Authorization| 代理服务器要求客户端的认证信息
| Range| 实体的字节范围请求
| Referer| 对请求中 URI 的原始获取方
| TE| 传输编码的优先级
| User-Agent| HTTP 客户端程序的信息
响应首部字段( Response Header Fields )从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息。
| 首部字段名 | 说明
| ————- |:————-:
| Accept-Ranges| 是否接受字节范围请求
| Age| 推算资源创建经过时间
| ETag| 资源的匹配信息
| Location| 令客户端重定向至指定 URI
| Proxy-Authenticate| 代理服务器对客户端的认证信息
| Retry-After| 对再次发起请求的时机要求
| Server| HTTP 服务器的安装信息
| Vary| 代理服务器缓存的管理信息
| WWW-Authenticate| 服务器对客户端的认证信息
实体首部字段( Entity Header Fields )针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的信息。
| 首部字段名 | 说明
| ————- |:————-:
| Allow| 资源可支持的 HTTP 方法
| Content-Encoding| 实体主体适用的编码方式
| Content-Language| 实体主体的自然语言
| Content-Length| 实体主体的大小(单位:字节)
| Content-Location| 替代对应资源的 URI
| Content-MD5| 实体主体的报文摘要
| Content-Range| 实体主体的位置范围
| Content-Type| 实体主体的媒体类型
| Expires| 实体主体过期的日期时间
| Last-Modified| 资源的最后修改日期时间
HTTP 首部字段将定义成缓存代理和非缓存代理的行为,分成 2 种类型
- 端到端首部( End-to-end Header )
分在此类别中的首部会转发给请求 / 响应对应的最终接收目标,且必须保存在由缓存生成的响应中,另外规定它必须被转发。 - 逐跳首部( Hop-by-hop Header )
分在此类别中的首部只对单次转发有效,会因通过缓存或代理而不再转发。 HTTP/1.1 和之后版本中,如果要使用 hop-by-hop 首部,需提供 Connection 首部字段。
下面列举了 HTTP/1.1 中的逐跳首部字段。除这 8 个首部字段之外,其他所有字段都属于端到端首部
- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authorization
- Trailer
- TE
- Transfer-Encoding
- Upgrade
关于各个首部命令的详细参数可参见HTTP Headers
HTTP进阶
如果这篇文章到上面为止,那跟手册貌似没有什么区别,之前听到过一位大神说的话,掌握一门技术,不仅需要知道这门技术能做到什么,也要知道它不能做到什么,在掌握一门技术的边界之后,便对一门技术游刃有余了,毕竟技术那么多,推陈换代那么快,对于大多数人来说没有精力貌似也不是十分必要去记忆技术的每一个细节(即使记忆了,有许多细节是我们平常用不到的,也就渐渐遗忘了)。在我们掌握边界以后,遇到问题时,我们知道这个问题能依靠该项技术解决,至于怎么解决,那时我们在去查相应手册即可。
那对于本篇的HTTP来说,我们就从以下两方面进行分析
HTTP能做到什么
我们下面结合实际工作中的所遇到的HTTP的应用场景分为两大方面
访问大数据(图片,视频,大文件)时
在访问这些大数据时,我们往往会遇到以下问题
1. 如何请求部分数据?
2. 假设已请求了某资源的部分数据,程序终止了,服务器更新了该资源,客户端如何做,即已缓存的数据是否可信?
我们通常在做下载功能的时候往往需要断点续传甚至多线程断点续传,那么上面的问题就肯定会遇到。下面我们一一解答
1.使用Range来请求某个资源的部分数据
Range: bytes=bytes=200-1000, 2000-6576, 19000-
表示请求获取从第 200 字节至第1000 ,2000-6576,19000之后所有字节的资源。
在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。
如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。
假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。
2.ETag以及If-Match If-None-Match If-Range
响应首部字段ETag 能告知客户端实体标识。它是一种可将资源以字符串形式做唯一性标识的方式。服务器会为每份资源分配对应的 ETag值。另外,当资源更新时, ETag 值也需要更新。生成 ETag 值时,并没有统一的算法规则,而仅仅是由服务器来分配。
ETag 中有强 ETag 值和弱 ETag 值之分。
强 ETag 值,不论实体发生多么细微的变化都会改变其值。ETag: “1234”
弱 ETag 值,只用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 ETag 值。这时,会在字段值最开始处附加 W/
ETag: W/”1234″
请求首部字段If-Match If-None-Match If-Range
形如 If-xxx 这种样式的请求首部字段,都可称为条件请求。服务器接收到附带条件的请求后,只有判断指定条件为真时,才会执行请求。
If-Match ,它会告知服务器匹配资源所用的实体标记( ETag )值。这时的服务器无法使用弱 ETag 值。服务器会比对 比If-Match 的字段值和资源的 ETag 值,仅当两者一致时,才会执行请求。反之,则返回状态码 412 Precondition Failed 的响应。还可以使用星号( * )指定 If-Match 的字段值。针对这种情况,服务器将会忽略 ETag 的值,只要资源存在就处理请求。
If-None-Match 只有在 If-None-Match 的字段值与 ETag 值不一致时,可处理该请求。与 If-Match 首部字段的作用相反
If-Range 浏览器告诉 WEB 服务器,如果我请求的对象没有改变,就把我缺少的部分给我,如果对象改变了,就把整个对象给我。浏览器通过发送请求对象的ETag 或者自己所知道的最后修改时间给 WEB 服务器,让其判断对象是否改变了。总是跟 Range 头部一起使用。
使用形式如下
If-Range: Wed, 21 Oct 2015 07:28:00 GMT
Range: bytes=bytes=200-1000
If-Range: "1234"
Range: bytes=bytes=200-1000
If-Range 头字段通常用于断点续传的下载过程中,用来自从上次中断后,确保下载的资源没有发生改变。
通常情况下的HTTP请求与响应
我们现在的服务器大多是符合RESTFUL规范的,作为客户端(网页、Android、IOS)来说,我们与服务器的通常交互是数据量比较小的操作,增删改查,传递以及解析显示JSON数据。这是我们通常使用HTTP的场景。这种场景下所常用的HTTP头部字段是包含上述访问大数据(图片,视频,大文件)时的请求字段的,这些首部字段各有含义,见HTTP Headers
HTTP不能做到什么(缺陷)
- 一条连接上只可发送一个请求。
- 请求只能从客户端开始。客户端不可以接收除响应以外的指令。
- 请求 / 响应首部未经压缩就发送。首部信息越多延迟越大。
- 发送冗长的首部。每次互相发送相同的首部造成的浪费较多。
备注:由于上述HTTP/1.1协议的缺陷,SPDY 和 WebSocket 等协议纷纷出现,还有HTTP/2.0版本,这些协议的出现在一定程度上弥补了HTTP/1.1协议的缺陷。感兴趣的同学可以深入了解。
本篇总结
本篇文章介绍了HTTP协议。从TCP/IP到HTTP以及HTTP进阶,层层递进,希望读者有些许收货。
此致,敬礼