21
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「RustでWebサーバー」って聞くと難しそうに感じませんか?

私もそう思ってました。でも実際にやってみたら...

思ったより簡単だった。

この記事では、Rustで簡単なWebサーバーを作る方法を紹介します。

目次

  1. 標準ライブラリだけで作る
  2. hyper で作る
  3. フレームワーク比較
  4. シンプルなAPI例
  5. まとめ

標準ライブラリだけで作る

まずは外部クレートなしで。

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();
}

まとめ

学んだこと

  1. 標準ライブラリだけでも動く(20行)
  2. hyperは低レベルで柔軟
  3. axumを使えば超簡単にAPI作れる

選び方

やりたいこと おすすめ
学習目的 標準ライブラリ → hyper → axum
実用 axum または actix-web
パフォーマンス重視 actix-web
型安全重視 axum

チェックリスト

  • axum の基本的なルーティングを理解
  • JSON のシリアライズ/デシリアライズ
  • パスパラメータとクエリパラメータの取得
  • エラーハンドリング
  • ミドルウェアの使い方

今すぐできるアクション

  1. axum で Hello World を動かす
  2. JSON を返すAPIを作る
  3. TODO API を完成させる

RustでWebサーバー、思ったより簡単でした。型安全なAPIが書けるの、意外と気持ちいいです。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

21
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?