GitHub 19k Star 的Java工程师成神之路,不来了解一下吗!
最近一段时间以来,关于HTTP/3的新闻有很多,越来越多的国际大公司已经开始使用HTTP/3了。
所以,HTTP/3已经是箭在弦上了,全面使用只是个时间问题,那么,作为一线开发者,我们也是时候了解下到底什么是HTTP/3,为什么需要HTTP/3了。
那么,本文就来讲解一下到底什么是HTTP/3?他用到了哪些技术?解决了什么问题?
HTTP/2 存在的问题
在撰写本文之前,我专门写了一篇文章《HTTP/2做错了什么?刚刚辉煌2年就要被弃用了!?》分析HTTP/2存在的问题以及背后的原因。
这里就不详细介绍了,强烈建议大家先阅读下这篇文章,有助于本文的学习。
在上一篇文章中我们提到过HTTP/2因为底层使用的传输层协议仍然是TCP,所以他存在着TCP队头阻塞、TCP握手延时长以及协议僵化等问题。
这导致HTTP/2虽然使用了多路复用、二进制分帧等技术,但是仍然存在着优化空间。
QUIC协议
我们知道,HTTP/2之所以"被弃用",是因为他使用的传输层协议仍然是TCP,所以HTTP/3首要解决的问题就是绕开TCP。
那么如果研发一种新的协议,同样还是会因为受到中间设备僵化的影响,导致无法被大规模应用。所以,研发人员们想到了一种基于UDP实现的方式。
于是,Google是最先采用这种方式并付诸于实践的,他们在2013年推出了一种叫做QUIC的协议,全称是Quick UDP Internet Connections。
从名字中可以看出来,这是一种完全基于UDP的协议。
在设计之初,Google就希望使用这个协议来取代HTTPS/HTTP协议,使网页传输速度加快。2015年6月,QUIC的网络草案被正式提交至互联网工程任务组。2018 年 10 月,互联网工程任务组 HTTP 及 QUIC 工作小组正式将基于 QUIC 协议的 HTTP(英语:HTTP over QUIC)重命名为HTTP/3。
所以,我们现在所提到的HTTP/3,其实就是HTTP over QUIC,即基于QUIC协议实现的HTTP。
那么,想要了解HTTP/3的原理,只需要了解QUIC就可以了。
QUIC协议有以下特点:
- 基于UDP的传输层协议:它使用UDP端口号来识别指定机器上的特定服务器。
- 可靠性:虽然UDP是不可靠传输协议,但是QUIC在UDP的基础上做了些改造,使得他提供了和TCP类似的可靠性。它提供了数据包重传、拥塞控制、调整传输节奏以及其他一些TCP中存在的特性。
- 实现了无序、并发字节流:QUIC的单个数据流可以保证有序交付,但多个数据流之间可能乱序,这意味着单个数据流的传输是按序的,但是多个数据流中接收方收到的顺序可能与发送方的发送顺序不同!
- 快速握手:QUIC提供0-RTT和1-RTT的连接建立
- 使用TLS 1.3传输层安全协议:与更早的TLS版本相比,TLS 1.3有着很多优点,但使用它的最主要原因是其握手所花费的往返次数更低,从而能降低协议的延迟。
那么,QUIC到底属于TCP/IP协议族中的那一层呢?我们知道,QUIC是基于UDP实现的,并且是HTTP/3的所依赖的协议,那么,按照TCP/IP的分层来讲,他是属于传输层的,也就是和TCP、UDP属于同一层。
如果更加细化一点的话,因为QUIC不仅仅承担了传输层协议的职责,还具备了TLS的安全性相关能力,所以,可以通过下图来理解QUIC在HTTP/3的实现中所处的位置。
接下来我们分别展开分析一下QUIC协议。先来看下他是如何建立连接的。
QUIC的连接建立
我们知道,TCP这种可靠传输协议需要进行三次握手,也正是因为三次握手,所以需要额外消耗1.5 RTT,而如果再加上TLS的话,则需要消耗3-4个 RTT连接。
那么,QUIC是如何建立连接的呢?如何减少RTT的呢?
QUIC提出一种新的连接建立机制,基于这种连接接机制,实现了快速握手功能,一次QUIC连接建立可以实现使用 0-RTT 或者 1-RTT 来建立连接。
QUIC在握手过程中使用Diffie-Hellman算法来保证数据交互的安全性并合并了它的加密和握手过程来减小连接建立过程中的往返次数。
Diffie–Hellman (以下简称DH)密钥交换是一个特殊的交换密钥的方法。它是密码学领域内最早付诸实践的密钥交换方法之一。 DH可以让双方在完全缺乏对方(私有)信息的前提条件下通过不安全的信道达成一个共享的密钥。此密钥用于对后续信息交换进行对称加密。
QUIC 连接的建立整体流程大致为:QUIC在握手过程中使用Diffie-Hellman算法协商初始密钥,初始密钥依赖于服务器存储的一组配置参数,该参数会周期性的更新。初始密钥协商成功后,服务器会提供一个临时随机数,双方根据这个数再生成会话密钥。客户端和服务器会使用新生的的密钥进行数据加解密。
以上过程主要分为两个步骤:初始握手(Initial handshake)、最终(与重复)握手(Final (and repeat) handshake),分别介绍下这两个过程。
初始握手(Initial handshake)
在连接开始建立时,客户端会向服务端发送一个打招呼信息,(inchoate client hello (CHLO)),因为是初次建立,所以,服务端会返回一个拒绝消息(REJ),表明握手未建立或者密钥已过期。
但是,这个拒绝消息中还会包含更多的信息(配置参数),主要有:
- Server Config:一个服务器配置,包括服务器端的Diffie-Hellman算法的长期公钥(long term Diffie-Hellman public value)
- Certificate Chain:用来对服务器进行认证的信任链
- Signature of the Server Config:将Server Config使用信任链的叶子证书的public key加密后的签名
- Source-Address Token:一个经过身份验证的加密块,包含客户端公开可见的IP地址和服务器的时间戳。
在客户端接收到拒绝消息(REJ)之后,客户端会进行数据解析,签名验证等操作,之后会将必要的配置缓存下来。
同时,在接收到REJ之后,客户端会为这次连接随机产生一对自己的短期密钥(ephemeral Diffie-Hellman private value) 和 短期公钥(ephemeral Diffie-Hellman public value)。
之后,客户端会将自己刚刚产生的短期公钥打包一个Complete CHLO的消息包中,发送给服务端。这个请求的目的是将自己的短期密钥传输给服务端,方便做前向保密,后面篇幅会详细介绍。
在发送了Complete CHLO消息给到服务器之后,为了减少RTT,客户端并不会等到服务器的响应,而是立刻会进行数据传输。
为了保证数据的安全性,客户端会自己的短期密钥和服务器返回的长期公钥进行运算,得到一个初始密钥(initial keys)。
有了这个初识密钥之后,客户端就可以用这个密钥,将想要传输的信息进行加密,然后把他们安全的传输给服务端了。
另外一面,接收到Complete CHLO请求的服务器,解析请求之后,就同时拥有了客户端的短期公钥和自己保存的长期密钥。这样通过运算,服务端就能得到一份和客户端一模一样的初始密钥(initial keys)。
接下来他接收到客户端使用初始密钥加密的数据之后,就可以使用这个初识密钥进行解密了,并且可以将自己的响应再通过这个初始密钥进行加密后返回给客户端。
所以,从开始建立连接一直到数据传送,只消耗了初始连接连接建立的 1 RTT
最终(与重复)握手
那么,之后的数据传输就可以使用初始密钥(initial keys)加密了吗?
其实并不完全是,因为初始密钥毕竟是基于服务器的长期公钥产生的,而在公钥失效前,几乎多有的连接使用的都是同一把公钥,所以,这其实存在着一定的危险性。
所以,为了达到前向保密 (Forward Secrecy) 的安全性,客户端和服务端需要使用彼此的短期公钥和自己的短期密钥来进行运算。
在密码学中,前向保密(英语:Forward Secrecy,F.........