阅读 161

浅谈网络协议:HTTP 篇

这是我参与更文挑战的第 9 天,活动详情查看: 更文挑战

1. 头部字段:

请求头:

  • Accept:客户端告诉服务端,自己支持的 MIME 类型,具体可以包括下面这些:
    • 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
  • Content-Type:通常只有使用 POST 请求的时候需要指定这个字段,用来告诉服务端请求报文的请求体的数据类型,即客户端向服务端提交了什么类型的数据。具体可以包括下面这些:
    • application/x-www-form-urlencoded:最常用的,用于传输简单数据,请求体类似于 a=1&b=2 这样的格式
    • multipart/form-data:用于上传文件
    • application/json:用于传输数据,请求体是序列化之后的 JSON 字符串

响应头:

  • Allow:服务端告诉客户端,自己支持的请求方法
  • Content-Type:响应报文的响应体的数据类型,即服务端返回给客户端什么类型的数据,可以是 text/html,image/gif,application/json 等

2. 请求方法

都有哪些请求方法?

  • GET: 通常用来获取资源
  • HEAD: 获取资源的元信息(HEAD 即 HEADER,保存资源元信息的请求头)
  • POST: 提交数据,即上传数据
  • PUT: 修改数据
  • DELETE: 删除资源(几乎用不到)
  • CONNECT: 建立连接隧道,用于代理服务器
  • OPTIONS: 列出可对资源实行的请求方法,用来跨域请求
  • TRACE: 追踪请求-响应的传输路径

GET 和 POST 的区别?

  • 参数:GET 的参数一般附加在 URL 中,POST 的参数一般出现在请求体(body)中(注意这里说的是一般的做法,实际上反过来也是可以的)

  • 安全性:实际上,如果使用的是明文传输的 HTTP,那么不管是 GET 还是 POST 都是不安全的,这和用 URL 传输数据还是 body 传输数据无关

  • 幂等性:重复 GET 始终返回相同的结果,对要访问的数据没有副作用,所以 GET 是幂等的;重复 POST 可能产生副作用,所以 POST 是非幂等的

  • 缓存:由于 GET 是幂等的,所以 GET 的结果可以缓存在浏览器或者代理服务器中,而 POST 不能

  • 编码:GET 通常是通过 URL 传输数据的,这决定了它只支持 ASCII 码;POST 通常是通过 body 传输数据的,这决定了它可以支持任意二进制

  • 长度:GET 通常是通过 URL 传输数据的,而 URL 长度是有限的,所以 GET 能够传输的数据长度是有限的;POST 通常是通过 body 传输数据的,因此可以认为它传输的数据长度是无限的

  • 请求次数:通常会认为,GET 请求只需要发一次,而 POST 请求需要发两次 —— 第一次发送请求头,收到 100(continue)响应状态码后,第二次再发送请求体。其实应该将这种情况理解为一种优化手段,而不是 GET 和 POST 本质上的区别。比如用户通过 POST 请求上传一个很大的文件,那么可以先发请求头,服务端若判断此次请求是不合法的(比如用户没有权限上传),那么就可以直接返回 4xx,客户端就没必要再去发送请求体了,相比一次性发送所有数据来说,这种做法减少了带宽的浪费。但话又说回来,如果这次 POST 的数据很小很小,那么请求头 + 请求体一次 POST 过去,其实也是可以的。最后,具体是发一次还是两次,也和浏览器实际采用的策略有关,并不是绝对的。

3. 缓存

HTTP 缓存

1)强缓存和协商缓存

