转载之http

http知识整理

0. 思维导图

思维导图

1. HTTP报文结构

  1. 请求行
  2. 请求头
  3. 请求体

2. HTTP的请求方法

2.1 HTTP有哪些请求方法

对于HTTP1.1

  1. GET: 获取静态资源或者查询数据
  2. POST: 提交数据
  3. PUT: 修改数据
  4. DELETE: 删除数据
  5. OPTIONS: 预请求,跨域时发送复杂请求
  6. CONNECT: 建立连接隧道,用于代理服务器
  7. TRACE: 追踪请求-响应的传输路径

2.2 get和post的区别

  1. 参数:GET通过url传递参数,POST通过请求体
  2. 缓存:GET会被浏览器缓存,留下历史记录,但是POST默认不会被缓存,除非手动设置。
  3. 编码方式:GET只能是url编码,POST支持多种编码
  4. 请求参数长度限制:浏览器对于url长度存在限制,所以会影响get请求传递参数;但是post请求使用请求体传参,所以不会收到限制。
  5. 安全性:相对于GET请求使用url传递参数,POST请求更适合传递敏感数据。
  6. GET请求的请求报文会一次性传输,而POST请求的请求头和请求体可能会分开传输,这是浏览器的一些特性,和HTTP本身并没有明确规定一定要分开传输。

2.3 备注

GET请求和POST请求如果脱离浏览器的话,本质上并没有什么区别,上面的区别更多的使用因为使用规范产生的,比如说,

  1. GET请求也可以使用请求体传参,只不过在实际开发中一般都不对GET请求的请求体进行处理,POST请求也可以使用url传参。
  2. 从传输的角度上讲,两种方式都不安全,因为HTTP在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文。要想安全传输,就只有加密,也就是 HTTPS。
  3. header 和 body 分开发送是部分浏览器或框架的请求方法,不属于 POST 必然行为。

2.4 参考资料:

  1. https://segmentfault.com/a/1190000018129846

3. 如何理解URI

URI是统一资源标识符。用来定位互联网上的资源的。
但是,它并不是我们常说的网址, 网址指的是URL, 实际上URI包含了URN和URL两个部分,由于 URL 过于普及,就默认将 URI 视为 URL 了。

3.1 URI的结构:

在这里插入图片描述

  1. scheme 表示协议名,比如http, https, file等等。后面必须和://连在一起。
  2. user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。
  3. host:port表示主机名和端口。
  4. path表示请求路径,标记资源所在位置。
  5. query表示查询参数,为key=val这种形式,多个键值对之间用&隔开。
  6. fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置。

3.2 URI编码

URI 只能使用ASCII, ASCII 之外的字符是不支持显示的,而且还有一部分符号是界定符,如果不加以处理就会导致解析出错。

因此,URI 引入了编码机制,将所有非 ASCII 码字符和界定符转为十六进制字节值,然后在前面加个%。

如,空格被转义成了%20,三元被转义成了%E4%B8%89%E5%85%83。

4. 状态码

HTTP 的状态码为三位数,被分为五类:

4.1 1xx: 表示目前是协议处理的中间状态,还需要后续操作。

101 Switching Protocols。在HTTP升级为WebSocket的时候,如果服务器同意变更,就会发送状态码 101。

4.2 2xx: 表示成功状态。

200 OK是见得最多的成功状态码。通常在响应体中放有数据。
204 No Content含义与 200 相同,但响应头后没有 body 数据。
206 Partial Content顾名思义,表示部分内容,它的使用场景为 HTTP 分块下载和断点续传,当然也会带上相应的响应头字段Content-Range。

4.3 3xx: 重定向状态,资源位置发生变动,需要重新请求。

301 Moved Permanently即永久重定向,对应着302 Found,即临时重定向。
比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。
而如果只是暂时不可用,那么直接返回302即可,和301不同的是,浏览器并不会做缓存优化。
304 Not Modified: 当协商缓存命中时会返回这个状态码。详见浏览器缓存

4.4 4xx: 请求报文有误。

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请求头的字段内容太大。

4.5 5xx: 服务器端发生错误。

500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。
501 Not Implemented: 表示客户端请求的功能还不支持。
502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。
503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。

5. 简要概括一下 HTTP 的特点?HTTP 有哪些缺点?

