Запуск асинхронного кода

HTTP-сервер должен иметь возможность обслуживать несколько клиентов одновременно, не дожидаясь завершения предыдущих запросов перед обработкой текущего запроса. В книге эта проблема решается путём создания пула потоков, в котором каждое соединение обрабатывается в отдельном потоке. Здесь вместо повышения пропускной способности за счёт добавления потоков мы добьёмся того же эффекта, используя асинхронный код.

Давайте изменим handle_connection, чтобы он возвращал футуру, объявив его async fn:

async fn handle_connection(mut stream: TcpStream) {
    //<-- snip -->
}

Добавление async к объявлению функции меняет тип возвращаемого значения с unit type () на тип, который реализует Future<Output=()>.

Если мы попытаемся скомпилировать это, компилятор предупредит нас, что это не сработает:

$ cargo check
    Checking async-rust v0.1.0 (file:///projects/async-rust)
warning: unused implementer of `std::future::Future` that must be used
  --> src/main.rs:12:9
   |
12 |         handle_connection(stream);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: futures do nothing unless you `.await` or poll them

Поскольку мы не сделали await или poll функции handle_connection, то она никогда не запустится. Если вы запустите сервер и зайдёте на 127.0.0.1:7878 в браузере, то увидите, что в соединении отказано, наш сервер не обрабатывает запросы.

Мы не можем выполнить await или poll футуры в синхронном коде, как если бы это был вызов обычной функции. Нам понадобится асинхронная среда выполнения для планирования и выполнения футур. Обратитесь к разделу о выборе среды выполнения для получения дополнительной информации об асинхронных средах выполнения, исполнителях и реакторах. Для этого проекта подойдёт любая из перечисленных сред выполнения, но для этих примеров мы решили использовать крейт async-std.

Добавление асинхронной среды выполнения

В следующем примере демонстрируется рефакторинг синхронного кода в асинхронный, мы используем async-std. #[async_std::main] из async-std позволяет нам написать асинхронную основную функцию. Чтобы использовать его, включите опцию attributes для async-std в Cargo.toml :

[dependencies.async-std]
version = "1.6"
features = ["attributes"]

В качестве первого шага мы переключимся на асинхронную основную функцию и будем ожидать (await) футуры, возвращаемой асинхронной версией handle_connection. Затем мы проверим, как сервер отвечает. Вот как это будет выглядеть:

#[async_std::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        // Warning: This is not concurrent!
        handle_connection(stream).await;
    }
}

Теперь давайте проверим, может ли наш сервер одновременно обрабатывать соединения. Сделать handle_connection асинхронным не достаточно, чтобы сервер мог обрабатывать несколько подключений одновременно, и скоро мы увидим почему.

Чтобы проиллюстрировать это, давайте смоделируем медленный запрос. Когда клиент делает запрос к 127.0.0.1:7878/sleep , наш сервер будет спать в течение 5 секунд:

use std::time::Duration;
use async_std::task;

async fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        task::sleep(Duration::from_secs(5)).await;
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };
    let contents = fs::read_to_string(filename).unwrap();

    let response = format!("{status_line}{contents}");
    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Это очень похоже на симуляцию медленного запроса из Книги, но с одним важным отличием: мы используем неблокирующую функцию async_std::task::sleep вместо блокирующей std::thread::sleep. Важно помнить, что даже если функция асинхронная (async fn) и будет ожидать выполнения (await), она все равно может быть блокирующей. Чтобы проверить, обрабатывает ли наш сервер соединения одновременно, нам нужно убедиться, что handle_connection неблокирующая.

Если вы запустите сервер, то увидите, что запрос на 127.0.0.1:7878/sleep блокирует любые другие входящие запросы на 5 секунд! Это связано с тем, что другие конкурентные задачи не могут выполняться, пока мы ожидаем (await) результата handle_connection. В следующем разделе мы увидим, как использовать асинхронный код для одновременной обработки подключений.