你知道吗?HTTP协义也分版本:HTTP1、HTTP2、HTTP3,讲讲版本功能和进化。

从 1989 年万维网(www)诞生,HTTP(HyperText Transfer Protocol)经历了众多版本迭代,WebSocket 也在期间萌芽。1991 年 HTTP0.9 被发明。1996 年出现了 HTTP1.0。2015 年 HTTP2 正式发布。2020 年 HTTP3 或能正式使用。以下将会简单介绍。

HTTP1.1 与 HTTP2

HTTP1.1 的缺陷

  1. 高延迟 — 队头阻塞(Head-Of-Line Blocking)
  2. 无状态特性 — 阻碍交互
  3. 明文传输 — 不安全性
  4. 不支持服务端推送

队头阻塞

队头阻塞是指当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。

针对队头阻塞:

1.将同一页面的资源分散到不同域名下,提升连接上限。虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。

2.减少请求数量

3.内联一些资源:css、base64 图片等

4.合并小文件减少资源数

无状态特性

无状态是指协议对于连接状态没有记忆能力。纯净的  HTTP  是没有  cookie  等机制的,每一个连接都是一个新的连接。上一次请求验证了用户名密码,而下一次请求服务器并不知道它与上一条请求有何关联,换句话说就是掉登录态

不安全性

传输内容没有加密,中途可能被篡改和劫持。

SPDY 协议

SPDY  是由 google 推行的改进版本的  HTTP1.1 (那时候还没有  HTTP2)。

特性:

  1. 多路复用 — 解决队头阻塞
  2. 头部压缩 — 解决巨大的  HTTP  头部
  3. 请求优先级 — 先获取重要数据
  4. 服务端推送 — 填补空缺
  5. 提高安全性

多路复用

SPDY  允许在一个连接上无限制并发流。因为请求在一个通道上,TCP  效率更高(参考  TCP 拥塞控制  中的慢启动)。更少的网络连接,发出更密集的包。

头部压缩

使用专门的  HPACK  算法,每次请求和响应只发送差异头部,一般可以达到  50%~90%  的高压缩率。

请求优先级

虽然无限的并发流解决了队头阻塞的问题,但如果带宽受限,客户端可能会因防止堵塞通道而阻止请求。在网络通道被非关键资源堵塞时,高优先级的请求会被优先处理。

服务端推送

服务端推送(ServerPush),可以让服务端主动把资源文件推送给客户端。当然客户端也有权利选择是否接收。

提高安全性

支持使用  HTTPS  进行加密传输。

HTTP2

HTTP2  基于  SPDY,专注于性能,最大的一个目标是在用户和网站间只用一个连接。

新增特性:

  1. 二进制分帧 - HTTP2  性能增强的核心
  2. 多路复用 - 解决串行的文件传输和连接数过多

二进制分帧

首先,HTTP2  没有改变  HTTP1  的语义,只是在应用层使用二进制分帧方式传输。因此,也引入了新的通信单位:帧、消息、流

分帧有什么好处?服务器单位时间接收到的请求数变多,可以提高并发数。最重要的是,为多路复用提供了底层支持。

多路复用

一个域名对应一个连接,一个流代表了一个完整的请求-响应过程。是最小的数据单位,每个会标识出该帧属于哪个也就是多个帧组成的数据流。多路复用,就是在一个  TCP  连接中可以存在多个流。演示

HTTP2 的缺陷

  1. TCP  以及  TCP+TLS  建立连接的延时
  2. TCP  的队头阻塞并没有彻底解决
  3. 多路复用导致服务器压力上升
  4. 多路复用容易  Timeout

建连延时

TCP 连接需要和服务器进行三次握手,即消耗完 1.5 个  RTT  之后才能进行数据传输。

TLS  连接有两个版本—— TLS1.2  和  TLS1.3,每个版本建立连接所花的时间不同,大致需要 1~2 个  RTT。

RTT(Round-Trip Time):
往返时延。表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。

