バツイチとインケンのエンジニアブログ
プログラムやプログラムじゃないこと

RustのWebフレームワークIronでWebサービスをつくる

2017-01-04
書いた人 : バツイチ
カテゴリ : rust | タグ : iron, json

この記事では、コンパイルが速い!バイナリ実行!シンタックスがかっこいい!と3拍子揃ったRustでWebサービスを作成します。
HTTPハンドラの部分はIronというフレームワークを使います。
Ironはルーティング等のコア機能といくつかのプラグイン(jsonパーザ等)という素軽い構成のWebフレームワークです。
hyperというもっとプリミティブなフレームワークにビルトインする形で作られています。

  • 0. わたしの開発環境
  • 1. プロジェクトの作成
  • 2. HTTPサービスを実行する
  • 3. ルーティングする
  • 4. 空のJSONを返す
  • 5. 構造体をシリアライズしたJSONを返す
  • 6. 1px透過GIFを返す
  • 7. ルーティングを別モジュールにする

ソースコードはGitHubで公開しています。
https://github.com/x1-/rust_web_service

0. わたしの開発環境

  • OS ・・・ MacOSX 10.10.5
  • rust ・・・ 1.14.0
  • ビルドシステム ・・・ Cargo
  • IDE ・・・ emacs + racer + company

rustのインストールは公式サイトに書いてある通りにするのが一番良さそうです。
rustupがcargoのセットアップからrustcのインストールまで行ってくれます。

※2017.01.05 追記) Rust Version Manager, rsvmの導入を検討しても良いかもしれません。

1. プロジェクトの作成

Cargoを使ってWebサービス用のプロジェクト rust_web_service を作成します。

$ cargo new rust_web_service --bin
$ cd rust_web_service
$ tree -a
.
├── .git
│   ├── HEAD
│   :
├── .gitignore
├── Cargo.toml
└── src
    └── main.rs

cargo new は git init も同時に行ってくれます。

Cargo.toml , main.rs にはそれぞれ次のように初期コードが出力されます。
わかりやすいですね。

Cargo.toml

[package]
name = "rust_web_service"
version = "0.1.0"
authors = ["x1- <viva008@gmail.com>"]

[dependencies]

main.rs

fn main() {
    println!("Hello, world!");
}

main()関数はバイナリのランタイムに実行される関数です。
このまま特に編集しないで実行できます。

$ cargo run
Compiling rust_web_service v0.1.0 (file:///Users/xxxx/repos/rust_web_service)
  Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
    Running `target/debug/rust_web_service`
Hello, world!

cargoによりコンパイル&実行されてコンソールに Hello World! が表示されます。

2. HTTPサービスを実行する

Ironフレームワークを使ってHTTPサービスを実行します。
Cargo.toml にIronへの依存を追加します。

[package]
name = "rust_web_service"
version = "0.1.0"
authors = ["x1- <viva008@gmail.com>"]

[dependencies]

[dependencies.iron]
version = "*"

Iron に用意されている examples/hello.rs を参考に main.rs を変更します。

main.rs

extern crate iron;

use iron::prelude::*;
use iron::status;

fn main() {
    Iron::new(|_: &mut Request| {
        Ok(Response::with((status::Ok, "Hello world!")))
    }).http("localhost:3000").unwrap();
}

これを実行するとポート3000番にバインドしてHTTPサーバが起動します。
ブラウザから http://localhost:3000 にアクセスすると、 Hello world! が表示されます。

helloworld

Iron::new() の中の |_: &mut Request| {..} はクロージャです。
Rustのクロージャはパイプ(|)の間に書きます。
see: クロージャ|プログラミング言語Rust

3. ルーティングする

エンドポイントを増やせるようにルーティングします。
ルーティングも examples/simple_routing.rs を参考に、 main.rs に書いていきます。

extern crate iron;

use std::collections::HashMap;

use iron::prelude::*;
use iron::{Handler};
use iron::status;

struct Router {
    // キーにパス、値にハンドラを取るHashMap。
    routes: HashMap<String, Box<Handler>>
}

impl Router {
    fn new() -> Self {
        Router { routes: HashMap::new() }
    }
    fn add_route<H>(&mut self, path: String, handler: H) where H: Handler {
        self.routes.insert(path, Box::new(handler));
    }
}

impl Handler for Router {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        match self.routes.get(&req.url.path().join("/")) {
            Some(handler) => handler.handle(req),
            None => Ok(Response::with(status::NotFound))
        }
    }
}

fn main() {
    let mut router = Router::new();

    router.add_route("hello".to_string(), |_: &mut Request| {
        Ok(Response::with((status::Ok, "Hello world !")))
    });

    router.add_route("error".to_string(), |_: &mut Request| {
       Ok(Response::with(status::BadRequest))
    });

    let host = "localhost:3000";

    println!("binding on {}", host);
    Iron::new(router).http(host).unwrap();
}

また cargo run を実行します。

今度はブラウザから http://localhost:3000/error にアクセスすると 400 Bad Request が返るようになります。

badrequest

4. 空のJSONを返す

次は空のJSONを返すエンドポイントを作成します。
/json にアクセスすると空のjsonが返るようにします。

