< 返回版块

Borber 发表于 2024-11-22 20:06

Tags:quic,async,crates

读作"快"的神秘协议 —— quic

quic(/kwɪk/)是一个基于UDP的通用可靠传输协议。

最初由Google设计,现今已由IETF标准化,成为RFC9000。它成了一个协议的正式名称,不再是“Quick Udp Internet Connection”的缩写。

quic传输协议毋庸置疑是下一代互联网的基础设施,也并非要取代TCP。它的安全性,性能,灵活性都很是突出。http协议因它而更新换代,推出http3——基于quic的http协议。

quic已经得到比较广泛的应用。Chrome, Microsoft Edge, Firefox 和Safari 等主流浏览器均已实现了quic协议。cloudflare,fastly等CDN服务商也使用quic协议,提供更快,更安全的服务!

下面简单介绍下quic的各种特性。

内部集成TLS,安全超出预期

quic内部集成了tls1.3,quic传输的所有数据都是受到加密的。

quic在连接握手的同时也进行了tls握手,随着握手的进行,双方的密钥也会不断升级。

quic的安全设计,简列如下:

  • 头部保护:不只是数据包里的数据,数据包的包头,也会尽可能多地加密保护起来。
  • 经验证的连接ID:通过秘密颁发、可废弃的连接ID标识连接。
  • 连接ID混淆:一个连接可以悄无声息使用多个路径、多个连接ID。数据包属哪条连接的?不知道。这两数据包是不是同一条连接的?不知道。
  • 数据包号加密:观察者甚至无法获取到实际的数据包号。
  • 抗放大攻击:限制向未验证地址发送的数据量。
  • 还有更多...

0-Rtt握手

quic更有0-RTT特性,也就是在握手的同时发送应用层的数据。对于应用层来说,握手的时间就是0:握手还没完成就已经传输数据了。

BBR传输控制

quic协议首次使用了以带宽测量/预测为指导的传输控制协议BBR,摆脱了传统以丢包、Rtt变化为主的传输控制,能够低延迟高效使用网络资源,具有指导意义。

quic还支持其他传输控制协议,如NewReno、Cubic等等..作为一个用户空间实现的传输协议,quic协议不受限于系统内核,可以享受到最新的研究成果。

网络切换而不断开连接

quic连接超脱于IP端口四元组,通过连接ID来标识连接。

已经建立的quic连接,无论是切换wifi到4g,还是移动漫游切换基站,只要数据包的“目标连接ID”是对端承认的连接ID,对端就会接受这个数据包。

这个过程,可以看作是连接从一条网络路径迁移到另一条网络路径。

这个特性使得quic可以在网络切换(比如从wifi和lte的切换,nat重绑定)的时候不断开连接,而tcp在网络切换的时候必须重新建立连接。

连接级多路复用,更新换代解决http队头阻塞问题

http2的多路复用将多条http流复用到一条TCP连接上:

图片来自<GitHub - rmarx/holblocking-blogpost: Blogpost on Head-of-Line blocking from HTTP/1 to HTTP/3>

假设数据包1丢失了,数据包2,3被收到了:

  • http:数据包1丢了,数据包2,3收到了,我可以先解析第二条流的数据,等收到1再处理第一条流的数据。

  • tcp: 数据包1丢了,我必须得等数据包1被重传,我收到数据包1,才能把数据包交给http。

这就是队头阻塞问题,quic提供连接级的多路复用解决了这个问题。一条quic连接上可以有多条流,每条流之间互不影响。那么,一条quic流可以直接对应到一条http流。

假如还是这个例子(数据包1丢失,数据包2,3被收到):

quic知道第二条流的数据已经收到了,可以先把第二条流的数据先交付;等数据包1的流数据被重传,再交付第一条流的数据。

对http协议来说,改掉底层输协议这么大的事情,那肯定不能继续叫http2了——叫http3

quic毋庸置疑是下一来互联网的传输协议!

quic或许因http而生,但它不只为http服务。作为一个通用的传输协议,可以用到任何你想要的地方!

纯Rust编写的高效异步IETF quic传输协议实现 gm-quic

github:<GitHub - genmeta/gm-quic: An IETF quic transport protocol implemented natively using async Rust>

gm-quic是一个原生异步Rust的quic协议实现,一个高效的、可扩展的RFC 9000实现,同时工程质量优良。

qm-quic的实现,尽量保持了RFC 9000原汁原味的语义概念,包括变量、结构命名,都尽力做到与RFC 9000保持一致,因次,RFC9000等相关rfc都是gm-quic的最佳文档。

