Начало работы

Для начала мы просто получим работающий простой 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