5.1 HTTP 特点

  1. 灵活可扩展,主要体现在两个方面。一个是语义上的自由,只规定了基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任意数据,非常方便。

  2. 可靠传输。HTTP 基于 TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。

  3. 请求-应答。也就是一发一收、有来有回, 当然这个请求方和应答方不单单指客户端和服务器之间,如果某台服务器作为代理来连接后端的服务端,那么这台服务器也会扮演请求方的角色。

  4. 无状态。这里的状态是指通信过程的上下文信息,而每次 http 请求都是独立、无关的,默认不需要保留状态信息。

5.2 HTTP 缺点

  1. 无状态
    所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态。
    在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。
    但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。

  2. 明文传输
    即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
    这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。

  3. 队头阻塞问题
    当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。接下来会有一小节讨论这个问题。

6. Accept 系列字段

Accept系列字段的介绍分为四个部分: 数据格式压缩方式支持语言字符集
在这里插入图片描述

6.1 数据格式

MIME(Multipurpose Internet Mail Extensions, 多用途互联网邮件扩展)。它首先用在电子邮件系统中,让邮件可以发任意类型的数据,这对于 HTTP 来说也是通用的。

HTTP 从MIME type取了一部分来标记报文 body 部分的数据类型,这些类型体现在Content-Type这个字段,当然这是针对于发送端而言,接收端想要收到特定类型的数据,也可以用Accept字段。

内容类型

text: text/html, text/plain, text/css 等
image: image/gif, image/jpeg, image/png 等
audio/video: audio/mpeg, video/mp4 等
application: application/json, application/javascript, application/pdf, application/octet-stream,application/x-www-form-urlencoded
multipart: multipart/form-data

6.2 压缩方式

一般这些数据都是会进行编码压缩的,采取什么样的压缩方式就体现在了发送方的Content-Encoding字段上, 同样的,接收什么样的压缩方式体现在了接受方的Accept-Encoding字段上。这个字段的取值有下面几种:

  1. gzip: 当今最流行的压缩格式
  2. deflate: 另外一种著名的压缩格式
  3. br: 一种专门为 HTTP 发明的压缩算法
// 发送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gzip

6.3 支持语言

对于发送方而言,还有一个Content-Language字段,在需要实现国际化的方案当中,可以用来指定支持的语言,在接受方对应的字段为Accept-Language。如:

// 发送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en

6.4 字符集

在接收端对应为Accept-Charset,指定可以接受的字符集,而在发送端并没有对应的Content-Charset, 而是直接放在了Content-Type中,以charset属性指定。如:

// 发送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8

7. 对于定长和不定长的数据,HTTP 是怎么传输的?

7.1 定长包体

对于定长包体而言,发送端在传输的时候一般会带上 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 传输过程起到了十分关键的作用,如果设置不当可以直接导致传输失败。

7.2 不定长包体

上述是针对于定长包体,那么对于不定长包体而言是如何传输的呢?

这里就必须介绍另外一个 http 头部字段了:

Transfer-Encoding: chunked

表示分块传输数据,设置这个字段后会自动产生两个效果:

  1. Content-Length 字段会被忽略
  2. 基于长连接持续推送动态内容

我们依然以一个实际的例子来模拟分块传输,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("成功启动");
})

8. HTTP 如何处理大文件的传输?

对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。因此,HTTP 针对这一场景,采取了 范围请求(断电续传) 的解决方案,允许客户端仅仅请求一个资源的一部分。

8.1 如何支持

服务端设置Accept-Ranges支持范围请求,

// 不支持
Accept-Ranges: none
// 支持
Accept-Ranges: bytes

8.2 Range 字段拆解

而对于客户端而言,它需要指定请求哪一部分,通过Range这个请求头字段确定,格式为bytes=x-y。接下来就来讨论一下这个 Range 的书写格式:

  1. 0-499表示从开始到第 499 个字节。
  2. 500- 表示从第 500 字节到文件终点。
  3. -100表示文件的最后100个字节。

服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回 416 错误码,否则读取相应片段,返回 206 状态码。

同时,服务器需要添加 Content-Range 字段,这个字段的格式根据请求头中 Range 字段的不同而有所差异。

具体来说,请求 单段数据 和请求 多段数据,响应头是不一样的。

// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39

8.2.1 单段数据

对于单段数据的请求,返回的响应如下:

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表示资源的总大小,很好理解。

8.2.2 多段数据

对于多段数据的请求,返回的响应如下:

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
它代表了信息量是这样的:

  1. 请求一定是多段数据请求
  2. 响应体中的分隔符是 00000010101

因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上 -- 表示结束。

以上就是 http 针对大文件传输所采用的手段。

9. HTTP 中如何处理表单数据的提交?

在 http 中,有两种主要的表单提交的方式,体现在两种不同的 Content-Type 取值:

  1. application/x-www-form-urlencoded
  2. multipart/form-data

由于表单提交一般是 POST 请求,很少考虑 GET,因此这里我们将默认提交的数据放在请求体中。

9.1 application/x-www-form-urlencoded

对于 application/x-www-form-urlencoded 格式的表单内容,有以下特点:

  1. 其中的数据会被编码成以&分隔的键值对
  2. 字符以URL编码方式编码。
// 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最终形式)
"a%3D1%26b%3D2"

9.2 multipart/form-data

对于multipart/form-data而言:

  1. 请求头中的 Content-Type 字段会包含boundary,且boundary的值有浏览器默认指定。例:
    Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe

  2. 数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP 头部描述子包体,如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--

9.3 小结

值得一提的是,multipart/form-data 格式最大的特点在于:每一个表单元素都是独立的资源表述。另外,你可能在写业务的过程中,并没有注意到其中还有boundary的存在,如果你打开抓包工具,确实可以看到不同的表单元素被拆分开了,之所以在平时感觉不到,是以为浏览器和 HTTP 给你封装了这一系列操作。

而且,在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间。

10. HTTP1.1 如何解决 HTTP 的队头阻塞问题?

HTTP 传输是基于 请求-应答 的模式进行的,报文必须是 一发一收,但值得注意的是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是著名的 HTTP队头阻塞 问题。

10.1 并发连接

对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。在RFC2616规定过客户端最多并发 2 个连接,不过事实上在现在的浏览器标准中,这个上限要多很多,Chrome 中是 6 个。
但其实,即使是提高了并发连接,还是不能满足人们对性能的需求。

10.2 域名分片

一个域名不是可以并发 6 个长连接吗?那我就多分几个域名。

比如 content1.sanyuan.com 、content2.sanyuan.com。

这样一个sanyuan.com域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了,事实上也更好地解决了队头阻塞的问题。

10.3 管道机制

在长连接的基础上,HTTP1.1进一步地支持在持久连接上使用管道化(pipelining)特性,这是相对于keep-alive连接的又一性能优化。在一个TCP连接上并行发送请求。

https://blog.csdn.net/fesfsefgs/article/details/108294050

10.3.1 管道化请求的致命弱点:

  1. 会造成队头阻塞,前一个响应未及时返回,后面的响应被阻塞。管道化存在响应阻塞,非管道化存在请求阻塞。
  2. 请求必须是幂等请求,也就是只有GET和HEAD请求才能管道化,不能修改资源。因为,意外中断时候,客户端需要把未收到响应的请求重发,非幂等请求,会造成资源破坏。

由于这个原因,目前大部分浏览器和Web服务器,都关闭了管道化,采用非管道化模式。

11. Cookie

11.1 Cookie 简介

前面说到了 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

11.1.1 Cookie机制

当用户第一次访问并登陆一个网站的时候,cookie的设置以及发送会经历以下4个步骤:

  1. 客户端发送一个请求到服务器
  2. 服务器发送一个HttpResponse响应到客户端,其中包含Set-Cookie的头部
  3. 客户端保存cookie,之后向服务器发送请求时,HttpRequest请求中会包含一个Cookie的头部。
  4. 服务器返回响应数据。
    在这里插入图片描述

11.2 Cookie 属性

属性 介绍
生存周期 有效期
expires 过期时间
Max-Age 时间间隔,单位是秒,从浏览器收到报文开始计算
作用域 域名或者路径不匹配,不会带上 Cookie
Domain 生成该 Cookie 的域名,如 domain=“www.baidu.com”
Path 该 Cookie 是在当前的哪个路径下生成的,如 path=/wp-admin/
安全相关
Secure 只能通过 HTTPS 传输 cookie
HttpOnly 只能通过 HTTP 协议传输,不能通过 JS 访问,预防 XSS 攻击
SameSite 预防CSRF 攻击,设置为三个值,StrictLax(默认)None
  1. Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其他网站请求都不能。
  2. Lax模式,就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能。
  3. None模式下,也就是默认模式,请求会自动携带上 Cookie。