队头阻塞没有彻底解决

TCP  为了保证可靠传输,有一个“超时重传”机制,丢失的包必须等待重传确认。HTTP2  出现丢包时,整个  TCP  都要等待重传,那么就会阻塞该  TCP  连接中的所有请求。

RTO:英文全称是 Retransmission TimeOut,即重传超时时间;  RTO 是一个动态值,会根据网络的改变而改变。RTO 是根据给定连接的往返时间 RTT 计算出来的。  接收方返回的 ack 是希望收到的下一组包的序列号。

多路复用导致服务器压力上升

多路复用没有限制同时请求数。请求的平均数量与往常相同,但实际会有许多请求的短暂爆发,导致瞬时  QPS  暴增。

多路复用容易 Timeout

大批量的请求同时发送,由于  HTTP2  连接内存在多个并行的流,而网络带宽和服务器资源有限,每个流的资源会被稀释,虽然它们开始时间相差更短,但却都可能超时。

即使是使用 Nginx 这样的负载均衡器,想正确进行节流也可能很棘手。  其次,就算你向应用程序引入或调整排队机制,但一次能处理的连接也是有限的。如果对请求进行排队,还要注意在响应超时后丢弃请求,以避免浪费不必要的资源。引用

QUIC

简介

Google
在推  SPDY  的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于  UDP  协议的  QUIC  协议。而这个就是  HTTP3。它真正“完美”地解决了“队头阻塞”问题。

主要特点

  1. 改进的拥塞控制、可靠传输
  2. 快速握手
  3. 集成了 TLS 1.3 加密
  4. 多路复用
  5. 连接迁移

改进的拥塞控制、可靠传输

从拥塞算法和可靠传输本身来看,QUIC  只是按照  TCP  协议重新实现了一遍,那么  QUIC  协议到底改进在哪些方面呢?主要有如下几点:

1. 可插拔 — 应用程序层面就能实现不同的拥塞控制算法。

一个应用程序的不同连接也能支持配置不同的拥塞控制。  应用程序不需要停机和升级就能实现拥塞控制的变更,可以针对不同业务,不同网络制式,甚至不同的  RTT,使用不同的拥塞控制算法。

关于应用层的可插拔拥塞控制模拟,可以对  socket  上的流为对象进行实验。

2. 单调递增的 Packet Number — 使用 Packet Number 代替了 TCP 的 seq。

每个  Packet Number  都严格递增,也就是说就算  Packet N  丢失了,重传的  Packet
N  的  Packet
Number  已经不是  N,而是一个比  N  大的值。而  TCP  重传策略存在二义性,比如客户端发送了一个请求,一个  RTO   后发起重传,而实际上服务器收到了第一次请求,并且响应已经在路上了,当客户端收到响应后,得出的  RTT  将会比真实  RTT  要小。当   Packet N  唯一之后,就可以计算出正确的  RTT。

3. 不允许 Reneging — 一个 Packet 只要被 Ack,就认为它一定被正确接收。

Reneging  的意思是,接收方有权把已经报给发送端  SACK(Selective Acknowledgment)  里的数据给丢了(如接收窗口不够而丢弃乱序的包)。

QUIC  中的  ACK  包含了与  TCP  中  SACK  等价的信息,但  QUIC  不允许任何(包括被确认接受的)数据包被丢弃。这样不仅可以简化发送端与接收端的实现难度,还可以减少发送端的内存压力。

4. 前向纠错(FEC)

早期的  QUIC  版本存在一个丢包恢复机制,但后来由于增加带宽消耗和效果一般而废弃。FEC  中,QUIC  数据帧的数据混合原始数据和冗余数据,来确保无论到达接收端的 n 次传输内容是什么,接收端都能够恢复所有 n 个原始数据包。FEC  的实质就是异或。示意图:

5. 更多的 Ack 块和增加 Ack Delay 时间。

