HTTP协议与请求方法
# 常见的HTTP
请求方法重要
get
向服务器获取数据post
将实体提交给指定的资源,通常造成服务器资源修改put
上传文件,更新数据delete
删除服务器上面的对象head
获取报文首部,与get
相比不返回报文主体部分options
:询问支持的请求方法,用来跨域请求connect
要求在与代理服务器通信时建立隧道,使用隧道进行tcp
通信trace
回显服务器收到的请求,主要用于测试或诊断
# get
和post
的区别特别重要
应用场景不同: 一般
get
请求用于服务器资源不会产生影响,比如请求一个网页的资源,但是post
会对服务器产生影响,比如注册用户之类的缓存不同: 浏览器一般会对
get
请求缓存,一般不会对post
请求缓存- 可以在
url
后面拼接时间戳
- 可以在
发送的报文格式不同:
get
请求的报文实体为空,post
请求的报文实体一般为向服务器发送的数据幂等:
get
是幂等的,post
不是(幂等
表示执行相同的操作,结果也是相同的)安全性不同:
get
将参数放在url
的后面,post
传递的参数不在query
上面请求长度: 浏览器对
url
的长度有限制,会影响get
请求发送数据的长度post
支持更多的数据类型的数据POST
会产生两个TCP
数据包:对于GET
方式的请求,浏览器会把http header
和data
一并发送出去,服务器响应200
(返回数据);而对于POST
,浏览器先发送header
,服务器响应100 continue
,浏览器再发送data
,服务器响应200 ok
(返回数据)。也就是post
请求,第一次将header
发送过去,确认服务器和网络没问题可以服务,才会将真正的data
数据提交。 因为POST
需要两步,时间上消耗的要多一点
# post
和put
的区别
put
向服务端发送数据,但不会增加数据的种类,无论进行多少次put
,结果并没有什么不同(理解为更新数据)post
向服务端发送请求,会改变数据的种类,可以理解为创建数据
# 对 Accept
系列字段了解多少?
四个部分: 数据格式、压缩方式、支持语言和字符集
数据格式
客户端支持格式: 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
压缩方式
压缩方式体现在发送方的Content-Encoding
字段上, 同样的,接收什么样的压缩方式体现在了接受方的Accept-Encoding
字段上。这个字段的取值有下面几种:
gzip
: 当今最流行的压缩格式deflate
: 另外一种著名的压缩格式br
: 一种专门为HTTP
发明的压缩算法
// 发送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gzip
2
3
4
支持语言
对于发送方而言,还有一个Content-Language
字段,在需要实现国际化的方案当中,可以用来指定支持的语言,在接受方对应的字段为Accept-Language
。如:
// 发送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en
2
3
4
字符集
最后是一个比较特殊的字段, 在接收端对应为Accept-Charset
,指定可以接受的字符集,而在发送端并没有对应的Content-Charset
, 而是直接放在了Content-Type
中,以 charset
属性指定。如:
// 发送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8
2
3
4
最后以一张图来总结一下吧:
# 常见的content-type
有哪些重要
application/x-www-form-urlencoded
按照key=value&key=value
进行编码multipart/form-data
通常用表单上传文件application/json
服务器消息主体是序列化的JSON
字符串text/xml
主要提交xml
格式数据
# 对于定长和不定长的数据,HTTP
是怎么传输的?
定长包体
对于定长包体而言,发送端在传输的时候一般会带上 Content-Length
, 来指明包体的长度。
如果设置长度小了,在http
响应体中直接被截去,如果设置长度大了,http
会导致传输失败,显示意外终止连接
不定长包体
上述是针对于定长包体
,那么对于不定长包体
而言是如何传输的呢?
这里就必须介绍另外一个 http
头部字段了:
Transfer-Encoding: chunked
表示分块传输数据,设置这个字段后会自动产生两个效果:
Content-Length
字段会被忽略- 基于长连接持续推送动态内容
响应体的结构如下所示:
chunk长度(16进制的数)
第一个chunk的内容
chunk长度(16进制的数)
第二个chunk的内容
......
0
2
3
4
5
6
最后是留有有一个空行
的
# HTTP
如何处理大文件的传输?
HTTP
采取了范围请求
的解决方案,允许客户端仅仅请求一个资源的一部分。
当然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:
Accept-Ranges: none
用来告知客户端这边是支持范围请求的。
Range
字段拆解
而对于客户端而言,它需要指定请求哪一部分,通过Range
这个请求头字段确定,格式为bytes=x-y
。接下来就来讨论一下这个 Range
的书写格式:
0-499
表示从开始到第499
个字节。500-
表示从第500
字节到文件终点。-100
表示文件的最后100
个字节。
服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回416
错误码,否则读取相应片段,返回 206
状态码。
同时,服务器需要添加Content-Range
字段,这个字段的格式根据请求头中Range
字段的不同而有所差异。
具体来说,请求单段数据
和请求多段数据
,响应头是不一样的。
举个例子:
// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39
2
3
4
5
接下来我们就分别来讨论着两种情况。
单段数据
对于单段数据
的请求,返回的响应如下:
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100
i am xxxxx
2
3
4
5
6
值得注意的是Content-Range
字段,0-9
表示请求的返回,100
表示资源的总大小,很好理解。
多段数据
接下来我们看看多段请求的情况。得到的响应会是下面这个形式:
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--
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101
,它代表了信息量是这样的:
- 请求一定是多段数据请求
- 响应体中的分隔符是
00000010101
因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上--
表示结束。
# 介绍一下options
请求重要
options
的用途主要有两个
- 获取服务器支持的所有
http
请求方法 - 用于检查访问权限,比如在进行
cors
跨域资源共享的时候,对于复杂请求,就是使用options
方法发送嗅探请求,判断是否有权限访问指定的资源
# http1.0
和http1.1
之间有哪些区别非常重要
连接方面:
http1.0
默认支持非持久连接,而http1.1
默认支持持久连接,http1.1
通过使用持久连接来使多个http
请求复用同一个tcp
连接,避免使用非持久连接每次需要建立的时延资源请求方面:
http1.0
方面存在浪费带宽(客户端只是需要某个对象的一部分,但是服务器却将整个对象送来了,并且不支持断点续传功能),http1.1
在请求头引入range
头域,它允许之请求资源的某个部分,即返回码是206(partial content)
,便于开发者自由选择充分利用带宽缓存方面:在
http1.0
主要使用header
里的cache-control
、expire
作为缓存标准判断,http1.1
加入了Etag、If-none-match
Last-Modified、If-Modified-Since
等更多可供选择的缓存头来控制缓存策略http1.1
新增host
字段,用来指定服务器的域名,http1.0
认为每台服务器都绑定一个唯一的ip
,所以请求url
没有传递主机名,但是随着技术发展,在一台物理机器可以存在多台虚拟主机,共享同一个ip
地址,这样可以将请求发往同一台服务器的不同网站1.1新增
put、head、options
等方法
# http1.1
和2.0
的区别非常重要
二进制编码:
2.0
是一个二进制协议,在1.1
版本中,报文头信息必须是文本(ASCII编码
),数据可以是文本,也可以是二进制,2.0
的头信息和数据体都是二进制,统称为帧,分为头信息帧(存放头部字段)和数据帧(存放请求体数据),都是乱序的二进制帧,不存在先后关系不需要排队。- 乱序的二进制帧如何组装成对应报文:
- 所谓的乱序,指的是不同
ID
的Stream
是乱序的,对于同一个Stream ID
的帧是按顺序传输的。 - 接收方收到二进制帧后,将相同的
Stream ID
组装成完整的请求报文和响应报文。 - 二进制帧中有一些字段,控制着
优先级
和流量控制
等功能,这样子的话,就可以设置数据帧的优先级,让服务器处理重要资源,优化用户体验。
- 所谓的乱序,指的是不同
- 乱序的二进制帧如何组装成对应报文:
多路复用:
2.0
实现多路复用,复用tcp
连接,但在一个连接里面客户端和服务端可以同时发送多个请求或响应,而且不用按照顺序一一发送,避免了队头堵塞的问题数据流:
2.0
采用了数据流的概念,因为前面的多路复用讲了,不用按顺序,所以同一个连接里面的数据包,可能属于不同的请求,所以要对数据包做标记,指出他属于哪一个请求。2.0
将每个请求或回应的所有数据包称为一个数据流,都有一个 独特的编号Stream ID
,数据包发送时候,都必须标记数据流id
,来区分属于哪个数据流头信息压缩:因为
1.1
协议不带状态,每次请求都得附上所有信息,请求很多字段都是重复的,比如cookies
和user agent
,一模一样的内容每次请求都得带上,浪费带宽和速度,2.0
使用gzip
或compress
压缩再发出,而客户端和服务端同时维护一张头信息表,所有字段都会存入这张表中,生成一个索引号,以后不发送相同字段 只发索引号,可以提速服务器推送:
2.0
允许服务器未经请求主动向客户端发送资源,交服务器推送,提前给客户端推送必要的资源,减少延迟时间,需要注意的是2.0
主动推送的是静态资源,跟ws
以及使用的sse
向客户端发送即时数据的推送是不同的
队头堵塞
有HTTP
基本的“请求 - 应答”模型导致的,http
规定报文必须是一发一收,形成了先进先出的串行队列,队列请求没有优先级,只有入队的顺序,最前面的请求最先被处理,如果队首的请求因为处理的太慢了耽误了时间,那么后面的所有请求也得跟着等待,造成了队头阻塞
# TCP
长连接和短连接的区别
长连接:在一个TCP
连接上可以连续发送多个数据包,在TCP
连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。
短连接:指通信双方有数据交互时,就建立一个TCP
连接,数据发送完成后,则断开此TCP
连接,一般银行都使用短连接。
# HTTP
中的 keep-alive
有了解吗?它和多路复用的区别重要
HTTP/1.x
是基于文本的,只能整体去传;HTTP/2
是基于二进制流的,可以分解为独立的帧,交错发送
HTTP/1.x keep-alive
必须按照请求发送的顺序返回响应;HTTP/2
多路复用不按序响应
HTTP/1.x keep-alive
为了解决队头阻塞,将同一个页面的资源分散到不同域名下,开启了多个 TCP
连接;HTTP/2
同域名下所有通信都在单个连接上完成
HTTP/1.x keep-alive
单个 TCP
连接在同一时刻只能处理一个请求(两个请求的生命周期不能重叠);HTTP/2
单个 TCP
同一时刻可以发送多个请求和响应
# HTTP2
的缺点重要
TCP
以及TCP+TLS
建立连接的延时,HTTP/2
使用TCP
协议来传输的,而如果使用HTTPS
的话,还需要使用TLS
协议进行安全传输,而使用TLS
也需要一个握手过程,在传输数据之前,导致我们需要花掉3~4
个RTT
。TCP
的队头阻塞并没有彻底解决。在HTTP/2
中,多个请求是跑在一个TCP
管道中的。但当HTTP/2
出现丢包时,整个TCP
都要开始等待重传,那么就会阻塞该TCP
连接中的所有请求。
# https
与http
的区别特别重要
https
需要ca
证书,http
不用http
是超文本传输协议,是明文传输,https
是具有安全性的ssl
加密传输http
端口是80
https
是443
http
是无状态的协议,https
是具有ssl
和http
协议构建的可加密、身份认证的网络协议,比http
安全
# GET
方法对URL
长度限制的原因
http
不对get
方法长度进行限制,其实这个限制是浏览器和服务器对url
的限制,ie
对url
长度限制是2083
字节(2k+35)
,ie
限制地最小,所以只要不超过2083
就不会有问题
GET的长度值 = URL(2083)- (你的Domain+Path)-2(2是get请求中?=两个字符的长度)
# 对keep-alive
的理解
http1.0
默认每次请求/应答,客户端和服务端都会新建一个连接,完成之后断开连接,这是短连接
使用keep-alive
使客户端到服务端的连接持续有效,当出现对服务器的后续请求时,可以避免建立或重新连接,这是长连接
使用方法:
1.0
默认没有keep-alive
,需要的话配置发送Connection: keep-alive
断开的话发送
Connection: close
1.1
版本默认保持长连接,数据传输完成的时候tcp
连接不断开,等待同域名下继续使用这个通道传输数据,需要关闭就发送Connection: close
Keep-Alive
的建立过程:
客户端向服务器在发送请求报文同时在首部添加发送
Connection
字段服务器收到请求并处理
Connection
字段服务器回送
Connection:Keep-Alive
字段给客户端客户端接收到
Connection
字段Keep-Alive
连接建立成功
服务端自动断开过程(也就是没有keep-alive
):
客户端向服务器只是发送内容报文(不包含
Connection
字段)服务器收到请求并处理
服务器返回客户端请求的资源并关闭连接
客户端接收资源,发现没有
Connection
字段,断开连接
客户端请求断开连接过程:
客户端向服务器发送
Connection:close
字段服务器收到请求并处理
connection
字段服务器回送响应资源并断开连接
客户端接收资源并断开连接
开启Keep-Alive
的优点:
较少的
CPU
和内存的使⽤(由于同时打开的连接的减少了);允许请求和应答的
HTTP
管线化;降低拥塞控制 (
TCP
连接减少了);减少了后续请求的延迟(⽆需再进⾏握⼿);
报告错误⽆需关闭
TCP
连接
开启Keep-Alive
的缺点:
- 长时间的
TCP
连接容易导致系统资源无效占用,浪费系统资源。
# 页面有多张图片,http
怎么加载
在http1
,浏览器对一个域名下最大tcp
连接数为6
,所以会请求多次,可以用多域名部署解决,可以提高同时请求的数目,加快页面图片的获取速度
在http2
中,支持多路复用,可以在一个tcp
连接中发送多个http
请求
# 如何理解 HTTP
代理?
我们知道在 HTTP
是基于请求-响应
模型的协议,一般由客户端发请求,服务器来进行响应。
当然,也有特殊情况,就是代理服务器的情况。引入代理之后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份。
那代理服务器到底是用来做什么的呢?
功能
- 负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,
IP
都是多少,客户端是不知道的。因此,这个代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台源服务器的负载尽量平均。当然,这样的算法有很多,包括随机算法、轮询、一致性hash
、LRU
(最近最少使用)
等等,不过这些算法并不是本文的重点,大家有兴趣自己可以研究一下。 - 保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进行过滤,对非法
IP
限流,这些都是代理服务器的工作。 - 缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得而不用到源服务器那里。
# http2
头部压缩算法如何实现重要
http2
头部压缩是HPACK
算法,在客户端和服务端两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码压缩整数和字符串
在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送;
首部表在
HTTP/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-Host
和X-Forwarded-Proto
,分别记录客户端(注意哦,不包括代理)的域名
和协议名
。
X-Forwarded-For
产生的问题
前面可以看到,X-Forwarded-For
这个字段记录的是请求方的 IP
,这意味着每经过一个不同的代理,这个字段的名字都要变,从客户端
到代理1
,这个字段是客户端的IP
,从代理1
到代理2
,这个字段就变为了代理1
的 IP
。
但是这会产生两个问题:
- 意味着代理必须解析
HTTP
请求头,然后修改,比直接转发数据性能下降。 - 在
HTTPS
通信加密的过程中,原始报文是不允许修改的。
由此产生了代理协议
,一般使用明文版本,只需要在 HTTP
请求行上面加上这样格式的文本即可:
// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
...
复制代码
2
3
4
5
这样就可以解决X-Forwarded-For
带来的问题了
# HTTP
的请求报文是什么样的
由请求行 请求头部 空行 请求体组成
请求⾏包括:请求⽅法字段、URL
字段、HTTP
协议版本字段
请求头部:请求头部由关键字/值对组成,每⾏⼀对,关键字和值⽤英⽂冒号“:”
分隔
User-Agent
:产⽣请求的浏览器类型。Accept
:客户端可识别的内容类型列表。Host
:请求的主机名,允许多个域名同处⼀个IP
地址,即虚拟主机。
请求体:post
put
等请求携带的数据
# HTTP
响应报文是什么样的
由响应行 响应头 空行 响应体
# http
的优缺点重要
http
是超文本传输协议,定义了客户端和服务端交换报文的格式和方式,默认使用80
端口,使用tcp
协议作为传输层协议,保证了数据传输的可靠
优点:
支持客户端/服务端
简单快捷 客户向服务器请求时 只需传送请求方法和路径 由于
http
协议简单 使得http
服务器规模小,通信速度快无连接 限制每次连接只处理一个请求,服务端处理完客户请求,客户收到应答就断开连接,可以节省传输时间
无状态 状态指的是通信上下文信息,缺少状态意味着如果后续处理需要前面的信息,就必须重传,导致每次连接传送的数据量增大,另一方面在服务器不需要之前的信息他的应答就比较快
灵活
http
允许传输任意类型的数据对象,由content-type
标记
缺点:
无状态 服务器不会保存关于客户的任何消息
明文传输 报文采用文本形式 不安全
不安全 使用明文 内容易被窃听;不验证通信方身份,可能伪装;无法验证报文完整性,可能被篡改
# RTT
往返时间是什么
RTT(Round-Trip Time)
,往返时间,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认,不包含数据传输时间)总共经历的时间,即通信一来一回的时间
# http
通信时间总和是多少
上一题我们知道了RTT
的概念,那么TCP
的三次握手🤝理论上来说花的时间应该是1.5RTT
,但是客户端第三次握手的时候不需要服务器的响应,所以节省了0.5RTT
,所以TCP
连接的时间为1RTT
HTTP
的交易时间为
- 一去(
HTTP Request
) - 二回 (
HTTP Responses
)
故 HTTP
交易时间 = 1 RTT
HTTP
通信时间总和 = TCP
建立连接时间 + HTTP
交易时间 = 1 RTT
+ 1 RTT
= 2 RTT
# HTTPS
通信时间总和是多少
- 一去: 客户端发送一个随机数
C
,客户端的TLS
版本号以及支持的密码套件列表给服务器端 - 二回: 服务端收到客户端的随机值,自己也产生一个随机值
S
,并根据客户端需求的协议和加密方式来使用对应的方式,并且发送自己的证书(如果需要验证客户端证书需要说明) - 三去: 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值
pre-master
,通过服务端证书的公钥去加密这个随机值并发送给服务端。如果服务端需要验证客户端证书的话会附带证书(双向认证,比如网上银行用U
盾) - 四回: 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值(
C/S
加pre-master
算出主密钥)按照之前约定的加密方式生成密钥,接下来的通信就可以通过该会话密钥来加密解密
HTTPS
基于TLS1.2
通信时间总和 = TCP
建立连接时间 + TLS
连接时间 + HTTP
交易时间 = 1 RTT
+ 2 RTT
+ 1 RTT
= 4 RTT
HTTPS
通信时间总和(基于TLS1.3
) = TCP
建立连接时间 + TLS1.3
连接时间 + HTTP
交易时间 = 1 RTT
+ 1 RTT
+ 1 RTT
= 3 RTT
# http
性能怎么样
基于tcp/ip
,使用请求和应答通信模式,所以性能来说这俩是关键
长连接 可以避免每次
tcp
连接三次握手花费http1.1
管道网络传输,就是一个tcp
连接里面客户端可以发送多个请求队头拥塞
http
传输是一发一收 ,里面的任务放在一个任务队列中串行执行 一旦队首请求的太慢会阻塞后面请求的处理解决方法:
- 并发连接 对于一个域名允许分配多个长连接,相当于增加了任务队列
- 域名分片 将域名分出很多个二级域名,全都指向同样的一台服务器 能够并发的长连接数变多
# 为什么HTTP3
不使用TCP
而是使用UDP
# 说一下http3.0
基于UDP
协议实现的类似于tcp
多路复用的数据流,传输可靠性,称为QUIC
协议
流量控制、传输可靠性:
QUIC
在udp
基础上增加一层保证数据传输可靠性,提供了数据包重传 拥塞控制等集成TLS加密功能,减少花费的
RTT
数多路复用 同一个物理连接上可以有多个独立的逻辑数据流,实现数据流单独传输,解决
tcp
队头阻塞问题快速握手 基于
udp
,可以实现0-1
个rtt
来建立连接
# HTTP3
怎么解决队头拥塞问题、0RTT
有了解过吗
QUIC
协议是基于 UDP
协议实现的,在传输层层面并没有固定的连接,可以根据需要开辟任意逻辑链路, QUIC
一次建立一个Connection
,一个Connection
下包含多个Stream
流(每个stream
独自维护一个逻辑连接,因为UDP
层面上是无连接的),每个流对应一个文件传输,并将不同的Stream
中的数据交付给不同的上层应用。QUIC
的一个Connection
对应多个Stream
,Stream
之间相互独立,因此任意一条链路断开都不会导致其他数据阻塞。
例如下图,stream2
丢了一个 UDP
包,不会影响后面跟着 Stream3
和 Stream4
。这样的技术就解决了之前 TCP
存在的队头阻塞问题。
并且 QUIC
在移动端的表现也会比 TCP
好。因为 TCP
是基于 IP
和端口去识别连接的,这种方式在多变的移动端网络环境下是很脆弱的。
但是 QUIC
是通过 ID
的方式去识别一个连接,不管你网络环境如何变化,只要 ID
不变,就能迅速重连上。
0RTT
通过使用类似 TCP
快速打开的技术,缓存当前会话的上下文,在下次恢复会话的时候,只需要将之前的缓存传递给服务端验证通过就可以进行传输了。
0RTT
建连可以说是 QUIC
相比 HTTP2
最大的性能优势。那什么是 0RTT
建连呢?
这里面有两层含义:
- 传输层
0RTT
就能建立连接。 - 加密层
0RTT
就能建立加密连接。
上图左边是 HTTPS
的一次完全握手的建连过程,需要 2-3
个 RTT
才开始传输数据,右边 QUIC
协议在第一个包就可以包含有效的应用数据
当然,QUIC
协议可以实现 0RTT
,但这也是有条件的,实际上是首次连接 1RTT
,非首次连接 0RTT
,首次连接过程:
可以看到,首次连接的时候,在第 4
步时,就已经开始发送实际的业务数据了,而第 1 - 3
步正好一去一回花费了 1RTT
时间,所以,首次连接的成本是 1RTT
非首次连接:
- 前面提到客户端和服务端首次连接时服务端传递了
config
包,里面包含了服务端公钥和两个随机数,客户端会将config
存储下来,后续再连接时可以直接使用,从而跳过这个1RTT
,实现0RTT
的业务数据交互。 - 客户端保存
config
是有时间期限的,在config
失效之后仍然需要进行首次连接时的密钥交换。
# 这么厉害,那你还知道HTTP3
的向前纠错机制吗
QUIC
协议有一个非常独特的特性,称为向前纠错 (Forward Error Correction
,FEC
),每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。
向前纠错牺牲了每个数据包可以发送数据的上限,但是减少了因为丢包导致的数据重传,因为数据重传将会消耗更多的时间(包括确认数据包丢失、请求重传、等待新数据包等步骤的时间消耗)。
假如说这次我要发送三个包,那么协议会算出这三个包的异或值并单独发出一个校验包,也就是总共发出了四个包。
当出现其中的非校验包丢包的情况时,可以通过另外三个包计算出丢失的数据包的内容。
当然这种技术只能使用在丢失一个包的情况下,如果出现丢失多个包就不能使用纠错机制了,只能使用重传的方式了。
# 那你还知道加密认证的报文吗
TCP
协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。
但是 QUIC
的 packet
可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET
和 CHLO
,所有报文头部都是经过认证的,报文 Body
都是经过加密的。
这样只要对 QUIC
报文任何修改,接收端都能够及时发现,有效地降低了安全风险。
如上图所示,红色部分是 Stream Frame
的报文头部,有认证。绿色部分是报文内容,全部经过加密
# QUIC
是如何进行包确认和重传的
QUIC
的Stream
流基于Stream ID+Offset
进行包确认,流量控制需要保证所发送的所有包offset
小于最大绝对字节偏移量(maximum absolute byte offset
), 该值是基于当前已经提交的字节偏移量(offset of data consumed
) 而进行确定的,QUIC
会把连续的已确认的offset
数据向上层应用提交。QUIC
支持乱序确认,但本身也是按序(offset
顺序)发送数据包。QUIC
利用ack frame
来进行数据包的确认,来保证可靠传输。一个ack frame
只包含多个确认信息,没有正文。- 如果数据包
N
超时,发送端将超时数据包N
重新设置编号M
(即下一个顺序的数据包编号) 后发送给接收端。 - 在一个数据包发生超时后,其余的已经发送的数据包依旧可以基于
Offset
得到确认,避免了TCP
利用SACK
才能解决的重传问题。
其实
QUIC
的乱序确认设计思想并不新鲜,大量网络视频流就是通过类似的基于UDP
的RUDP
、RTP
、UDT
等协议来实现快速可靠传输的。他们同样支持乱序确认,所以就会导致这样的观看体验:明明进度条显示还有一段缓存,但是画面就是卡着不动了,如果跳过的话视频又能够播放了。
- 如图所示,当前缓冲区大小为
8
,QUIC
按序(offset
顺序)发送29-36
的数据包:
31
、32
、34
数据包先到达,基于offset
被优先乱序确认,但30
数据包没有确认,所以当前已提交的字节偏移量不变,缓存区不变。
30
到达并确认,缓存区收缩到阈值,接收方发送MAX_STREAM_DATA frame
(协商缓存大小的特定帧)给发送方,请求增长最大绝对字节偏移量。
- 协商完毕后最大绝对字节偏移量右移,缓存区变大,同时发送方发现数据包
33
超时
- 发送方将超时数据包重新编号为
42
继续发送
以上就是最基本的数据包发送-接收过程,控制数据发送的唯一限制就是最大绝对字节偏移量,该值是接收方基于当前已经提交的偏移量(连续已确认并向上层应用提交的数据包offset
)和发送方协商得出。