PS:下面说的是设置 cache-control: must-revalidate 的情况

  • 浏览器针对资源 A 初次发起请求,先查看是否有缓存。因为没有,所以请求到达服务器,拿到资源
  • 片刻后,浏览器再次针对资源 A 发起请求,查看是否有缓存,此时有上次的缓存,所以接着查看是否过期(或者说缓存是否“新鲜”)
    • 缓存没有过期:此时就直接从这个缓存中获取资源 A。这时候称为走强缓存路线、命中强缓存;
    • 缓存已经过期:此时浏览器就需要和服务器进行协商,协商的内容就是:浏览器应该使用上次的旧缓存,还是使用可能已经发生更新的新缓存?这时候称为走协商缓存路线、没有命中强缓存。
      • 如果第一次的响应携带了 e-tag 字段:浏览器将 e-tag 的字段值作为 if-none-match 的字段值,向服务器发送条件请求,相当于是在问服务器:==当时发送资源给我的时候,这个资源的唯一标识是 e-tag,是否这个哈希值仍然和目前资源的哈希值一致呢?==服务器就会拿收到的这个字段值与目前最新的资源哈希值进行比较,如果一致说明资源没有发生修改,此时返回 304 状态码,让浏览器使用之前的旧缓存;如果不一致说明资源发生了修改,此时重新响应新资源给浏览器
      • 如果第一次的响应没有携带 e-tag 字段,但是携带了 last-modified 字段:浏览器将 last-modified 的字段值作为 if-modified-since 的字段值,向服务器发送条件请求,相当于是在问服务器:==当时发送资源给我的时候,最后一次修改资源的时间是 last-modified,是否自从这个时间之后,资源没有再次被修改呢?==服务器就会拿收到的这个字段值与目前最新的资源修改时间进行比较,如果时间吻合说明资源没有发生修改,此时返回 304 状态码,让浏览器使用之前的旧缓存;如果时间不吻合说明资源发生了修改,此时重新响应新资源给浏览器
      • 如果两个字段都没有携带:此时就进行正常的请求响应

PS:这里优先检查 e-tag 字段,是因为这个字段更加精确。使用 last-modified 进行校验,实际上会有两个问题,而这两个问题会影响到关于“资源是否真的发生了修改”的判断:

  • 其一,把没修改当成了修改。last-modified 更准确地说应该是上次编辑时间而不是上次修改时间,所谓编辑,意思就是不一定发生了修改,或者发生的是无关紧要的修改,但由于编辑时间确实改动了,所以服务器给出的结果依然是资源发生了修改;
  • 其二,把修改当成了没修改。last-modified 最小只能精确到秒这个量级,这就是说,如果资源在一秒内发生了多次修改,其实服务器是看不出来的,给出的结果依然是资源没有发生修改。

结合下面这张图进行记忆:

当然,这里说的只是设置 cache-control: must-revalidate 的情况,总结一下就是,没过期就走强缓存,过期了就走协商缓存;如果是设置 cache-control:no-cache,则无论缓存是否过期,都会首先通过 e-tag 或者 last-modified 进行服务器校验。如果是设置 cache-control:no-store,则不会进行任何缓存操作,也不会有强缓存和协商缓存之说。

2)缓存过期时间

实际上,客户端是可以使用过期缓存的,可以通过设置相关的头部字段实现。

  • max-stale = 5:允许客户端使用过期的缓存,但是最多允许过期 5s,过期超过 5s 就不能用了
  • min-fresh = 5:客户端要求一个新鲜的、未过期的缓存,并且至少过了 5s 还是新鲜的

实际上,客户端是可以使用过期缓存的 —— 可以通过 max-stale 设置允许客户端使用过期多久的缓存(只要没超过这个时间,就算过期了也能用)

代理服务器缓存

这里只说一下响应报文中 vary 头部字段在代理服务器缓存中的作用。实际上,vary 字段是协助进行内容协商的,可以防止客户端错误返回缓存资源。

假设有两个客户端 A 和 B,A 支持 gzip 编码而 B 不支持。

  • 第一次 A 发出请求,经代理服务器转发后到达服务器,服务器返回资源到代理服务器,并且会在响应报文中增加一个 vary 字段,比如说 vary:accept-encoding。代理服务器会针对资源做一个缓存,同时通过响应报文中的 vary 给资源打上一个标记,比如 accept-encoding:gzip,代表这个资源是使用 gzip 编码压缩的。
  • 假设 A 在片刻后发出第二次请求,还是请求同样的资源,因为也有一个 accept-encoding:gzip,所以代理服务器可以返回缓存给客户端 A
  • 之后假设 B 也发出了一次请求,但是由于 B 不支持 gzip 编码,所以是不携带 accept-encoding:gzip 字段的,代理服务器不会把缓存的这个资源返回给 B

因此 vary 实际上就相当于是给代理服务器的缓存资源打上一个标记,如果当时源服务器不返回 vary 字段,那么 B 请求资源的时候,代理服务器会错误地把资源返回给 B,而 B 是使用不了这个资源的,因为它不支持 gzip 编码。

