< 返回版块

黑豆腐 发表于 2020-03-06 00:49

Tags:rust

本文是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 = "<password>";

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

你可能会注意到,我们写的库是围绕着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<usize> {
    let b = fs::read(p).await.status(501)?;
    Ok(b.len())
}

这是我们迈向把错误处理作为如Tide和Surf之类的http框架中一等公民的第一步。我们还在探索中,也希望听到你的声音!

TRAIT转发

我们不仅仅在错误处理中使用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<http_types::Headers>) {
    // set cors headers here
}

fn forwarded_for(headers: impl AsRef<http_types::Headers>) {
    // 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提供一个方向。 希望你能喜欢我们的工作,也希望你能自己去尝试一下!

评论区

写评论
jmjoy 2020-03-07 20:36

不错,希望Rust的web生态越来越好。

1 共 1 条评论, 1 页