はじめに
- Rust で Web サーバーの HTTP ハンドラの中から別のサービスへ HTTP リクエストを送信するだけのコードを書こうと思ったのですが意外にむずかしくてびっくりしました。
- 一応動くものはできましたが、なんだか仰々しくなってしまい何か間違っている気がするので「普通はもっとこうするよ」とかご指摘あったら大歓迎の気持ちで公開してみます。
コード
Cargo.toml
[dependencies]
hyper = "0.12"
hyper-tls = "0.3.2"
futures = "0.1"
src/main.rs
use futures::future::Future;
use hyper::rt::{self, Stream};
use hyper::service::service_fn_ok;
use hyper::{Body, Client, Request, Response, Server};
use hyper_tls::HttpsConnector;
fn main() {
let addr = ([127, 0, 0, 1], 3000).into();
// HTTP ハンドラの登録
let new_svc = || service_fn_ok(handler);
// HTTP サーバーの作成と実行
let server = Server::bind(&addr)
.serve(new_svc)
.map_err(|e| eprintln!("server error: {}", e));
hyper::rt::run(server);
}
fn handler(_req: Request<Body>) -> Response<Body> {
// 別スレッドで HTTP リクエストを送信する
rt::spawn(rt::lazy(|| {
// HTTPS 対応したクライアントを作る
let https = HttpsConnector::new(4).unwrap();
let client = Client::builder().build::<_, hyper::Body>(https);
let uri = "http://httpbin.org/ip".parse().unwrap();
// リクエストを実行する Future を作って返す
client
// get リクエスト
.get(uri)
// エラーだったらここで処理
.map_err(|e| println!("request error: {}", e))
// エラーでなければこれを実行
.and_then(|res| {
// レスポンスの Body の取得も Future で行う
res.into_body()
.concat2()
// Body の取得エラーがあればここで処理
.map_err(|err| {
println!("Error: {}", err);
})
// Body が正常に取得できたら文字列にして表示
.map(|chunk| {
let v = chunk.to_vec();
let s = String::from_utf8_lossy(&v).to_string();
println!("HTTPS Body {}", s);
})
})
}));
Response::new(Body::from(""))
}
解説
- HTTP サーバーの実行は hyper の Example の通りです。
- HTTP ハンドラ関数
handler
では HTTP クライアントのhyper::Client
を使って HTTP リクエストを送信しています。 - hyper::Client のリクエストの実行方法はちょっと変わっていて、get() などでリクエストを行うと Future という処理のリストを返し、Future を非同期処理ライブラリが受け取ると Future に記載の処理が非同期で順番に実行される、という流れになります。
- コードで見ると
map_err
やmap
やand_then
なんかが Future で処理を定義するメソッドで、ユーザーはここにリクエストに成功した場合や失敗した場合などの処理を実装していきます。
おわりに
- 最初は reqwest 使ってサクッとできるだろうと思ったのですが、tokio::runtime が nested だぜ!だめだぜ!とか怒られてできませんでした。非同期な reqwest も検討しましたが hyper とあまり変わらないことになりそうだったのでやめました。
- hyper にも非同期にもこだわっていないのでもっと簡単な方法あったら誰か教えてください!