应用:建立HTTP服务器

让我们用async/.await来构建一个回显(echo)服务器吧!

首先,运行rustup update nightly,确保我们用的是伟大的Rust的最新拷贝——我们要用最新潮 的特性,所以保持更新很必要。然后,运行cargo +nightly new async-await-echo来创建 新项目,并打开生成的async-await-echo文件夹。

现在给Cargo.toml文件添加依赖:

[dependencies]
# The latest version of the "futures" library, which has lots of utilities
# for writing async code. Enable the "compat" feature to include the
# functions for using futures 0.3 and async/await with the Hyper library,
# which use futures 0.1.
futures-preview = { version = "=0.3.0-alpha.16", features = ["compat"] }

# Hyper is an asynchronous HTTP library. We'll use it to power our HTTP
# server and to make HTTP requests.
hyper = "0.12.9"

现在我们搞定了依赖,让我们开始写代码。打开src/main.rs并在文件开头启用async_await 特性:


# #![allow(unused_variables)]
#![feature(async_await)]
#fn main() {
#}

这使我们现在就可以使用夜版专属(nightly-only)的async/await语法。这个语法很快就会 稳定的。

此外,我们还要加些导入(import):


# #![allow(unused_variables)]
#fn main() {
use {
    hyper::{
        // `Hyper` 中用于处理 `HTTP` 的类型.
        Body, Client, Request, Response, Server, Uri,

        // 这个函数将一个 `future` 的闭包返回转换为 `Hyper Server` trait.
        // 这是一个从普通的请求到响应的异步函数.
        service::service_fn,

        // 这个函数使用 `Hyper` 运行时运行 `future` 直到完成.
        rt::run,
    },
    futures::{
        // `futures 0.1` 的扩展 `trait`, 加上这个 `.compat()` 方法
        // 这可以使我们在 `futures 0.1` 上使用 `.await`.
        compat::Future01CompatExt,
        // 扩展的 `trait` 提供对 `future` 的额外补充方法.
        // `FutureExt` 增加了适用于所有 `future` 的方法,
        // 而 `TryFutureExt` 则向返回 `Result` 类型的 `future` 添加方法.
        future::{FutureExt, TryFutureExt},
    },
    std::net::SocketAddr,
};
#}

导入之后,我们就能开始拼模板写服务了:

async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    // 总是返回一个包含 `hello, world!` 的成功响应.
    Ok(Response::new(Body::from("hello, world!")))
}

async fn run_server(addr: SocketAddr) {
    println!("Listening on http://{}", addr);

    // 在指定的地址上创建服务器.
    let serve_future = Server::bind(&addr)
        // 使用 `async serve_req` 函数来处理请求.
        // `serve` 接受一个闭包,它将返回一个实现了 `Service` trait的类型.
        // `service_fn` 返回一个实现了 `Service` trait的值,并接受一个从
        // 请求到响应的 `future` 闭包, 要在 `Hyper` 中使用 `serve_req` 函
        // 数,我们必须将它打包好并将其放入兼容性容器中,以便从 `futures 0.3`
        // (由 `async fn`返回的那种) 转换到 `futures 0.1` (由 `Hyper` 使
        // 用的那种 ).
        .serve(|| service_fn(|req| serve_req(req).boxed().compat()));
    
    // 等待服务器完成服务或者因错误退出.  
    // 如果发生错误,将错误输出到 `stderr`.
    if let Err(e) = serve_future.compat().await {
        eprintln!("server error: {}", e);
    }
}

fn main() {
    // 设置 `socket` 地址.
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    // 调用我们的 `run_server` 函数, 它将返回一个 `future`.
    // 和 `async fn` 一样, 要让 `run_server` 执行任何操作,
    // 都需要运行返回的 `future`. 并且我们需要将返回的 
    // `future` 从 `0.3` 转换为 `0.1`.
    let futures_03_future = run_server(addr);
    let futures_01_future = futures_03_future.unit_error().boxed().compat();

    // 最后,我们使用 `Hyper` 提供的 `run` 函数来运行未完成的 `future`.
    run(futures_01_future);
}

如果你此时执行cargo run,你应该能看到"Listening on http://127.0.0.1:3000"打印到 终端上。如果你用浏览器打开这URL,你会看到"Hello, world!"。可喜可贺!你刚用Rust写了 第一个异步Web服务器!

你也可以检查请求,里面包含了很多信息,像请求URI,HTTP版本,报文头和其他元数据。例如, 我们可以输出请求URI,像这样:


# #![allow(unused_variables)]
#fn main() {
println!("Got request at {:?}", req.uri());
#}

你可能注意到我们在处理请求时还没有做任何异步操作——我们立刻返回,所以我们并没有充分利用 async fn函数提供给我们的灵活优势。比起返回静态信息,我们来试着来用Hyper的HTTP客户端 把用户请求代理到另外的网站。

我们从解析我们想要发送请求的URL开始:


# #![allow(unused_variables)]
#fn main() {
let url_str = "http://www.rust-lang.org/en-US/";
let url = url_str.parse::<Uri>().expect("failed to parse URL");
#}

然后我们创建一个hyper::Client,并用它发送Get请求,并返回响应给用户:


# #![allow(unused_variables)]
#fn main() {
let res = Client::new().get(url).compat().await;
// 将请求的结果直接返回给调用者.
println!("request finished-- returning response");
res
#}

Client::get返回一个hyper::client::FutureResponse, 它实现了 Future<Output = Result<Response, Error>> (或者在futures 0.1我们叫Future<Item = Response, Error = Error>)。 当我们.await这个future时, 发送了一个HTTP request, 当前任务挂起了,然后排队等待响应 可用时继续执行。

现在,如果你执行cargo run并打开http://127.0.0.1:3000/foo,你会看到Rust主页,以及 以下命令行输出:

Listening on http://127.0.0.1:3000
Got request at /foo
making request to http://www.rust-lang.org/en-US/
request finished-- returning response

再次恭喜!你刚刚代理了一个HTTP请求!