MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust使用hyper进行HTTP客户端开发

2022-10-087.4k 阅读

一、Rust 与 hyper 简介

Rust 是一种系统级编程语言,注重性能、安全和并发。它通过所有权系统、借用检查和生命周期管理等机制,在编译时就能避免许多常见的内存安全问题,如空指针引用、数据竞争等。同时,Rust 提供了高效的底层控制能力,适用于编写网络、操作系统、数据库等对性能要求较高的应用程序。

hyper 是 Rust 生态系统中一个流行的 HTTP 库,支持构建 HTTP 客户端和服务器。它基于 futures 库,采用异步编程模型,能够高效地处理大量并发请求。hyper 提供了简洁易用的 API,支持 HTTP/1.1 和 HTTP/2 协议,使得在 Rust 中进行 HTTP 开发变得相对轻松。

二、环境搭建

在开始使用 hyper 进行 HTTP 客户端开发之前,需要确保你已经安装了 Rust 环境。可以通过官方网站 rustup.rs 下载并安装 rustup,这是 Rust 的版本管理工具。安装完成后,通过以下命令检查 Rust 是否安装成功:

rustc --version

如果输出 Rust 的版本号,说明安装成功。

接下来,创建一个新的 Rust 项目:

cargo new hyper_client_demo
cd hyper_client_demo

然后,在 Cargo.toml 文件中添加 hyper 依赖:

[dependencies]
hyper = "0.14"
tokio = { version = "1", features = ["full"] }

这里使用的 hyper 版本是 0.14,同时引入了 tokio 库。tokio 是 Rust 中一个流行的异步运行时,hyper 依赖它来提供异步执行环境。features = ["full"] 表示引入 tokio 的全部特性,以支持更丰富的功能。

三、基本 GET 请求

在 Rust 中使用 hyper 发送基本的 GET 请求非常简单。以下是一个示例代码:

use hyper::{Client, Uri};
use tokio::main;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();
    let uri = Uri::try_from("https://www.example.com")?;

    let response = client.get(uri).await?;
    println!("Status: {}", response.status());
    println!("Headers: {:?}", response.headers());

    let body = hyper::body::to_bytes(response.into_body()).await?;
    println!("Body: {}", std::str::from_utf8(&body).unwrap());

    Ok(())
}

在这段代码中:

  1. 创建客户端let client = Client::new(); 创建了一个 hyper 的 Client 实例,用于发送 HTTP 请求。
  2. 构建请求 URIlet uri = Uri::try_from("https://www.example.com")?; 将字符串转换为 Uri 类型,这是 hyper 中表示请求地址的类型。try_from 方法会尝试解析字符串,如果解析失败会返回一个错误。
  3. 发送请求并获取响应let response = client.get(uri).await?; 使用 client.get 方法发送 GET 请求,并通过 .await 等待请求完成,获取 Response 实例。? 操作符用于处理可能出现的错误,如果请求过程中发生错误,函数会提前返回并将错误传递出去。
  4. 处理响应
    • println!("Status: {}", response.status()); 打印响应的状态码。
    • println!("Headers: {:?}", response.headers()); 打印响应的头部信息。{:?} 是 Rust 格式化输出中用于调试输出的格式说明符,适用于结构体等复杂类型。
    • let body = hyper::body::to_bytes(response.into_body()).await?; 将响应体转换为字节数组。response.into_body() 获取响应的 Body 类型,hyper::body::to_bytes 函数将其转换为字节数组。同样,这里使用 .await 等待转换完成,并使用 ? 处理可能的错误。
    • println!("Body: {}", std::str::from_utf8(&body).unwrap()); 将字节数组转换为字符串并打印。这里假设响应体是 UTF - 8 编码的文本,unwrap 方法在转换失败时会导致程序 panic,在实际应用中可能需要更稳健的错误处理。

四、处理查询参数

在实际开发中,经常需要在 GET 请求中携带查询参数。可以通过构建 Uri 时包含查询参数来实现。以下是一个示例:

use hyper::{Client, Uri};
use tokio::main;
use url::form_urlencoded;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();

    let mut params = form_urlencoded::Serializer::new(String::new());
    params.append_pair("key1", "value1");
    params.append_pair("key2", "value2");
    let query_string = params.finish();

    let uri = format!("https://www.example.com?{}", query_string);
    let uri = Uri::try_from(uri.as_str())?;

    let response = client.get(uri).await?;
    println!("Status: {}", response.status());

    let body = hyper::body::to_bytes(response.into_body()).await?;
    println!("Body: {}", std::str::from_utf8(&body).unwrap());

    Ok(())
}

