上回就已经承诺过你们,必定会出 HTTP 的系列文章,今天终于整理完成了。做为一个 web 开发,HTTP 几乎是每天要打交道的东西,但我发现大部分人对 HTTP 只是浅尝辄止,对更多的细节及原理就了解不深了,在面试的时候感受很是吃力。这篇文章就是为了帮助你们树立完整的 HTTP 知识体系,并达到必定的深度,从容地应对各类灵魂之问,也同时提高本身做为一个 web 开发的专业素养吧。这是本文的思惟导图:javascript
对于 TCP 而言,在传输的时候分为两个部分:TCP头和数据部分。css
而 HTTP 相似,也是header + body
的结构,具体而言:html
起始行 + 头部 + 空行 + 实体
复制代码
因为 http 请求报文
和响应报文
是有必定区别,所以咱们分开介绍。前端
对于请求报文来讲,起始行相似下面这样:java
GET /home HTTP/1.1 复制代码
也就是方法 + 路径 + http版本。node
对于响应报文来讲,起始行通常张这个样:git
HTTP/1.1 200 OK 复制代码
响应报文的起始行也叫作状态行
。由http版本、状态码和缘由三部分组成。github
值得注意的是,在起始行中,每两个部分之间用空格隔开,最后一个部分后面应该接一个换行,严格遵循ABNF
语法规范。web
展现一下请求头和响应头在报文中的位置:面试
无论是请求头仍是响应头,其中的字段是至关多的,并且牵扯到http
很是多的特性,这里就不一一列举的,重点看看这些头部字段的格式:
_
:
很重要,用来区分开头部
和实体
。
问: 若是说在头部中间故意加一个空行会怎么样?
那么空行后的内容所有被视为实体。
就是具体的数据了,也就是body
部分。请求报文对应请求体
, 响应报文对应响应体
。
http/1.1
规定了如下请求方法(注意,都是大写):
首先最直观的是语义上的区别。
然后又有这样一些具体的差异:
GET
是幂等的,而POST
不是。(幂等
表示执行相同的操做,结果也是相同的)URI, 全称为(Uniform Resource Identifier), 也就是统一资源标识符,它的做用很简单,就是区分互联网上不一样的资源。
可是,它并非咱们常说的网址
, 网址指的是URL
, 实际上URI
包含了URN
和URL
两个部分,因为 URL 过于普及,就默认将 URI 视为 URL 了。
URI 真正最完整的结构是这样的。
可能你会有疑问,好像跟平时见到的不太同样啊!先别急,咱们来一一拆解。
scheme 表示协议名,好比http
, https
, file
等等。后面必须和://
连在一块儿。
user:passwd@ 表示登陆主机时的用户信息,不过很不安全,不推荐使用,也不经常使用。
host:port表示主机名和端口。
path表示请求路径,标记资源所在位置。
query表示查询参数,为key=val
这种形式,多个键值对之间用&
隔开。
fragment表示 URI 所定位的资源内的一个锚点,浏览器能够根据这个锚点跳转到对应的位置。
举个例子:
https://www.baidu.com/s?wd=HTTP&rsv_spt=1 复制代码
这个 URI 中,https
即scheme
部分,www.baidu.com
为host:port
部分(注意,http 和 https 的默认端口分别为80、443),/s
为path
部分,而wd=HTTP&rsv_spt=1
就是query
部分。
URI 只能使用ASCII
, ASCII 以外的字符是不支持显示的,并且还有一部分符号是界定符,若是不加以处理就会致使解析出错。
所以,URI 引入了编码
机制,将全部非 ASCII 码字符和界定符转为十六进制字节值,而后在前面加个%
。
如,空格被转义成了%20
,三元被转义成了%E4%B8%89%E5%85%83
。
RFC 规定 HTTP 的状态码为三位数,被分为五类:
接下来就一一分析这里面具体的状态码。
101 Switching Protocols。在HTTP
升级为WebSocket
的时候,若是服务器赞成变动,就会发送状态码 101。
200 OK是见得最多的成功状态码。一般在响应体中放有数据。
204 No Content含义与 200 相同,但响应头后没有 body 数据。
206 Partial Content顾名思义,表示部份内容,它的使用场景为 HTTP 分块下载和断点续传,固然也会带上相应的响应头字段Content-Range
。
301 Moved Permanently即永久重定向,对应着302 Found,即临时重定向。
好比你的网站从 HTTP 升级到了 HTTPS 了,之前的站点不再用了,应当返回301
,这个时候浏览器默认会作缓存优化,在第二次访问的时候自动访问重定向的那个地址。
而若是只是暂时不可用,那么直接返回302
便可,和301
不一样的是,浏览器并不会作缓存优化。
304 Not Modified: 当协商缓存命中时会返回这个状态码。详见浏览器缓存
400 Bad Request: 开发者常常看到一头雾水,只是笼统地提示了一下错误,并不知道哪里出错了。
403 Forbidden: 这实际上并非请求报文出错,而是服务器禁止访问,缘由有不少,好比法律禁止、信息敏感。
404 Not Found: 资源未找到,表示没在服务器上找到相应的资源。
405 Method Not Allowed: 请求方法不被服务器端容许。
406 Not Acceptable: 资源没法知足客户端的条件。
408 Request Timeout: 服务器等待了太长时间。
409 Conflict: 多个请求发生了冲突。
413 Request Entity Too Large: 请求体的数据过大。
414 Request-URI Too Long: 请求行里的 URI 太大。
429 Too Many Request: 客户端发送的请求过多。
431 Request Header Fields Too Large请求头的字段内容太大。
500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。
501 Not Implemented: 表示客户端请求的功能还不支持。
502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。
503 Service Unavailable: 表示服务器当前很忙,暂时没法响应服务。
HTTP 的特色归纳以下:
灵活可扩展,主要体如今两个方面。一个是语义上的自由,只规定了基本格式,好比空格分隔单词,换行分隔字段,其余的各个部分都没有严格的语法限制。另外一个是传输形式的多样性,不只仅能够传输文本,还能传输图片、视频等任意数据,很是方便。
可靠传输。HTTP 基于 TCP/IP,所以把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。
请求-应答。也就是一发一收
、有来有回
, 固然这个请求方和应答方不仅仅指客户端和服务器之间,若是某台服务器做为代理来链接后端的服务端,那么这台服务器也会扮演请求方的角色。
无状态。这里的状态是指通讯过程的上下文信息,而每次 http 请求都是独立、无关的,默认不须要保留状态信息。
所谓的优势和缺点仍是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态。
在须要长链接的场景中,须要保存大量的上下文信息,以避免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。
但与此同时,另一些应用仅仅只是为了获取一些数据,不须要保存链接上下文信息,无状态反而减小了网络开销,成为了 http 的优势。
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
这固然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱
就是利用 HTTP 明文传输的缺点,诱导你连上热点,而后疯狂抓你全部的流量,从而拿到你的敏感信息。
当 http 开启长链接时,共用一个 TCP 链接,同一时刻只能处理一个请求,那么当前请求耗时过长的状况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。接下来会有一小节讨论这个问题。
对于Accept
系列字段的介绍分为四个部分: 数据格式、压缩方式、支持语言和字符集。
上一节谈到 HTTP 灵活的特性,它支持很是多的数据格式,那么这么多格式的数据一块儿到达客户端,客户端怎么知道它的格式呢?
固然,最低效的方式是直接猜,有没有更好的方式呢?直接指定能够吗?
答案是确定的。不过首先须要介绍一个标准——MIME(Multipurpose Internet Mail Extensions, 多用途互联网邮件扩展)。它首先用在电子邮件系统中,让邮件能够发任意类型的数据,这对于 HTTP 来讲也是通用的。
所以,HTTP 从MIME type取了一部分来标记报文 body 部分的数据类型,这些类型体如今Content-Type
这个字段,固然这是针对于发送端而言,接收端想要收到特定类型的数据,也能够用Accept
字段。
具体而言,这两个字段的取值能够分为下面几类:
固然通常这些数据都是会进行编码压缩的,采起什么样的压缩方式就体如今了发送方的Content-Encoding
字段上, 一样的,接收什么样的压缩方式体如今了接受方的Accept-Encoding
字段上。这个字段的取值有下面几种:
// 发送端 Content-Encoding: gzip // 接收端 Accept-Encoding: gzip 复制代码
对于发送方而言,还有一个Content-Language
字段,在须要实现国际化的方案当中,能够用来指定支持的语言,在接受方对应的字段为Accept-Language
。如:
// 发送端 Content-Language: zh-CN, zh, en // 接收端 Accept-Language: zh-CN, zh, en 复制代码
最后是一个比较特殊的字段, 在接收端对应为Accept-Charset
,指定能够接受的字符集,而在发送端并无对应的Content-Charset
, 而是直接放在了Content-Type
中,以charset属性指定。如:
// 发送端 Content-Type: text/html; charset=utf-8 // 接收端 Accept-Charset: charset=utf-8 复制代码
最后以一张图来总结一下吧:
对于定长包体而言,发送端在传输的时候通常会带上 Content-Length
, 来指明包体的长度。
咱们用一个nodejs
服务器来模拟一下:
const http = require('http'); const server = http.createServer(); server.on('request', (req, res) => { if(req.url === '/') { res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', 10); res.write("helloworld"); } }) server.listen(8081, () => { console.log("成功启动"); }) 复制代码
启动后访问: localhost:8081。
浏览器中显示以下:
helloworld
复制代码
这是长度正确的状况,那不正确的状况是如何处理的呢?
咱们试着把这个长度设置的小一些:
res.setHeader('Content-Length', 8); 复制代码
重启服务,再次访问,如今浏览器中内容以下:
hellowor
复制代码
那后面的ld
哪里去了呢?实际上在 http 的响应体中直接被截去了。
而后咱们试着将这个长度设置得大一些:
res.setHeader('Content-Length', 12); 复制代码
此时浏览器显示以下:
直接没法显示了。能够看到Content-Length
对于 http 传输过程起到了十分关键的做用,若是设置不当能够直接致使传输失败。
上述是针对于定长包体
,那么对于不定长包体
而言是如何传输的呢?
这里就必须介绍另一个 http 头部字段了:
Transfer-Encoding: chunked
复制代码
表示分块传输数据,设置这个字段后会自动产生两个效果:
咱们依然以一个实际的例子来模拟分块传输,nodejs 程序以下:
const http = require('http'); const server = http.createServer(); server.on('request', (req, res) => { if(req.url === '/') { res.setHeader('Content-Type', 'text/html; charset=utf8'); res.setHeader('Content-Length', 10); res.setHeader('Transfer-Encoding', 'chunked'); res.write("<p>来啦</p>"); setTimeout(() => { res.write("第一次传输<br/>"); }, 1000); setTimeout(() => { res.write("第二次传输"); res.end() }, 2000); } }) server.listen(8009, () => { console.log("成功启动"); }) 复制代码
访问效果入下:
用 telnet 抓到的响应以下:
注意,Connection: keep-alive
及以前的为响应行和响应头,后面的内容为响应体,这两部分用换行符隔开。
响应体的结构比较有意思,以下所示:
chunk长度(16进制的数)
第一个chunk的内容
chunk长度(16进制的数)
第二个chunk的内容
......
0
复制代码
最后是留有有一个空行
的,这一点请你们注意。
以上即是 http 对于定长数据和不定长数据的传输方式。
对于几百 M 甚至上 G 的大文件来讲,若是要一口气所有传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。所以,HTTP 针对这一场景,采起了范围请求
的解决方案,容许客户端仅仅请求一个资源的一部分。
固然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:
Accept-Ranges: none
复制代码
用来告知客户端这边是支持范围请求的。
而对于客户端而言,它须要指定请求哪一部分,经过Range
这个请求头字段肯定,格式为bytes=x-y
。接下来就来讨论一下这个 Range 的书写格式:
服务器收到请求以后,首先验证范围是否合法,若是越界了那么返回416
错误码,不然读取相应片断,返回206
状态码。
同时,服务器须要添加Content-Range
字段,这个字段的格式根据请求头中Range
字段的不一样而有所差别。
具体来讲,请求单段数据
和请求多段数据
,响应头是不同的。
举个例子:
// 单段数据 Range: bytes=0-9 // 多段数据 Range: bytes=0-9, 30-39 复制代码
接下来咱们就分别来讨论着两种状况。
对于单段数据
的请求,返回的响应以下:
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100
i am xxxxx
复制代码
值得注意的是Content-Range
字段,0-9
表示请求的返回,100
表示资源的总大小,很好理解。
接下来咱们看看多段请求的状况。获得的响应会是下面这个形式:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96
i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96
eex jspy e
--00000010101--
复制代码
这个时候出现了一个很是关键的字段Content-Type: multipart/byteranges;boundary=00000010101
,它表明了信息量是这样的:
所以,在响应体中各段数据之间会由这里指定的分隔符分开,并且在最后的分隔末尾添上--
表示结束。
以上就是 http 针对大文件传输所采用的手段。
在 http 中,有两种主要的表单提交的方式,体如今两种不一样的Content-Type
取值:
因为表单提交通常是POST
请求,不多考虑GET
,所以这里咱们将默认提交的数据放在请求体中。
对于application/x-www-form-urlencoded
格式的表单内容,有如下特色:
&
分隔的键值对如:
// 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 以下(最终形式) "a%3D1%26b%3D2" 复制代码
对于multipart/form-data
而言:
Content-Type
字段会包含boundary
,且boundary
的值有浏览器默认指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe
。Content-Type
,在最后的分隔符会加上--
表示结束。相应的请求体
是下面这样:
Content-Disposition: form-data;name="data1"; Content-Type: text/plain data1 ----WebkitFormBoundaryRRJKeWfHPGrS4LKe Content-Disposition: form-data;name="data2"; Content-Type: text/plain data2 ----WebkitFormBoundaryRRJKeWfHPGrS4LKe-- 复制代码
值得一提的是,multipart/form-data
格式最大的特色在于:每个表单元素都是独立的资源表述。另外,你可能在写业务的过程当中,并无注意到其中还有boundary
的存在,若是你打开抓包工具,确实能够看到不一样的表单元素被拆分开了,之因此在平时感受不到,是觉得浏览器和 HTTP 给你封装了这一系列操做。
并且,在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data
而不用application/x-www-form-urlencoded
,由于没有必要作 URL 编码,带来巨大耗时的同时也占用了更多的空间。
从前面的小节能够知道,HTTP 传输是基于请求-应答
的模式进行的,报文必须是一发一收,但值得注意的是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是著名的HTTP队头阻塞
问题。
对于一个域名容许分配多个长链接,那么至关于增长了任务队列,不至于一个队伍的任务阻塞其它全部任务。在RFC2616规定过客户端最多并发 2 个链接,不过事实上在如今的浏览器标准中,这个上限要多不少,Chrome 中是 6 个。
但其实,即便是提升了并发链接,仍是不能知足人们对性能的需求。
一个域名不是能够并发 6 个长链接吗?那我就多分几个域名。
好比 content1.sanyuan.com 、content2.sanyuan.com。
这样一个sanyuan.com
域名下能够分出很是多的二级域名,而它们都指向一样的一台服务器,可以并发的长链接数更多了,事实上也更好地解决了队头阻塞的问题。
前面说到了 HTTP 是一个无状态的协议,每次 http 请求都是独立、无关的,默认不须要保留状态信息。但有时候须要保存一些状态,怎么办呢?
HTTP 为此引入了 Cookie。Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面板的Application这一栏能够看到)。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。而服务端能够经过响应头中的Set-Cookie
字段来对客户端写入Cookie
。举例以下:
// 请求头 Cookie: a=xxx;b=xxx // 响应头 Set-Cookie: a=xxx set-Cookie: b=xxx 复制代码
Cookie 的有效期能够经过Expires和Max-Age两个属性来设置。
过时时间
若 Cookie 过时,则这个 Cookie 会被删除,并不会发送给服务端。
关于做用域也有两个属性: Domain和path, 给 Cookie 绑定了域名和路径,在发送请求以前,发现域名或者路径和这两个属性不匹配,那么就不会带上 Cookie。值得注意的是,对于路径来讲,/
表示域名下的任意路径都容许使用 Cookie。
若是带上Secure
,说明只能经过 HTTPS 传输 cookie。
若是 cookie 字段带上HttpOnly
,那么说明只能经过 HTTP 协议传输,不能经过 JS 访问,这也是预防 XSS 攻击的重要手段。
相应的,对于 CSRF 攻击的预防,也有SameSite
属性。
SameSite
能够设置为三个值,Strict
、Lax
和None
。
a. 在Strict
模式下,浏览器彻底禁止第三方请求携带Cookie。好比请求sanyuan.com
网站只能在sanyuan.com
域名当中请求才能携带 Cookie,在其余网站请求都不能。
b. 在Lax
模式,就宽松一点了,可是只能在 get 方法提交表单
况或者a 标签发送 get 请求
的状况下能够携带 Cookie,其余状况均不能。
c. 在None
模式下,也就是默认模式,请求会自动携带上 Cookie。
容量缺陷。Cookie 的体积上限只有4KB
,只能用来存储少许的信息。
性能缺陷。Cookie 紧跟域名,无论域名下面的某一个地址需不须要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会形成巨大的性能浪费的,由于请求携带了不少没必要要的内容。但能够经过Domain
和Path
指定做用域来解决。
安全缺陷。因为 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,而后进行一系列的篡改,在 Cookie 的有效期内从新发送给服务器,这是至关危险的。另外,在HttpOnly
为 false 的状况下,Cookie 信息能直接经过 JS 脚原本读取。
咱们知道在 HTTP 是基于请求-响应
模型的协议,通常由客户端发请求,服务器来进行响应。
固然,也有特殊状况,就是代理服务器的状况。引入代理以后,做为代理的服务器至关于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具备双重身份。
那代理服务器究竟是用来作什么的呢?
负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP 都是多少,客户端是不知道的。所以,这个代理服务器能够拿到这个请求以后,能够经过特定的算法分发给不一样的源服务器,让各台源服务器的负载尽可能平均。固然,这样的算法有不少,包括随机算法、轮询、一致性hash、LRU(最近最少使用)
等等,不过这些算法并非本文的重点,你们有兴趣本身能够研究一下。
保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。而且对于上下行的数据进行过滤,对非法 IP 限流,这些都是代理服务器的工做。
缓存代理。将内容缓存到代理服务器,使得客户端能够直接从代理服务器得到而不用到源服务器那里。下一节详细拆解。
代理服务器须要标明本身的身份,在 HTTP 传输中留下本身的痕迹,怎么办呢?
经过Via
字段来记录。举个例子,如今中间有两台代理服务器,在客户端发送请求后会经历这样一个过程:
客户端 -> 代理1 -> 代理2 -> 源服务器
复制代码
在源服务器收到请求后,会在请求头
拿到这个字段:
Via: proxy_server1, proxy_server2
复制代码
而源服务器响应时,最终在客户端会拿到这样的响应头
:
Via: proxy_server2, proxy_server1
复制代码
能够看到,Via
中代理的顺序即为在 HTTP 传输中报文传达的顺序。
字面意思就是为谁转发
, 它记录的是请求方的IP
地址(注意,和Via
区分开,X-Forwarded-For
记录的是请求方这一个IP)。
是一种获取用户真实 IP 的字段,无论中间通过多少代理,这个字段始终记录最初的客户端的IP。
相应的,还有X-Forwarded-Host
和X-Forwarded-Proto
,分别记录客户端(注意哦,不包括代理)的域名
和协议名
。
前面能够看到,X-Forwarded-For
这个字段记录的是请求方的 IP,这意味着每通过一个不一样的代理,这个字段的名字都要变,从客户端
到代理1
,这个字段是客户端的 IP,从代理1
到代理2
,这个字段就变为了代理1的 IP。
可是这会产生两个问题:
意味着代理必须解析 HTTP 请求头,而后修改,比直接转发数据性能降低。
在 HTTPS 通讯加密的过程当中,原始报文是不容许修改的。
由此产生了代理协议
,通常使用明文版本,只须要在 HTTP 请求行上面加上这样格式的文本便可:
// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口 PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222 GET / HTTP/1.1 ... 复制代码
这样就能够解决X-Forwarded-For
带来的问题了。
关于强缓存
和协商缓存
的内容,我已经在能不能说一说浏览器缓存作了详细分析,小结以下:
首先经过 Cache-Control
验证强缓存是否可用
If-Modified-Since
或者If-None-Match
这些条件请求字段检查资源是否更新
这一节咱们主要来讲说另一种缓存方式: 代理缓存。
对于源服务器来讲,它也是有缓存的,好比Redis, Memcache,但对于 HTTP 缓存来讲,若是每次客户端缓存失效都要到源服务器获取,那给源服务器的压力是很大的。
由此引入了缓存代理的机制。让代理服务器
接管一部分的服务端HTTP缓存,客户端缓存过时后就近到代理缓存中获取,代理缓存过时了才请求源服务器,这样流量巨大的时候能明显下降源服务器的压力。
那缓存代理到底是如何作到的呢?
总的来讲,缓存代理的控制分为两部分,一部分是源服务器端的控制,一部分是客户端的控制。
在源服务器的响应头中,会加上Cache-Control
这个字段进行缓存控制字段,那么它的值当中能够加入private
或者public
表示是否容许代理服务器缓存,前者禁止,后者为容许。
好比对于一些很是私密的数据,若是缓存到代理服务器,别人直接访问代理就能够拿到这些数据,是很是危险的,所以对于这些数据通常是不会容许代理服务器进行缓存的,将响应头部的Cache-Control
设为private
,而不是public
。
must-revalidate
的意思是客户端缓存过时就去源服务器获取,而proxy-revalidate
则表示代理服务器的缓存过时后到源服务器获取。
s
是share
的意思,限定了缓存在代理服务器中能够存放多久,和限制客户端缓存时间的max-age
并不冲突。
讲了这几个字段,咱们不妨来举个小例子,源服务器在响应头中加入这样一个字段:
Cache-Control: public, max-age=1000, s-maxage=2000
复制代码
至关于源服务器说: 我这个响应是容许代理服务器缓存的,客户端缓存过时了到代理中拿,而且在客户端的缓存时间为 1000 秒,在代理服务器中的缓存时间为 2000 s。
在客户端的请求头中,能够加入这两个字段,来对代理服务器上的缓存进行宽容和限制操做。好比:
max-stale: 5
复制代码
表示客户端到代理服务器上拿缓存的时候,即便代理缓存过时了也没关系,只要过时时间在5秒以内,仍是能够从代理中获取的。
又好比:
min-fresh: 5
复制代码
表示代理缓存须要必定的新鲜度,不要等到缓存恰好到期再拿,必定要在到期前 5 秒以前的时间拿,不然拿不到。
这个字段加上后表示客户端只会接受代理缓存,而不会接受源服务器的响应。若是代理缓存无效,则直接返回504(Gateway Timeout)
。
以上即是缓存代理的内容,涉及的字段比较多,但愿能好好回顾一下,加深理解。
在先后端分离的开发模式中,常常会遇到跨域问题,即 Ajax 请求发出去了,服务器也成功响应了,前端就是拿不到这个响应。接下来咱们就来好好讨论一下这个问题。
回顾一下 URI 的组成:
浏览器遵循同源政策(scheme(协议)
、host(主机)
和port(端口)
都相同则为同源
)。非同源站点有这样一些限制:
当浏览器向目标 URI 发 Ajax 请求时,只要当前 URL 和目标 URL 不一样源,则产生跨域,被称为跨域请求
。
跨域请求的响应通常会被浏览器所拦截,注意,是被浏览器拦截,响应实际上是成功到达客户端了。那这个拦截是如何发生呢?
首先要知道的是,浏览器是多进程的,以 Chrome 为例,进程组成以下:
WebKit 渲染引擎和V8 引擎都在渲染进程当中。
当xhr.send
被调用,即 Ajax 请求准备发送的时候,其实还只是在渲染进程的处理。为了防止黑客经过脚本触碰到系统资源,浏览器将每个渲染进程装进了沙箱,而且为了防止 CPU 芯片一直存在的Spectre 和 Meltdown漏洞,采起了站点隔离
的手段,给每个不一样的站点(一级域名不一样)分配了沙箱,互不干扰。具体见YouTube上Chromium安全团队的演讲视频。
在沙箱当中的渲染进程是没有办法发送网络请求的,那怎么办?只能经过网络进程来发送。那这样就涉及到进程间通讯(IPC,Inter Process Communication)了。接下来咱们看看 chromium 当中进程间通讯是如何完成的,在 chromium 源码中调用顺序以下:
可能看了你会比较懵,若是想深刻了解能够去看看 chromium 最新的源代码,IPC源码地址及Chromium IPC源码解析文章。
总的来讲就是利用Unix Domain Socket
套接字,配合事件驱动的高性能网络并发库libevent
完成进程的 IPC 过程。
好,如今数据传递给了浏览器主进程,主进程接收到后,才真正地发出相应的网络请求。
在服务端处理完数据后,将响应返回,主进程检查到跨域,且没有cors(后面会详细说)响应头,将响应体所有丢掉,并不会发送给渲染进程。这就达到了拦截数据的目的。
接下来咱们来讲一说解决跨域问题的几种方案。
CORS 实际上是 W3C 的一个标准,全称是跨域资源共享
。它须要浏览器和服务器的共同支持,具体来讲,非 IE 和 IE10 以上支持CORS,服务器须要附加特定的响应头,后面具体拆解。不过在弄清楚 CORS 的原理以前,咱们须要清楚两个概念: 简单请求和非简单请求。
浏览器根据请求方法和请求头的特定字段,将请求作了一下分类,具体来讲规则是这样,凡是知足下面条件的属于简单请求:
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)浏览器画了这样一个圈,在这个圈里面的就是简单请求, 圈外面的就是非简单请求,而后针对这两种不一样的请求进行不一样的处理。
请求发出去以前,浏览器作了什么?
它会自动在请求头当中,添加一个Origin
字段,用来讲明请求来自哪一个源
。服务器拿到请求以后,在回应时对应地添加Access-Control-Allow-Origin
字段,若是Origin
不在这个字段的范围中,那么浏览器就会将响应拦截。
所以,Access-Control-Allow-Origin
字段是服务器用来决定浏览器是否拦截这个响应,这是必需的字段。与此同时,其它一些可选的功能性的字段,用来描述若是不会拦截,这些字段将会发挥各自的做用。
Access-Control-Allow-Credentials。这个字段是一个布尔值,表示是否容许发送 Cookie,对于跨域请求,浏览器对这个字段默认值设为 false,而若是须要拿到浏览器的 Cookie,须要添加这个响应头并设为true
, 而且在前端也须要设置withCredentials
属性:
let xhr = new XMLHttpRequest(); xhr.withCredentials = true; 复制代码
Access-Control-Expose-Headers。这个字段是给 XMLHttpRequest 对象赋能,让它不只能够拿到基本的 6 个响应头字段(包括Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
和Pragma
), 还能拿到这个字段声明的响应头字段。好比这样设置:
Access-Control-Expose-Headers: aaa
复制代码
那么在前端能够经过 XMLHttpRequest.getResponseHeader('aaa')
拿到 aaa
这个字段的值。
非简单请求相对而言会有些不一样,体如今两个方面: 预检请求和响应字段。
咱们以 PUT 方法为例。
var url = 'http://xxx.com'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'xxx'); xhr.send(); 复制代码
当这段代码执行后,首先会发送预检请求。这个预检请求的请求行和请求体是下面这个格式:
OPTIONS / HTTP/1.1
Origin: 当前地址
Host: xxx.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
复制代码
预检请求的方法是OPTIONS
,同时会加上Origin
源地址和Host
目标地址,这很简单。同时也会加上两个关键的字段:
这是预检请求
。接下来是响应字段,响应字段也分为两部分,一部分是对于预检请求的响应,一部分是对于 CORS 请求的响应。
预检请求的响应。以下面的格式:
HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 复制代码
其中有这样几个关键的响应头字段:
*
表示容许任意源请求。在预检请求的响应返回后,若是请求不知足响应头的条件,则触发XMLHttpRequest
的onerror
方法,固然后面真正的CORS请求也不会发出去了。
CORS 请求的响应。绕了这么一大转,到了真正的 CORS 请求就容易多了,如今它和简单请求的状况是同样的。浏览器自动加上Origin
字段,服务端响应头返回Access-Control-Allow-Origin。能够参考以上简单请求部分的内容。
虽然XMLHttpRequest
对象遵循同源政策,可是script
标签不同,它能够经过 src 填上目标地址从而发出 GET 请求,实现跨域请求并拿到响应。这也就是 JSONP 的原理,接下来咱们就来封装一个 JSONP:
const jsonp = ({ url, params, callbackName }) => { const generateURL = () => { let dataStr = ''; for(let key in params) { dataStr += `${key}=${params[key]}&`; } dataStr += `callback=${callbackName}`; return `${url}?${dataStr}`; }; return new Promise((resolve, reject) => { // 初始化回调函数名称 callbackName = callbackName || Math.random().toString.replace(',', ''); // 建立 script 元素并加入到当前文档中 let scriptEle = document.createElement('script'); scriptEle.src = generateURL(); document.body.appendChild(scriptEle); // 绑定到 window 上,为了后面调用 window[callbackName] = (data) => { resolve(data); // script 执行完了,成为无用元素,须要清除 document.body.removeChild(scriptEle); } }); } 复制代码
固然在服务端也会有响应的操做, 以 express 为例:
let express = require('express') let app = express() app.get('/', function(req, res) { let { a, b, callback } = req.query console.log(a); // 1 console.log(b); // 2 // 注意哦,返回给script标签,浏览器直接把这部分字符串执行 res.end(`${callback}('数据包')`); }) app.listen(3000) 复制代码
前端这样简单地调用一下就行了:
jsonp({ url: 'http://localhost:3000', params: { a: 1, b: 2 } }).then(data => { // 拿到数据进行处理 console.log(data); // 数据包 }) 复制代码
和CORS
相比,JSONP 最大的优点在于兼容性好,IE 低版本不能使用 CORS 但能够使用 JSONP,缺点也很明显,请求方法单一,只支持 GET 请求。
Nginx 是一种高性能的反向代理
服务器,能够用来轻松解决跨域问题。
what?反向代理?我给你看一张图你就懂了。
正向代理帮助客户端访问客户端本身访问不到的服务器,而后将结果返回给客户端。
反向代理拿到客户端的请求,将请求转发给其余的服务器,主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,而后选择一个合适的服务器,将请求转交给它。
所以,二者的区别就很明显了,正向代理服务器是帮客户端作事情,而反向代理服务器是帮其它的服务器作事情。
好了,那 Nginx 是如何来解决跨域的呢?
好比说如今客户端的域名为client.com,服务器的域名为server.com,客户端向服务器发送 Ajax 请求,固然会跨域了,那这个时候让 Nginx 登场了,经过下面这个配置:
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
复制代码
Nginx 至关于起了一个跳板机,这个跳板机的域名也是client.com
,让客户端首先访问 client.com/api
,这固然没有跨域,而后 Nginx 服务器做为反向代理,将请求转发给server.com
,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。
其实还有一些不太经常使用的方式,你们了解便可,好比postMessage
,固然WebSocket
也是一种方式,可是已经不属于 HTTP 的范畴,另一些奇技淫巧就不建议你们去死记硬背了,一方面历来不用,名字都可贵记住,另外一方面临时背下来,面试官也不会对你印象加分,由于看得出来是背的。固然没有背并不表明减分,把跨域原理和前面三种主要的跨域方式理解清楚,经得起更深一步的推敲,反而会让别人以为你是一个靠谱的人。
以前谈到了 HTTP 是明文传输的协议,传输保文对外彻底透明,很是不安全,那如何进一步保证安全性呢?
由此产生了 HTTPS
,其实它并非一个新的协议,而是在 HTTP 下面增长了一层 SSL/TLS 协议,简单的讲,HTTPS = HTTP + SSL/TLS。
那什么是 SSL/TLS 呢?
SSL 即安全套接层(Secure Sockets Layer),在 OSI 七层模型中处于会话层(第 5 层)。以前 SSL 出过三个大版本,当它发展到第三个大版本的时候才被标准化,成为 TLS(传输层安全,Transport Layer Security),并被当作 TLS1.0 的版本,准确地说,TLS1.0 = SSL3.1。
如今主流的版本是 TLS/1.2, 以前的 TLS1.0、TLS1.1 都被认为是不安全的,在不久的未来会被彻底淘汰。所以咱们接下来主要讨论的是 TLS1.2, 固然在 2018 年推出了更加优秀的 TLS1.3,大大优化了 TLS 握手过程,这个咱们放在下一节再去说。
TLS 握手的过程比较复杂,写文章以前我查阅了大量的资料,发现对 TLS 初学者很是不友好,也有不少知识点说的含糊不清,能够说这个整理的过程是至关痛苦了。但愿我下面的拆解可以帮你理解得更顺畅些吧 : )
先来讲说传统的 TLS 握手,也是你们在网上常常看到的。我以前也写过这样的文章,(传统RSA版本)HTTPS为何让数据传输更安全,其中也介绍到了对称加密
和非对称加密
的概念,建议你们去读一读,再也不赘述。之因此称它为 RSA 版本,是由于它在加解密pre_random
的时候采用的是 RSA 算法。
如今咱们来说讲主流的 TLS 1.2 版本所采用的方式。
刚开始你可能会比较懵,先别着急,过一遍下面的流程再来看会豁然开朗。
首先,浏览器发送 client_random、TLS版本、加密套件列表。
client_random 是什么?用来最终 secret 的一个参数。
加密套件列表是什么?我举个例子,加密套件列表通常张这样:
TLS_ECDHE_WITH_AES_128_GCM_SHA256
复制代码
意思是TLS
握手过程当中,使用ECDHE
算法生成pre_random
(这个数后面会介绍),128位的AES
算法进行对称加密,在对称加密的过程当中使用主流的GCM
分组模式,由于对称加密中很重要的一个问题就是如何分组。最后一个是哈希摘要算法,采用SHA256
算法。
其中值得解释一下的是这个哈希摘要算法,试想一个这样的场景,服务端如今给客户端发消息来了,客户端并不知道此时的消息究竟是服务端发的,仍是中间人伪造的消息呢?如今引入这个哈希摘要算法,将服务端的证书信息经过这个算法生成一个摘要(能够理解为比较短的字符串
),用来标识这个服务端的身份,用私钥加密后把加密后的标识和本身的公钥传给客户端。客户端拿到这个公钥来解密,生成另一份摘要。两个摘要进行对比,若是相同则能确认服务端的身份。这也就是所谓数字签名的原理。其中除了哈希算法,最重要的过程是私钥加密,公钥解密。
能够看到服务器一口气给客户端回复了很是多的内容。
server_random
也是最后生成secret
的一个参数, 同时确认 TLS 版本、须要使用的加密套件和本身的证书,这都不难理解。那剩下的server_params
是干吗的呢?
咱们先埋个伏笔,如今你只须要知道,server_random
到达了客户端。
客户端验证服务端传来的证书
和签名
是否经过,若是验证经过,则传递client_params
这个参数给服务器。
接着客户端经过ECDHE
算法计算出pre_random
,其中传入两个参数:server_params和client_params。如今你应该清楚这个两个参数的做用了吧,因为ECDHE
基于椭圆曲线离散对数
,这两个参数也称做椭圆曲线的公钥
。
客户端如今拥有了client_random
、server_random
和pre_random
,接下来将这三个数经过一个伪随机数函数来计算出最终的secret
。
刚刚客户端不是传了client_params
过来了吗?
如今服务端开始用ECDHE
算法生成pre_random
,接着用和客户端一样的伪随机数函数生成最后的secret
。
TLS的过程基本上讲完了,但还有两点须要注意。
第一、实际上 TLS 握手是一个双向认证的过程,从 step1 中能够看到,客户端有能力验证服务器的身份,那服务器能不能验证客户端的身份呢?
固然是能够的。具体来讲,在 step3
中,客户端传送client_params
,实际上给服务器传一个验证消息,让服务器将相同的验证流程(哈希摘要 + 私钥加密 + 公钥解密)走一遍,确认客户端的身份。
第二、当客户端生成secret
后,会给服务端发送一个收尾的消息,告诉服务器以后的都用对称加密,对称加密的算法就用第一次约定的。服务器生成完secret
也会向客户端发送一个收尾的消息,告诉客户端之后就直接用对称加密来通讯。
这个收尾的消息包括两部分,一部分是Change Cipher Spec
,意味着后面加密传输了,另外一个是Finished
消息,这个消息是对以前全部发送的数据作的摘要,对摘要进行加密,让对方验证一下。
当双方都验证经过以后,握手才正式结束。后面的 HTTP 正式开始传输加密报文。
ECDHE 握手,也就是主流的 TLS1.2 握手中,使用ECDHE
实现pre_random
的加密解密,没有用到 RSA。
使用 ECDHE 还有一个特色,就是客户端发送完收尾消息后能够提早抢跑
,直接发送 HTTP 报文,节省了一个 RTT,没必要等到收尾消息到达服务器,而后等服务器返回收尾消息给本身,直接开始发请求。这也叫TLS False Start
。
TLS 1.2 虽然存在了 10 多年,经历了无数的考验,但历史的车轮老是不断向前的,为了得到更强的安全、更优秀的性能,在2018年
就推出了 TLS1.3,对于TLS1.2
作了一系列的改进,主要分为这几个部分:强化安全、提升性能。
在 TLS1.3 中废除了很是多的加密算法,最后只保留五个加密套件:
能够看到,最后剩下的对称加密算法只有 AES 和 CHACHA20,以前主流的也会这两种。分组模式也只剩下 GCM 和 POLY1305, 哈希摘要算法只剩下了 SHA256 和 SHA384 了。
那你可能会问了, 以前RSA
这么重要的非对称加密算法怎么不在了?
我以为有两方面的缘由:
第一、2015年发现了FREAK
攻击,即已经有人发现了 RSA 的漏洞,可以进行破解了。
第二、一旦私钥泄露,那么中间人能够经过私钥计算出以前全部报文的secret
,破解以前全部的密文。
为何?回到 RSA 握手的过程当中,客户端拿到服务器的证书后,提取出服务器的公钥,而后生成pre_random
并用公钥加密传给服务器,服务器经过私钥解密,从而拿到真实的pre_random
。当中间人拿到了服务器私钥,而且截获以前全部报文的时候,那么就能拿到pre_random
、server_random
和client_random
并根据对应的随机数函数生成secret
,也就是拿到了 TLS 最终的会话密钥,每个历史报文都能经过这样的方式进行破解。
但ECDHE
在每次握手时都会生成临时的密钥对,即便私钥被破解,以前的历史消息并不会收到影响。这种一次破解并不影响历史信息的性质也叫前向安全性。
RSA
算法不具有前向安全性,而 ECDHE
具有,所以在 TLS1.3 中完全取代了RSA
。
流程以下:
大致的方式和 TLS1.2 差很少,不过和 TLS 1.2 相比少了一个 RTT, 服务端没必要等待对方验证证书以后才拿到client_params
,而是直接在第一次握手的时候就可以拿到, 拿到以后当即计算secret
,节省了以前没必要要的等待时间。同时,这也意味着在第一次握手的时候客户端须要传送更多的信息,一口气给传完。
这种 TLS 1.3 握手方式也被叫作1-RTT握手。但其实这种1-RTT
的握手方式仍是有一些优化的空间的,接下来咱们来一一介绍这些优化方式。
会话复用有两种方式: Session ID和Session Ticket。
先说说最先出现的Seesion ID,具体作法是客户端和服务器首次链接后各自保存会话的 ID,并存储会话密钥,当再次链接时,客户端发送ID
过来,服务器查找这个 ID 是否存在,若是找到了就直接复用以前的会话状态,会话密钥不用从新生成,直接用原来的那份。
但这种方式也存在一个弊端,就是当客户端数量庞大的时候,对服务端的存储压力很是大。
于是出现了第二种方式——Session Ticket。它的思路就是: 服务端的压力大,那就把压力分摊给客户端呗。具体来讲,双方链接成功后,服务器加密会话信息,用Session Ticket消息发给客户端,让客户端保存下来。下次重连的时候,就把这个 Ticket 进行解密,验证它过没过时,若是没过时那就直接恢复以前的会话状态。
这种方式虽然减少了服务端的存储压力,但与带来了安全问题,即每次用一个固定的密钥来解密 Ticket 数据,一旦黑客拿到这个密钥,以前全部的历史记录也被破解了。所以为了尽可能避免这样的问题,密钥须要按期进行更换。
总的来讲,这些会话复用的技术在保证1-RTT
的同时,也节省了生成会话密钥这些算法所消耗的时间,是一笔可观的性能提高。
刚刚说的都是1-RTT
状况下的优化,那能不能优化到0-RTT
呢?
答案是能够的。作法其实也很简单,在发送Session Ticket的同时带上应用数据,不用等到服务端确认,这种方式被称为Pre-Shared Key
,即 PSK。
这种方式虽然方便,但也带来了安全问题。中间人截获PSK
的数据,不断向服务器重复发,相似于 TCP 第一次握手携带数据,增长了服务器被攻击的风险。
TLS1.3 在 TLS1.2 的基础上废除了大量的算法,提高了安全性。同时利用会话复用节省了从新生成密钥的时间,利用 PSK 作到了0-RTT
链接。
因为 HTTPS 在安全方面已经作的很是好了,HTTP 改进的关注点放在了性能方面。对于 HTTP/2 而言,它对于性能的提高主要在于两点:
固然还有一些颠覆性的功能实现:
这些重大的提高本质上也是为了解决 HTTP 自己的问题而产生的。接下来咱们来看看 HTTP/2 解决了哪些问题,以及解决方式具体是如何的。
在 HTTP/1.1 及以前的时代,请求体通常会有响应的压缩编码过程,经过Content-Encoding
头部字段来指定,但你有没有想过头部字段自己的压缩呢?当请求字段很是复杂的时候,尤为对于 GET 请求,请求报文几乎全是请求头,这个时候仍是存在很是大的优化空间的。HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK,对请求头进行压缩。
HPACK 算法是专门为 HTTP/2 服务的,它主要的亮点有两个:
HTTP/2 当中废除了起始行的概念,将起始行中的请求方法、URI、状态码转换成了头字段,不过这些字段都有一个":"前缀,用来和其它请求头区分开。
咱们以前讨论了 HTTP 队头阻塞的问题,其根本缘由在于HTTP 基于请求-响应
的模型,在同一个 TCP 长链接中,前面的请求没有获得响应,后面的请求就会被阻塞。
后面咱们又讨论到用并发链接和域名分片的方式来解决这个问题,但这并无真正从 HTTP 自己的层面解决问题,只是增长了 TCP 链接,分摊风险而已。并且这么作也有弊端,多条 TCP 链接会竞争有限的带宽,让真正优先级高的请求不能优先处理。
而 HTTP/2 便从 HTTP 协议自己解决了队头阻塞
问题。注意,这里并非指的TCP队头阻塞
,而是HTTP队头阻塞
,二者并非一回事。TCP 的队头阻塞是在数据包
层面,单位是数据包
,前一个报文没有收到便不会将后面收到的报文上传给 HTTP,而HTTP 的队头阻塞是在 HTTP 请求-响应
层面,前一个请求没处理完,后面的请求就要阻塞住。二者所在的层次不同。
那么 HTTP/2 如何来解决所谓的队头阻塞呢?
首先,HTTP/2 认为明文传输对机器而言太麻烦了,不方便计算机的解析,由于对于文本而言会有多义性的字符,好比回车换行究竟是内容仍是分隔符,在内部须要用到状态机去识别,效率比较低。因而 HTTP/2 干脆把报文所有换成二进制格式,所有传输01
串,方便了机器的解析。
原来Headers + Body
的报文格式现在被拆分红了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧以后,服务器看到的再也不是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在前后关系,所以也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。
通讯双方均可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫作流
(Stream)。HTTP/2 用流
来在一个 TCP 链接上来进行多个数据帧的通讯,这就是多路复用的概念。
可能你会有一个疑问,既然是乱序首发,那最后如何来处理这些乱序的数据帧呢?
首先要声明的是,所谓的乱序,指的是不一样 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧必定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文和响应报文。固然,在二进制帧当中还有其余的一些字段,实现了优先级和流量控制等功能,咱们放到下一节再来介绍。
另外值得一说的是 HTTP/2 的服务器推送(Server Push)。在 HTTP/2 当中,服务器已经再也不是彻底被动地接收请求,响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 链接创建以后,好比浏览器请求一个 HTML 文件,服务器就能够在返回 HTML 的基础上,将 HTML 中引用到的其余资源文件一块儿返回给客户端,减小客户端的等待。
固然,HTTP/2 新增那么多的特性,是否是 HTTP 的语法要从新学呢?不须要,HTTP/2 彻底兼容以前 HTTP 的语法和语义,如请求头、URI、状态码、头部字段都没有改变,彻底不用担忧。同时,在安全方面,HTTP 也支持 TLS,而且如今主流的浏览器都公开只支持加密的 HTTP/2, 所以你如今能看到的 HTTP/2 也基本上都是跑在 TLS 上面的了。最后放一张分层图给你们参考:
HTTP/2 中传输的帧结构以下图所示:
每一个帧分为帧头
和帧体
。先是三个字节的帧长度,这个长度表示的是帧体
的长度。
而后是帧类型,大概能够分为数据帧和控制帧两种。数据帧用来存放 HTTP 报文,控制帧用来管理流
的传输。
接下来的一个字节是帧标志,里面一共有 8 个标志位,经常使用的有 END_HEADERS表示头数据结束,END_STREAM表示单方向数据发送结束。
后 4 个字节是Stream ID
, 也就是流标识符
,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。
从前面能够知道,在 HTTP/2 中,所谓的流
,其实就是二进制帧的双向传输的序列。那么在 HTTP/2 请求和响应的过程当中,流的状态是如何变化的呢?
HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里咱们以一个普通的请求-响应
过程为例来讲明:
最开始二者都是空闲状态,当客户端发送Headers帧
后,开始分配Stream ID
, 此时客户端的流
打开, 服务端接收以后服务端的流
也打开,两端的流
都打开以后,就能够互相传递数据帧和控制帧了。
当客户端要关闭时,向服务端发送END_STREAM
帧,进入半关闭状态
, 这个时候客户端只能接收数据,而不能发送数据。
服务端收到这个END_STREAM
帧后也进入半关闭状态
,不过此时服务端的状况是只能发送数据,而不能接收数据。随后服务端也向客户端发送END_STREAM
帧,表示数据发送完毕,双方进入关闭状态
。
若是下次要开启新的流
,流 ID 须要自增,直到上限为止,到达上限后开一个新的 TCP 链接重头开始计数。因为流 ID 字段长度为 4 个字节,最高位又被保留,所以范围是 0 ~ 2的 31 次方,大约 21 亿个。
刚刚谈到了流的状态变化过程,这里顺便就来总结一下流
传输的特性:
发送方
或者接收方
。以上就是对 HTTP/2 中二进制帧的介绍,但愿对你有所启发。
文章首发于个人博客,若是以为对你有帮助的话,但愿能帮忙点一个 star,很是感谢~
参考: