PONOS Advent Calendar 2022の16日目の記事です。
昨日は、@ackyla さんの NESエミュレータを作る-2 でした。
はじめに
今までRustでWebsocketを試してみたり、ゲームエンジンを試してみたりしていきました。速度、並行性、安全性を言語仕様として保証するC言語、C++に代わるシステムプログラミングに適したプログラミング言語を目指しているRustですので、リアルタイム通信やゲームエンジンなど速度が求められるものを試しに作成してきましたが、本業であるWebAPIもRustで実装できないかということを考えています。
ソーシャルゲームで使用するWebAPIも速度を求められることが多いですし、軽量言語のPHPやRubyでも本番環境に耐えられるシステムを構築することが可能ですが、Rustで構築することにより速度が向上し多くのリクエストを処理できると考えています。多くのリクエストを処理できるということは、運用するサーバの台数も削減することができます。
今回は、双方通信などではなくWebAPIをRustで構築して単純なJSON形式のレスポンスを返却するものを作成していきたいと思います。
実装
使用ライブラリ
[dependencies]
# axumは、tokioとhyperで動作するように設計されています。
axum = "0.6.1"
tokio = { version = "1.23.0", features = ["full"] }
hyper = "0.14.23"
serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89"
axum
RustでWebAPIを構築するために、Webフレームワークを使用して実装していきます。Rustを調べるとさまざまなWebフレームワークが存在しています。
tokio
tokioは、Rustで非同期アプリケーションを実装するために不可欠なさまざまな機能を提供する多数のモジュールです。開始する最も簡単な方法は、すべての機能を有効にします。axumを使用する場合は、すべての機能を有効にするためにfeatures = ["full"]を指定します。
hyper
hyperは、Rustの高速なHTTPリクエストを実現できます。非同期型で動作し、クライアント、サーバー両方のAPIを提供していて、本番環境においても広く使われているようです。
serde(serde_json)
serde(serde_json)は、Rustのデータ構造をシリアライズ/デシリアライズするためのフレームワークです。Rustで定義されたデータ構造を、自動的にJSONやYAML等のフォーマットに変換することができます。APIではJSONでやり取りすることが多いと思いますのでこちらを使うことになるかと思います。
Derive属性に指定されたデータ構造をシリアライズ/デシリアライズする機能を提供できるため、features = ["derive"]を指定します。
開発環境
開発環境 | バージョン |
---|---|
macOS | 12.4 |
Rust(rustc、cargo) | 1.65.0 |
ソースコード
use axum::{
routing::get,
Router,
response::Json,
response::IntoResponse,
http::StatusCode
};
use serde_json::json;
use std::net::SocketAddr;
mod controller;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/users", get(controller::users::get_users)
.post(controller::users::create_users)
.put(controller::users::update_users)
.delete(controller::users::delete_users));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> impl IntoResponse {
(
StatusCode::OK,
Json(json!("OK"))
)
}
pub mod users;
use axum::{
response::Json,
response::IntoResponse,
http::StatusCode
};
use serde::{Deserialize};
use serde_json::json;
#[derive(Deserialize)]
pub struct CreateUsersRequest {
user_name: String
}
#[derive(Deserialize)]
pub struct UpdateUsersRequest {
user_name: String
}
#[derive(Deserialize)]
pub struct DeleteUsersRequest {
id: i64
}
pub async fn get_users() -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({"message":"get users!"}))
)
}
pub async fn create_users(Json(payload): Json<CreateUsersRequest>) -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({"message": "create users!"}))
)
}
pub async fn update_users(Json(payload): Json<UpdateUsersRequest>) -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({"message":"update users!"}))
)
}
pub async fn delete_users(Json(payload): Json<DeleteUsersRequest>) -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({"message":"delete users!"}))
)
}
動作確認
$ cargo run
でアプリケーションを起動し、curlまたはPOSTMANなどでアクセスします。
今回は、POSTMANで動作確認してみます。
上記のように想定通り、JSON形式のリクエストデータを受信することが確認できたら成功です。
おわりに
今回は、Rustで単純なリクエストデータを返却するWebAPIを実装してみました。このぐらい単純だとRubyやPHPなど軽量言語でもあまり変わらない調査や作業量だと感じておりこれで高速で安全なWebAPIが構築できるのは素晴らしいものだと感じました。しかし、今回は本当に単純なものでここからDBへの更新等の実装を含めると、難易度が向上しRubyのような速度で実装することは難しいと調査して感じました。
次回は、DBの更新周りを実装し、軽量言語と比べてどのくらいの実装量になるのか、また実行速度はどのくらいなるのかを測っていきたいと考えております。この経験で高速なWebAPI実現の1つとして知識に入れていきたいと思います。
明日は @abcdeff さんになります。お楽しみに!