QUIC协议和HTTP3.0技术研究

什么是QUIC

QUIC的全称是“Quick UDP Internet Connection”,它是一个新的加密传输层网络协议。QUIC被设计成用于使得HTTP流更加安全、高效且快速。理论上讲,QUIC集合了TCP连接和TLS加密的长处,并且在UDP上实现。这看起来和TCP+TLS+HTTP/2类似,但是他们的具体区别见下图:

QUIC与现有的TCP+TLS+HTTP/2相比,主要有以下几个特征:

  • 利用缓存,显著减少连接建立时间
  • 改善拥塞控制,拥塞控制从内核空间到用户空间
  • 没有 head of line 阻塞的多路复用
  • 前向纠错,减少重传
  • 连接平滑迁移,网络状态的变更不会影响连接断线

QUIC的用途

QUIC是一个低延时的传输协议,常用于一些需要高速在线服务的场景。对于游戏玩家,视频直播主或者其他任何需要VoIP通讯的人来说,这种协议是很有必要的。以下是QUIC给在线会话带来的改变:

  • 减少了连接次数。为了建立TLS加密连接,客户端和服务端需要进行TLS握手并交换加密秘钥。这是一个较冗长的过程,它需要4个往返请求。如果是在TCP上传输,那么这么过程就需要更多步骤,这就导致更加拖慢了连接的建立。在QUIC下这个些都只需要一个握手就能搞定。

  • 在丢包情况下有更好的表现。HTTP/2+TCP的场景下,会带来TCP队头阻塞(head-of-line bocking)的问题。所以,在弱网环境下,HTTP/2+TCP的表现就会很差。当一个包丢失了,接收方就需要等待重传,这就带来了很大性能损失。而QUIC通过允许数据流能够能独立地到达目的地解决这个这个问题,接收方不需要等待丢失的包。

  • 当网络变化后依然存有稳定的连接。当你已经和服务器通过TCP建立好了连接,而此时你的网络突然通WIFI变为4G,那么所有的连接都会超时,并且需要重新建立。QUIC通过给每一个连接标上独有的身份信息,来使得这种网络的转换更加流畅。当网络发生改变时,这些连接的恢复只需要很简单的发送一个包而不是建立一个全新的连接。即使是IP的改变也是如此。

  • 更易于优化和开发。TCP是已经实现于操作系统内核的协议,要想改变它是较为不实际的。QUIC是实现在应用层的协议,使得它更加灵活。

QUIC面临的挑战

  • 小地方,路由封杀UDP 443端口( 这正是QUIC 部署的端口)
  • UDP包过多,由于QS限定,会被服务商误认为是攻击,UDP包被丢弃
  • 无论是路由器还是防火墙目前对QUIC都还没有做好准备

QUIC协议实践

本次实验使用aioquic进行,aioquic是一个用python实现的QUIC网络库

实验环境:

  • 操作系统:MacOS 10.15.7
  • Python版本:3.7.2

实验步骤:

  • 安装aioquic库和必要的依赖包:
    $ pip install aioquic
    $ pip install aiofiles asgiref dnslib httpbin starlette wsproto

  • 在Mac下还需要安装openssl,并添加一下环境变量:
    $ brew install openssl
    $ export CFLAGS=-I/usr/local/opt/openssl/include
    $ export LDFLAGS=-L/usr/local/opt/openssl/lib

  • 获取aioquic库源码以及例程源码
    git clone https://github.com/aiortc/aioquic.git

  • 运行示例服务器,它同时处理http/0.9和http/3
    $ python examples/http3_server.py --certificate tests/ssl_cert.pem --private-key tests/ssl_key.pem

  • 行示例客户机来执行http/3请求:
    python examples/http3_client.py https://localhost:4433/

  • wireshark监听到的quic包:

    说明能够成功实现quic

源码分析

服务端程序:

  • HttpRequestHandler类:
    用于初始化处理句柄,并处理请求收到的请求

  • HttpServerProtocol类:
    该类接受一个一个QuicConnectionProtocol类为默认参数,其中quic_event_received函数:

def quic_event_received(self, event: QuicEvent) -> None:
        if isinstance(event, ProtocolNegotiated):
            if event.alpn_protocol.startswith("h3-"):
                self._http = H3Connection(self._quic)
            elif event.alpn_protocol.startswith("hq-"):
                self._http = H0Connection(self._quic)
        elif isinstance(event, DatagramFrameReceived):
            if event.data == b"quack":
                self._quic.send_datagram_frame(b"quack-ack")

        #  pass event to the HTTP layer
        if self._http is not None:
            for http_event in self._http.handle_event(event):
                self.http_event_received(http_event)

该方法定义了根据不同的协议类型建立对应的连接,例如若是h3的协议,则建立一个H3Connection(H3Connection是一个低级的HTTP/3的连接对象)

  • SessionTicketStore类:
class SessionTicketStore:
    """
    Simple in-memory store for session tickets.
    """

    def __init__(self) -> None:
        self.tickets: Dict[bytes, SessionTicket] = {}

    def add(self, ticket: SessionTicket) -> None:
        self.tickets[ticket.ticket] = ticket

    def pop(self, label: bytes) -> Optional[SessionTicket]:
        return self.tickets.pop(label, None)

用于存取SessionTicket,SessionTicket是用于低开销恢复中断会话的


  • 在服务端的main函数中有:
loop = asyncio.get_event_loop()
    loop.run_until_complete(
        serve(
            args.host,
            args.port,
            configuration=configuration,
            create_protocol=HttpServerProtocol,
            session_ticket_fetcher=ticket_store.pop,
            session_ticket_handler=ticket_store.add,
            retry=args.retry,
        )
    )
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass

不断监听收到的请求,其中的session_ticket是给每个会话存一个凭据,用于上面所说的恢复中断会话

posted @ 2021-01-30 19:00  USTC-chicken  阅读(1134)  评论(0编辑  收藏  举报