在这段代码中:

  1. 构建查询参数:使用 url::form_urlencoded::Serializer 来构建查询参数。append_pair 方法用于添加键值对,最后通过 finish 方法得到完整的查询字符串。
  2. 构建包含查询参数的 URI:将查询字符串拼接到基础 URL 后面,然后转换为 Uri 类型。
  3. 发送请求和处理响应:与之前的基本 GET 请求类似,发送请求并处理响应的状态码和响应体。

五、POST 请求

发送 POST 请求与 GET 请求类似,但需要设置请求体。以下是一个发送 JSON 格式数据的 POST 请求示例:

use hyper::{Body, Client, Request, Uri};
use serde::{Deserialize, Serialize};
use serde_json;
use tokio::main;

#[derive(Serialize, Deserialize)]
struct RequestData {
    key: String,
    value: i32,
}

#[derive(Deserialize)]
struct ResponseData {
    result: String,
}

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();
    let uri = Uri::try_from("https://www.example.com/api")?;

    let request_data = RequestData {
        key: "example_key".to_string(),
        value: 42,
    };
    let json_data = serde_json::to_string(&request_data)?;
    let body = Body::from(json_data);

    let mut request = Request::post(uri);
    request = request.header("Content-Type", "application/json");
    request = request.body(body)?;

    let response = client.request(request).await?;
    println!("Status: {}", response.status());

    let body = hyper::body::to_bytes(response.into_body()).await?;
    let response_data: ResponseData = serde_json::from_slice(&body)?;
    println!("Response Data: {:?}", response_data);

    Ok(())
}

在这个示例中:

  1. 定义数据结构:使用 serde 库的 SerializeDeserialize 特性定义了 RequestDataResponseData 结构体,分别用于表示请求体和响应体的数据。
  2. 构建请求体:将 RequestData 结构体转换为 JSON 字符串,并通过 Body::from 方法将其转换为 Body 类型。
  3. 构建 POST 请求:使用 Request::post 方法创建一个 POST 请求,并设置 Content - Type 头部为 application/json。然后将请求体添加到请求中。
  4. 发送请求和处理响应:使用 client.request 方法发送请求,等待响应完成。将响应体转换为字节数组后,使用 serde_json::from_slice 方法将其反序列化为 ResponseData 结构体,并打印出来。

六、自定义请求头

在实际应用中,可能需要在请求中添加自定义的请求头。以下是在前面 POST 请求示例基础上添加自定义请求头的代码:

use hyper::{Body, Client, Request, Uri};
use serde::{Deserialize, Serialize};
use serde_json;
use tokio::main;

#[derive(Serialize, Deserialize)]
struct RequestData {
    key: String,
    value: i32,
}

#[derive(Deserialize)]
struct ResponseData {
    result: String,
}

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();
    let uri = Uri::try_from("https://www.example.com/api")?;

    let request_data = RequestData {
        key: "example_key".to_string(),
        value: 42,
    };
    let json_data = serde_json::to_string(&request_data)?;
    let body = Body::from(json_data);

    let mut request = Request::post(uri);
    request = request.header("Content-Type", "application/json");
    request = request.header("Custom - Header", "custom_value"); // 添加自定义请求头
    request = request.body(body)?;

    let response = client.request(request).await?;
    println!("Status: {}", response.status());

    let body = hyper::body::to_bytes(response.into_body()).await?;
    let response_data: ResponseData = serde_json::from_slice(&body)?;
    println!("Response Data: {:?}", response_data);

    Ok(())
}

在上述代码中,通过 request = request.header("Custom - Header", "custom_value"); 这一行添加了一个名为 Custom - Header,值为 custom_value 的自定义请求头。

七、处理重定向

hyper 默认会自动处理 HTTP 重定向。但在某些情况下,可能需要手动控制重定向的行为。以下是一个示例,展示如何禁用自动重定向并手动处理重定向:

use hyper::{Body, Client, Request, Response, Uri};
use hyper::client::HttpConnector;
use hyper::header::{LOCATION, USER_AGENT};
use hyper::rt::Future;
use std::convert::Infallible;
use std::io::{self, Write};
use tokio::main;

