HTTP: 超文本快递小哥

54 阅读16分钟

基本属性

http 是两点之间传输超文本的协议

报文

  • 请求报文和相应报文大同小异

image.png

头部字段:

  • Host 指定服务器域名(可以实现发向同一台服务器的不同域名下)
  • Connection :Keep-Alive 开启长连接 (http1.1默认长连接)
  • Content-Type 发送的类型
  • Accept 接收的类型
  • Content-Encoding 字段压缩方法和压缩格式
  • Accept-Encoding 可接受的压缩方法

粘包

TCP 是面向字节流的,会把多个包的内容融合在一起,也就是“粘包问题” ,这个问题 TCP 不处理,而是交给应用层去处理

  1. 固定包长度,应用读到一定长度就认为读出了一个报文
  2. 回车、换行符作为边界字符 ( HTTP 方案 ) 。注意需要对报文中的回车和换行做转义处理
  3. 自定义消息结构
strct {
    u_int32_t message_length; // 保存长度
    char message_data[]; // 保存数据
}

状态码

状态码状态备注
200 OKok没有问题
204 No Contentok没有body响应
206 Partial ContentokHTTP分块,返回的不是全部
301 Moved Permanentlymove永久重定向,要用新的URL
302 Foundmove临时重定向
304 Not Modifyok命中缓存,不跳转的意思
400 Bad Requestbad客户端报文有误
403 Forbiddenbad服务器禁止访问资源
404 Not Foundbad找不到资源
500 Internal Server Errorbad笼统的服务端错误码
501 Not Implementedbad即将开业,敬请期待
502 Bad Gatewaybad服务器作为网关和代理报错服务器自身正常,后端服务器出错
503 Service Unaviablebad系统繁忙

优点

  1. 无需维护状态,协议更加简单易用
  2. 只需要一个无状态的服务器,能够适配更多的客户端

缺点

  1. 对于需要状态的场景需要手动维护状态,能力欠佳

  2. 手动维护状态的方案引入和更多的问题

GET

语义上是从服务器获取指定的资源

  • URL 只支持 ASCll
  • HTTP 不限制 URL 长度,但是浏览器会限制
  • get 是幂等而且安全的,因此可以做缓存

POST

语义上是通过data对服务器上的资源做指定修改

  • body 数据可以是任何格式 ( 最终是基于文本的)

  • post 不是安全的,也不是幂等的,所以一般不缓存

以上对比是基于 RFC 规范定义的语义出发的,具体安不安全,幂不幂等要看具体开发者如何用

get & post 请求其实都可以有 params 和 body ,RFC 规范从语义上认为他们不需要,但是在技术上是能够这么做的

  • 条件获取GET

    • 代理服务器向源服务器发送一个请求,源服务器检查对象是否修改,如果修改了就返回新的东西,如果没修改,就不反悔完整报文,而是只给一个响应头

image.png

优化

  1. 通过缓存避免发送HTTP

  2. 减少发送HTTP请求

    1. 使用服务器代理减少重定向次数
    2. 合并请求
    3. 延迟发送请求 (懒加载)
  3. 通过压缩减小数据大小 ( gzip , brotli )

缓存

  1. HTTP Cache

http缓存老生常谈了:

  • 强缓存 :检查 expires cache-control

    • expires 就是秒级的缓存绝对时间

    • cache-control 是更多维度的控制

      • max - age 秒级的过期相对时间
      • no-cache 不能直接使用强制缓存 (需要确认实时性)
      • no-store 最严格的缓存控制(需要保证安全性)
      • must-revalidate 超过 max-age 就必须发送请求验证
      • proxy-revalidate 超过 max-age 就必须发送请求到源服务器验证
      • private 只能被单个用户的浏览器缓存
      • public 允许被任何浏览器和中间代理服务器缓存

cache-control 并非只是控制本地缓存,private , public , proxy-revalidate 实际上也控制中间代理服务器对资源的缓存能力

  • 协商缓存:涉及两个重要数据 E-Tag | Last-Modify

    • 请求头携带 If-None-Match (ETag匹配), If-Modify-Since (修改判断)
    • 返回304 / 200

image.png

image.png

强制缓存

cache-control

  1. 缓存配置策略控制

  2. 开关,过期时间等

Expires

  1. http1.0网页缓存控制字段
  2. 请求的到期时间,http1.1中被cache-control代替
  3. 其粒度只到秒,快于秒级的修改可能不准确

协商缓存

Etag / If-None-Match

  1. 文件的(hash)唯一表示
  2. 优先级高