use iron::headers::ContentType;
を追加して、

router.add_route(“hello”.to_string()… の部分を変更します。

main.rs

extern crate iron;

use std::collections::HashMap;

use iron::prelude::*;
use iron::{Handler};
use iron::status;

// ↓ ここを追加
use iron::headers::ContentType;

~省略~

    // hello から json に変更
    router.add_route("json".to_string(), |_: &mut Request| {
        Ok(Response::with((ContentType::json().0, status::Ok, "{}")))
    });

cargo run を実行します。

ブラウザから http://localhost:3000/json にアクセスすると空のjsonが返ります。

json_empty

examples/content_type.rs にあるように、 Content-Type はいろいろな方法で指定できるようです。

json を出力するなら ContentType::json().0 を使うのが一番スマートに思いますが、 image/gif のように予めメソッドが用意されていない Content-Type を使いたいときは mime!(Image/gif); を使うのが直感的に感じました。

5. 構造体をシリアライズしたJSONを返す

空のJSONを返しても意味が無いので(笑)、構造体をシリアライズしたJSONを返すように修正します。
構造体のシリアライズにはrustc_serialize を使うので、まずは Cargo.toml を変更します。

Cargo.toml

[package]
~省略~

[dependencies]
rustc-serialize = "*"

[dependencies.iron]
version = "*"

main.rs に Letter という構造体を作成します。

main.rs

extern crate iron;
extern crate rustc_serialize;

use iron::status;
use iron::headers::ContentType;
use iron::prelude::*;

use rustc_serialize::json;

// 構造体
// #[derive(RustcEncodable)] ≒ シリアライズ可能属性(rustc_serializeのattribute)
#[derive(RustcEncodable)]
pub struct Letter {
    title: String,
    message: String
}
~省略~

ついでに、 fn main() 内で add_route していたクロージャも 名前付き関数に切り出します。

main.rs

extern crate iron;
extern crate rustc_serialize;

use iron::status;
use iron::headers::ContentType;
use iron::prelude::*;

use rustc_serialize::json;

#[derive(RustcEncodable)]
pub struct Letter {
    title: String,
    message: String
}
struct Router {
~省略~
}

fn json(_: &mut Request) -> IronResult<Response> {
    let letter = Letter {
        title: "PPAP!".to_string(),
        message: "I have a pen. I have an apple.".to_string()
    };
    let payload = json::encode(&letter).unwrap();
    Ok(Response::with((ContentType::json().0, status::Ok, payload)))
}

fn bad(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with(status::BadRequest))
}

fn main() {
    let mut router = Router::new();

    router.add_route("json".to_string(), json);
    router.add_route("error".to_string(), bad);

    let host = "localhost:3000";

    println!("binding on {}", host);
    Iron::new(router).http(host).unwrap();
}

rustc_serialize::json の encode メソッドに構造体を渡すだけでシリアライズできます。

cargo run を実行してブラウザから http://localhost:3000/json にアクセスすると今度はシリアライズされたJSONが返ります。

json

ところで、 _: &mut Request の _ ですが、これは名前無し引数を表します。
インターフェイスとして Request を引数にとりますが、関数内では使用していないのでこんなことをしています。
&mut はミュータブル参照です。
ミュータブル参照に束縛された変数は変化しうります。
ミュータブル参照とは何ぞや、とかRustの超重要概念である参照・借用 については プログラミング言語Rust をご一読頂くのが良いかと思います。

6. 1px透過GIFを返す

Webサービスあるあるで、1px透過GIFを返すエンドポイントも作成します。

今度は mime クレイトを使うので、また Cargo.toml を変更します。

Cargo.toml

[package]
~省略~

[dependencies]
mime = "*"
rustc-serialize = "*"

[dependencies.iron]
version = "*"

main.rs に mime クレイトと rustc_serialize::base64 を追加します。

mime は Gifの Content-Type を指定するために使います。
rustc_serialize::base64 は 1px透過GIFのBase64文字列をデコードするために使います。

main.rs

#[macro_use]
extern crate mime;
extern crate iron;
extern crate rustc_serialize;

use iron::status;
use iron::headers::ContentType;
use iron::prelude::*;

use rustc_serialize::base64::FromBase64;
use rustc_serialize::json;

/gif エンドポイントを追加します。

main.rs

~省略~
fn bad(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with(status::BadRequest))
}

fn gif(_: &mut Request) -> IronResult<Response> {
    // 1px透過GIF文字列
    let px1 = "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
    Ok(Response::with((mime!(Image/Gif), status::Ok, px1.from_base64().unwrap())))
}
fn main() {
    let mut router = routing::Router::new();

    router.add_route("json".to_string(), json);
    // /gifを追加
    router.add_route("gif".to_string(), gif);

    router.add_route("error".to_string(), bad);

    let host = "localhost:3000";

    println!("binding on {}", host);
    Iron::new(router).http(host).unwrap();
}

cargo run を実行してブラウザから http://localhost:3000/gif にアクセスするとgifが返ります。

gif1px