QUIC  可以同时提供  256  个  Ack Block,因此在重排序时,QUIC  相对于  TCP(使用  SACK)更有弹性,这也使得在重排序丢失出现时,QUIC  可以在网络上保留更多的在途字节。在丢包率比较高的网络下,可以提升网络的恢复速度,减少重传量。

TCP  的  Timestamp   选项存在一个问题:发送方在发送报文时设置发送时间戳,接收方在确认该报文段时把时间戳字段值复制到确认报文时间戳,但是没有计算接收端接收到包到发送 Ack  的时间。这个时间可以简称为  Ack Delay,会导致  RTT  计算误差。现在就是把这个东西加进去计算  RTT  了。

6. 基于 stream 和 connection 级别的流量控制。

为什么需要两类流量控制呢?主要是因为  QUIC  支持多路复用。  Stream  可以认为就是一条  HTTP  请求。  Connection  可以类比一条  TCP   连接。多路复用意味着在一条  Connetion  上会同时存在多条  Stream。

QUIC 接收者会通告每个流中最多想要接收到的数据的绝对字节偏移。随着数据在特定流中的发送,接收和传送,接收者发送 WINDOW_UPDATE 帧,该帧增加该流的通告偏移量限制,允许对端在该流上发送更多的数据。

除了每个流的流控制外,QUIC 还实现连接级的流控制,以限制 QUIC 接收者愿意为连接分配的总缓冲区。连接的流控制工作方式与流的流控制一样,但传送的字节和最大的接收偏移是所有流的总和。

最重要的是,我们可以在内存不足或者上游处理性能出现问题时,通过流量控制来限制传输速率,保障服务可用性。

快速握手

由于  QUIC  是基于  UDP  的,所以  QUIC  可以实现  0-RTT  或者  1-RTT  来建立连接,可以大大提升首次打开页面的速度。

集成了 TLS 1.3 加密

TLS 1.3  支持 3 种基本密钥交换模式:

(EC)DHE (基于有限域或椭圆曲线的 Diffie-Hellman)
PSK - only
PSK with (EC)DHE

在完全握手情况下,需要  1-RTT  建立连接。 TLS1.3  恢复会话可以直接发送加密后的应用数据,不需要额外的  TLS  握手,也就是  0-RTT。

TLS 1.3 0-RTT  简单原理示意(基于  DHE):

但是  TLS1.3  也并不完美。TLS 1.3  的  0-RTT  无法保证前向安全性(Forward secrecy)。简单讲就是,如果当攻击者通过某种手段获取到了  Session Ticket Key,那么该攻击者可以解密以前的加密数据。

要缓解该问题可以通过设置使得与  Session Ticket Key  相关的  DH  静态参数在短时间内过期(一般几个小时)。

多路复用

QUIC  是为多路复用从头设计的,携带个别流的的数据的包丢失时,通常只影响该流。QUIC  连接上的多个  stream  之间并没有依赖,也不会有底层协议限制。假如  stream2  丢了一个包,也只会影响  stream2  的处理。

连接迁移

TCP  是按照 4 要素(客户端 IP、端口, 服务器 IP、端口)确定一个连接的。而  QUIC  则是让客户端生成一个  Connection ID (64 位)来区别不同连接。只要  Connection ID   不变,连接就不需要重新建立,即便是客户端的网络发生变化。由于迁移客户端继续使用相同的会话密钥来加密和解密数据包,QUIC   还提供了迁移客户端的自动加密验证。

挑战

NAT 问题

NAT 概念

为了解决  IP  地址不足的问题,NAT  给一个局域网络只分配一个  IP   地址,这个网络内的主机,则分配私有地址,这些私有地址对外是不可见的,他们对外的通信都要借助那个唯一分配的  IP  地址。所有离开本地网络去往 Internet  的数据报的源  IP  地址需替换为相同的  NAT,区别仅在于端口号不同。

原因

TCP  和  UDP  的报文头部不同导致  NAT  问题的出现。

NAT 设备的端口记忆问题

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java