读作"快"的神秘协议 —— 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感兴趣,欢迎收听交流!
评论区
写评论还没有评论