以下是我自己复习网络相关知识整理的笔记,发上来和大家分享,如有不对欢迎指出
前情提要:
IP头部
ARP
IP地址和MAC地址的转换【如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。】
ICMP
确认IP包是否成功到达、返回发送过程中IP包被废弃的原因
DHCP
为接入网络的电脑动态分配IP、DNS服务器IP;提供了即插即用的连网方式,用户不再需要手动配置 IP 地址等信息。DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址
NAT
用于内部网络IP和外部公网IP的地址转换【解决曾经ipv4不够用问题】
FTP
文件传输协议【两个连接,一个控制连接用于等待客户端连接,一个数据连接用于实际传输文件】
tcp三次握手
tcp四次挥手
tcp头部
udp头部
tcp的可靠性
三次握手建立连接
校验和:检验数据是否异常
序列号:每个包都有自己的顺序号
确认应答:接收端收到包后要对包进行确认ack
停止等待APQ:在收到对方的确认ACK后再发送下一个包
超时重传:在发送一个包后,会启动一个定时器,如果定时器超时还没收到ACK确认包,则会重传
快速重传:在连续收到三个相同的ACK包时,立马重传
SACK:允许确认一段
滑动窗口:发送窗口【只有发送且收到ACK后才能从发送窗口中移除】、接收窗口
流量控制:流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
拥塞控制:慢启动【每收到对方的ACK确认就会指数级增加拥塞窗口的大小,1 2 4 8..】
拥塞避免【当拥塞窗口大于阈值时,就会启动线性增长,每次窗口+1】
快重传【在连续收到三次相同的ACK确认后启动快重传,阈值设置为当前窗口的一半,窗口大小设置为新阈值+3】
超时重传【在发生超时重传的情况下,会认为网络拥堵,阈值设置为当前窗口的一半,窗口大小设置为1,进入慢启动阶段】
DNS过程/URL解析过程/访问一个网页的过程
查询浏览器本地缓存有没有 域名IP的映射
查询本地host文件有无 域名IP的映射
向本地DNS服务器发起域名查询,本地DNS服务器会先检查自己管辖范围是否有查询域名,在检查自己的dns缓存
迭代模式:dns服务器查询根域名服务器——顶级域名服务器——二级域名服务器——三级域名服务器…,直到查找到结果,本地dns服务器会缓存解析结果
递归模式:dns服务器查询上一级、上上一级…直到根域名服务器
ICMP应用
traceroute追踪路径、ping查看对方网络是否有响应以及响应速度,达到测试的目的
traceroute
故意设置特殊的 TTL,来追踪去往目的地时沿途经过的路由器;它的原理就是利用 IP 包的生存期限 从 1 开始按照顺序递增的同时发送 UDP 包,强制接收 ICMP 超时消息的一种方法。比如,将 TTL 设置 为 1,则遇到第一个路由器,就牺牲了,接着返回 ICMP 差错报文网络包,类型是时间超时。接下来将 TTL 设置为 2,第一个路由器过了,遇到第二个路由器也牺牲了,也同时返回了 ICMP 差错报文数据包,如此往复,直到到达目的主机。这样的过程,traceroute 就可以拿到了所有的路由器 IP。
发送方如何知道发出的 UDP 包是否到达了目的主机呢?
答:traceroute 在发送 UDP 包时,会填入一个不可能的端口号值作为 UDP 目标端口号(大于 3000 )。当目的主机,收到 UDP 包后,会返回 ICMP 差错报文消息,但这个差错报文消息的类型是「端口不可达」。所以,当差错报文类型是端口不可达时,说明发送方发出的 UDP 包到达了目的主机。
故意设置不分片,从而确定路径的 MTU;首先在发送端主机发送 IP 数据报时,将 IP 包首部的分片禁止标志位设置为 1。根据这个标志位,途中的路由器不会对大数据包进行分片,而是将包丢弃。随后,通过一个 ICMP 的不可达消息将数据链路上 MTU 的值一起给发送主机,不可达消息的类型为「需要进行分片但设置了不分片位」。发送主机端每次收到 ICMP 差错报文时就减少包的大小,以此来定位一个合适的 MTU 值,以便能到达目标主机。
ping过程
在主机A执行ping命令向主机B通信的时候,主机A会构建一个ICMP回送请求消息数据报,这个数据报最重要的字段是类型和序号。回送消息请求的类型是8,序号的作用是为了区分连续ping的时候发出的多个数据包。因此每发送一个请求数据包,序号就会自动加1,同时为了计算往返时间RTT,还在报文的数据部分插入发送时间;在前面添加IP头部、MAC头部,在物理层传输;
- 主机B收到数据包
首先检查MAC地址然后和本机MAC地址进行比对,如果相同则接受,不同则丢弃
接收后提取IP数据包交给本机IP层协议栈处理,抽出IP后然后抽出ICMP检查
接着主机B会构建一个ICMP回送响应消息数据包,类型是0,序号为接受请求数据包中的序号,然后和上面一样的流程发送给主机A
在规定时间内如果收到,则减去该数据包最初的时间得到延迟
- 通过以上可以看出,ping命令使用的是ICMP协议中类型字段ECHO REQUEST(类型为 8 ) 和 ECHO REPLY (类型为 0)
DHCP过程
- 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
- DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
- 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
- DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。
路由协议
http请求报文
请求行【GET URL HTTP协议】
请求头部
/n
请求主体
http响应报文
响应头【HTTP协议 200 OK】
响应头部
/n
响应主体
TLS握手过程
- 客户端发送 随机数1、支持的加密算法、认证算法、密钥交换算法
- 服务端发送 随机数2、选择的算法、证书(公钥+申请者信息+颁布者信息)、证书签名(对证书进行hash的结果,再对这个结果进行用CA私钥加密)
- 客户端验证证书(用公钥解密证书签名得到hash结果,自己对证书进行hash计算,把两者进行对比),客户端产生 随机数3,且根据随机数123和密钥生成算法得到对称密钥
- 客户端发送 用对称密钥加密的{对前面的所有握手信息取hash的结果}、用服务端公钥加密的随机数3
- 服务端用私钥解密得到随机数3,根据随机数123和密钥生成算法得到对称密钥,验证握手信息(用对称密钥解密客户端发来的握手信息hash值,自己对前面的握手信息hash值进行对比)
- 服务端发送 用对称密钥加密的前面的所有握手信息hash值
- 客户端结算一遍hash值与收到的hash值对比
- 握手完成,用对称密钥进行后续通信
TLS1.3握手过程
【密钥协商(1.3 版本),好像就是上面的,上面的还有身份验证,所以看着很多】
从图中可以看出,TLS 握手需要 1 个 RTT,也就是 1 次 RTT 就把通信密钥协商好了,这是怎么做到的?
(1)客户端:生成随机数 a,选择公开的大数 G 和 P,计算 A=a*G%P,将 A 和 G 发送给服务器,也就是 Client Hello 消息
(2)服务器:生成随机数 b,计算 B=b*G%P,将 B 发送给客户端,也就是 Server Hello 消息
(3)客户端:使用 ECDH 算法生成通信密钥 KEY = aB = ab*G%P
(4)服务器:使用 ECDH 算法生成通信密钥 KEY = bA = ba*G%P
所以,这里的关键就是 ECDH 算法,a 和 b 是客户端和服务器的私钥,是不公开的,而其他参数是公开的。
ECDH 算法有个非常重要的特征:即使知道 A、G、P,通过 A = a*G%P 公式也是无法推到出 a 的,保证了私钥的安全性。
身份认证过程
我们需要验证公钥是否真的来自于服务器。这时候我们想到可以找一个受信的第三方(也就是CA),把公钥,域名,第三方机构等信息放在一起做一次hash(做hash的目的是把这些信息变成定长的比特串,降低非对称加密所需的时间,因为如果内容很长的话加密时间也会很长),再用CA的私钥加密,得到一个对内容的签名,由于中间人无法得到CA的信任,因此即使中间人可以伪造证书,也无法伪造签名。
现在,服务器直接发送证书和证书签名,当客户机收到证书和签名时,他首先找到是哪个CA发行了证书,再用CA的公钥解密签名,与客户机自己对证书生成的hash对比,如果一致就说明这个公钥是可行的,可以确定是服务器传过来的公钥了!(证书
quic的数据包结构
一个 QUIC 数据包的格式如下:
由 header 和 data 两部分组成。
header 是明文的,包含 4 个字段:Flags、Connection ID、QUIC Version、Packet Number;
data 是加密的,可以包含 1 个或多个 frame,每个 frame 又分为 type 和 payload,其中 payload 就是应用数据;
数据帧有很多类型:Stream、ACK、Padding、Window_Update、Blocked 等,这里重点介绍下用于传输应用数据的 Stream 帧。
Frame Type: 帧类型,占用 1 个字节
(1)Bit7:必须设置为 1,表示 Stream 帧
(2)Bit6:如果设置为 1,表示发送端在这个 stream 上已经结束发送数据,流将处于半关闭状态
(3)Bit5:如果设置为 1,表示 Stream 头中包含 Data length 字段
(4)Bit432:表示 offset 的长度。000 表示 0 字节,001 表示 2 字节,010 表示 3 字节,以此类推
(5)Bit10:表示 Stream ID 的长度。00 表示 1 字节,01 表示 2 字节,10 表示 3 字节,11 表示 4 字节
Stream ID: 流 ID,用于标识数据包所属的流。后面的流量控制和多路复用会涉及到
**Offset:**偏移量,表示该数据包在整个数据中的偏移量,用于数据排序。
Data Length: 数据长度,占用 2 个字节,表示实际应用数据的长度
Data: 实际的应用数据
quic过程
首次需要1RTT(TLS密钥协商),后续可以达到0RTT
ps:HTTPS 建立连接需要 3 个 RTT,由于 QUIC 的握手是基于 TLS1.3 实现的,所以首次建立连接时也是需要 1 次 RTT,那 QUIC 是如何做到 0-RTT 握手的呢?
客户端缓存了 ServerConfig(B=b*G%P),下次建连直接使用缓存数据计算通信密钥
(1)客户端:生成随机数 c,选择公开的大数 G 和 P,计算 A=c*G%P,将 A 和 G 发送给服务器,也就是 Client Hello 消息
(2)客户端:客户端直接使用缓存的 ServerConfig 计算通信密钥 KEY = cB = cb*G%P,加密发送应用数据
(3)服务器:根据 Client Hello 消息计算通信密钥 KEY = bA = bc*G%P
也就是说,客户端不需要经过握手就可以发送应用数据,这就是 0-RTT 握手。
再来思考一个问题:假设攻击者记录下所有的通信数据和公开参数(A1=aG%P,A2=cG%P,......),一旦服务器的随机数 b(私钥)泄漏了,那之前通信的所有数据就都可以破解了。
quic的前向安全:
为了解决上面的问题,需要为每次会话都创建一个新的通信密钥,来保证前向安全性
前向安全:是指用来产生会话密钥的长期密钥泄露出去,不会泄漏以前的通讯内容。
(1)客户端:生成随机数 a,选择公开的大数 G 和 P,计算 A=a*G%P,将 A 和 G 发送给服务器,也就是 Client Hello 消息
(2)客户端:客户端直接使用缓存的 ServerConfig 计算初始密钥 initKey = aB = ab*G%P,加密发送应用数据 1
(3)服务器:根据 Client Hello 消息计算初始密钥 initKey = bA = ba*G%P
(4)服务器:生成随机数 c,计算 C=c*G%P,使用 initKey 加密 C,发送给客户端,也就是 Server Hello 消息
(5)客户端:使用 initKey 解码获取 C,计算会话密钥 sessionKey = aC = ac*G%P,加密发送应用数据 2
(6)服务器:计算会话密钥 sessionKey = cA = ca*G%P,解密获取应用数据 2
客户端缓存的 ServerConfig 是服务器静态配置的,是可以长期使用的。
客户端通过 ServerConfig 实现 0-RTT 握手,使用会话密钥 sessionKey 保证通信数据的前向安全。
quic保证可靠性
(完整 + 有序)
QUIC 是基于 UDP 协议的,而 UDP 是不可靠传输协议,那 QUIC 是如何实现可靠传输的呢?
可靠传输有 2 个重要特点:
(1)完整性:发送端发出的数据包,接收端都能收到
(2)有序性:接收端能按序组装数据包,解码得到有效的数据
问题 1:发送端怎么知道发出的包是否被接收端收到了?
解决方案:通过包号(PKN)和确认应答(SACK)
(1)客户端:发送 3 个数据包给服务器(PKN = 1,2,3)
(2)服务器:通过 SACK 告知客户端已经收到了 1 和 3,没有收到 2
(3)客户端:重传第 2 个数据包(PKN=4)
由此可以看出,QUIC 的数据包号是单调递增的。也就是说,之前发送的数据包(PKN=2)和重传的数据包(PKN=4),虽然数据一样,但包号不同。
问题 2:既然包号是单调递增的,那接收端怎么保证数据的有序性呢?
解决方案:通过数据偏移量 offset
每个数据包都有一个 offset 字段,表示在整个数据中的偏移量。
接收端根据 offset 字段就可以对异步到达的数据包进行排序了。
ps:为什么 QUIC 要将 PKN 设计为单调递增?
解决 TCP 的重传歧义问题:由于原始包和重传包的序列号是一样的,客户端不知道服务器返回的 ACK 包到底是原始包的,还是重传包的。但 QUIC 的原始包和重传包的序列号是不同的,也就可以判断 ACK 包的归属。
quic的流量控制
(也是通过滑动窗口控制,但略有不同)
和 TCP 一样,QUIC 也是利用滑动窗口机制实现流量控制:
发送端的窗口大小由接收端告知,包括发送窗口和可用窗口,如果发送端收到了接收端的 ACK 确认应答(比如 ACK 36),那整个窗口就会向右滑动,发送新的数据包。
和 TCP 不同的是,QUIC 的滑动窗口分为 Connection 和 Stream 两种级别。
Connection 流量控制:规定了所有数据流的总窗口大小;Stream 流量控制:规定了每个流的窗口大小。
假设现在有 3 个 Stream,滑动窗口分别如下:
则整个 Connection 的可用窗口大小为:20+30+10 = 60
quic的拥塞控制
(和TCP一样)
拥塞控制是通过拥塞窗口限制发送方的数据量,避免整个网络发生拥塞。
那拥塞窗口(cwnd)和滑动窗口(发送窗口:swnd,接收窗口:rwnd)有什么关系呢?
swnd = min(cwnd,rwnd)
也就是说,发送窗口的大小是由接收窗口和拥塞窗口共同决定的。
那拥塞窗口的大小是如何计算的?通过 4 个拥塞控制算法:慢启动、拥塞避免、拥塞发生、快速恢复
慢启动:
初始拥塞窗口大小 cwnd=1,也就是可以传输 1 个 MDS(Max Datagram Size)大小的数据包,一般网卡允许传输的最大数据单元 MTU 的大小是 1500 字节。
对于 UDP 数据报而言:MDS = 1500(MTU)- 20(IP 首部)- 8(UDP 首部) = 1472 字节
慢启动算法: 当发送方每收到一个 ACK,拥塞窗口就加 1(cwnd++)指数增加
由此可以看出,慢启动阶段,拥塞窗口呈指数增长,那增长到多少是个头?
有一个上限值:ssthresh(slow start threshold),从源码看,这个值是 2000 * MDS
const QuicPacketCount kDefaultMaxCongestionWindowPackets = 2000;
- 当 cwnd < ssthresh 时,使用慢启动算法
- 当 cwnd >= ssthresh 时,使用拥塞避免算法
拥塞避免:
当拥塞窗口大小超过慢启动上限后,就会进入拥塞避免阶段。
拥塞避免算法: 当发送方每收到一个 ACK,拥塞窗口就加 1
假设现在的 cwnd=8,可以发送 8 个数据包,当收到这 8 个包的 ACK 时,拥塞窗口才会加 1,由此可知,在拥塞避免阶段,拥塞窗口是线性增长的。
那啥时候是个头呢?不管,让它继续增长,直到网络发生拥塞,出现丢包,这时就会触发重传机制,进入拥塞发生阶段
拥塞发生——超时重传:
重传有 2 种:超时重传和快速重传
如果发生超时重传,使用的拥塞发生算法为:
重新使用慢启动和拥塞避免算法增加拥塞窗口的大小。
如果发生快速重传(发送方收到 3 个相同的 ACK),使用的拥塞发生算法为:
- ssthresh = cwnd / 2【阈值变为当前窗口的一半】
- cwnd = 1【窗口大小变为1】
重新使用慢启动和拥塞避免算法增加拥塞窗口的大小。
如果发生快速重传(发送方收到 3 个相同的 ACK),使用的拥塞发生算法为:
- cwnd = cwnd / 2【窗口变为当前窗口的一半+3】
- ssthresh = cwnd【阈值变为当前窗口的一半】
接下来就会进入快速恢复阶段。
快速恢复:
快速恢复算法:cwnd = ssthresh + 3(因为收到 3 个 ACK),然后进入拥塞避免阶段。
和 TCP 不同的是,QUIC 是在用户空间实现的拥塞控制,可以非常灵活的设置,甚至可以为每一个请求都设置一种拥塞控制算法。
quic的多路复用
多路复用是 HTTP/2 的主要特性之一。
概念:单条 TCP 连接上可以同时发送多个 HTTP 请求,解决了 HTTP1.1 中单个连接 1 次只能发送 1 个请求的性能瓶颈。
HTTP/2 能实现多路复用的根本原因是采用了二进制帧格式的数据结构。
- Length:表示 Payload 的长度
- Type:表示帧类型
- Flags:帧标识
- Stream ID:数据帧所属的流
- Payload:应用数据,长度由 Length 字段指定
一个请求就对应一条流,通过 Stream ID 就可以判断该数据帧属于哪个请求,假设有 A 和 B 两个请求,对应的 Stream ID 分别为 1 和 2,那这个 TCP 连接上传输的数据大概如下:
虽然在 HTTP 应用层,可以同时发送多个请求,但是在 TCP 传输层,仍然只有 1 个滑动窗口来发送这些数据包,考虑下面的情形:
客户端发送的 5 个数据包(56789)服务器都收到了,并且回应了 5 个 ACK,但是第 5 个数据包的 ACK 丢失了,导致客户端的发送窗口无法向前移动,也就无法发送新的数据,这就是 TCP 层的队头阻塞问题。
HTTP/2 虽然通过多路复用解决了 HTTP 层的队头阻塞,但仍然存在 TCP 层的队头阻塞。
那 QUIC 是如何解决 TCP 层的队头阻塞问题的呢?其实很简单,HTTP/2 之所以存在 TCP 层的队头阻塞,是因为所有请求流都共享一个滑动窗口,那如果给每个请求流都分配一个独立的滑动窗口,是不是就可以解决这个问题了?
QUIC 就是这么做的:
A 请求流上的丢包不会影响 B 请求流上的数据发送。但是,对于每个请求流而言,也是存在队头阻塞问题的,也就是说,
虽然 QUIC 解决了 TCP 层的队头阻塞,但仍然存在单条流上的队头阻塞。这就是 QUIC 声明的无队头阻塞的多路复用。
quic的连接迁移
连接迁移:当客户端切换网络时,和服务器的连接并不会断开,仍然可以正常通信,对于 TCP 协议而言,这是不可能做到的。
因为 TCP 的连接基于 4 元组:源 IP、源端口、目的 IP、目的端口,只要其中 1 个发生变化,就需要重新建立连接。
但 QUIC 的连接是基于 64 位的 Connection ID,网络切换并不会影响 Connection ID 的变化,连接在逻辑上仍然是通的。
假设客户端先使用 IP1 发送了 1 和 2 数据包,之后切换网络,IP 变更为 IP2,发送了 3 和 4 数据包,服务器根据数据包头部的 Connection ID 字段可以判断这 4 个包是来自于同一个客户端。
QUIC 能实现连接迁移的根本原因是底层使用 UDP 协议就是面向无连接的。
TCP握手
2个RTT
TLS握手
1个RTT
HTTPS建立连接
3个RTT
正向代理/反向代理
http1.0
无连接【tcp三次握手——传输数据——tcp四次挥手、tcp三次握手——传输数据——tcp四次挥手。。。】
对头阻塞:请求1——请求1的响应、请求2——请求2的响应【每个请求只能在前一个请求响应后才能发送】
缓存:主要使用If-Modified-Since/Last-Modified
域名host:没有host这个头部域【认为一个host对应一个ip】
断点续传:只支持200响应
http1.1
长链接:支持一个tcp链接可以处理多个http请求【tcp三次握手——数据传输——数据传输——数据传输——tcp四次挥手】
支持管道传输:允许客户端连续发送多个请求,不需要等待前一个请求响应就可以发送下一个请求,但是服务端必须按照接收请求的顺序进行响应【解决了请求的对头阻塞,但没有解决响应的队头阻塞】
缓存处理:引入了缓存控制策略,协商缓存etag\if-none-match、强缓存cache-control
支持range头部,206响应
新增状态码:409、410
host头处理:请求和响应都支持host头部,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
http2.0
头部压缩:客户端服务端同时维护一张头信息表,所有字段都会存在这个表中,生成一个索引号,发送的时候使用索引号就可以了
二进制格式:把头部和主体分别封装成帧,以数据流的形式发送【将请求分到了不同的stram中,然后将请求进行分帧,进行二进制传输,每个stram有自己的stram_id,可以让stream不用保证顺序的乱序发送,客户端会自己重组】
多路复用:可以在一个tcp链接中并发多个请求和响应【响应不需要按照顺序进行,解决了响应的队头阻塞】
服务端推送:可以主动向客户端发送消息、推送额外的资源【原本是客户端请求什么,服务端就响应什么】
tcp导致对头阻塞:虽然 HTTP/2.0 解决了队头阻塞问题,但是每个 HTTP 连接都是由 TCP 进行连接建立和传输的,TCP 协议在处理包时有严格的顺序要求。这也就是说,当某个包切分的 stream 由于某些原因丢失后,服务器不会处理其他 stream,而会优先等待客户端发送丢失的 stream 。举个例子来说,假如有一个请求有三个 stream,其中 stream2 由于某些原因丢失了,那么 stream1 和 stream 2 的处理也会阻塞,只有收到重发的 stream2 之后,服务器才会再次进行处理。
图中发送方发送了很多个 packet,每个 packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 packet 3 在网络中丢失了,即使 packet 4-6 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 packet 3 重传后,接收方的应用层才可以从内核中读取到数据,这就是 HTTP/2 的队头阻塞问题,是在 TCP 层面发生的。
https
TCP+TLS:tcp三次握手【syn—syn+ack—ack】+tls握手【ClientHello:client-随机数+客户端支持的加密算法/认证算法/密钥交换算法——ServerHello:server-随机数+选择的算法+证书——客户端验证证书,客户端产生随机数字pre-master,根据client-随机数+server-随机数+pre-master三个随机数和密钥生成算法生成对称密钥,把前面的握手信息得到hash值且用对称密钥加密,把随机数pre-master用服务端公钥加密——服务端用私钥解密得到pre-master,根据client-随机数+server-随机数+pre-master三个随机数和密钥生成算法生成对称密钥,用对称密钥解密握手信息得到hash值,与服务端自己计算前面所有握手信息的hash值进行比对,比对成功则也发送给客户端一份结合所有当前信息/握手信息的hash值用对称密钥加密后发送——客户端结算一遍hash值与解密得到的进行对比——握手完成,开始用对称密钥加密数据进行通信
quic前情提要
quic数据包格式:(和上面一样)
一个 QUIC 数据包的格式如下:
由 header 和 data 两部分组成。
header 是明文的,包含 4 个字段:Flags、Connection ID、QUIC Version、Packet Number;
data 是加密的,可以包含 1 个或多个 frame,每个 frame 又分为 type 和 payload,其中 payload 就是应用数据;
数据帧有很多类型:Stream、ACK、Padding、Window_Update、Blocked 等,这里重点介绍下用于传输应用数据的 Stream 帧。
Frame Type: 帧类型,占用 1 个字节
(1)Bit7:必须设置为 1,表示 Stream 帧
(2)Bit6:如果设置为 1,表示发送端在这个 stream 上已经结束发送数据,流将处于半关闭状态
(3)Bit5:如果设置为 1,表示 Stream 头中包含 Data length 字段
(4)Bit432:表示 offset 的长度。000 表示 0 字节,001 表示 2 字节,010 表示 3 字节,以此类推
(5)Bit10:表示 Stream ID 的长度。00 表示 1 字节,01 表示 2 字节,10 表示 3 字节,11 表示 4 字节
Stream ID: 流 ID,用于标识数据包所属的流。后面的流量控制和多路复用会涉及到
**Offset:**偏移量,表示该数据包在整个数据中的偏移量,用于数据排序。
Data Length: 数据长度,占用 2 个字节,表示实际应用数据的长度
Data: 实际的应用数据
quic握手过程:(和上面一样)
首次需要1RTT(TLS密钥协商),后续可以达到0RTT
ps:HTTPS 建立连接需要 3 个 RTT,由于 QUIC 的握手是基于 TLS1.3 实现的,所以首次建立连接时也是需要 1 次 RTT,那 QUIC 是如何做到 0-RTT 握手的呢?
客户端缓存了 ServerConfig(B=b*G%P),下次建连直接使用缓存数据计算通信密钥
(1)客户端:生成随机数 c,选择公开的大数 G 和 P,计算 A=c*G%P,将 A 和 G 发送给服务器,也就是 Client Hello 消息
(2)客户端:客户端直接使用缓存的 ServerConfig 计算通信密钥 KEY = cB = cb*G%P,加密发送应用数据
(3)服务器:根据 Client Hello 消息计算通信密钥 KEY = bA = bc*G%P
也就是说,客户端不需要经过握手就可以发送应用数据,这就是 0-RTT 握手。
再来思考一个问题:假设攻击者记录下所有的通信数据和公开参数(A1=aG%P,A2=cG%P,......),一旦服务器的随机数 b(私钥)泄漏了,那之前通信的所有数据就都可以破解了。
quic的前向安全
为了解决上面的问题,需要为每次会话都创建一个新的通信密钥,来保证前向安全性
前向安全:是指用来产生会话密钥的长期密钥泄露出去,不会泄漏以前的通讯内容。
(1)客户端:生成随机数 a,选择公开的大数 G 和 P,计算 A=a*G%P,将 A 和 G 发送给服务器,也就是 Client Hello 消息
(2)客户端:客户端直接使用缓存的 ServerConfig 计算初始密钥 initKey = aB = ab*G%P,加密发送应用数据 1
(3)服务器:根据 Client Hello 消息计算初始密钥 initKey = bA = ba*G%P
(4)服务器:生成随机数 c,计算 C=c*G%P,使用 initKey 加密 C,发送给客户端,也就是 Server Hello 消息
(5)客户端:使用 initKey 解码获取 C,计算会话密钥 sessionKey = aC = ac*G%P,加密发送应用数据 2
(6)服务器:计算会话密钥 sessionKey = cA = ca*G%P,解密获取应用数据 2
客户端缓存的 ServerConfig 是服务器静态配置的,是可以长期使用的。
客户端通过 ServerConfig 实现 0-RTT 握手,使用会话密钥 sessionKey 保证通信数据的前向安全。
http3.0【quic】
Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势 :
- 减少了 TCP 三次握手及 TLS 握手时间【可实现1RTT 或者 0RTT】
- 单调递增的包ID【数据完整性】
- stream offset【数据的有序性】
- 基于 stream 和 connecton 级别的流量控制【Connection ——规定了所有数据流的总窗口大小;Stream ——规定了每个流的窗口大小】
- 改进的拥塞控制【应该就是9】
- 避免队头阻塞的多路复用【给每个请求流都分配一个独立的滑动窗口】
- 连接迁移【四元组更改不会影响连接】
- 前向冗余纠错【在重要的包比如握手消息发生丢失时,能够根据冗余信息还原出握手消息】
- 可插拔【应用层面可实现不同的拥塞控制算法,且修改配置+reload就可以生效】
- 不允许 Reneging【不允许接收方丢弃已经接收并且上报给 SACK 选项的内容】
- 更多的 Ack 块【Quic Ack Frame 可以同时提供 256 个 Ack Block】
- Ack Delay 时间【接收端收到seq到发送ack中间的时间】
- 加密认证的报文【quic的所有报文头部都是经过认证的,报文 Body 都是经过加密的】
基于udp:基于udp的握手更快,且允许客户端无需等待TLS握手完成就可以发送数据,支持1RTT,0RTT,达到快速建立链接的效果【https会导致三次握手的时间延迟【TCP一次,TLS两次】
解决tcp对头阻塞:给每个请求流都分配一个独立的滑动窗口
QUIC 就是这么做的:
A 请求流上的丢包不会影响 B 请求流上的数据发送。但是,对于每个请求流而言,也是存在队头阻塞问题的,也就是说,
虽然 QUIC 解决了 TCP 层的队头阻塞,但仍然存在单条流上的队头阻塞。这就是 QUIC 声明的无队头阻塞的多路复用。
--------------------------------------------------------------------------------
QUIC 的多路复用和 HTTP2 类似。在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (stream)。但是 QUIC 的多路复用相比 HTTP2 有一个很大的优势。
QUIC 一个连接上的多个 stream 之间没有依赖。这样假如 stream2 丢了一个 udp packet,也只会影响 stream2 的处理。不会影响 stream2 之前及之后的 stream 的处理。
这也就在很大程度上缓解甚至消除了队头阻塞的影响。
多路复用是 HTTP2 最强大的特性 [7],能够将多条请求在一条 TCP 连接上同时发出去。但也恶化了 TCP 的一个问题,队头阻塞 ,如下图示:
图 6 HTTP2 队头阻塞
HTTP2 在一个 TCP 连接上同时发送 4 个 Stream。其中 Stream1 已经正确到达,并被应用层读取。但是 Stream2 的第三个 tcp segment 丢失了,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据,虽然这个时候 Stream3 和 Stream4 的全部数据已经到达了接收端,但都被阻塞住了。
不仅如此,由于 HTTP2 强制使用 TLS,还存在一个 TLS 协议层面的队头阻塞。
Record 是 TLS 协议处理的最小单位,最大不能超过 16K,一些服务器比如 Nginx 默认的大小就是 16K。
由于一个 record 必须经过数据一致性校验才能进行加解密,所以一个 16K 的 record,就算丢了一个字节,也会导致已经接收到的 15.99K 数据无法处理,因为它不完整。
那 QUIC 多路复用为什么能避免上述问题呢?
- QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞。
- Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 和 Stream4。不存在 TCP 队头阻塞。
当然,并不是所有的 QUIC 数据都不会受到队头阻塞的影响,比如 QUIC 当前也是使用 Hpack 压缩算法 [10],由于算法的限制,丢失一个头部数据时,可能遇到队头阻塞。
总体来说,QUIC 在传输大量数据时,比如视频,受到队头阻塞的影响很小。
数据包ID:序列号是递增的,不管服务器有没有接收到数据包,这个 Packet Number 都会 + 1,而 syn 是只有服务器发送 ack 响应之后,syn 才会 + 1【数据包的可靠性】
Ps:数据包ID+1可以解决TCP重传导致的采样RTT歧义问题:
如上图所示,超时事件 RTO 发生后,客户端发起重传,然后接收到了 Ack 数据。由于序列号一样,这个 Ack 数据到底是原始请求的响应还是重传请求的响应呢?不好判断。
如果算成原始请求的响应,但实际上是重传请求的响应(上图左),会导致采样 RTT 变大。如果算成重传请求的响应,但实际上是原始请求的响应,又很容易导致采样 RTT 过小。
由于 Quic 重传的 Packet 和原始 Packet 的 Pakcet Number 是严格递增的,所以很容易就解决了这个问题。
如上图所示,RTO 发生后,根据重传的 Packet Number 就能确定精确的 RTT 计算。如果 Ack 的 Packet Number 是 N+M,就根据重传请求计算采样 RTT。如果 Ack 的 Pakcet Number 是 N,就根据原始请求的时间计算采样 RTT,没有歧义性。
stream-offset:一个 stream 可以传输多个 stream offset,每个 stream offset 其实就是一个 PN 标识的数据,即使某个 PN 标识的数据丢失,PN + 1 后,它重传的仍旧是 PN 所标识的数据,等到所有 PN 标识的数据发送到服务器,就会进行重组,以此来保证数据可靠性。到达服务器的 stream offset 会按照顺序进行组装,这同时也保证了数据的顺序性。
基于 stream 和 connecton 级别的流量控制:
QUIC 的流量控制 类似 HTTP2,即在 Connection 和 Stream 级别提供了两种流量控制。
为什么需要两类流量控制呢?主要是因为 QUIC 支持多路复用。
- Stream 可以认为就是一条 HTTP 请求。
- Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。
既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。
QUIC 实现流量控制的原理比较简单:
通过 window_update 帧告诉对端自己可以接收的字节数,这样发送方就不会发送超过这个数量的数据。
通过 BlockFrame 告诉对端由于流量控制被阻塞了,无法发送数据。
QUIC 的流量控制和 TCP 有点区别,TCP 为了保证可靠性,窗口左边沿向右滑动时的长度取决于已经确认的字节数。如果中间出现丢包,就算接收到了更大序号的 Segment,窗口也无法超过这个序列号。
但 QUIC 不同,就算此前有些 packet 没有接收到,它的滑动只取决于接收到的最大偏移字节数。
针对 Stream:
针对 Connection:
同样地,STGW 也在连接和 Stream 级别设置了不同的窗口数。
最重要的是,我们可以在内存不足或者上游处理性能出现问题时,通过流量控制来限制传输速率,保障服务可用性。
实现动态可插拔,在应用层实现了拥塞控制算法,可以随时切换
什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。体现在如下方面:
- 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。
- 即使是单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。
- 应用程序不需要停机和升级就能实现拥塞控制的变更,我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。
STGW 在配置层面进行了优化,我们可以针对不同业务,不同网络制式,甚至不同的 RTT,使用不同的拥塞控制算法。
QUIC 中的报文头部都是经过认证,报文也经过加密处理。这样只要对 QUIC 的报文有任何修改,接收端都能够及时发现,保证了安全性
连接能够平滑迁移:wifi和4G在切换的时候应用层是感知不到的【原本的tcp链接是原IP-目的IP-源port-目的port来确定一个tcp连接,客户端由wifi切换到4G,源IP更改了,就要重新建立连接,quic基于udp实现的,不需要四元组确定唯一连接,而是根据连接的req_id唯一标识连接】
QUIC 也是需要三次握手来建立连接的,主要目的是为了确定连接 ID。
------------------------------------------------------------------------
一条 TCP 连接 是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。
比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。
又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。
针对 TCP 的连接变化,MPTCP[5] 其实已经有了解决方案,但是由于 MPTCP 需要操作系统及网络协议栈支持,部署阻力非常大,目前并不适用。
所以从 TCP 连接的角度来讲,这个问题是无解的。
那 QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。
由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
更多的 Ack 块:
TCP 的 Sack 选项能够告诉发送方已经接收到的连续 Segment 的范围,方便发送方进行选择性重传。
由于 TCP 头部最大只有 60 个字节,标准头部占用了 20 字节,所以 Tcp Option 最大长度只有 40 字节,再加上 Tcp Timestamp option 占用了 10 个字节 [25],所以留给 Sack 选项的只有 30 个字节。
每一个 Sack Block 的长度是 8 个,加上 Sack Option 头部 2 个字节,也就意味着 Tcp Sack Option 最大只能提供 3 个 Block。
但是 Quic Ack Frame 可以同时提供 256 个 Ack Block,在丢包率比较高的网络下,更多的 Sack Block 可以提升网络的恢复速度,减少重传量。
Ack Delay 时间:
Tcp 的 Timestamp 选项存在一个问题 ,它只是回显了发送方的时间戳,但是没有计算接收端接收到 segment 到发送 Ack 该 segment 的时间。这个时间可以简称为 Ack Delay。
这样就会导致 RTT 计算误差。如下图:
可以认为 TCP 的 RTT 计算:
而 Quic 计算如下:
当然 RTT 的具体计算没有这么简单,需要采样,参考历史数值进行平滑计算,参考如下公式 [9]。
加密认证的报文:
TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。
这些行为有可能是出于性能优化,也有可能是主动攻击。
但是 QUIC 的 packet 可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。
这样只要对 QUIC 报文任何修改,接收端都能够及时发现,有效地降低了安全风险。
如下图所示,红色部分是 Stream Frame 的报文头部,有认证。绿色部分是报文内容,全部经过加密。
其他亮点
此外,QUIC 还能实现前向冗余纠错,在重要的包比如握手消息发生丢失时,能够根据冗余信息还原出握手消息。
QUIC 还能实现证书压缩,减少证书传输量,针对包头进行验证等。
http协议层次结构图
quic相关知识是学习的这两个大佬的文章,文章链接: