Echo, echo ...
У вас уже есть сервер Hello World? Отлично! Обычно серверы делают больше, чем просто выдают одно и то же тело для каждого запроса. Чтобы продемонстрировать несколько других возможностей hyper, это руководство пройдет через создание эхо-сервера.
Эхо-сервер будет прослушивать входящие соединения и возвращать тело запроса как тело ответа для POST запросов.
Маршрутизация
Первое, что мы сделаем, помимо переименования нашего сервиса в echo, это настройка маршрутизации. Мы хотим иметь маршрут с инструкциями по использованию нашего сервера и другой для приема данных. О, и мы также должны обработать случай, когда кто-то запрашивает неизвестный маршрут!
Прежде чем начать, нам нужно добавить несколько новых импортов:
extern crate hyper; extern crate http_body_util; use hyper::body::Frame; use hyper::{Method, StatusCode}; use http_body_util::{combinators::BoxBody, BodyExt}; fn main() {}
Далее нам нужно внести некоторые изменения в нашу функцию Service, но как вы можете видеть, это все еще просто асинхронная функция, которая принимает Request и возвращает future Response, и вы можете передать ее вашему серверу так же, как мы делали для сервиса hello.
В отличие от нашего сервиса hello, где мы не заботились о теле запроса и всегда возвращали один фрагмент байтов с нашим приветствием, теперь мы хотим немного больше свободы в формировании тела нашего Response. Для этого мы изменим тип Body в нашем Response на упакованный объект трейта. Нас интересует только то, что тело ответа реализует трейт Body, что его данные - это Bytes, а его ошибка - hyper::Error.
extern crate hyper; extern crate http_body_util; use hyper::body::Bytes; use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::{Method, Request, Response, StatusCode}; async fn echo( req: Request<hyper::body::Incoming>, ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> { match (req.method(), req.uri().path()) { (&Method::GET, "/") => Ok(Response::new(full( "Try POSTing data to /echo", ))), (&Method::POST, "/echo") => { // мы вернемся к этому Ok(Response::new(req.into_body().boxed())) }, // Возвращаем 404 Not Found для других маршрутов. _ => { let mut not_found = Response::new(empty()); *not_found.status_mut() = StatusCode::NOT_FOUND; Ok(not_found) } } } // Мы создаем некоторые вспомогательные функции, чтобы Empty и Full тела // соответствовали нашему расширенному типу тела Response. fn empty() -> BoxBody<Bytes, hyper::Error> { Empty::<Bytes>::new() .map_err(|never| match never {}) .boxed() } fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> { Full::new(chunk.into()) .map_err(|never| match never {}) .boxed() } fn main() {}
Мы построили супер простую таблицу маршрутизации, просто сопоставляя method и path входящего Request. Если кто-то запрашивает GET /, наш сервис сообщит им, что они должны попробовать наши эхо-возможности. Мы также проверяем POST /echo, но в настоящее время ничего с этим не делаем.
Наше третье правило перехватывает любую другую комбинацию метода и пути и изменяет StatusCode Response. Статус по умолчанию для Response - это HTTP 200 OK (StatusCode::OK), что правильно для других маршрутов. Но третий случай вместо этого отправит обратно 404 Not Found.
Потоки тела
Теперь давайте реализуем это эхо. HTTP тело - это поток Frame, каждый Frame содержит части данных Body или трейлеры. Поэтому вместо того, чтобы читать все Body в буфер перед отправкой нашего ответа, мы можем передавать каждый кадр по мере его поступления. Мы начнем с самого простого решения, а затем внесем изменения, демонстрируя более сложные вещи, которые вы можете делать с потоками Body.
Первое - простое эхо. И Request, и Response имеют потоки тела, и по умолчанию вы можете легко передать Body Request в Response.
extern crate hyper; extern crate http_body_util; use hyper::body::Bytes; use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::{Method, Request, Response, StatusCode}; async fn echo( req: Request<hyper::body::Incoming>, ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> { match (req.method(), req.uri().path()) { // Внутри match из предыдущего кода (&Method::POST, "/echo") => Ok(Response::new(req.into_body().boxed())), _ => unreachable!(), } } fn main() {}
Запуск нашего сервера теперь будет возвращать эхо любых данных, которые мы отправляем через POST на /echo. Это было легко. Что если мы хотим преобразовать весь текст в верхний регистр? Мы могли бы использовать map на наших потоках.
Отображение тела
Каждый data Frame нашего потока тела - это фрагмент байтов, который мы можем удобно представить с помощью типа Bytes из hyper. Его можно легко преобразовать в другие типичные контейнеры байтов.
Далее давайте добавим новый маршрут /echo/uppercase, отображающий каждый байт в data Frame нашего тела запроса в верхний регистр и возвращающий поток в нашем Response:
extern crate hyper; extern crate http_body_util; use hyper::body::Bytes; use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::body::Frame; use hyper::{Method, Request, Response, StatusCode}; async fn echo( req: Request<hyper::body::Incoming>, ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> { match (req.method(), req.uri().path()) { // Еще один маршрут внутри нашего блока match... (&Method::POST, "/echo/uppercase") => { // Отображаем frame этого тела в другой тип let frame_stream = req.into_body().map_frame(|frame| { let frame = if let Ok(data) = frame.into_data() { // Преобразуем каждый байт в каждом Data frame в верхний регистр data.iter() .map(|byte| byte.to_ascii_uppercase()) .collect::<Bytes>() } else { Bytes::new() }; Frame::data(frame) }); Ok(Response::new(frame_stream.boxed())) }, _ => unreachable!(), } } fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> { Full::new(chunk.into()) .map_err(|never| match never {}) .boxed() } fn main() {}
И вот так у нас есть два эхо-маршрута: /echo, который не выполняет преобразований, и /echo/uppercase, который возвращает все байты после преобразования их в ASCII верхний регистр.
Буферизация тела запроса
Что если мы хотим, чтобы наш эхо-сервис переворачивал полученные данные и отправлял их обратно нам? Мы не можем действительно передавать данные по мере их поступления, поскольку нам нужно найти конец, прежде чем мы сможем ответить. Чтобы сделать это, мы можем изучить, как легко собрать полное тело.
Мы хотим собрать все тело запроса и отобразить результат в нашу функцию reverse, затем вернуть окончательный результат. Если мы импортируем трейт-расширение http_body_util::BodyExt, мы можем вызвать метод collect на нашем теле, который доведет поток до завершения, собирая все data и trailer frames в тип Collected. Мы можем легко превратить Collected тело в единый Bytes, вызвав его метод into_bytes.
Примечание: Вы всегда должны быть осторожны, чтобы не буферизовать без максимального ограничения. Мы установим здесь максимум 64 КБ.
extern crate hyper; extern crate http_body_util; use hyper::body::Bytes; use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::{body::Body, Method, Request, Response, StatusCode}; async fn echo( req: Request<hyper::body::Incoming>, ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> { match (req.method(), req.uri().path()) { // Еще один маршрут внутри нашего блока match... (&Method::POST, "/echo/reversed") => { // Защищаем наш сервер от огромных тел. let upper = req.body().size_hint().upper().unwrap_or(u64::MAX); if upper > 1024 * 64 { let mut resp = Response::new(full("Body too big")); *resp.status_mut() = hyper::StatusCode::PAYLOAD_TOO_LARGE; return Ok(resp); } // Ожидаем, пока все тело будет собрано в единый `Bytes`... let whole_body = req.collect().await?.to_bytes(); // Итерируем все тело в обратном порядке и собираем в новый Vec. let reversed_body = whole_body.iter() .rev() .cloned() .collect::<Vec<u8>>(); Ok(Response::new(full(reversed_body))) }, _ => unreachable!(), } } fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> { Full::new(chunk.into()) .map_err(|never| match never {}) .boxed() } fn main() {}
Вы можете увидеть компилируемый [пример здесь][example].
[example]: {{ site.examples_url }}/echo.rs