11.3 Cookie 的缺点

  1. 容量缺陷。Cookie 的体积上限只有4KB,只能用来存储少量的信息。

  2. 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。但可以通过Domain和Path指定作用域来解决。

  3. 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在HttpOnly为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。

12. 如何理解 HTTP 代理?

我们知道在 HTTP 是基于请求-响应模型的协议,一般由客户端发请求,服务器来进行响应。

当然,也有特殊情况,就是代理服务器的情况。引入代理之后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份

12.1 代理服务器的功能

  1. 负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP 都是多少,客户端是不知道的。因此,这个代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台源服务器的负载尽量平均。当然,这样的算法有很多,包括随机算法轮询一致性hashLRU(最近最少使用)等等。

  2. 保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进行过滤,对非法 IP 限流,这些都是代理服务器的工作。

  3. 缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得而不用到源服务器那里。

12.2 相关头部字段

Via

代理服务器需要标明自己的身份,在 HTTP 传输中留下自己的痕迹,怎么办呢?

通过Via字段来记录。举个例子,现在中间有两台代理服务器,在客户端发送请求后会经历这样一个过程:

客户端 -> 代理1 -> 代理2 -> 源服务器

源服务器收到请求后,会在请求头拿到这个字段:

Via: proxy_server1, proxy_server2

而源服务器响应时,最终在客户端会拿到这样的响应头:

Via: proxy_server2, proxy_server1

可以看到,Via中代理的顺序即为在 HTTP 传输中报文传达的顺序

X-Forwarded-For

字面意思就是为谁转发, 它记录的是请求方的IP地址(注意,和Via区分开,X-Forwarded-For记录的是请求方这一个IP)。

X-Real-IP

是一种获取用户真实 IP 的字段,不管中间经过多少代理,这个字段始终记录最初的客户端的IP

相应的,还有X-Forwarded-HostX-Forwarded-Proto,分别记录客户端(注意哦,不包括代理)的域名和协议名。

12.3 X-Forwarded-For产生的问题

前面可以看到,X-Forwarded-For这个字段记录的是请求方的 IP,这意味着每经过一个不同的代理,这个字段的名字都要变,从客户端到代理1,这个字段是客户端的 IP,从代理1到代理2,这个字段就变为了代理1的 IP。
但是这会产生两个问题:

  1. 意味着代理必须解析 HTTP 请求头,然后修改,比直接转发数据性能下降。

  2. 在 HTTPS 通信加密的过程中,原始报文是不允许修改的。

由此产生了代理协议,一般使用明文版本,只需要在 HTTP 请求行上面加上这样格式的文本即可:

// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
...

13. 缓存代理?

13.1 为什么产生代理缓存?

对于源服务器来说,它也是有缓存的,比如Redis, Memcache,但对于 HTTP 缓存来说,如果每次客户端缓存失效都要到源服务器获取,那给源服务器的压力是很大的。

由此引入了缓存代理的机制。让代理服务器接管一部分的服务端HTTP缓存,客户端缓存过期后就近到代理缓存中获取,代理缓存过期了才请求源服务器,这样流量巨大的时候能明显降低源服务器的压力。

总的来说,缓存代理的控制分为两部分,一部分是源服务器端的控制,一部分是客户端的控制

13.2 源服务器的缓存控制

13.2.1 private 和 public

在源服务器的响应头中,会加上Cache-Control这个字段进行缓存控制字段,那么它的值当中可以加入private或者public表示是否允许代理服务器缓存,前者禁止,后者为允许

比如对于一些非常私密的数据,如果缓存到代理服务器,别人直接访问代理就可以拿到这些数据,是非常危险的,因此对于这些数据一般是不会允许代理服务器进行缓存的,将响应头部的Cache-Control设为private,而不是public。

13.2.2 proxy-revalidate

  1. must-revalidate的意思是客户端缓存过期就去源服务器获取。
  2. proxy-revalidate则表示代理服务器的缓存过期后到源服务器获取。

