566
社区成员
在长期发展中,TCP存在着以下问题:
TCP队头阻塞:由于TCP的可靠性机制,HTTP2是在一个TCP连接上并行发送多个资源,TCP队头阻塞会导致所有资源的传输需要停等,对网络质量影响较大。
握手延迟:TCP需要进行3次握手和4次挥手来建立和断开连接,至少需要消耗一次RTT。而主流的HTTPS协议,需要2消耗两次RTT握手才能建立连接。
网络中间设备僵化:网络中间设备在传输TCP协议时设置了很多潜规则,例如部分防火墙只允许通过80和443端口;部分NAT网关在转换网络地址时会重写传输层头部,可能导致双方无法使用新的传输格式;部分中间代理有时候出于安全需要,删除一些它不认识的选项字段。因此升级基于TCP的网络协议时,就必须要考虑和兼容这些影响。
协议僵化:TCP是在操作系统内核和中间设备固件中实现的。要对TCP进行大更改,就必须要通信双方升级操作系统,中间设备更新固件。这基本难以大规模实现。
因为这些问题的困扰,人们研发出了新一代的互联网传输协议 QUIC 并投入使用,并在后续被重命名为 HTTP/3。
QUIC 全称 Quick UDP Internet Connection, 是谷歌公司研发的一种基于 UDP 协议的低时延互联网传输协议。在2018年IETF会议中,HTTP-over-QUIC协议被重命名为HTTP/3,并成为 HTTP 协议的第三个正式版本。
QUIC协议就是基于UDP重新实现了一遍HTTP2的特性。
以下是HTTP协议的相关比较:
TCP最少需要花费1RTT的时间来建立连接。可以看到,TLS1.2下,首次建立连接,首先需要1次RTT建立连接(蓝色线),2次RTT交换密钥和加密策略(黑色线),然后开始通信。再次建立连接时,由于已缓存了密钥,因此少1次RTT。 TLS1.3和QUIC都采用了Diffie-Hellman密钥交换算法来交换密钥。该算法的优点是交换密钥只需要1次RTT。在QUIC下,只有首次建立连接交换密钥时消耗1RTT时间,再次连接时就是0RTT了。这已最大限度的减少握手延迟带来的影响。这个特性在连接延迟较大的移动网络上有较好的性能提升。
比较 | TCP | QUIC |
---|---|---|
连接时间 | 至少1RTT | 首次1RTT,再次0RTT |
连接标识 | SrcIp,SrcPort,DstIp,DstPort | 64位随机数 |
流量控制 | 滑动窗口机制 | 在Connection和Stream两个级别分别进行流控 |
多路复用 | TCP协议有队头阻塞问题,如果某个资源的某个包丢失了,由于TCP是保证时序的,就会在接收端形成队头阻塞。TCP协议无法区分各个资源的包是否关联,因此会停止处理所有资源,直到丢包恢复 | 不存在队头阻塞问题。如果某个资源的某个包丢失了,只会影响单个资源,其他资源会继续传输 |
升级迭代 | 迭代周期长,以年为单位 | 迭代周期短,以月/周为单位 |
1)利用缓存,显著减少连接建立时间;
2)改善拥塞控制,拥塞控制从内核空间到用户空间;
3)没有 head of line 阻塞的多路复用;
4)前向纠错,减少重传;
5)连接平滑迁移,网络状态的变更不会影响连接断线。
首先是因为这些年性能的优化提升都针对 TCP ,使得 UDP 性能没有任何改进。当然随着 QUIC3 的发布,相信后续应该会有相对的投入。
其次是安全问题,也就是反射攻击,即伪造原地址。这个指发送数据包时的原地址是伪造的,不是真正的地址,会引起放大攻击。原因是 QUIC 握手过程是不对称的,特别是第一次请求时,客户端只需要发送几个字节的信息到服务器,而服务器则需要把证书等很多东西返还给客户端,这个不对称的机会造成了放大。草案 27 定义了两个规则和机制来限制反射攻击:客户端发送Initial包,即第一个数据包时,其长度必须在 1200 bytes以上,不足部分用 Padding 帧填充,同时,当服务端不确定客户端可靠性时,可以发送 Retry 包要求客户端再次提供验证信息。
目前支持 QUIC 协议的 web 服务只有 0.9 版本以后的 Caddy 。其他常用 web 服务如 nginx、apache 等都未开始支持。
开源实现有哪些:
1)Chromium:
这个是官方支持的。优点自然很多,Google 官方维护基本没有坑,随时可以跟随 chrome 更新到最新版本。不过编译 Chromium 比较麻烦,它有单独的一套编译工具。暂时不建议考虑这个方案。
2)proto-quic:
从 chromium 剥离的一个 QUIC 协议部分,但是其 github 主页已宣布不再支持,仅作实验使用。不建议考虑这个方案。
3)goquic:
goquic 封装了 libquic 的 go 语言封装,而 libquic 也是从 chromium 剥离的,好几年不维护了,仅支持到 quic-36, goquic 提供一个反向代理,测试发现由于 QUIC 版本太低,最新 chrome 浏览器已无法支持。不建议考虑这个方案。
4)quic-go:
quic-go 是完全用 go 写的 QUIC 协议栈,开发很活跃,已在 Caddy 中使用,MIT 许可,目前看是比较好的方案。
那么,对于中小团队或个人开发者来说,比较推荐的方案是最后一个,即采用 caddy 来部署实现 QUIC。caddy 这个项目本意并不是专门用来实现 QUIC 的,它是用来实现一个免签的 HTTPS web 服务器的(caddy 会自动续签证书)。而QUIC 只是它的一个附属功能。
从上图的数据可以看出,QUIC的总体性能比HTTP2略有提升
从上图的数据可以看出,在弱网络、高丢包率的情况下,QUIC才能凸显其独特优势。
当出现如上图这行alt-svc: quic=xxx时,即表示该网站支持QUIC。该行具体的含义是服务器在443端口开启了QUIC支持,最大缓存时间是2592000秒(30天),支持的QUIC版本号是44、43、39。
roundTripper,实现net.RoundTripper接口,使http客户端将发起请求的过程交由该中间件来处理。
roundTripper := &http3.RoundTripper{
TLSClientConfig: &tls.Config{
RootCAs: pool,
InsecureSkipVerify: *insecure,
KeyLogWriter: keyLog,
},
QuicConfig: &qconf,
}
defer roundTripper.Close()
hclient := &http.Client{
Transport: roundTripper,
}
var wg sync.WaitGroup
wg.Add(len(urls))
for _, addr := range urls {
logger.Infof("GET %s", addr)
go func(addr string) {
rsp, err := hclient.Get(addr)
if err != nil {
log.Fatal(err)
}
logger.Infof("Got response for %s: %#v", addr, rsp)
body := &bytes.Buffer{}
_, err = io.Copy(body, rsp.Body)
if err != nil {
log.Fatal(err)
}
if *quiet {
logger.Infof("Request Body: %d bytes", body.Len())
} else {
logger.Infof("Request Body:")
logger.Infof("%s", body.Bytes())
}
wg.Done()
}(addr)
}
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
DialAddr
func DialAddr(addr string,tlsConf *tls.Config,config *Config,
) (Session, error) {
return DialAddrContext(context.Background(), addr, tlsConf, config)
}
函数的参数列表:
addr
表示服务端的地址,tlsConf
表示tls
的配置,最后一个config
表示QUIC
的配置,当填入nil
的时候将使用默认配置。
config *Config常用选项:
返回值(Session, error)
receiveStream
sendSatream
客户端最后返回的是stream。
ListenAddr
listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)
func ListenAddr(addr string, tlsConf *tls.Config, config *Config) (Listener, error) {
return listenAddr(addr, tlsConf, config, false)
}
func listenAddr(addr string, tlsConf *tls.Config, config *Config, acceptEarly bool) (*baseServer, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return nil, err
}
serv, err := listen(conn, tlsConf, config, acceptEarly)
if err != nil {
return nil, err
}
serv.createdPacketConn = true
return serv, nil
}
这个函数返回一个baseServer
,这是一个QUIC
的listener
。
该结构体添加了如下的结构体成员
Session
队列Session
队列的长度接着新建一个线程,不断地监听端口,等待一个新的客户端连接请求
Accept
sess, err := listener.Accept(context.Background())
func (s *baseServer) Accept(ctx context.Context) (Session, error) {
return s.accept(ctx)
}
atomic.AddInt32(&s.sessionQueueLen, -1)
这里当收到一个新的请求的时候添加到sessionQueue
中,返回一个客户端Session
,用这个Session
可以和客户端进行发送和接收数据。
AddConn
type connMultiplexer struct {
mutex sync.Mutex
conns map[string] /* LocalAddr().String() */ connManager
newPacketHandlerManager func(net.PacketConn, int, []byte, logging.Tracer, utils.Logger) (packetHandlerManager, error)
logger utils.Logger
}
func (m *connMultiplexer) AddConn(
c net.PacketConn, connIDLen int, statelessResetKey []byte, tracer logging.Tracer,
) (packetHandlerManager, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
addr := c.LocalAddr()
connIndex := addr.Network() + " " + addr.String()
p, ok := m.conns[connIndex]
if !ok {
manager, err := m.newPacketHandlerManager(c, connIDLen, statelessResetKey, tracer, m.logger)
if err != nil {
return nil, err
}
p = connManager{
connIDLen: connIDLen,
statelessResetKey: statelessResetKey,
manager: manager,
tracer: tracer,
}
m.conns[connIndex] = p
} else {
if p.connIDLen != connIDLen {
return nil, fmt.Errorf("cannot use %d byte connection IDs on a connection that is already using %d byte connction IDs", connIDLen, p.connIDLen)
}
if statelessResetKey != nil && !bytes.Equal(p.statelessResetKey, statelessResetKey) {
return nil, fmt.Errorf("cannot use different stateless reset keys on the same packet conn")
}
if tracer != p.tracer {
return nil, fmt.Errorf("cannot use different tracers on the same packet conn")
}
}
return p.manager, nil
}
connMultiplexer
这个结构定义了互斥锁、连接map、新建函数和日志处理。AddConn
函数首先加了互斥锁,然后将连接信息新建了一个连接管理器connManager
,加入到conns
这个map当中,map以ip地址作为key,连接管理器作为value。
参考:
QUIC网络协议简介
天下武功,唯'QUICK'不破,揭秘QUIC的五大特性及外网表现
QUIC协议和HTTP3.0技术研究
HTTP/3 Deep Dive
作者:NP118