#[main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = Client::builder()
      .build::<_, hyper::Body>(HttpConnector::new());

    let uri = Uri::try_from("https://www.example.com/redirect")?;

    let mut request = Request::get(uri);
    request = request.header(USER_AGENT, "hyper/0.14.12");

    let mut response = client.request(request).await?;

    loop {
        match response.status().as_u16() {
            301 | 302 | 303 | 307 | 308 => {
                let location = response.headers().get(LOCATION).ok_or_else(|| {
                    io::Error::new(io::ErrorKind::Other, "missing location header")
                })?;
                let new_uri = location.to_str().map_err(|_| {
                    io::Error::new(io::ErrorKind::Other, "invalid location header")
                })?;
                let new_uri = Uri::try_from(new_uri)?;

                let mut new_request = Request::get(new_uri);
                new_request = new_request.header(USER_AGENT, "hyper/0.14.12");

                response = client.request(new_request).await?;
            }
            _ => break,
        }
    }

    let body = hyper::body::to_bytes(response.into_body()).await?;
    println!("Final Body: {}", std::str::from_utf8(&body).unwrap());

    Ok(())
}

在这段代码中:

  1. 创建自定义客户端Client::builder().build::<_, hyper::Body>(HttpConnector::new()); 创建了一个可以自定义配置的客户端。
  2. 发送初始请求:发送 GET 请求,并设置 USER_AGENT 头部。
  3. 处理重定向:通过循环检查响应的状态码,如果是重定向状态码(301、302、303、307、308),则从响应头中获取 LOCATION 字段,构建新的请求并发送,直到不再遇到重定向。
  4. 处理最终响应:获取并打印最终的响应体。

八、HTTP/2 支持

hyper 对 HTTP/2 提供了良好的支持。在大多数情况下,只需使用默认配置,hyper 会根据服务器支持情况自动协商使用 HTTP/2 协议。以下是一个简单示例,展示如何在 hyper 中使用 HTTP/2:

use hyper::{Body, Client, Request, Uri};
use tokio::main;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();
    let uri = Uri::try_from("https://www.example.com")?;

    let mut request = Request::get(uri);
    request = request.header("accept", "application/json");

    let response = client.request(request).await?;
    println!("Status: {}", response.status());

    let body = hyper::body::to_bytes(response.into_body()).await?;
    println!("Body: {}", std::str::from_utf8(&body).unwrap());

    Ok(())
}

这段代码与普通的 HTTP 请求代码基本相同。当服务器支持 HTTP/2 且客户端与服务器成功协商后,hyper 会自动使用 HTTP/2 协议进行通信。如果要强制使用 HTTP/2,可以在构建客户端时进行相关配置:

use hyper::{Body, Client, Request, Uri};
use hyper::client::HttpConnector;
use hyper_rustls::HttpsConnector;
use rustls::ClientConfig;
use tokio::main;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let https = HttpsConnector::with_native_roots(ClientConfig::new());
    let client = Client::builder()
      .build::<_, Body>(HttpConnector::new().with_connector(https));

    let uri = Uri::try_from("https://www.example.com")?;

    let mut request = Request::get(uri);
    request = request.header("accept", "application/json");

    let response = client.request(request).await?;
    println!("Status: {}", response.status());

    let body = hyper::body::to_bytes(response.into_body()).await?;
    println!("Body: {}", std::str::from_utf8(&body).unwrap());

    Ok(())
}

在这个示例中,通过 hyper_rustls 库构建了一个支持 HTTP/2 的 HttpsConnector,并将其传递给 HttpConnector,从而强制客户端使用 HTTP/2 协议进行 HTTPS 通信。

九、错误处理

在 hyper 开发中,合理处理错误非常重要。前面的示例中使用了 ? 操作符来简单处理错误,但在实际应用中,可能需要更详细的错误处理逻辑。以下是一个更复杂的错误处理示例:

use hyper::{Body, Client, Request, Response, Uri};
use hyper::client::HttpConnector;
use hyper::http::Error as HyperError;
use hyper::rt::Future;
use std::convert::Infallible;
use std::io::{self, Write};
use tokio::main;

