<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="https://www.gentoo-zh.org/extern.php?action=feed&amp;tid=477&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / linux源码解读（三十）：quic协议分析（一）]]></title>
		<link>https://www.gentoo-zh.org/viewtopic.php?id=477</link>
		<description><![CDATA[linux源码解读（三十）：quic协议分析（一） 最近发表的帖子。]]></description>
		<lastBuildDate>Wed, 12 Oct 2022 02:42:19 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（三十）：quic协议分析（一）]]></title>
			<link>https://www.gentoo-zh.org/viewtopic.php?pid=484#p484</link>
			<description><![CDATA[<p>1、网络通信时，为了确保数据不丢包，早在几十年前就发明了tcp协议！然而此一时非彼一时，随着技术进步和业务需求增多，tcp也暴露了部分比较明显的缺陷，比如:</p><p>&#160; &#160; &#160;建立连接的3次握手延迟大； TLS需要至少需要2个RTT，延迟也大<br />&#160; &#160; &#160; &#160; 协议缺陷可能导致syn反射类的DDOS攻击<br />&#160; &#160; &#160; &#160; tcp协议紧耦合到了操作系统，升级需要操作系统层面改动，无法快速、大面积推广升级补丁包<br />&#160; &#160; &#160; &#160; 对头阻塞：数据被分成sequence，一旦中间的sequence丢包，后面的sequence也不会处理<br />&#160; &#160; &#160; &#160; 中转设备僵化：路由器、交换机等设备“认死理”，比如只认80、443等端口，其他端口一律丢弃</p><p>&#160; 为了解决这些问题，牛逼plus的google早在10年前，也就是2012年发布了基于UDP的quic协议！为啥不基于tcp了，因为tcp有上述5条缺陷的嘛，所以干脆“另起炉灶”重新开搞！</p><p>&#160; &#160; &#160; &#160;2、正式介绍前，先看一张图：quci在右边，底层用了udp的协议；自生实现了Multistreaming、tls、拥塞控制，然后支撑了上层的http/2，所以我个人理解quic是一个夹在应用层和传输层之间的协议！</p><p><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220313162917852-822623337.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;上面“数落”了tcp协议的5点不是，quic又是怎么基于udp解决这些问题的了？quic 是基于 UDP 实现的协议，而 UDP 是不可靠的面向报文的协议，这和 TCP 基于 IP 层的实现并没有什么本质上的不同，都是：</p><p>&#160; &#160; 底层只负责尽力而为的，以 packet 为单位的传输;<br />&#160; &#160; 上层协议实现更关键的特性，如可靠，有序，安全等。</p><p>&#160; （1）由于quic并未改造udp，而是直接使用udp，所以不需要改动现有的操作系统，也兼容了现有的网络中转设备，这些都不需要做任何改动，所以quic部署的改造成本相对较低！但是quic毕竟是新的协议，在哪部署和使用了？只有应用层了！这个和操作系统是解耦的，全靠3环的app自己想办法实现（和之前介绍的协程是不是类似了？）！google已经开源了算法，下载连接见文章末尾的参考5；PS：微软也实现了QUIC协议，名称叫MsQuic，源码在这：https://github.com/microsoft/msquic；</p><p>&#160; 这里多说几句：应用层app能操作的最底层协议就是传输层了。大家在用libc库编写通信代码时可以对指定的ip地址和端口收发数据，没法改自己的mac地址吧？也没法改自己的ip地址吧？这些都是操作系统内核封装的，app的开发人员是不需要、也是没法改变的，所以站在安全防护的角度，部分大厂基于传输层自研了类似quic的通信协议，逆向时需要人工挨个分析协议字段的含义了，现成的fiddler/charles/burpsuit等https/http的抓包工具是无效的，用wireshark这类工具抓包也无法自动解析这些厂家自研的协议！</p><p>&#160; &#160; （2）TCP连接需要3次握手，tls最少需要2次RTT，两个加起来一共要耗费5个RTT，究其原因一方面是 TCP 和 TLS 分层设计导致的：分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是 TLS 的握手阶段复杂的密钥协商机制导致的，quic又是怎么改进的了？quic建立握手的步骤如下：</p><p>&#160; &#160; 客户端判断本地是否已有服务器的全部配置参数（证书配置信息），如果有则直接跳转到(5)，否则继续 。<br />&#160; &#160; 客户端向服务器发送 inchoate client hello(CHLO) 消息，请求服务器传输配置参数。<br />&#160; &#160; 服务器收到 CHLO，回复 rejection(REJ) 消息，其中包含服务器的部分配置参数<br />&#160; &#160; 客户端收到 REJ，提取并存储服务器配置参数，跳回到 (1) 。<br />&#160; &#160; 客户端向服务器发送 full client hello 消息，开始正式握手，消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数，可以计算出初始密钥 K1。<br />&#160; &#160; 服务器收到 full client hello，如果不同意连接就回复 REJ，同(3)；如果同意连接，根据客户端的公开数计算出初始密钥 K1，回复 server hello(SHLO) 消息， SHLO 用初始密钥 K1 加密，并且其中包含服务器选择的一个临时公开数。<br />&#160; &#160; 客户端收到服务器的回复，如果是 REJ 则情况同(4)；如果是 SHLO，则尝试用初始密钥 K1 解密，提取出临时公开数。<br />&#160; &#160; 客户端和服务器根据临时公开数和初始密钥 K1，各自基于 SHA-256 算法推导出会话密钥 K2。<br />&#160; &#160; 双方更换为使用会话密钥 K2 通信，初始密钥 K1 此时已无用，QUIC 握手过程完毕。之后会话密钥 K2 更新的流程与以上过程类似，只是数据包中的某些字段略有不同。这里为啥不继续使用key1，而是要重新生成key2来加密了？核心是为了前向安全！万一key1泄漏，之前用key1加密的数据全都被解密。所以为了前向安全，每次通信时会重新生成key2加密！<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220313175256398-1702007794.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; &#160; &#160; 总的来说：</p><p>&#160; &#160; udp本身就不是面向连接的协议，所以省略了tcp 3次握手连接的耗时；直接通过事先内置的服务器参数发起通信请求；<br />&#160; &#160; 既然不是面向连接的，怎么确保所有的数据都能到达了？通过stream id和stream offset确保数据包不会丢失，接收方能收到完整的全量数据<br />&#160; &#160; 第一次用DH算法计算对称加密的密钥需要1个RTT；后续每次都用这个缓存的密钥加密，又省了一个RTT；本质上是把tcp的打招呼、握手，还有tls交换密钥的工作在1个RTT中全做了，这就是相比于tcp实现的tls效率高的根本原因！</p><p>&#160; &#160; &#160; &#160; &#160; 注意：通信双方用于密钥交换的DH算法无法防止中间人攻击，所以仅通过密钥交换是无法防止被抓包的，所以还要通过证书等其他方式验证身份！x音就是通过libboringssl.so（google开源的一个openssl分支）SSL_CTX_set_custom_verify函数验证客户端是否是原来的client，而不是抓包软件！</p><p>&#160; （3）拥塞控制：QUIC 使用可插拔的拥塞控制，相较于 TCP，它能提供更丰富的拥塞控制信息。比如对于每一个包，不管是原始包还是重传包，都带有一个新的序列号(seq)，这使得 QUIC 能够区分 ACK 是重传包还是原始包，从而避免了 TCP 重传模糊的问题。QUIC 同时还带有收到数据包与发出 ACK 之间的时延信息。这些信息能够帮助更精确的计算 RTT！同时，因为quic不依赖操作系统，而是在应用层实现，所以开发人员对于quic有非常强的操控能力：完全可以根据不同的业务场景，实现和配置不同的拥塞控制算法以及参数；比如Google 提出的 BBR&#160; 拥塞控制算法与 CUBIC 是思路完全不一样的算法，在弱网和一定丢包场景，BBR 比 CUBIC 更不敏感，性能也更好；</p><p>&#160; &#160; &#160; （4）队头阻塞：TCP 为了保证可靠性，使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达；一旦中间某个sequence的包丢失，哪怕是这个sequence后面的数据已经到达接收端，操作系统也不会立即把数据发给上层的应用来接受处理，而是一直等待发送端重新发送丢失的sequence包，举例如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220313192726526-1840422379.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; &#160;</p><p>&#160; 应用层可以顺利读取 stream1 中的内容，但由于 stream2 中的第三个 segment 发生了丢包，TCP 为了保证数据的可靠性，需要发送端重传第 3 个&#160; segment 才能通知应用层读取接下去的数据。所以即使 stream3、stream4 的内容已顺利抵达，应用层仍然无法读取，只能等待 stream2 中丢失的包进行重传。在弱网环境下，HTTP2 的队头阻塞问题在用户体验上极为糟糕！quic是怎么既确保数据传输可靠不丢失，又解决队头阻塞的这个问题的了？</p><p>&#160; 对于数据包的传输，肯定是要编号的，否则接受方在拼接这些数据包的时候怎么知道顺序了？quic协议用Packet Number 代替了 TCP 的 Sequence Number，不同之处在于：</p><p>&#160; &#160; 每个 Packet Number 都严格递增，也就是说就算 Packet N 丢失了，重传的 Packet N 的 Packet Number 已经不是 N，而是一个比 N 大的值，比如Packet N+M；<br />&#160; &#160; 数据包支持乱序确认，不再要求 TCP 那样必须有序确认</p><p>&#160; &#160;当数据包 Packet N 丢失后，只要有新的已接收数据包确认，当前窗口就会继续向右滑动。待发送端获知数据包 Packet N 丢失后，会将需要重传的数据包放到待发送队列，重新编号比如数据包 Packet N+M 后重新发送给接收端，对重传数据包的处理跟发送新的数据包类似，这样就不会因为丢包重传将当前窗口阻塞在原地，从而解决了队头阻塞问题；但是问题又来了：怎么确认Package N+M就是重传PackageN的数据包了？这就涉及到quic另一个重要的特性了：多路复用！比如用户访问某个网页，这个页面有两个文件，分别是index.htm和index.js，可以同时、分别传输这两个文件！每个传输的stream都有各自的id，所以可以通过id确认是哪个stream超时丢包了！但包的Packet 编号是N+M，怎么进一步确认就是重传的Packet N包了？这就需要另一个重要的变量了：offset！怎么样，单从英语是不是就能猜到这个变量的作用了？每个数据包都有个offset字段，用于标识在stream id中的偏移！接收方完全可以根据offset来拼接收到的数据包！</p><p>&#160; &#160; &#160; &#160;总结：quic协议可以在乱序发送的情况下任然可靠不丢失，靠的就是每个数据包的offset字段；再搭配上stream id字段，接收方完全可以在乱序的情况下无误拼接收到的数据包了！</p><p>&#160; （4）除了以上通过stream id和stream offset确保数据不丢失外，quic还采用了另一个叫向前纠错 (Forward Error Correction，FEC)的校验方式：即每个数据包除了它本身的内容之外，还包括了部分其他数据包的数据，因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。向前纠错牺牲了每个数据包可以发送数据的上限，但是减少了因为丢包导致的数据重传，因为数据重传将会消耗更多的时间(包括确认数据包丢失、请求重传、等待新数据包等步骤的时间消耗)；这个原理和纠删码没有本质区别！</p><p>&#160; （5）通信双方不论使用何种协议，发送的数据必须事前约定好格式，否则接受方怎么从数据包（本质就是一段字符串）中解析和提取关键的信息了？quic协议的格式如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220313211617270-230300579.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;数据包中除了个别报文比如 PUBLIC_RESET 和 CHLO，所有报文头部（上图红色部分）都是经过认证的（哈希散列值），报文 Body （上图绿色部分）都是经过加密的，这样只要对 QUIC 报文任何修改，接收端都能够及时发现；每个字段的含义如下：</p><p>&#160; &#160; Flags：用于表示 Connection ID 长度、Packet Number 长度等信息；<br />&#160; &#160; Connection ID：客户端随机选择的最大长度为64位的无符号整数，用于标识连接；如果app更换了ip地址（比如wifi和4G之间切换了），仍然可以通过这个id和服务端在0 RTT下通信！<br />&#160; &#160; QUIC Version：QUIC 协议的版本号，32 位的可选字段。如果 Public Flag &amp; FLAG_VERSION != 0，这个字段必填。客户端设置 Public Flag 中的 Bit0 为1，并且填写期望的版本号。如果客户端期望的版本号服务端不支持，服务端设置 Public Flag 中的 Bit0 为1，并且在该字段中列出服务端支持的协议版本（0或者多个），并且该字段后不能有任何报文；<br />&#160; &#160; Packet Number：长度取决于 Public Flag 中 Bit4 及 Bit5 两位的值，最大长度 6 字节。发送端在每个普通报文中设置 Packet Number。发送端发送的第一个包的序列号是 1，随后的数据包中的序列号的都大于前一个包中的序列号；<br />&#160; &#160; Stream ID：用于标识当前数据流属于哪个资源请求，用于消除队头阻塞；<br />&#160; &#160; Offset：标识当前数据包在当前 Stream ID 中的字节偏移量，用于消除队头阻塞。</p><p>&#160; （6）为了便于理解和记忆，这里把quic的要点做了总结，如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220313214503240-179330877.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 3、正式因为quic有这么多优点，国内很多互联网一、二线厂商都开始采用，其中比较著名的app就是x音了！lib库中有个libsscronet.so就支持quic协议！</p><p> </p><p>参考：</p><p>1、 <a href="https://zhuanlan.zhihu.com/p/32553477" rel="nofollow">https://zhuanlan.zhihu.com/p/32553477</a><br />2、https://www.bilibili.com/video/BV1fr4y1F7BD/<br />3、https://www.sofastack.tech/blog/deeper-into-http/3-evolution-of-the-protocol-from-the-creation-and-closing-of-quic-links/<br />4、https://cloud.tencent.com/developer/article/1802343 quic协议浅析<br />5、https://www.chromium.org/developers/how-tos/get-the-code/&#160; chromium内核源码<br />6、https://www.shangmayuan.com/a/cfe4bc1f10b147aab9ccac26.html&#160; cronet用例与原理实践</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Oct 2022 02:42:19 +0000</pubDate>
			<guid>https://www.gentoo-zh.org/viewtopic.php?pid=484#p484</guid>
		</item>
	</channel>
</rss>