13.2.3 s-maxage

s是share的意思,限定了缓存在代理服务器中可以存放多久,和限制客户端缓存时间的max-age并不冲突。

讲了这几个字段,我们不妨来举个小例子,源服务器在响应头中加入这样一个字段:

Cache-Control: public, max-age=1000, s-maxage=2000

相当于源服务器说: 我这个响应是允许代理服务器缓存的,客户端缓存过期了到代理中拿,并且在客户端的缓存时间为 1000 秒,在代理服务器中的缓存时间为 2000 s。

13.3 客户端的缓存控制

13.3.1 max-stale 和 min-fresh

在客户端的请求头中,可以加入这两个字段,来对代理服务器上的缓存进行宽容限制操作。比如:

max-stale: 5

表示客户端到代理服务器上拿缓存的时候,即使代理缓存过期了也不要紧,只要过期时间在5秒之内,还是可以从代理中获取的。

又比如:

min-fresh: 5

表示代理缓存需要一定的新鲜度,不要等到缓存刚好到期再拿,一定要在到期前 5 秒之前的时间拿,否则拿不到。

13.3.2 only-if-cached

这个字段加上后表示客户端只会接受代理缓存,而不会接受源服务器的响应。如果代理缓存无效,则直接返回504(Gateway Timeout)。

14. HTTP/2 有哪些改进?

由于 HTTPS 在安全方面已经做的非常好了,HTTP 改进的关注点放在了性能方面。对于 HTTP/2 而言,它对于性能的提升主要在于两点:

  1. 头部压缩
  2. 多路复用

当然还有一些颠覆性的功能实现:

  1. 设置请求优先级
  2. 服务器推送

这些重大的提升本质上也是为了解决 HTTP 本身的问题而产生的。接下来我们来看看 HTTP/2 解决了哪些问题,以及解决方式具体是如何的。

14.1 头部压缩

在 HTTP/1.1 及之前的时代,请求体一般会有响应的压缩编码过程,通过Content-Encoding头部字段来指定,但你有没有想过头部字段本身的压缩呢?当请求字段非常复杂的时候,尤其对于 GET 请求,请求报文几乎全是请求头,这个时候还是存在非常大的优化空间的。HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK,对请求头进行压缩。

HPACK 算法是专门为 HTTP/2 服务的,它主要的亮点有两个:

  1. 首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,…)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。
    在这里插入图片描述

HTTP/2 当中废除了起始行的概念,将起始行中的请求方法、URI、状态码转换成了头字段,不过这些字段都有一个":"前缀,用来和其它请求头区分开。

  1. 其次是对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。

14.1.1 头部压缩原理

  1. https://imququ.com/post/header-compression-in-http2.html
  2. https://juejin.cn/post/6909253017430654990

14.2 多路复用

14.2.1 HTTP 队头阻塞

我们之前讨论了 HTTP 队头阻塞的问题,其根本原因在于HTTP 基于请求-响应的模型,在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。

后面我们又讨论到用并发连接域名分片的方式来解决这个问题,但这并没有真正从 HTTP 本身的层面解决问题,只是增加了 TCP 连接,分摊风险而已。而且这么做也有弊端,多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。

HTTP/2 便从 HTTP 协议本身解决了队头阻塞问题。注意,这里并不是指的TCP队头阻塞,而是HTTP队头阻塞,两者并不是一回事。TCP 的队头阻塞是在数据包层面,单位是数据包,前一个报文没有收到便不会将后面收到的报文上传给 HTTP,而HTTP 的队头阻塞是在 HTTP 请求-响应层面,前一个请求没处理完,后面的请求就要阻塞住。两者所在的层次不一样。

那么 HTTP/2 如何来解决所谓的队头阻塞呢?

14.2.1.1 二进制分帧

首先,HTTP/2 认为明文传输对机器而言太麻烦了,不方便计算机的解析,因为对于文本而言会有多义性的字符,比如回车换行到底是内容还是分隔符,在内部需要用到状态机去识别,效率比较低。于是 HTTP/2 干脆把报文全部换成二进制格式,全部传输01串,方便了机器的解析。

原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。

通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流(Stream)。HTTP/2 用来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。

可能你会有一个疑问,既然是乱序首发,那最后如何来处理这些乱序的数据帧呢?

首先要声明的是,所谓的乱序,指的是不同 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧一定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文和响应报文。当然,在二进制帧当中还有其他的一些字段,实现了优先级和流量控制等功能,我们放到下一节再来介绍。

14.2.2 服务器推送

另外值得一说的是 HTTP/2 的服务器推送(Server Push)。在 HTTP/2 当中,服务器已经不再是完全被动地接收请求,响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 连接建立之后,比如浏览器请求一个 HTML 文件,服务器就可以在返回 HTML 的基础上,将 HTML 中引用到的其他资源文件一起返回给客户端,减少客户端的等待。

14.2.3 TCP队头阻塞

TCP队头阻塞和HTTP队头阻塞完全不是一回事,上文所说的队头阻塞全部指的是HTTP队头阻塞。

HTTP 只是应用层协议TCP 是传输层协议。TCP 的阻塞问题是因为传输阶段可能会丢包,一旦丢包就会等待重新发包,阻塞后续传输,这个问题虽然有滑动窗口(Sliding Window)这个方案,但是只能增强抗干扰,并没有彻底解决。

14.3 总结

当然,HTTP/2 新增那么多的特性,是不是 HTTP 的语法要重新学呢?不需要,HTTP/2 完全兼容之前 HTTP 的语法和语义,如请求头URI状态码、头部字段都没有改变,完全不用担心。同时,在安全方面,HTTP 也支持 TLS,并且现在主流的浏览器都公开支持加密的 HTTP/2, 因此你现在能看到的 HTTP/2 也基本上都是跑在 TLS 上面的了。最后放一张分层图给大家参考:
在这里插入图片描述

15. HTTP/2 中的二进制帧是如何设计的?

15.1 帧结构

HTTP/2 中传输的帧结构如下图所示:
在这里插入图片描述
每个帧分为帧头帧体。先是三个字节的帧长度,这个长度表示的是帧体的长度。

然后是帧类型,大概可以分为数据帧控制帧两种。数据帧用来存放 HTTP 报文,控制帧用来管理流的传输。

接下来的一个字节是帧标志,里面一共有 8 个标志位,常用的有 END_HEADERS表示头数据结束,END_STREAM表示单方向数据发送结束。

后 4 个字节是Stream ID, 也就是流标识符,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。

15.2 流的状态变化

从前面可以知道,在 HTTP/2 中,所谓的,其实就是二进制帧的双向传输的序列。那么在 HTTP/2 请求和响应的过程中,流的状态是如何变化的呢?

HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里我们以一个普通的请求-响应过程为例来说明:
在这里插入图片描述
最开始两者都是空闲状态,当客户端发送Headers帧后,开始分配Stream ID, 此时客户端的流打开, 服务端接收之后服务端的流也打开,两端的流都打开之后,就可以互相传递数据帧和控制帧了。

客户端要关闭时,向服务端发送END_STREAM帧,进入半关闭状态, 这个时候客户端只能接收数据,而不能发送数据

服务端收到这个END_STREAM帧后也进入半关闭状态,不过此时服务端的情况是只能发送数据,而不能接收数据。随后服务端也向客户端发送END_STREAM帧,表示数据发送完毕,双方进入关闭状态。

如果下次要开启新的流,流 ID 需要自增,直到上限为止,到达上限后开一个新的 TCP 连接重头开始计数。由于流 ID 字段长度为 4 个字节,最高位又被保留,因此范围是 0 ~ 2的 31 次方,大约 21 亿个。

15.3 流的特性

刚刚谈到了流的状态变化过程,这里顺便就来总结一下流传输的特性:

  1. 并发性。一个 HTTP/2 连接上可以同时发多个帧,这一点和 HTTP/1 不同。这也是实现多路复用的基础。
  2. 自增性。流 ID 是不可重用的,而是会按顺序递增,达到上限之后又新开 TCP 连接从头开始。
  3. 双向性。客户端和服务端都可以创建流,互不干扰,双方都可以作为发送方或者接收方。
  4. 可设置优先级。可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。

作者:神三元
链接:https://juejin.cn/post/6844904100035821575
来源:掘金

猜你喜欢

转载自blog.csdn.net/qq_36968599/article/details/119780129