Применение: HTTP сервер

Давайте используем async/.await для создания echo-сервера!

Для начала, запустите rustup update stable чтобы быть уверенным, что используете стабильную версию Rust - 1.39 или более новую. Когда вы закончите это, создайте новый проект с помощь cargo new async-await-echo и откройте созданную директорию async-await-echo.

Добавим некоторые зависимости в файл Cargo.toml:

[dependencies]
# Hyper is an asynchronous HTTP library. We'll use it to power our HTTP
# server and to make HTTP requests.
hyper = "0.13"
# To setup some sort of runtime needed by Hyper, we will use the Tokio runtime.
tokio = { version = "0.2", features = ["full"] }

# (only for testing)
anyhow = "1.0.31"
reqwest = { version = "0.10.4", features = ["blocking"] }

Теперь, когда у нас есть свои зависимости, давайте начнём писать код. Вот список зависимостей, которые необходимо добавить:


#![allow(unused)]
fn main() {
use {
    hyper::{
        // Following functions are used by Hyper to handle a `Request`
        // and returning a `Response` in an asynchronous manner by using a Future
        service::{make_service_fn, service_fn},
        // Miscellaneous types from Hyper for working with HTTP.
        Body,
        Client,
        Request,
        Response,
        Server,
        Uri,
    },
    std::net::SocketAddr,
};
}

Как только закончим с импортами, мы можем собрать вместе весь шаблонный код, который позволит обрабатывать запросы:

async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    // Always return successfully with a response containing a body with
    // a friendly greeting ;)
    Ok(Response::new(Body::from("hello, world!")))
}

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

    // Create a server bound on the provided address
    let serve_future = Server::bind(&addr)
        // Serve requests using our `async serve_req` function.
        // `serve` takes a type which implements the `MakeService` trait.
        // `make_service_fn` converts a closure into a type which
        // implements the `MakeService` trait. That closure must return a
        // type that implements the `Service` trait, and `service_fn`
        // converts a request-response function into a type that implements
        // the `Service` trait.
        .serve(make_service_fn(|_| async {
            Ok::<_, hyper::Error>(service_fn(serve_req))
        }));

    // Wait for the server to complete serving or exit with an error.
    // If an error occurred, print it to stderr.
    if let Err(e) = serve_future.await {
        eprintln!("server error: {}", e);
    }
}

#[tokio::main]
async fn main() {
  // Set the address to run our socket on.
  let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

  // Call our `run_server` function, which returns a future.
  // As with every `async fn`, for `run_server` to do anything,
  // the returned future needs to be run using `await`;
  run_server(addr).await;
}

Если вы сейчас запустите cargo run, в консоли вы увидите сообщение "Listening on http://127.0.0.1:3000". Если вы откроете URL в вашем любимом браузере, вы увидите как в нём отобразится "hello, world!". Поздравляем! Вы только что написали свой первый асинхронный web-сервер на Rust.

Вы также можете посмотреть сам запрос, который содержит такую информацию, как URI, версию HTTP, заголовки и другие метаданные. Например, мы можем вывести URI запроса следующим образом:


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

Вы могли заметить, что мы до сих пор не делали ничего асинхронного для обработки запроса - мы только незамедлительно ответили на него, мы не пользуемся гибкостью, которую нам даёт async fn. Вместо этого, мы только возвращаем статическое сообщение. Давайте попробуем проксировать пользовательский запрос на другой web-сайт используя HTTP-клиент Hyper'а.

Мы начнём с парсинга URL, который мы хотим запросить:


#![allow(unused)]
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)]
fn main() {
        let res = Client::new().get(url).await?;
        // Return the result of the request directly to the user
        println!("request finished-- returning response");
        Ok(res)
}

Client::get возвращает hyper::client::FutureResponse, который реализует Future<Output = Result<Response, Error>> (или Future<Item = Response, Error = Error> в терминах futures 0.1). Когда мы разрешаем (.await) футуру, отправляется HTTP-запрос, текущая задача приостанавливается и становится в очередь, чтобы продолжить работу после получения ответа.

Если вы сейчас запустите 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 запрос.