Начало работы
Для начала мы просто получим работающий простой GET запрос к веб-странице, чтобы увидеть все взаимодействующие части. Сначала нам нужны зависимости. Сообщим Cargo о наших зависимостях, добавив это в Cargo.toml.
Зависимости
[dependencies]
hyper = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
Теперь нам нужно импортировать компоненты из наших зависимостей:
extern crate http_body_util; extern crate hyper; extern crate tokio; extern crate hyper_util; use http_body_util::Empty; use hyper::Request; use hyper::body::Bytes; use hyper_util::rt::TokioIo; use tokio::net::TcpStream; fn main() {}
Runtime (среда выполнения)
Теперь мы сделаем запрос в main нашей программы. Это может показаться несколько сложным для простого запроса, и вы будете правы, но цель здесь — просто показать всю необходимую настройку. Как только у вас это есть, вы готовы эффективно выполнять тысячи клиентских запросов.
Нам нужно настроить какую-либо среду выполнения (runtime). Вы можете использовать любой асинхронный runtime, но в этом руководстве мы будем использовать tokio. Если вы никогда раньше не использовали futures в Rust, вам может быть полезно прочитать руководство Tokio по Futures.
extern crate tokio; mod no_run { #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { // Здесь мы настроим наши HTTP клиентские запросы. Ok(()) } } fn main() {}
Настройка
Для начала нам нужно настроить несколько вещей. В этом руководстве мы собираемся отправить GET [Request][Request] на http://httpbin.org/ip, который вернет 200 OK и IP-адрес отправителя в теле ответа.
Нам нужно открыть TCP-соединение с удаленным хостом, используя имя хоста и порт, в данном случае это httpbin.org и порт по умолчанию для HTTP: 80. После открытия соединения мы передаем его в функцию client::conn::http1::handshake, выполняя рукопожатие для подтверждения готовности удаленной стороны принимать наши запросы.
Успешное рукопожатие даст нам future [Connection][Connection], который обрабатывает все состояние HTTP, и структуру [SendRequest][SendRequest], которую мы можем использовать для отправки наших Request по соединению.
Чтобы начать управлять состоянием HTTP, мы должны опрашивать (poll) Connection, поэтому для завершения настройки мы породим задачу tokio::task и будем awaitить её.
extern crate http_body_util; extern crate hyper; extern crate hyper_util; extern crate tokio; use http_body_util::Empty; use hyper::body::Bytes; use hyper::Request; use hyper_util::rt::TokioIo; use tokio::net::TcpStream; async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { // Парсим наш URL... let url = "http://httpbin.org/ip".parse::<hyper::Uri>()?; // Получаем хост и порт let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); let address = format!("{}:{}", host, port); // Открываем TCP-соединение с удаленным хостом let stream = TcpStream::connect(address).await?; // Используем адаптер для доступа к чему-то, реализующему трейты `tokio::io`, как если бы они реализовывали // трейты ввода-вывода `hyper::rt`. let io = TokioIo::new(stream); // Создаем клиент Hyper let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; // Порождаем задачу для опроса соединения, управляя состоянием HTTP tokio::task::spawn(async move { if let Err(err) = conn.await { println!("Connection failed: {:?}", err); } }); let authority = url.authority().unwrap().clone(); let req = Request::builder() .uri(url) .header(hyper::header::HOST, authority.as_str()) .body(Empty::<Bytes>::new())?; let mut res = sender.send_request(req).await?; Ok(()) } fn main() {}
GET
Теперь, когда мы настроили наше соединение, мы готовы сконструировать и отправить наш первый Request! Поскольку SendRequest не требует URI в абсолютной форме, мы обязаны включать заголовок HOST в наши запросы. И хотя мы можем отправить наш Request с пустым Body, нам нужно явно его установить, что мы сделаем с помощью утилитарной структуры [Empty][Empty].
Все, что нам нужно сделать теперь, это передать Request в SendRequest::send_request, это возвращает future, который разрешится в [Response][Response] от httpbin.org. Мы выведем статус ответа, чтобы убедиться, что он вернул ожидаемый статус 200 OK.
extern crate http_body_util; extern crate hyper; extern crate hyper_util; extern crate tokio; use http_body_util::Empty; use hyper::body::Bytes; use hyper::Request; use hyper_util::rt::TokioIo; use tokio::net::TcpStream; async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { let url = "http://httpbin.org/ip".parse::<hyper::Uri>()?; let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); let addr = format!("{}:{}", host, port); let stream = TcpStream::connect(addr).await?; let io = TokioIo::new(stream); let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; tokio::task::spawn(async move { if let Err(err) = conn.await { println!("Connection failed: {:?}", err); } }); // Authority нашего URL будет именем хоста удаленного httpbin let authority = url.authority().unwrap().clone(); // Создаем HTTP запрос с пустым телом и заголовком HOST let req = Request::builder() .uri(url) .header(hyper::header::HOST, authority.as_str()) .body(Empty::<Bytes>::new())?; // Ожидаем ответ... let mut res = sender.send_request(req).await?; println!("Response status: {}", res.status()); Ok(()) } fn main() {}
Тела ответов
Мы знаем, что отправка GET Request на httpbin.org/ip вернет наш IP-адрес в теле Response. Чтобы увидеть возвращенное тело, мы просто запишем его в stdout.
Тела в hyper являются асинхронными потоками [Frame][Frame], поэтому нам не нужно ждать, пока все тело придет, буферизуя его в памяти, а затем записывать. Мы можем просто awaitить каждый Frame и записывать их непосредственно в stdout по мере поступления!
В дополнение к импорту stdout, нам нужно использовать трейт BodyExt:
#![allow(unused)] fn main() { extern crate http_body_util; extern crate tokio; use http_body_util::BodyExt; use tokio::io::{AsyncWriteExt as _, self}; }
extern crate http_body_util; extern crate hyper; extern crate hyper_util; extern crate tokio; use http_body_util::{BodyExt, Empty}; use hyper::body::Bytes; use hyper::Request; use hyper_util::rt::TokioIo; use tokio::net::TcpStream; use tokio::io::{self, AsyncWriteExt as _}; async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { let url = "http://httpbin.org/ip".parse::<hyper::Uri>()?; let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); let addr = format!("{}:{}", host, port); let stream = TcpStream::connect(addr).await?; let io = TokioIo::new(stream); let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; tokio::task::spawn(async move { if let Err(err) = conn.await { println!("Connection failed: {:?}", err); } }); let authority = url.authority().unwrap().clone(); let req = Request::builder() .uri(url) .header(hyper::header::HOST, authority.as_str()) .body(Empty::<Bytes>::new())?; let mut res = sender.send_request(req).await?; // Потоково передаем тело, записывая каждый фрейм в stdout по мере его поступления while let Some(next) = res.frame().await { let frame = next?; if let Some(chunk) = frame.data_ref() { io::stdout().write_all(chunk).await?; } } Ok(()) } fn main() {}
И это все! Вы можете увидеть [полный пример здесь][example].
[StatusCode]: {{ site.hyper_docs_url }}/hyper/struct.StatusCode.html [Response]: {{ site.http_docs_url }}/http/response/struct.Response.html [Request]: {{ site.http_docs_url }}/http/request/struct.Request.html [Connection]: {{ site.hyper_docs_url }}/hyper/client/conn/http1/struct.Connection.html [SendRequest]: {{ site.hyper_docs_url }}/hyper/client/conn/http1/struct.SendRequest.html [Frame]: {{ site.hyper_docs_url }}/hyper/body/struct.Frame.html [Empty]: {{ site.http_body_util_url }}/http_body_util/struct.Empty.html
[example]: {{ site.examples_url }}/client.rs