本文是Yoshua Wuyts大佬博客关于async http文章的大部分内容的翻译。原文地址https://blog.yoshuawuyts.com/async-http/
Friedel Ziegelmayer、Ryan Levick和我向大家强力推荐一组http库,有了他们就可以轻松快速地开发加密的异步http/1.1服务器端和客户端。它们是
async-h1 – 支持streaming的HTTP/1.1客户端和服务端的协议实现
http-types – 从http服务器端和客户端框架Tide和Surf中提取出的可复用部分
async-native-tls – 支持stream和TLS的服务器端和客户端实现
于是加密的基于stream的http客户端只需要15行代码
use async_std::io::prelude::*;
use async_std::net::TcpStream;
use http_types::{Method, Request, Url};
#[async_std::main]
async fn main() -> http_types::Result<()> {
// 建立一个tcp连接
let stream = TcpStream::connect("127.0.0.1:8080").await?;
let peer_addr = stream.peer_addr()?;
// 创建一个请求
let url = Url::parse(&format!("https://{}/foo", peer_addr))?;
let req = Request::new(Method::Get, url);
// 加密tls连接
let host = req.url().host_str()?;
let stream = async_native_tls::connect(host, stream).await?;
// 通过加密信道发送请求
let res = async_h1::connect(stream, req).await?;
println!("{:?}", res);
Ok(())
}
而一个异步的http服务器则不超过30行代码(而且import,循环,print等还占据了绝大部分代码)
use async_std::net::{TcpStream, TcpListener};
use async_std::prelude::*;
use async_std::task;
use http_types::{Response, StatusCode};
#[async_std::main]
async fn main() -> http_types::Result<()> {
// 建立TCP连接并创建一个URL
let listener = TcpListener::bind(("127.0.0.1", 8080)).await?;
let addr = format!("http://{}", listener.local_addr()?);
println!("listening on {}", addr);
// 对每个TCP连接,spawn一个任务并用accept去处理
let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream = stream?;
let addr = addr.clone();
task::spawn(async {
if let Err(err) = accept(addr, stream).await {
eprintln!("{}", err);
}
});
}
Ok(())
}
// 把TCP流转化成顺序的http请求/响应对
async fn accept(addr: String, stream: TcpStream) -> http_types::Result<()> {
println!("starting new connection from {}", stream.peer_addr()?);
async_h1::accept(&addr, stream.clone(), |_req| async move {
let mut res = Response::new(StatusCode::Ok);
res.insert_header("Content-Type", "text/plain")?;
res.set_body("Hello");
Ok(res)
})
.await?;
Ok(())
}
而只需要再加几行代码就能实现一个加密的版本:读取证书,并把用它往接受到的stream外包一层
use async_std::prelude::*;
use async_std::net::TcpListener;
use async_std::fs::File;
let key = File::open("identity.pfx").await?;
let pass = "";
let listener = TcpListener::bind("127.0.0.1:8080").await?;
let (stream, _) = listener.accept().await?;
let stream = async_native_tls::accept(key, pass, stream).await?;
// 处理stream的逻辑
你可能会注意到,我们写的库是围绕着stream展开的。因为Rust中的stream向我们提供了极大的可组合性。比如说,用surf库把http请求的的body部分复制到文件中,就等价于先发起一个http请求,然后复制到文件中。
let req = surf::get("https://example.com").await?;
io::copy(req, File::open("example.txt")).await?;
而使用async-h1和async-native-tls,我们希望这一可组合性不仅仅在框架层被实现,还能在协议层这一点也实现。我们坚信,如果技术栈的组件能更容易被组合到一起,那么整个生态系统都将更容易地使用这一技术栈。
具体说,如果你想在UnixStraem上跑async-h1从而和守护进程通行,那你只需要用UnixStream替换掉TcpStream就好了。既不用从头造轮子,也不用fork已有的项目。
##共享抽象 就像我们一开始提到的:http-types是从Tide和Surf框架中提取出来的。在此之前我们使用了hyperium/http库。它提供了对多种http风格的抽象,但是比如说没有提供http体、cookie、媒体类型,也没有实现url标准。这当然都是取舍问题。不过我们发现Tide和Surf之间有很多重复的代码,这成为了一个维护的负担。
所以我们选择写出了http-types。这是一个覆盖了各种http风格并提供了stream体的共享抽象。我们发现这样更有助于我们创建丰富的http抽象。因为我们觉得Tide和Surf会是在http-types、http-service和http-client外各自包装少量内容——在框架里只需要加入中间层、路由等框架独有的内容就好。
此外http-types提供了一个和状态码相关的错误类型。所以从错误中就可以得到状态码。这一工作极大程度上依赖于David Tolnay在anyhow的杰出贡献。
http_types::Error的底层是加上是boxed error加上状态码。就像在anyhow,任何错误都可以用?转化成这种错误。
此外我们还提供ResultExt trait,它可以给Result增加一个有关状态码的方法。这一可以快速地把现有的错误转化成状态码:
/// 获取文件的字节数
/// 把`io::Error`转化为`http_types::Error`.
async fn file_length(p: Path) -> http_types::Result {
let b = fs::read(p).await.status(501)?;
Ok(b.len())
}
这是我们迈向把错误处理作为如Tide和Surf之类的http框架中一等公民的第一步。我们还在探索中,也希望听到你的声音!
我们不仅仅在错误处理中使用AsRef,我们几乎处处都在使用它。这是受到rustwasm在web-sys的event的启发:每个DOM对象都实现了AsRef,而不是重新定义一个trait。这使得对象可以被当成EventTarget引用。在http-types,我们实现了AsRef和AsMut以实现类型之间的转换。比如说一个Request就是字节流、头和URL的组合,所以它实现了AsRef,AsRef和AsyncRead。类似的,Response就是字节流、头和状态码的组合,所以它实现了AsRef, AsRef和AsyncRead。这一模式贯穿了整个库。值得注意的是,我们不允许单独生成一个头类型,虽然头在Request、Response和Trailers中是一个公有的部分。也就是说如果你想读或者写成它们之中的任何一个,你可以:
fn set_cors(headers: impl AsMut) {
// set cors headers here
}
fn forwarded_for(headers: impl AsRef) {
// get the X-forwarded-for header
}
这段代码可以接受Request,Response, 或Trailers并执行操作
let fwd1 = forwarded_for(&req);
let fwd2 = forwarded_for(&res);
let fwd3 = forwarded_for(&trailers);
Tide和Surf的Request和Response对按计划也可以这么转发。所以无论你是直接使用http-types,还是使用任何一个框架,代码都会相同。
我们还对该技术栈的兼容性十分满意。现在服务端可以以Lambda函数、游览器中的http客户端、以及Rust服务器、TLS、DNS和trasport的各种组合形式运行。为了能让http-types能够和hyperium/http兼容,我们为所有的类型实现了From和Into,只需通过一个feature:
[dependencies]
http-types = { version = "*", features = ["hyperium_http"] }
因为hyperium/http并没有实现body,当然也就谈不上无缝兼容。不过这已经能覆盖大部分情况,也是我们能做的最好得了。也就是说,在http-client,http-service和http-types间我们提供了非常灵活兼容性极强的一层。
好奇心推动着大部分工作的前几。尝试回答诸如:为异步Rust设计的http库应该长什么样?为了让TCP和TLS成为可配置,范性是必须的吗?http/1.1可以用stream表述么?我们尝试用这些库降低使用Rust异步http编程的障碍。我们用了极少的新trait,限制了我们提供的范性数量,遵循Rust命名规范。当然编译起来也贼快。我们认为这些库的成品是易于上手、使用和维护的。用async-h1实现客户端或服务器端也就几百行代码,所以如果你遇到bug或者像做些修改,自己可能就能解决。我们希望这能够让程序猿们多尝试多前进。
我们介绍了三个全新的http库:async-h1,async-native-tls和http-types。他们专为围绕异步字节流构建,并提供了兼顾易用性和性能的API。要想抓住所有的细节是非常困难的,不过我们在之前的六个月里不停的构建和重构这些库,所以我们非常自豪地在这里与你分享,希望能够给Rust的http提供一个方向。希望你能喜欢我们的工作,也希望你能自己去尝试一下!