Last-Modify / If-Modified-Since

  1. 文件最后修改的时间戳
  2. 优先级低
  3. 其粒度只到秒,快于秒级的修改可能不准确

优缺点

无状态协议

优点

  1. 不需要维护状态,协议更加简单
  2. 协议更加简单,支持的平台就更多,拓展性好
  3. 不维护状态,服务器的负担就比较小

缺点

  1. 在需要状态的时候需要额外维护,比如cookies
  2. 维护额外的状态可能带来新的问题

Cookie

解决无状态问题

  • 首次请求不发cookie
  • HTTP响应报文和请求报文都含有一个cookie的首部行
  • 前端浏览器管理的cookie和后台数据库维护cookie保持同步

明文传输

优点

  1. 方便阅读
  2. 不需要加密解密,成本更低

缺点

  1. 明文裸奔不安全

总结

  • 使用 明文 容易导致信息泄漏
  • 不验证通信对象容易导致请求目标被伪造
  • 不验证报文完整性

版本差距

http 1 - 1.1 - 2.0 - 3.0

HTTP 1 - HTTP 1.1

非持久HTTP

  • 每一个请求使用一个 TCP,使用后关闭

  • HTTP/1.0 使用非持久连接

  • 下载多个对象需要多个TCP连接

非流水线模式

  • 最多只有一个对象在TCP连接上发送
  • 每个对象都需要两个 rount trip time rrt
  • HTTP 1 只能是非流水线,HTTP 1.1 可以使用流水线

持久HTTP

  • 对于一个已经建立的 tcp 连接,完成传输后不会关闭

  • 如果一段时间管道中无消息则自动断开

  • HTTP/1.1 默认使用持久连接

    •   Connection : keep-alive

流水线模式(管道模式)

  • 多个对象可以在一个(客户端和服务器之间的)TCP连接上传输 (并非默认支持)

队头阻塞

HTTP 1.1 使用管道模式解决了请求队头阻塞问题,但是还是响应队头阻塞问题,所以并没有被广泛适配

image.png

  1. 1.0版本 : 串行连接(短轮询),对于现在网页,通信很大 最多六个持久 TCP 连接,超出的排队等待

  2. 1.1版本 : 实现长连接和长轮询 (阻塞模式) connection:keep-alive

  3. 2.0版本 : 多路复用,并发请求,通过HTTP序列标识保序(管道)

    1. 添加了一个HTTP分层帧,数据被分为带有请求ID的帧
    2. 一个TCP连接上有多个Stream,一个Stream上有多个Frame (一个Stream就是一个请求)
    3. 同一个Stream内的帧必须是有序的,不同Stream之间可以乱序
    4. 客户端请求资源的时候StreamID是奇数
    5. 服务端发送资源的时候StreamID是偶数
    6. SteamID不可复用,到顶关闭
  4. 3.0版本 : 基于udp实现QUIC,解决了超时阻滞

http1 的对头阻滞是 http 的请求响应机制决定的,1.1 更新后使用管道模式解决了问题。 http 1.1 的响应请求阻滞是因为 TCP 保证了两个不同的请求是保序的,但实际上我们不需要这部分保序,这个多余的能力造成了时间的浪费。http2 通过分层帧将这份保序的能力只保留在请求内,请求间可以不保序。

HTTP 1 - 2

注意 http2 是基于 https 的,所以我们讨论 http2 的时候,默认是在讨论 https 的基础上

http2 相对于 http1 改进有如下三点

  1. http 头部比较臃肿 - 头部压缩
  2. http 基于纯文本协议,速度比较慢 - 二进制格式
  3. http 1 ,1.1 都有队头阻塞问题 - 并发传输
  4. 服务端推送

HPACK

HPACK 其实就是头部压缩,在客户端和服务端都维护一张表,记录 head value 生成索引号

如果出现一样的 head value ,直接发送索引号,从而减小头部

二进制

http2 传输内容全面采用二进制,头信息和数据体更名为头信息帧和数据帧

  • 对于计算机友好,无需讲明文转换为二进制的消耗,提升了传输的效率
  • 使用二进制,对于数字的编码会节约不少空间 ,比如 200 由三个字符的三个字节变成了 一个字节(索引)

服务端推送

用于服务端主动推送,减少客户端重复请求的消耗

比如发送 HTML 的时候推送 CSS

注意被推送的内容只能被浏览器缓存,而不能通过 API 直接获取

客户端在发送请求的时候就会去读这个缓存,实际上这个请求就被优化掉了

Stream

在 http 1.1 中,虽然没有了请求的队头阻塞,但是由于依赖请求-响应模型,所以还是被响应队头堵塞干掉了。