#[main]
async fn main() {
    let client = Client::new();
    let uri = match Uri::try_from("https://www.example.com/invalid - url") {
        Ok(uri) => uri,
        Err(e) => {
            eprintln!("Invalid URI: {}", e);
            return;
        }
    };

    let mut request = Request::get(uri);
    request = request.header("accept", "application/json");

    match client.request(request).await {
        Ok(response) => {
            println!("Status: {}", response.status());
            let body = hyper::body::to_bytes(response.into_body()).await;
            match body {
                Ok(body) => {
                    println!("Body: {}", std::str::from_utf8(&body).unwrap());
                }
                Err(e) => {
                    eprintln!("Error reading body: {}", e);
                }
            }
        }
        Err(e) => {
            eprintln!("Request error: {}", e);
        }
    }
}

在这段代码中:

  1. URI 解析错误处理:使用 match 表达式处理 Uri::try_from 可能返回的错误,如果 URI 无效,打印错误信息并提前结束程序。
  2. 请求错误处理:使用 match 表达式处理 client.request 可能返回的错误,如果请求失败,打印错误信息。
  3. 响应体读取错误处理:同样使用 match 表达式处理 hyper::body::to_bytes 可能返回的错误,如果读取响应体时发生错误,打印错误信息。

通过这种详细的错误处理,可以让程序在面对各种异常情况时更加健壮。

十、性能优化

  1. 连接池:在处理大量请求时,使用连接池可以显著提高性能。hyper 本身没有内置连接池,但可以结合 hyper - reuse 等库来实现连接池功能。以下是一个简单的示例,展示如何使用 hyper - reuse 构建连接池:
use hyper::client::HttpConnector;
use hyper::{Body, Client, Request, Uri};
use hyper_reuse::client::ReusableClient;
use tokio::main;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let connector = HttpConnector::new();
    let client = ReusableClient::new(connector);

    let uri1 = Uri::try_from("https://www.example.com/api1")?;
    let uri2 = Uri::try_from("https://www.example.com/api2")?;

    let request1 = Request::get(uri1);
    let request2 = Request::get(uri2);

    let response1 = client.request(request1).await?;
    let response2 = client.request(request2).await?;

    println!("Status 1: {}", response1.status());
    println!("Status 2: {}", response2.status());

    Ok(())
}

在这个示例中,ReusableClienthyper - reuse 库提供的连接池客户端,通过它可以复用连接,减少连接建立和销毁的开销。 2. 异步处理:充分利用 Rust 的异步特性,通过 tokio 运行时并发处理多个请求。例如,可以使用 tokio::join! 宏并发发送多个请求:

use hyper::{Body, Client, Request, Uri};
use tokio::main;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();

    let uri1 = Uri::try_from("https://www.example.com/api1")?;
    let uri2 = Uri::try_from("https://www.example.com/api2")?;

    let request1 = Request::get(uri1);
    let request2 = Request::get(uri2);

    let (response1, response2) = tokio::join!(
        client.request(request1),
        client.request(request2)
    );

    let response1 = response1?;
    let response2 = response2?;

    println!("Status 1: {}", response1.status());
    println!("Status 2: {}", response2.status());

    Ok(())
}

通过 tokio::join!,两个请求会并发执行,从而提高整体的处理效率。 3. 优化请求体和响应体处理:在处理大请求体或响应体时,避免一次性将所有数据加载到内存中。可以使用流处理方式,逐块处理数据。例如,对于响应体,可以使用 hyper::body::BytesStream 逐块读取:

use hyper::{Body, Client, Request, Uri};
use tokio::main;

#[main]
async fn main() -> Result<(), hyper::Error> {
    let client = Client::new();
    let uri = Uri::try_from("https://www.example.com/large - file")?;

    let request = Request::get(uri);
    let response = client.request(request).await?;

    let stream = response.into_body();
    let mut buffer = Vec::new();
    hyper::body::aggregate(stream).await.map(|chunk| {
        buffer.extend_from_slice(&chunk);
        // 在这里对 buffer 中的数据进行处理
    })?;

    Ok(())
}

在这个示例中,hyper::body::aggregate 函数将响应体流逐块处理,并将数据追加到 buffer 中,你可以在 map 闭包中对每块数据进行相应的处理,而不是一次性读取整个响应体。

通过以上性能优化手段,可以使基于 hyper 的 HTTP 客户端在处理大量请求或大数据量时表现更加出色。