4. HTTP 性能优化

http 的性能优化可以从三个角度入手,分别是服务端、客户端和传输链路

  • 服务端:相关的指标有吞吐量(RPS、TPS、QPS,即每秒的请求数)、并发数(可以承载支持多少个客户端)、响应时间、其他资源的利用率(CPU、内存、网卡和硬盘等)
  • 客户端:基本的指标是延迟,延迟和地理距离、带宽、DNS 查询、TCP 握手等有关
  • 传输链路:第一公里的带宽、中间一公里(CDN)

5. HTTP/2 带来的变化

头部压缩

http 1.x 中,可以通过 content-encoding:gzip 对响应报文的响应实体(body)进行压缩,但对于可能比响应实体更大的响应头,却反而没有对应的压缩优化手段。为此,http2 引入了头部压缩,其实现的基础是 HPACK 算法。

二进制

报文不再采用 ASCII 码的文本形式,而是直接使用二进制格式。在此基础上,把之前的 header + body 结构打散成一个个二进制帧,HEADERS 类型的帧负责存放头部数据,DATA 类型的帧负责存放实体数据

在解释流之前,先看看在 http 的发展历程中,是如何发送多个 http 请求的:

  1. 短连接:一个 TCP 连接只能处理一轮请求-响应,一旦服务端返回响应,TCP 连接就会断开。下一轮的请求-响应必须重新创建一个 TCP 连接

    缺点:短连接需要频繁地创建 TCP 连接,频繁地握手和挥手,非常耗时

  2. 长连接/持久连接/连接保活/连接复用:允许一个 TCP 连接供多轮请求-响应使用。http 1.0 通过显式声明 connection:keep-alive 开启长连接,而 http 1.1 默认就是开启长连接的,无需声明

    缺点:无法解决队头阻塞问题。请求-响应是串行的,只有 A 请求返回响应,B 请求才能发出,若 A 请求迟迟无法返回响应,则会阻塞 B 请求

  3. 管道化/管线化/流水线/pipelining:http 1.1 引入了管道化,允许同时发出多个请求,无需等待响应的返回

    缺点:依然无法从根本上解决队头阻塞问题。因为响应的返回顺序和请求的发出顺序是一致的,若 A 请求迟迟无法返回响应,则 B 请求的响应也会无法返回

  4. TCP 并发连接:既然在同一个 TCP 连接上会阻塞,那么就多开几个 TCP 连接,然后把被阻塞的请求“挪到”其它 TCP 连接上,这样就互不影响了。浏览器允许针对一个域名开最多 6 -8 个并发 TCP 连接

  5. 域名分片:如果单纯的 TCP 并发连接还无法满足需求,那么可以在此基础上进行域名分片,即将多个域名映射到同一台服务器,这样,实际可以达到的并发 TCP 连接数将是 域名数*6-8 个

  6. 多路复用:http 2 实现了多路复用,真正意义上解决了 http 层面的队头阻塞问题。多路复用允许在同一个 TCP 连接上同时进行多个请求-响应,这些请求-响应互不影响(假设不存在依赖)、没有先后之分,不存在阻塞问题

流是实现多路复用的基础,可以避免队头阻塞问题。在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现队头阻塞问题,降低了延迟,大幅度提高了连接的利用率。

server push

server push 即服务端推送,指的是服务端可以提前返回客户端将来可能会请求的资源。比如客户端只向服务端请求 index.html,但是 index.html 里面引用了其它资源(比如 style.css 等),那么服务端会把这些资源也一并返回,避免客户端需要针对这些资源再发一次请求。

伪首部字段代替请求行/状态行

http 1.x 中有请求行和状态行,而 http 2 直接将请求行和状态行变成了伪首部字段。

在 http 1.x 中,请求行是这样的:

GET / HTTP/1.1
Host: www.example.com
复制代码

而在 http 2 中,这些将转化为如下的伪首部字段:

:scheme: https
:method: GET
:path: /
:authority: www.example.com
复制代码

在 http 1.x 中,状态行是这样的:

HTTP/1.1 200 OK
复制代码

而在 http 2 中,这将转化为如下的伪首部字段:

:status: 200
复制代码
文章分类
前端