はじめに
「RustでWebサーバー」って聞くと難しそうに感じませんか?
私もそう思ってました。でも実際にやってみたら...
思ったより簡単だった。
この記事では、Rustで簡単なWebサーバーを作る方法を紹介します。
目次
標準ライブラリだけで作る
まずは外部クレートなしで。
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\nHello, World!";
stream.write_all(response.as_bytes()).unwrap();
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
println!("Server running on http://127.0.0.1:8080");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
handle_client(stream);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}
cargo run
# 別ターミナルで
curl http://localhost:8080
# Hello, World!
20行で動く!
でもこれだと:
- マルチスレッド対応してない
- HTTPのパースが雑
- ルーティングがない
実用には不十分なので、ライブラリを使いましょう。
hyper で作る
hyper は低レベルなHTTPライブラリ。
[dependencies]
hyper = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, body::Incoming};
use hyper::body::Bytes;
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
async fn hello(_req: Request<Incoming>) -> Result<Response<Full<Bytes>>, hyper::Error> {
Ok(Response::new(Full::new(Bytes::from("Hello, World!"))))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Server running on http://127.0.0.1:8080");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
if let Err(e) = http1::Builder::new()
.serve_connection(io, service_fn(hello))
.await
{
eprintln!("Error: {:?}", e);
}
});
}
}
非同期で動いて、並行リクエストも捌ける。でもまだ低レベル。
フレームワーク比較
実用的には Web フレームワークを使うのが一般的。
主要フレームワーク
| 名前 | 特徴 | GitHub Stars |
|---|---|---|
| axum | tokio製、Tower互換、型安全 | ⭐17k+ |
| actix-web | 高パフォーマンス、成熟 | ⭐20k+ |
| Rocket | マクロ多用、使いやすい | ⭐23k+ |
| warp | フィルターベース、関数型 | ⭐9k+ |
私のおすすめ: axum
理由:
- tokio チームが作ってる(tokio との親和性◎)
- 型安全でエラーがわかりやすい
- エコシステムが充実(Tower との統合)
- モダンな設計
シンプルなAPI例
axum でRESTful APIを作ってみます。
セットアップ
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Hello World
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
println!("Server running on http://localhost:8080");
axum::serve(listener, app).await.unwrap();
}
これだけ! めっちゃシンプル。
JSONレスポンス
use axum::{routing::get, Router, Json};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
async fn get_user() -> Json<User> {
Json(User {
id: 1,
name: "Alice".to_string(),
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/user", get(get_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl http://localhost:8080/user
# {"id":1,"name":"Alice"}
パスパラメータ
use axum::{routing::get, Router, extract::Path};
async fn get_user_by_id(Path(id): Path<u64>) -> String {
format!("User ID: {}", id)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:id", get(get_user_by_id));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl http://localhost:8080/users/42
# User ID: 42
クエリパラメータ
use axum::{routing::get, Router, extract::Query};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
}
async fn list_items(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(10);
format!("Page: {}, Limit: {}", page, limit)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/items", get(list_items));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl "http://localhost:8080/items?page=2&limit=20"
# Page: 2, Limit: 20
POSTリクエスト
use axum::{routing::post, Router, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct UserResponse {
id: u64,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<UserResponse> {
Json(UserResponse {
id: 1,
name: payload.name,
email: payload.email,
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Bob","email":"bob@example.com"}'
# {"id":1,"name":"Bob","email":"bob@example.com"}
エラーハンドリング
use axum::{
routing::get,
Router,
http::StatusCode,
response::IntoResponse,
};
async fn might_fail() -> Result<String, AppError> {
// エラーが起きる可能性のある処理
Err(AppError::NotFound)
}
enum AppError {
NotFound,
InternalError,
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
AppError::InternalError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"),
};
(status, message).into_response()
}
}
ミドルウェア
use axum::{
routing::get,
Router,
middleware,
extract::Request,
middleware::Next,
response::Response,
};
async fn logging_middleware(
request: Request,
next: Next,
) -> Response {
println!("Request: {} {}", request.method(), request.uri());
let response = next.run(request).await;
println!("Response: {}", response.status());
response
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
.layer(middleware::from_fn(logging_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
完全な例:TODO API
use axum::{
routing::{get, post, put, delete},
Router,
Json,
extract::{Path, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
#[derive(Clone, Serialize, Deserialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
type Db = Arc<Mutex<HashMap<u64, Todo>>>;
async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
let db = db.lock().unwrap();
Json(db.values().cloned().collect())
}
async fn create_todo(
State(db): State<Db>,
Json(input): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
let mut db = db.lock().unwrap();
let id = db.len() as u64 + 1;
let todo = Todo {
id,
title: input.title,
completed: false,
};
db.insert(id, todo.clone());
(StatusCode::CREATED, Json(todo))
}
async fn get_todo(
State(db): State<Db>,
Path(id): Path<u64>,
) -> Result<Json<Todo>, StatusCode> {
let db = db.lock().unwrap();
db.get(&id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn delete_todo(
State(db): State<Db>,
Path(id): Path<u64>,
) -> StatusCode {
let mut db = db.lock().unwrap();
if db.remove(&id).is_some() {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let db: Db = Arc::new(Mutex::new(HashMap::new()));
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/:id", get(get_todo).delete(delete_todo))
.with_state(db);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
println!("Server running on http://localhost:8080");
axum::serve(listener, app).await.unwrap();
}
まとめ
学んだこと
- 標準ライブラリだけでも動く(20行)
- hyperは低レベルで柔軟
- axumを使えば超簡単にAPI作れる
選び方
| やりたいこと | おすすめ |
|---|---|
| 学習目的 | 標準ライブラリ → hyper → axum |
| 実用 | axum または actix-web |
| パフォーマンス重視 | actix-web |
| 型安全重視 | axum |
チェックリスト
- axum の基本的なルーティングを理解
- JSON のシリアライズ/デシリアライズ
- パスパラメータとクエリパラメータの取得
- エラーハンドリング
- ミドルウェアの使い方
今すぐできるアクション
- axum で Hello World を動かす
- JSON を返すAPIを作る
- TODO API を完成させる
RustでWebサーバー、思ったより簡単でした。型安全なAPIが書けるの、意外と気持ちいいです。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!