Запуск асинхронного кода
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
. В следующем разделе мы увидим, как использовать асинхронный код для одновременной обработки подключений.