Rust HTTP客户端reqwest快速入门教程

文章目录

reqwest是rust中比较知名和流行的一个http客户端, 这篇教程主要是收集了reqwest一些常用的代码片段,便于以后直接复制使用,文章结构主要是对标之前go语言的net/http客户端的快速入门教程。

reqwest版本: 0.11.22

参考依赖配置:

[dependencies]
reqwest = { version="0.11.22", features=["json", "multipart"]}
tokio = { version = "1", features = ["full"] }
serde_json = "1.0.107"

下面代码使用的请求地址https://youerning.top对于一部分代码可能不起作用, 比如上传表单之类的请求,大家测试的时候需要更换请求的地址。

快速入门

use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()>{
    let body = reqwest::get("https://youerning.top")
    .await?
    .text()
    .await?;

    println!("body: {}", body);
    Ok(())
}

如果不自己构建client对象, reqwest默认只提供get方法, 在库层面,reqwest只暴露了get方法, 如果需要使用其他的方法可以自己构造客户端。

请求前

查询参数

查询参数值一般指url中问号后的部分, 比如https://youerning.top/?ie=UTF-8&wd=test中的ie=UTF-8&wd=test

use std::collections::HashMap;
use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let client = reqwest::Client::new();
    let mut params = HashMap::new();
    params.insert("key", "value");
    let resp = client
        .get(url)
        .query(&params)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

http请求头参数

常见的http请求头有User-Agent, 用于简单的反爬以及反反爬。

reqwest同时支持两种设定请求头的方式,方法如下:

use std::collections::HashMap;
use reqwest::Result;
use reqwest::header;


#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let client = reqwest::Client::new();
    let mut headers = header::HeaderMap::new();
    headers.insert("custom", header::HeaderValue::from_static("youerning.top"));
    let resp = client
        .get(url)
        .header("User-Agent", "youerning.top")
        .headers(headers)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

请求体参数

既然有了查询参数为啥还需要请求体参数? 因为查询参数在url中,总不可能上传个文件也把文件的编码到url中,那么这个url太长了,并且url的长度有有限制的。

一般来说,常用的请求体参数有以下三种。

  1. 表单 对应的Content-Type是application/x-www-form-urlencoded
  2. json 对应的Content-Type是application/json
  3. 包含文件的表单 对应的Content-Type是multipart/form-data

上传表单

use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let client = reqwest::Client::new();
    let mut params = HashMap::new();
    params.insert("key2", "value2");
    let resp = client
        .get(url)
        .form(&[("key1", "value1")])
        .form(&params)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

reqwest支持两种方式设置form, 但是不能像headers那样反复追加,这里因为.form(&params)最后调用,所以上传的表单只有key2=value2

JSON请求

rust不像golang那样内置了很多实用的标准库,rust很多功能需要依赖外部库,比如这里的json, 在reqwest中我们需要引入serde_json, 当然也可以不使用serde_json而是使用hashmap, json和表单一样后面的会覆盖前面的调用。

如果需要使用json方法,reqwest的依赖需要启用json特性!

use serde_json;
use reqwest::Result;
use std::collections::HashMap;


#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let client = reqwest::Client::new();
    let mut payload = HashMap::new();
    payload.insert("key2", "value2");
    let resp = client
        .post(url)
        .json(&serde_json::json!({
            "key1": "value1"
        }))
        .json(&payload)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

上传文件的表单

use reqwest::Result;
use reqwest::multipart::{Form, Part};

#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let client = reqwest::Client::new();
    let form: Form = Form::new();
    // 也可以不指定mime
    let file = Part::text("file").file_name("test.txt").mime_str("text/plain").unwrap();
    let form = form.part("uploadfile", file);
    let resp = client
        .post(url)
        .multipart(form)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

编码后的请求体内容如下:

--e0f1b497af95f167-38dc8fccee705209-d805583dc901b5f7-4bb5deae2f0db3bb
Content-Disposition: form-data; name="uploadfile"; filename="test.txt"
Content-Type: text/plain

file
--e0f1b497af95f167-38dc8fccee705209-d805583dc901b5f7-4bb5deae2f0db3bb--

上传表单带有文件的表单还是比较复杂的。

默认情况下reqwest也是不会启用cookie这个特性的, 所以需要使用cookie的话,要在reqwest的依赖设置中设置cookie这个依赖, 比如reqwest = { version="0.11.22", features=["json", "multipart", "cookies"]}, 除此之前还需要一个额外的库来创建cookie, 也就是reqwest_cookie_store

这个cookie我没使用明白,这里只是简单的copy一下reqwest_cookie_store的示例代码

更详细的例子可以查看: https://docs.rs/reqwest_cookie_store/latest/reqwest_cookie_store/

// Load an existing set of cookies, serialized as json
let cookie_store = {
  if let Ok(file) = std::fs::File::open("cookies.json")
    .map(std::io::BufReader::new)
    {
      // use re-exported version of `CookieStore` for crate compatibility
      reqwest_cookie_store::CookieStore::load_json(file).unwrap()
    }
    else
    {
      reqwest_cookie_store::CookieStore::new(None)
    }
};
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(cookie_store);
let cookie_store = std::sync::Arc::new(cookie_store);
{
  // Examine initial contents
  println!("initial load");
  let store = cookie_store.lock().unwrap();
  for c in store.iter_any() {
    println!("{:?}", c);
  }
}