http2 通过映入 Stream 的概念,通过多个 Stream 复用同一个 TCP 连接,实现了并发的响应

  • 一个 HTTP2 连接中有多个 Stream ( 默认 128 )
  • 每一个 Stream 可以有多个 Message
  • 每一个 Message 相当于 HTTP1 的一个请求或者响应
  • 每一个 Message 中有多个 Frame ,Frame 是 HTTP2 的最小单位
  • Stream 之间可以是乱序的,所以 Stream 是能够并发的。通过 StreamID 标识所属 Stream
  • 但是一个 Stream 之间必须是有序的
  • StreamID 只能递增,用尽关闭

image.png

由于对于每一个请求,我们的要求是保序的,但是对于多个请求间,我们可以不用保序

http1 - 1.1 中,对于请求也是保序的,所以我们可以优化掉

将每一个请求分配一个 Stream,当然一个 Stream 可以被多个请求使用,这些请求就是 Message

HTTP2 将 Message 分为 Frame 二进制帧,通过 TCP 发送。这一步是多个 Stream 交错进行的,但是对于每一个 Stream 中的 Frame, 保序的这一步是 TCP 保证的 。所以瓶颈变成了 TCP 的 队头阻塞

随后 HTTP2 将 Frame 重组为 Stream 中的 Message 返回。这就实现了请求之间的并发。

HTTP 1.1 将瓶颈从请求推到了响应

HTTP 2 将瓶颈从 HTTP 推到的 TCP

那么可以预见 HTTP 如果要继续优化就要放弃 TCP 了

HTTP 2 - 3

我们讨论 http3 其实是在讨论了 QUIC

http 3 将 http2 的 TCP 层换成了 UDP ,并通过 QUIC 层来实现可靠传输 (quick UDP internect connect)

QUIC 无队头阻塞

quic 的思想其实和 HTTP2 的思想一致,只不过他将这一层思想在下一层实践了(替换UDP并使用应用层能力加强UDP)

  • 在 TCP 中,http2 分出来的帧之间,原本只是 Stream 内有依赖关系,但是由于 TCP 的保序性,每一个 Frame 之前其实都变得保序而有依赖了
  • 在 QUIC 中,在 UDP (不保序)的基础上,对 Frame 进行分组,(这个分组可以类比 HTTP2 的Stream)也称为 Stream,只有每一个分组内有依赖,这就抛弃了 TCP 原本对于不同组之间 Frame 的多余依赖实现了优化

QUIC 更快的连接建立

在 HTTP2 中, TLS (基于 openssl) 在表示层, TCP (基于内核)在传输层,所以他们之间的握手很难放在一起,各自需要握手,TCP 三次 + TLS 四次成本很高

QUIC 在内部包含了 TLS 1.3 ,TLS 1.3 只需要一个 RRT ,也就是两次握手就能实现,而 QUIC 通过连接 ID 实现,之需要一个 RTT,之需要在握手的时候捎带 TLS 数据,就能实现 1 RTT ,两次握手建立连接

QUIC 连接迁移

TCP 通过维护四元组标识连接对象

当用户从 WIFI 切换到移动网络,IP 发生变化,就需要刷新 / 重开。

QUIC 通过 ID 建立连接,只要保证 ID 和 TLS 相关上下文正确保留,就无需重新建立 HTTP 连接


HTTPS

在 HTTP 的优缺点的时候我们说到,http 有 3 大缺点

  • 明文传输 ---> 加密算法
  • 不校验身份 ---> 通过数字证书校验身份
  • 不确认数据完整性 ---> 通过摘要算法验证完整性

TLS/SSL 安全层就弥补了这三个问题

TLS 的验证分为两部分

  1. 握手协议,通过四次握手获取会话密钥
  2. 记录协议,复杂使用会话密钥验证数据的完整和来源可靠性

TLS 1.2

  • 通过对称密钥实现高效的加密

  • 通过非对称密钥实现对称密钥的约定

    • 如果使用公钥加密,则是为了保密
    • 如果使用私钥加密,则是为了验证来源

image.png

2 个 RTT

****RSA

  1. 客户端和服务端 Hello

    1. 客户端发送:随机字符串 + 加密套件
    2. 服务端发送:随机字符串 + 选择算法 + 证书
  2. 生成和传递会话密钥

    1. 客户端验证证书,生成新随机字符串,用公钥加密后发送 + 密钥变更 + finished
    2. 客户端接受会话密钥 + finished
  • 最终有双方的随机字符串 + 公钥加密的随机字符串生成的会话密钥

