TCP → TLS → HTTP 连接协商

HTTP/1和HTTP/2都是建立在TCP连接的基础上

# 如何建立TCP连接?

  1. 客户端发送「SYN」包, 内容「Seq=0」
  2. 服务端响应「SYN, ACK」包,内容「Seq=0 Ack=1」
  3. 客户端发送「ACK」包,内容「Seq=1 Ack=1」

3次TCP握手成功,则成功建立TCP连接。

客户端和服务端之间的一个来回时间称为RTT(Round Trip Time),所以建立TCP连接的时间是1.5RTT

Untitled

这两个序号列都是随着数据的发送根据数据包的长度来递增的(排除一些特殊情况会产生的序列号重置),如果序列号不对,则说明中间传输可能发生了数据丢失

例如客户端本次发送数据时「Seq=1 Ack=10 Len=100」,「Seq=1」代表自己之前发过的数据包的长度是1,「Ack=10」代表自己之前接收到来自对方的数据包的长度是10,所以期望下次对方发过来的数据里面的「Seq」值为10,这样就校对正确,「Len=100」代表自己本次要发送的数据包的长度是100

那么可以推测的是服务端下次发送的数据应该是「Seq=10 Ack=101 Len=…」,这次的「Seq」应该和客户端的「Ack」相等为10,这次的「Ack」应该等于客户端的「Seq」+「Len」=1 + 100=101

客户端和服务端就按照上述规则来递增「Seq」和「Ack」值

(上述规则为示例,其实里面有更多场景,比如确认连接后会重置「Seq」和「Ack」为1,「SYN」包没有数据但是也会让「Ack」的值+1,「ACK」包也没有数据却不会导致「Ack」+1,这些都是协议里规定的一些细节内容)

# 如何建立TLS连接?

如果网站是https的,那么在建立了TCP连接之后还需要接着建立TLS连接来保证之后传输数据的安全性

# TLS/1.1及TLS/1.2 的过程

  1. 客户端发送Client Hello包,内容是自己支持的加密套件以及随机码等内容
  2. 服务端响应Server Hello包,里面是根据客户端Client Hello包中内容确定最后选择的加密方法以及随机码等内容
    1. 服务端紧接着还会发送一个Certificate证书文件包,把自己的网站证书发送给客户端验证,和一个Server Hello Done包,告诉客户端初步协商完成
  3. 客户端发送Change Cipher SpecHandshake Finished包来告诉服务端加密验证相关的信息以及客户端TLS握手完成
  4. 服务端同样响应Change Cipher Spec包和Handshake Finished包来告诉客户端加密相关信息以及服务端TLS握手完成

4次TLS握手完成,则成功建立TLS连接。

初次建立TLS连接的时间是2RTT(后续TLS连接只需要1RTT,这里暂不展开说明为什么减少1RTT)

Untitled

# 如何确定使用TLS1.1还是TLS1.2?

  1. 客户端在Client Hello的握手信息Handshake Protocol中提供自己的TLS版本(默认为自己支持的最高版本)

Untitled

  1. 服务端根据客户端Client Hello中的版本以及自己支持的版本确定最后使用的TLS版本, 然后在Server Hello的握手信息Handshake Protocol中返回

Untitled

# 那TLS1.3 怎么建立连接呢?

TLS1.3相比于之前认证和连接过程都有了很大改变,但是为了兼容之前的TLS版本,所以仍然套的是之前Client Hello,Server Hello这种方式的壳子,但是在数据包里面加了supported_versions字段来让支持TLS1.3的服务端能够切换到TLS1.3。如果服务端支持TLS1.3,那么二者将会使用这个字段来协商使用TLS1.3,如果服务端不支持,那么就会自动回退到之前TLS1.2的过程建立TLS1.2连接

  1. 客户端(支持TLS1.3)发送Client Hello包,在supported_versions中填写自己支持的TLS版本,如下:TLS1.3和TLS1.2

Untitled

  1. 服务端(支持TLS1.3)响应Server Hello包,在supported_versions中根据客户端和自己的支持情况选择了TLS1.3

Untitled

另外这里可以看到Change Cipher Spec不再是单独发送,而是和Server Hello包合并一起发送,服务端在响应握手的同时把加密相关的内容一起发给了客户端,客户端的Change Cipher Spec也可以跟随之后的应用数据包一起发送,这样就只需要两步就建立了TLS连接,时间缩短到了1RTT(TLS1.3后续连接是0RTT,这里暂不展开说明)

TLS1.2 和 TLS1.3 握手差别:

TLS1.2 和 TLS1.3 握手差别