// Build a `reqwest` Client, providing the deserialized store
let client = reqwest::Client::builder()
    .cookie_provider(std::sync::Arc::clone(&cookie_store))
    .build()
    .unwrap();

// Make a sample request
client.get("https://google.com").send().await.unwrap();
{
  // Examine the contents of the store.
  println!("after google.com GET");
  let store = cookie_store.lock().unwrap();
  for c in store.iter_any() {
    println!("{:?}", c);
  }
}

超时

超时可以很简单的设置,比如

use std::time;
use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let client = reqwest::Client::new();
    let resp = client
        .post(url)
        .timeout(time::Duration::from_secs(1))
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

这里设置了一个总的超时时间。

SSL证书

对于自签名证书最常见的就是不验证证书,代码如下

use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    
    let client = reqwest::Client::builder().danger_accept_invalid_certs(true).build().unwrap();
    let resp = client
        .get(url)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

不验证证书肯定是不安全的,所以可以加载自签名证书, 代码如下

use std::fs::File;
use std::io::Read;
use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";

     let mut buf = Vec::new();
     File::open("my_cert.pem").unwrap()
         .read_to_end(&mut buf).unwrap();
     let cert = reqwest::Certificate::from_pem(&buf)?;
    let client = reqwest::Client::builder().add_root_certificate(cert).build().unwrap();
    
    let resp = client
        .get(url)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

代理

直接抄的官方的example的代码,不想写了。。。。。

#![deny(warnings)]


#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Make sure you are running tor and this is your socks port
    let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050").expect("tor proxy should be there");
    let client = reqwest::Client::builder()
        .proxy(proxy)
        .build()
        .expect("should be able to build reqwest client");

    let res = client.get("https://check.torproject.org").send().await?;
    println!("Status: {}", res.status());

    let text = res.text().await?;
    let is_tor = text.contains("Congratulations. This browser is configured to use Tor.");
    println!("Is Tor: {}", is_tor);
    assert!(is_tor);

    Ok(())
}

重定向

有时候可以限制重定向的测试来避免重定向次数过多。

use reqwest::Result;
use reqwest::redirect::Policy;



#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    let policy = Policy::custom(|attempt| {
        if attempt.previous().len() > 5 {
            attempt.error("too many redirects")
        } else if attempt.url().host_str() == Some("example.domain") {
            // prevent redirects to 'example.domain'
            attempt.stop()
        } else {
            attempt.follow()
        }
    });
    
    let client = reqwest::Client::builder().redirect(policy).build().expect("build client failed");
    
    let resp = client
        .get(url)
        .send()
        .await?
        .text()
        .await?;

    println!("resp: {}", resp);
    Ok(())
}

reqwest 默认的重定向检查是10次。

请求后

请求后会获得一个Response对象, 这个结构体有许多比较有用的字段。

响应头信息/状态码

响应头信息可以用于一些特殊字段的判断,比如字符集,而状态码可以简单的判断请求是否成功,

use reqwest::Result;


#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    
    let resp = reqwest::Client::new()
        .get(url)
        .send()
        .await?;

    println!("headers: {:?}", resp.headers());
    println!("status: {}", resp.status());
    Ok(())
}

而作者判断响应码给出了下面这个例子

use reqwest::Error;

async fn handle_error() -> Result<(), Error> {
    let response = reqwest::get("https://www.example.com").await?;

    match response.status().as_u16() {
        200..=299 => {
            let body = response.text().await?;
            println!("Success! Body:\n{}", body);
        }
        400..=599 => {
            let status = response.status();
            let error_message = response.text().await?;
            println!("Error {}: {}", status, error_message);
        }
        _ => {
            println!("Unexpected status code: {}", response.status());
        }
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    handle_error().await?;
    Ok(())
}

响应体

如果默认响应体的编码是utf8,就可以直接处理响应体, 可以通过text方法直接获得解码后的String对象

use reqwest::Result;


#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    
    let resp = reqwest::Client::new()
        .get(url)
        .send()
        .await?;

    println!("resp content: {}", resp.text().await?);
    Ok(())
}

reqwest的text方法默认使用utf8解码

编码

一般来说大家都是用utf-8了,但是总有一些例外情况,所以为了安全的解析响应体内容,需要检测响应体的编码格式 如果我们需要自己解析字节流,我们可以通过bytes获得字节流,但是,如果我们知道对应的编码,则可以通过text_with_charset方法来解码。

use reqwest::Result;


#[tokio::main]
async fn main() -> Result<()>{
    let url = "https://youerning.top";
    
    let resp = reqwest::Client::new()
        .get(url)
        .send()
        .await?;

    println!("resp content: {}", resp.text_with_charset("utf8").await?);
    Ok(())
}

总结

学习一门编程语言除了学习这门语言本身的语法和标准库之外使用得最多的可能就是第三方库了, 而http客户端是几乎必学的,由于笔者比较喜欢异步的生态,所以这里用的是reqwest作为学习对象

值得说明的是: reqwest也可以以同步的方式使用。

参考链接

下面是一些本文在写代码时用到的参考连接