ECDHE

  1. 客户端和服务端 Hello

    1. 客户端发送:随机字符串 + 加密套件
    2. 服务端发送:随机字符串 + 选择算法 + 新公私钥对和签名
  2. 生成和传递会话密钥

    1. 客户端验证证书:根据选择的算法生成公私钥 + finished
    2. 客户端接受会话密钥 + finished
  • 最终有双方的随机字符串 + 双方的公钥组成的会话密钥

前向安全性

  • RSA 不保证前向安全性

    • 服务端随机字符串的加密依靠长期使用的私钥
    • 私钥泄漏 - 每个随机字符串和选择的算法泄漏 - 任意会话的会话密钥泄漏
  • ECDHE 保证前向安全性

    • 由于每次的私钥和公钥都是随机的,泄漏了也不会导致所有的会话内容泄漏

TLS 1.3

TLS 1.3 解决性能问题

TLS1.2的时候双方需要 2 RTT 才能建立连接

TLS1.3的时候在第一次请求

  1. 客户端发送随机数,圆锥曲线公钥,支持的算法和圆锥曲线
  2. 服务端发送随机数,圆锥曲线公钥,选择的算法和圆锥曲线

全程只需要 1 次 RTT

数字签名

签名是一种加密的摘要

  • 摘要是为了验证完整性
  • 但是只有摘要不够,可能原文和摘要都被替换了,所以需要验证摘要来源,通过私钥加密摘要

数字证书

如果公钥也是被伪造的,那么数字签名也就一起失效了,所以要保证公钥有效

  • 分发公钥的过程也验证一下完整性和来源可靠性
  • 完整性通过对公钥的摘要实现
  • 可靠性通过 CA 颁发数字证书实现
  • 获取到数字证书后逐层检验证书链的的可靠性

CA 对公钥等信息的数字签名 === 数字证书

证书链的验证

为什么要证书链

  • 防止根证书颁发机构单点故障(风险分散)
  • 利用中间证书提供一定的灵活性,去实现不同场景/政策的要求 (灵活和可拓展性)

双向验证

默认情况下 HTTPS 只开启单向验证,客户端去验证服务端的身份。

如果电脑被植入的病毒或者用户愚蠢的信任了未知的证书,HTTPS 的安全性就无法保证了

这时候开启双向验证,服务端也去验证客户端的身份,S和M的合法 TLS 就无法建立。就不会被中间人监听了

image.png

响应时间模型

RTT 往返时间 (round-trip-time)

  • 一个分组到服务器再回到客户端的时间

响应时间

  • 一个 RTT 用来发起 TCP 连接
  • 一个 RTT 用来 HTTP 请求并等待 HTTP 响应
  • 文件传输时间共: 2 RTT + 传输时间

为什么还要有 RPC

RPC 和 HTTP 都是应用层协议,他们其实蛮相似的

RPC 流行的时间会早于 HTTP 流行的时间,所以我们的讨论思路

是 HTTP 为什么会抢占 RPC 的位置,以及 RPC 为什么还在用?

rpc 和 http 是可以定义在 TCP 协议上的应用层协议

RPC 不是具体一种协议,而是一种调用方法,gRPC / thrift 才是协议

结论:RPC 更适合 CS 架构 ,HTTP 更适合 BS 架构

  • rpc 协议百花齐放,不利于统一多平台,所以 HTTP 作为一种跨平台协议出现了
  • 但是跨平台兼容也就意味着他需要做很多兜底处理,需要满足很多种情况,那么他的头部设计会比较全面,这也就意味着会有贷款浪费
  • RPC 无需兼容多平台,所以能够做定制化的设计,抛弃很多指定场景没用的字段,性能上得以提升
  • RPC 协议是基于二进制的,但是HTTP协议设计之初是基于文本的,文本的序列化和反序列化需要消耗一定的性能 (HTTP2 还没有广泛使用)
  • HTTP2 的性能其实比 RPC 更好,甚至gRPC 就是基于 HTTP2 的,但是由于历史缘故,RPC 和 http2就共存了

为什么还要有 WebSocket

TCP 是全双工的,但是 HTTP 只实现了 半双工,因为初期没有运用场景

Websokcet 继承了 TCP 的双工能力,解决了 TCP 痛点粘包,实现应用层双工

HTTP 场景如何实现服务端主动推送

  1. 通过短轮训,不断的发送请求
  2. 使用长轮询,发送一个请求,等待服务端返回(百度网盘的实现方案)

连接升级

  • WebSocket 链接需要先从 TCP 三次握手建立 HTTP 开始
  • 建立 HTTP 连接后通过发送升级协议升级为 Websocket(返回状态 101)
  • 但是 websocket 是基于 TCP 的而不是 HTTP 的