rustc_serialize::base64::FromBase64 トレイトの from_base64() メソッドは、 str と u8に対して実装されています。( str と u8 で使えると思って頂ければと)
で、 Result, FromBase64Error> 型を返します。

↑の例では px1.from_base64().unwrap() と、特に注意を払わず Vec を取り出していますが、 match 構文で分岐したり、 mapでエラーだったら空配列にしたり、ということもできます。
※ この例では px1 が デコードできることは変化しないので、 unwrap() で十分ですが。

7. ルーティングを別モジュールにする

最後に、ルーティングの部分を別モジュールに切り出します。

routing.rs を作成します。

routing.rs

extern crate iron;

use std::collections::HashMap;

use iron::{Handler};
use iron::status;
use iron::prelude::*;

pub struct Router {
    // Routes here are simply matched with the url path.
    routes: HashMap<String, Box<Handler>>
}

impl Router {
    pub fn new() -> Self {
        Router { routes: HashMap::new() }
    }

    pub fn add_route<H>(&mut self, path: String, handler: H) where H: Handler {
        self.routes.insert(path, Box::new(handler));
    }
}

impl Handler for Router {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        match self.routes.get(&req.url.path().join("/")) {
            Some(handler) => handler.handle(req),
            None => Ok(Response::with(status::NotFound))
        }
    }
}

main.rs で routing モジュールを使うように変更します。

main.rs

~省略~
use rustc_serialize::json;

// ↓この行を追加
mod routing;

#[derive(RustcEncodable)]
pub struct Letter {
~省略~

fn main() {
    // router = Router::new() から router = routing::Router::new() に変更
    let mut router = routing::Router::new();

    router.add_route("json".to_string(), json);
~省略~
}

今回は以上になります。
rust によるWebサービスはもっと深掘りしてみたいと思います。

このエントリーをはてなブックマークに追加
Tweet

← ジョブ・スケジューラ Rundeck で高機能 cron を実現する
Real-Time Google Finance API を使って株価を取得する →

 

最近書いた記事

  • Ryzen7 3800XT でmini ITXオープンフレームPCを作る
  • Pythonで機械学習入門 競馬予測
  • HP ENVY 15 クリエイターモデルレビューとRAID0解除
  • JRA-VAN データラボを使って、競馬データを収集する
  • Surface Pro 3 にubuntu18.04を入れる

カテゴリー

  • Android
  • Apache Flink
  • API
  • AWS
  • bazel
  • BigQuery
  • Cassandra
  • Docker
  • Druid
  • Elasticsearch
  • Git
  • Golang
  • gradle
  • HDFS
  • JavaScript
  • jvm
  • Linux
  • MongoDB
  • MySQL
  • Nginx
  • Nodejs
  • PaaS
  • PHP
  • Python
  • RabbitMQ
  • Raspberry Pi
  • React Native
  • Redis
  • Riak
  • rust
  • scala
  • Scheme
  • SEO
  • solr
  • Spark
  • spray
  • Sublime Text
  • Swift
  • Tableau
  • Unity
  • WebIDE
  • Wordpress
  • Youtube
  • ひとこと
  • カンファレンス
  • スケジューラ
  • マイクロマウス
  • 広告
  • 技術じゃないやつ
  • 株
  • 機械学習
  • 競馬
  • 自作キーボード
  • 自然言語処理

アーカイブ

  • 2021年4月
  • 2021年2月
  • 2021年1月
  • 2020年3月
  • 2020年2月
  • 2020年1月
  • 2019年10月
  • 2019年9月
  • 2019年8月
  • 2019年7月
  • 2019年6月
  • 2019年5月
  • 2019年4月
  • 2019年2月
  • 2019年1月
  • 2018年12月
  • 2018年11月
  • 2018年9月
  • 2018年5月
  • 2018年3月
  • 2018年2月
  • 2017年9月
  • 2017年8月
  • 2017年6月
  • 2017年4月
  • 2017年3月
  • 2017年1月
  • 2016年10月
  • 2016年9月
  • 2016年8月
  • 2016年6月
  • 2016年5月
  • 2016年4月
  • 2016年3月
  • 2016年2月
  • 2016年1月
  • 2015年12月
  • 2015年11月
  • 2015年10月
  • 2015年9月
  • 2015年8月
  • 2015年6月
  • 2015年5月
  • 2015年2月
  • 2015年1月
  • 2014年12月
  • 2014年11月
  • 2014年9月
  • 2014年6月
  • 2014年5月
  • 2014年3月
  • 2014年2月
  • 2014年1月
  • 2013年12月
  • 2013年11月
  • 2013年10月
  • 2013年9月
  • 2013年8月

書いた人

  • バツイチちゃん
  • インケンくん

このブログについて

エンジニアとしての考え方が間逆な2人がしょーもないこと書いてます。

バツイチ

アイコン

IT業界で働くエンジニアです。名前の通りバツイチです。
理論や抽象的概念が好きだけど人に説明するのが下手。

インケン

アイコン

バツイチちゃんと同じ業界で働いています。
理論とか開発手法とかは正直どうでもよくて、
生活する上で役に立つことに使いたい

Copyright 2025 バツイチとインケンのエンジニアブログ