(一个小插曲,在测试nginx对TLS各版本的支持情况时,发现必须所有开启了https的server的ssl_protocols都有TLS1.3,nginx才会开启TLS1.3,例如nginx如果同时代理了两个网站,不能仅对其中一个开启TLS1.3,另外的不开,也就是要么全部启用TLS1.3,要么全部关闭TLS1.3,只配置部分网站的话最后结果是都不会开启)

# 如何确定使用HTTP/1还是HTTP/2?

客户端和服务端都有各自支持的HTTP版本,那么怎么决定在二者通信时应该使用哪个版本呢?

这里就是使用HTTP/2的协商机制来决定是否从HTTP/1升级到HTTP/2,分为一下两种情况:

也就是说现在这个HTTP/2时代我们用的都是ALPN的方式,由服务器选择使用哪个版本的HTTP来通讯

  1. 例如客户端这里的Client Hello包中application_layer_protocol_negotiation列出了自己所支持的HTTP版本:h2(HTTP/2)和http/1.1

Untitled

  1. 然后服务端的Server Helloapplication_layer_protocol_negotiation根据客户端以及自己的支持情况选择了高版本的h2

Untitled

不论是HTTP/1还是HTTP/2, 都需要建立在TCP连接的基础上,那么最基础的TCP的3RTT的时间就会一直存在,是否搭配TLS(http还是https)以及搭配的是哪个版本的TLS(TLS1.1,TLS/1.2还是TLS1.3)有如下结果(注意:都是计算的初次连接过程,另外客户端TCP最后一次握手可以和TLS的第一次握手可以紧挨着发送,所以下面的计算过程可以节省0.5RTT)

# 0-RTT

这是最理想的情况,无需等待连接建立,即可直接发送请求,在TLS1.3中开始支持TSL层的0-RTT。

注意0-RTT在客户端和服务端初次建立连接时不可用,仅对后续请求可以生效,原理就是服务端和客户端在第一次连接建立成功之后会把加密等相关信息都缓存下来,在有效期内下次通讯可以直接通过Session来复用缓存的加密信息来传输数据,而无需重复交换加密信息这一过程,所以对TLS1.3而言后续通讯直接减少1RTT变成了0RTT,这个缓存有效期一般会设定在一个合适的时间范围内,既保证内存占用不会太高,又能对一段时间内持续的数据传输起到减少RTT的作用。如果超过有效期,那么就需要重新进行1RTT的建连过程。

(这里的0-RTT仅指TLS层,如果使用的还是HTTP/1或者HTTP/2,那么TCP层的1.5RTT怎么都无法省略,只有到了HTTP/3,才可以连TCP的1.5RTT都省略,变成真正的0-RTT)

那么TCP的1.5RTT能不能省呢?能!这就是目前已经开始推广使用的HTTP/3。

# HTTP/3

# HTTP/3带来了什么改变?

HTTP/3抛弃TCP拥抱UDP,然后基于UDP制定了QUIC协议,由Google最初提出并在2012年实现,QUIC把原本独立的一层TLS包含在内,内建了TLS1.3

Untitled

TCP与UDP协议的对比是老生常谈,最常说的一句话是「TCP是可靠连接,UDP是不可靠连接」,这主要是因为他们在是否需要提前建立稳定的连接,是否保证数据正确性和顺序上的区别。

虽然规范是更加自由的, 但是浏览器的实际实现只允许如下这些情况存在:

底层的修改为HTTP带来了更多的可能性,因为不需要建立TCP连接,比如我们追求的0-RTT在HTTP/3中成为了现实,在初次连接时仅需要1-RTT来交互必要的加密信息等数据,后续连接可以使用之前缓存的信息来直接发送数据实现0-RTT

# 如何从HTTP/1,HTPP/2切换到HTTP/3?

(部分网络代理软件会影响HTTP/3,导致实际看到的是使用旧版的HTTP,所以使用或者测试HTTP/3时保险起见先关闭本机的网络代理,之后等各个软件适配,这种情况应该会减少)

前面说HTTP/1升级到HTTP/2是通过ALPN方式来协商升级的,但是HTTP/3的升级却不是ALPN,而是通过新的HTTP Alternative Services(该方式晚于HTTP/2一年发布),具体做法是服务端会在响应头增加一个叫做「Alt-Svc」字段,来告诉客户端除了你当前正在使用的这种连接方式,我还支持额外这些连接方式,客户端可以自己决定是否切换到新的方式

(这种方式除了可以用以HTTP/3协议的升级,还可以用在负载均衡等场景, 例如服务端当前繁忙的时候可以通过这个方式指定一个替代的服务器,用户可以到另外的服务器上去获取数据,或者科学上网以及免备案接入国内主机等)

例如:

alt-svc: h3-29=":443"; ma=86400, h3=":443"; ma=86400

这代表服务端在443端口还支持h3(HTTP/3)和h3-29(HTTP/3的一个草案)连接方式,在未来的86400s内客户端都可以选择使用新的这两种方式来和客户端通讯(当然也可以不使用,这取决于客户端自己)

升级对于服务端也有要求

  1. 「Alt-Svc」里的替代服务必须部署TLS
  2. 如果原始服务也是使用TLS,那么替代服务必须使用同一张SSL证书(Chrome中进一步要求原始服务必须是TLS)

从协议升级过程可以看出在初次连接的时候仍然会使用HTTP/1或HTTP/2,从第二次或者更后面的请求才可以切换到HTTP/3,所以即使某个网站支持HTTP/3,也许你也需要多刷新几次才会发现开始使用HTTP/3,随着HTTP/3的发展后面也许可以出现更优雅的方式能够从初次连接开始就使用HTTP/3

# HTTP发展过程

  1. HTTP/0.9 - 在1991年由W3C组织(World Wide Web)实施(最早是在1989年由万维网之父-蒂姆·伯纳斯-李(Tim Berners-Lee)发明),已废弃

  2. HTTP/1.0 - 在1996年由W3C的HTTP工作组(HTTP Worging Group)发布,已废弃

  3. HTTP/1.1 - 在1997年由W3C的HTTP工作组(HTTP Worging Group)发布,仍在使用中

  4. SPDY - 在2009年由Google发布并开始实验(后续正式化为HTTP/2.0),已废弃

  5. HTTP/2.0 - 在2015年由W3C的HTTP工作组(HTTP Worging Group)发布(因为SPDY实验很成功,Google把相关内容都提交给了W3C来进行标准化),仍在使用中

  6. HTTP/3.0 - 在2022年由IETF(IETF HTTP Working Group)发布,主流浏览器陆续提供支持

# 使用curl测试

  1. 确认curl基础库版本
curl --version
curl 7.79.1 (x86_64-apple-darwin21.0) libcurl/7.79.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.11 nghttp2/1.45.1
Release-Date: 2021-09-22
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL UnixSockets

如果要测试TLS1.3, 那么LibreSSL版本需要>=3.2.2, LibreSSL从这个版本开始支持TLS1.3, Changelog for LibreSSL 3.2.2

3.2.2 - Stable release

  • This is the first stable release with the new TLSv1.3 implementation enabled by default for both client and server.
curl 7.84.0 (x86_64-apple-darwin21.5.0) libcurl/7.84.0 (SecureTransport) OpenSSL/1.1.1q zlib/1.2.11 brotli/1.0.9 zstd/1.5.2 libidn2/2.3.2 libssh2/1.10.0 nghttp2/1.48.0 librtmp/2.3 OpenLDAP/2.6.2
Release-Date: 2022-06-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd

如果要测试TLS1.3, 那么OpenSSL版本需要>=1.1.1, OpenSSL从这个版本开始支持TLS1.3, Changes between 1.1.0i and 1.1.1 [11 Sep 2018]

*) Support for TLSv1.3 added.

brew install curl

# 安装后需要按照brew提示进行配置
# 因为macos自带curl, 新安装的curl不能直接取代原有的
# 所以需要在修改配置文件来使用新安装的版本
# 你会看到类似如下提示内容, 按照提示语句配置
# If you need to have curl first in your PATH, run:
#   echo 'export PATH="/usr/local/opt/curl/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="/usr/local/opt/curl/bin:$PATH"' >> ~/.zshrc

# 修改了配置文件后,需要重载配置文件才能生效
source ~/.zshrc

# 再次查看curl版本
curl --version
  1. 使用curl测试连接
# -I 仅显示响应头
# -v 显示连接过程详细信息
# --tlsv1.3 最低使用tls1.3版本(类比 --tls1.1 --tls1.2 可限定最低版本)
# --tls-max 1.3 最高使用tls1.3版本(类比 --tls-max 1.2 可限定其它最高版本)
# --tlsv1.3 --tls-max 1.3 配合使用可限定到仅使用某个tls版本
curl https://kric.cc -Iv
*   Trying 47.117.70.235:443...
* Connected to kricsleo.com (47.117.70.235) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=kricsleo.com
*  start date: Nov 16 00:00:00 2021 GMT
*  expire date: Nov 16 23:59:59 2022 GMT
*  subjectAltName: host "kricsleo.com" matched cert's "kricsleo.com"
*  issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=Encryption Everywhere DV TLS CA - G1
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
...

# 使用浏览器测试

image-20220714100424892

image-20220714100604813

image-20220714100528491

# 参考文档