基本属性
http 是两点之间传输超文本的协议
报文
- 请求报文和相应报文大同小异
头部字段:
- Host 指定服务器域名(可以实现发向同一台服务器的不同域名下)
- Connection :Keep-Alive 开启长连接 (http1.1默认长连接)
- Content-Type 发送的类型
- Accept 接收的类型
- Content-Encoding 字段压缩方法和压缩格式
- Accept-Encoding 可接受的压缩方法
粘包
TCP 是面向字节流的,会把多个包的内容融合在一起,也就是“粘包问题” ,这个问题 TCP 不处理,而是交给应用层去处理
- 固定包长度,应用读到一定长度就认为读出了一个报文
- 回车、换行符作为边界字符 ( HTTP 方案 ) 。注意需要对报文中的回车和换行做转义处理
- 自定义消息结构
strct {
u_int32_t message_length; // 保存长度
char message_data[]; // 保存数据
}
状态码
| 状态码 | 状态 | 备注 |
|---|---|---|
| 200 OK | ok | 没有问题 |
| 204 No Content | ok | 没有body响应 |
| 206 Partial Content | ok | HTTP分块,返回的不是全部 |
| 301 Moved Permanently | move | 永久重定向,要用新的URL |
| 302 Found | move | 临时重定向 |
| 304 Not Modify | ok | 命中缓存,不跳转的意思 |
| 400 Bad Request | bad | 客户端报文有误 |
| 403 Forbidden | bad | 服务器禁止访问资源 |
| 404 Not Found | bad | 找不到资源 |
| 500 Internal Server Error | bad | 笼统的服务端错误码 |
| 501 Not Implemented | bad | 即将开业,敬请期待 |
| 502 Bad Gateway | bad | 服务器作为网关和代理报错服务器自身正常,后端服务器出错 |
| 503 Service Unaviable | bad | 系统繁忙 |
优点
- 无需维护状态,协议更加简单易用
- 只需要一个无状态的服务器,能够适配更多的客户端
缺点
-
对于需要状态的场景需要手动维护状态,能力欠佳
-
手动维护状态的方案引入和更多的问题
GET
语义上是从服务器获取指定的资源
- URL 只支持 ASCll
- HTTP 不限制 URL 长度,但是浏览器会限制
- get 是幂等而且安全的,因此可以做缓存
POST
语义上是通过data对服务器上的资源做指定修改
-
body 数据可以是任何格式 ( 最终是基于文本的)
-
post 不是安全的,也不是幂等的,所以一般不缓存
以上对比是基于 RFC 规范定义的语义出发的,具体安不安全,幂不幂等要看具体开发者如何用
get & post 请求其实都可以有 params 和 body ,RFC 规范从语义上认为他们不需要,但是在技术上是能够这么做的
-
条件获取GET
- 代理服务器向源服务器发送一个请求,源服务器检查对象是否修改,如果修改了就返回新的东西,如果没修改,就不反悔完整报文,而是只给一个响应头
优化
-
通过缓存避免发送HTTP
-
减少发送HTTP请求
- 使用服务器代理减少重定向次数
- 合并请求
- 延迟发送请求 (懒加载)
-
通过压缩减小数据大小 ( gzip , brotli )
缓存
-
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
强制缓存
cache-control
-
缓存配置策略控制
-
开关,过期时间等
Expires
- http1.0网页缓存控制字段
- 请求的到期时间,http1.1中被cache-control代替
- 其粒度只到秒,快于秒级的修改可能不准确
协商缓存
Etag / If-None-Match
- 文件的(hash)唯一表示
- 优先级高
Last-Modify / If-Modified-Since
- 文件最后修改的时间戳
- 优先级低
- 其粒度只到秒,快于秒级的修改可能不准确
优缺点
无状态协议
优点
- 不需要维护状态,协议更加简单
- 协议更加简单,支持的平台就更多,拓展性好
- 不维护状态,服务器的负担就比较小
缺点
- 在需要状态的时候需要额外维护,比如cookies
- 维护额外的状态可能带来新的问题
Cookie
解决无状态问题
- 首次请求不发cookie
- HTTP响应报文和请求报文都含有一个cookie的首部行
- 前端浏览器管理的cookie和后台数据库维护cookie保持同步
明文传输
优点
- 方便阅读
- 不需要加密解密,成本更低
缺点
- 明文裸奔不安全
总结
- 使用 明文 容易导致信息泄漏
- 不验证通信对象容易导致请求目标被伪造
- 不验证报文完整性
版本差距
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 使用管道模式解决了请求队头阻塞问题,但是还是响应队头阻塞问题,所以并没有被广泛适配
-
1.0版本 : 串行连接(短轮询),对于现在网页,通信很大 最多六个持久 TCP 连接,超出的排队等待
-
1.1版本 : 实现长连接和长轮询 (阻塞模式) connection:keep-alive
-
2.0版本 : 多路复用,并发请求,通过HTTP序列标识保序(管道)
- 添加了一个HTTP分层帧,数据被分为带有请求ID的帧
- 一个TCP连接上有多个Stream,一个Stream上有多个Frame (一个Stream就是一个请求)
- 同一个Stream内的帧必须是有序的,不同Stream之间可以乱序
- 客户端请求资源的时候StreamID是奇数
- 服务端发送资源的时候StreamID是偶数
- SteamID不可复用,到顶关闭
-
3.0版本 : 基于udp实现QUIC,解决了超时阻滞
http1 的对头阻滞是 http 的请求响应机制决定的,1.1 更新后使用管道模式解决了问题。 http 1.1 的响应请求阻滞是因为 TCP 保证了两个不同的请求是保序的,但实际上我们不需要这部分保序,这个多余的能力造成了时间的浪费。http2 通过分层帧将这份保序的能力只保留在请求内,请求间可以不保序。
HTTP 1 - 2
注意 http2 是基于 https 的,所以我们讨论 http2 的时候,默认是在讨论 https 的基础上
http2 相对于 http1 改进有如下三点
- http 头部比较臃肿 - 头部压缩
- http 基于纯文本协议,速度比较慢 - 二进制格式
- http 1 ,1.1 都有队头阻塞问题 - 并发传输
- 服务端推送
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 只能递增,用尽关闭
由于对于每一个请求,我们的要求是保序的,但是对于多个请求间,我们可以不用保序
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 的验证分为两部分
- 握手协议,通过四次握手获取会话密钥
- 记录协议,复杂使用会话密钥验证数据的完整和来源可靠性
TLS 1.2
-
通过对称密钥实现高效的加密
-
通过非对称密钥实现对称密钥的约定
- 如果使用公钥加密,则是为了保密
- 如果使用私钥加密,则是为了验证来源
2 个 RTT
****RSA
-
客户端和服务端 Hello
- 客户端发送:随机字符串 + 加密套件
- 服务端发送:随机字符串 + 选择算法 + 证书
-
生成和传递会话密钥
- 客户端验证证书,生成新随机字符串,用公钥加密后发送 + 密钥变更 + finished
- 客户端接受会话密钥 + finished
- 最终有双方的随机字符串 + 公钥加密的随机字符串生成的会话密钥
ECDHE
-
客户端和服务端 Hello
- 客户端发送:随机字符串 + 加密套件
- 服务端发送:随机字符串 + 选择算法 + 新公私钥对和签名
-
生成和传递会话密钥
- 客户端验证证书:根据选择的算法生成公私钥 + finished
- 客户端接受会话密钥 + finished
- 最终有双方的随机字符串 + 双方的公钥组成的会话密钥
前向安全性
-
RSA 不保证前向安全性
- 服务端随机字符串的加密依靠长期使用的私钥
- 私钥泄漏 - 每个随机字符串和选择的算法泄漏 - 任意会话的会话密钥泄漏
-
ECDHE 保证前向安全性
- 由于每次的私钥和公钥都是随机的,泄漏了也不会导致所有的会话内容泄漏
TLS 1.3
TLS 1.3 解决性能问题
TLS1.2的时候双方需要 2 RTT 才能建立连接
TLS1.3的时候在第一次请求
- 客户端发送随机数,圆锥曲线公钥,支持的算法和圆锥曲线
- 服务端发送随机数,圆锥曲线公钥,选择的算法和圆锥曲线
全程只需要 1 次 RTT
数字签名
签名是一种加密的摘要
- 摘要是为了验证完整性
- 但是只有摘要不够,可能原文和摘要都被替换了,所以需要验证摘要来源,通过私钥加密摘要
数字证书
如果公钥也是被伪造的,那么数字签名也就一起失效了,所以要保证公钥有效
- 分发公钥的过程也验证一下完整性和来源可靠性
- 完整性通过对公钥的摘要实现
- 可靠性通过 CA 颁发数字证书实现
- 获取到数字证书后逐层检验证书链的的可靠性
CA 对公钥等信息的数字签名 === 数字证书
证书链的验证
为什么要证书链
- 防止根证书颁发机构单点故障(风险分散)
- 利用中间证书提供一定的灵活性,去实现不同场景/政策的要求 (灵活和可拓展性)
双向验证
默认情况下 HTTPS 只开启单向验证,客户端去验证服务端的身份。
如果电脑被植入的病毒或者用户愚蠢的信任了未知的证书,HTTPS 的安全性就无法保证了
这时候开启双向验证,服务端也去验证客户端的身份,S和M的合法 TLS 就无法建立。就不会被中间人监听了
响应时间模型
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 场景如何实现服务端主动推送
- 通过短轮训,不断的发送请求
- 使用长轮询,发送一个请求,等待服务端返回(百度网盘的实现方案)
连接升级
- WebSocket 链接需要先从 TCP 三次握手建立 HTTP 开始
- 建立 HTTP 连接后通过发送升级协议升级为 Websocket(返回状态 101)
- 但是 websocket 是基于 TCP 的而不是 HTTP 的