不多说了,看一个简单的例子吧!

网络传输例子演示

首先在Cargo.toml加上依赖。

[dependencies]
# 我们已经在crates.io上发布了早期版本!
gm-quic = "0.0.1"
# quic内置了tls,所以我们需要添加rustls作为依赖
rustls = "0.23"
# 别的依赖,比如tokio...

Echo客户端

use std::sync::Arc;
use tokio::io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    // quic内部集成tls,所以我们需要一系列根证书,用来验证服务器证书
    let mut root_cert_store = rustls::RootCertStore::empty();
    // 解析根证书文件,这里可以使用自己签署的证书
    let cert = rustls::pki_types::pem::PemObject::from_pem_slice(include_bytes!("../ca.cert"))
        .expect("faild to parse pem file");
    root_cert_store.add(cert).expect("faild to add cert");
    let root_cert_store = Arc::new(root_cert_store);

    // 创建一个quic客户端,用来发起连接
    let client = gm_quic::quicClient::builder()
        .with_root_certificates(root_cert_store) // 添加根证书
        .without_cert() // 不进行客户端认证(通常都不需要)
        .build();

    // 发起一个到服务器的连接。connect会立刻返回,连接握手自动进行
    let server_addr = "[::1]:4433".parse().unwrap();
    let quic_connectoin = client.connect("localhost", server_addr)?;

    // 标准输入/输出,用于和用户交互
    let mut stdin = io::BufReader::new(io::stdin());
    let mut stdout = io::stdout();
    // 缓存用户输入的一行字符
    let mut line = String::new();

    loop {
        // 打印提示
        stdout.write_all(b"Echo> ").await?;
        stdout.flush().await?;

        // 从stdin读取一行文字
        line.clear();
        stdin.read_line(&mut line).await?;

        // 打开一条新的quic双向流。握手完成后函数返回
        let (_stream_id, (mut stream_reader, mut stream_writer)) =
            quic_connectoin.open_bi_stream().await?.unwrap();

        // 发送读取到的文字到服务器
        stream_writer.write_all(line.as_bytes()).await?;
        // 关闭写端,告诉服务器这边不会再写数据了
        stream_writer.shutdown().await?;

        // 从服务器读取
        let mut resp = String::new();
        stream_reader.read_to_string(&mut resp).await?;
        // 数据一定未经修改
        assert_eq!(resp, line);

        // 将resp打印到控制台,给予用户反馈
        stdout.write_all(b"Server: ").await?;
        stdout.write_all(resp.as_bytes()).await?;
        stdout.flush().await?;
    }
}

Echo服务端

use std::sync::Arc;
use tokio::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    // 创建一个quic服务器用于接受连接
    let server = gm_quic::quicServer::buidler()
        .without_cert_verifier()
        .with_single_cert_files("server.cert", "server.key")? // 添加服务器证书和密钥,这里填的是路径
        .listen("[::1]:4433")?; // 监听地址 [::1]:4433

    // 接受新的连接
    while let Ok((conn, pathway)) = server.accept().await {
        println!("New connection from {}", pathway.local_addr());
        tokio::spawn(handle_connection(conn));
    }

    Ok(())
}

async fn handle_connection(conn: Arc<gm_quic::quicConnection>) -> io::Result<()> {
    // 接受新的双向流
    while let Ok((_stream_id, (mut stream_reader, mut stream_writer))) =
        conn.accept_bi_stream().await
    {
        // 原路返回数据
        use tokio::io::{AsyncReadExt, AsyncWriteExt};
        let mut line = String::new();
        stream_reader.read_to_string(&mut line).await?;

        stream_writer.write_all(line.as_bytes()).await?;
        stream_writer.shutdown().await?;
    }

    Ok(())
}

进展

目前gm-quic已经实现了其基本功能,亦在crates.io发布了早期版本,欢迎尝鲜,提出建议,或者直接参与开发提交你的贡献!

未来,我们会不断优化此项目,继续迭代,API可能仍会有所变动。gm-quic将加强对quic分布式负载均衡的支持,提供更灵活,同时更好用的高性能的使用体验。

当前,遗留功能选项有:

  • 支持qlog
  • 对ECN的善加使用
  • MTU自动探测
  • 多server间的连接恢复,即0Rtt发送早期数据
  • 多server间的负载均衡,主要是通过对Retry包的处理

联系我们

我们目前在B站有一账号建元科技制作与quic相关指导内容,如rfc领读,gm-quic代码review等栏目,如果对quic感兴趣,欢迎收听交流!

评论区

写评论

还没有评论

1 共 0 条评论, 1 页