はじめに
Rust初心者です、よろしくお願いします。
数日前より'The Rust Programming Language' E-BooksのページからPDFをダウンロードして読み始めたのですが、323ページとかなりのボリュームで全く進みません…。
折れそう少し手を動かしたいなと思い、Webアプリケーションフレームワークを使って簡単なWebアプリを作成してみる事にしました。
環境構築には、先日の@nacika_insさんの記事、Rustをはじめよう! Rustの環境構築 Atom & SublimeTextを参考にさせて頂きました。ありがとうございます。
動作確認環境
- Rust 1.4.0 (on Ubuntu 14.04)
- nickel 0.7.3
- PostgreSQL 9.3.10
nickel.rsとは
公式サイトの紹介によると、
nickel.rs is a simple and lightweight foundation for web applications written in Rust. Its API is inspired by the popular >express framework for JavaScript.
とのこと。
- シンプル
- 軽量
- Node.jsのExpress.jsにインスパイアされた(APIが似ている?)
という特徴があるようですね。
GitHubの星の数から判断すると、RustのWebフレームワークで一番人気なのはironですが(iron:2365 vs nickel:1225)、
nickel.rsもシンプルさ、使いやすさで利点がありそうです。
また、nickelではデータベース接続のミドルウェアが一通り揃っています。
- PostgreSQL
- MySQL
- SQLite
- Redis
今回はPostgreSQLのミドルウェアを使ってみます。
速度は?
色々なフレームワークのベンチマークを取っているTechEmpowerの最新結果によると、nickel.rsはJSON Serialization部門で41位。余り振いませんが、rackやnode.jsより少し速い結果となっています。
早速始めてみる
公式のGetting Started ガイドが分かりやすいです。
基本的にガイドの通り進めればOK。
Ubuntu 14.04 に rust / cargo をインストール
まずはインストール。
sudo -i
add-apt-repository -y ppa:hansjorg/rust
apt-get update
apt-get install rust-stable cargo-nightly
exit
rustc --version
rustc 1.4.0-dev
ls -lH $(which rustc)
-rwxr-xr-x 1 root root 6064 Nov 1 19:59 /usr/bin/rustc
cargo --version
cargo 0.7.0 (b6cc27a 2015-11-28)
Hello World
ひな型の作成
「--bin」オプションが、コマンドラインから実行可能なプロジェクトであることの指示になります。
--binオプションで作成したプロジェクトは、「cargo run」で実行可能です。
$ cargo new nickel-helloworld --bin
プロジェクト定義ファイルの記述(Cargo.toml)
[package]
name = "nickel-helloworld"
version = "0.1.0"
authors = ["johndoe"]
[dependencies]
nickel = "*"
アプリ本体の記述
サーバのインスタンスを生成し、ルーティングを定義、ハンドラを登録して完了。
慣れ親しんだやり方ですね。
パラメータの取得は request.param("name")
。
#[macro_use] extern crate nickel;
use nickel::{Nickel, HttpRouter};
fn main() {
let mut serv = Nickel::new();
serv.get("/bar", middleware!("This is the /bar handler"));
serv.get("/user/:userid", middleware! { |request|
format!("<h1>This is user: {:?}</h1>", request.param("userid").unwrap())
});
serv.get("/a/*/d", middleware!("matches /a/b/d but not /a/b/c/d"));
serv.get("/a/**/d", middleware!("This matches /a/b/d and also /a/b/c/d"));
serv.listen("127.0.0.1:6767");
}
動作確認
# cargo runだと ビルド・実行を連続で行う
#
$ cargo build
Compiling rand v0.3.12
Compiling matches v0.1.2
Compiling rustc-serialize v0.3.16
Compiling mustache v0.6.3
Compiling num v0.1.28
Compiling serde v0.6.6
Compiling unicase v1.0.1
Compiling modifier v0.1.0
Compiling time v0.1.34
Compiling num_cpus v0.2.10
Compiling uuid v0.1.18
Compiling url v0.2.38
Compiling cookie v0.1.21
Compiling mime v0.1.1
Compiling typemap v0.3.3
Compiling plugin v0.2.6
Compiling memchr v0.1.7
Compiling aho-corasick v0.4.0
Compiling regex v0.1.43
Compiling language-tags v0.0.7
Compiling hyper v0.6.16
Compiling nickel v0.7.3
Compiling nickel-helloworld v0.1.0 (file:///home/ubuntu/devel/rust/nickel-helloworld)
$ cargo run
Running `target/debug/nickel-helloworld`
Listening on http://127.0.0.1:6767
Ctrl-C to shutdown server
$ curl http://127.0.0.1:6767/user/dseg
<h1>This is user: "dseg"</h1>
一番シンプルなHello worldはこれにて完成です。
テンプレートを使う (Mustache)
nickelでは、Mustacheテンプレートが標準で使えます(パッケージの追加不要)。
Responseのrenderメソッドに、テンプレートファイルとパラメータを渡して呼ぶと、結果の文字列が帰ってきます。
簡単ですね。
// Mustacheテンプレートを使うバージョン
#[macro_use] extern crate nickel;
use std::collections::HashMap;
use nickel::{Nickel, HttpRouter};
fn main() {
let mut server = Nickel::new();
server.get("/", middleware! {|_, response|
let mut data = HashMap::new();
data.insert("color", "Green");
data.insert("name", "California Apple");
data.insert("price", "2.50");
return response.render("assets/hello.tpl", &data);
});
server.listen("127.0.0.1:6767");
}
<html>
<head>
<title>A Simple Mustache Demo</title>
<meta charset="utf-8">
</head>
<body>
<h1>A Simple Mustache Demo</h1>
<h4>Product Info: {{name}}</h4>
<ul>
<li>Product: {{name}}</li>
<li>Color: {{color}}</li>
<li>Price: ${{price}}</li>
</ul>
</body>
</html>
$ cargo run
~/devel/rust/nickel-helloworld$ cargo run
Running `target/debug/nickel-helloworld`
Listening on http://127.0.0.1:6767
Ctrl-C to shutdown server
curlで動作確認
$:~/devel/rust/nickel-helloworld$ curl http://localhost:6767
<html>
<head>
<title>A Simple Mustache Demo</title>
<meta charset="utf-8">
</head>
<body>
<h1>A Simple Mustache Demo</h1>
<h4>Product Info: California Apple</h4>
<ul>
<li>Product: California Apple</li>
<li>Color: Green</li>
<li>Price: $2.50</li>
</ul>
</body>
</html>
テンプレート変数が展開されています。OKです。
PostgreSQLと連携
nickel-postgresミドルウェアを使って、PostgreSQLと連携してみます。
https://github.com/nickel-org/nickel-postgres/
nickel-helloworld-postgressプロジェクト作成
cargo new nickel-helloworld-postgres --bin
Cargo.tomlにnickel-postgres、依存パッケージ追加
[package]
name = "nickel-helloworld-postgres"
version = "0.1.0"
authors = ["johndoe"]
[dependencies]
nickel = "*"
r2d2 = "*"
postgres = "*"
openssl = "*"
[dependencies.nickel_postgres]
git = "https://github.com/nickel-org/nickel-postgres.git"
-- スキーマと初期データ
CREATE TABLE counter (
id SERIAL,
counter SMALLINT NOT NULL DEFAULT 0
);
INSERT INTO counter (id, counter) VALUES (0, 1);
#[macro_use] extern crate nickel;
extern crate r2d2;
extern crate postgres;
extern crate openssl;
extern crate nickel_postgres;
use nickel::{Nickel,HttpRouter};
use r2d2::NopErrorHandler;
use postgres::SslMode;
use nickel_postgres::{PostgresMiddleware, PostgresRequestExtensions};
fn main() {
let mut serv = Nickel::new();
let dsn = "postgres://dbuser:dbpassword@127.0.0.1/counter";
let dbpool = PostgresMiddleware::new(&*dsn,
SslMode::None,
5,
Box::new(NopErrorHandler)).unwrap();
serv.utilize(dbpool);
serv.get("/count",
middleware! {|req, res|
let conn = req.db_conn();
let stmt = conn.prepare("SELECT counter FROM counter WHERE id = 0").unwrap();
let rows = &stmt.query(&[]).unwrap();
let mut counter:i16 = 0; // Int2(smallint) of Postgres is i16
for row in rows {
counter = row.get(0);
}
// also print to stdout
println!("counter value is {}", counter);
// Up and save the counter value (+1)
conn.execute("UPDATE counter SET counter = counter + 1 WHERE id = 0", &[]).unwrap();
format!("<h1>Hello</h1><br>your are the visitor # {}.\n", counter)
});
serv.listen("localhost:6767");
}
動作確認
$ ~/devel/rust/nickel-helloworld-postgres$ curl http://localhost:6767/count
<h1>Hello</h1><br>your are the visitor # 21.
$ ~/devel/rust/nickel-helloworld-postgres$ curl http://localhost:6767/count
<h1>Hello</h1><br>your are the visitor # 22.
$ ~/devel/rust/nickel-helloworld-postgres$ curl http://localhost:6767/count
<h1>Hello</h1><br>your are the visitor # 23.
HelloWorldサーバーの応答速度の確認
ab で簡易的にベンチマークを取ってみます。
$ cd devel/rust/nickel-helloworld
~/devel/rust/nickel-helloworld$ ab -n 10 -c 1 http://localhost:6767/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: Nickel
Server Hostname: localhost
Server Port: 6767
Document Path: /
Document Length: 23 bytes
Concurrency Level: 1
Time taken for tests: 0.009 seconds
Complete requests: 10
Failed requests: 0
Total transferred: 1670 bytes
HTML transferred: 230 bytes
Requests per second: 1133.92 [#/sec] (mean)
Time per request: 0.882 [ms] (mean)
Time per request: 0.882 [ms] (mean, across all concurrent requests)
Transfer rate: 184.93 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 0
Processing: 0 1 0.9 1 3
Waiting: 0 1 0.9 1 3
Total: 1 1 1.0 1 4
Percentage of the requests served within a certain time (ms)
50% 1
66% 1
75% 1
80% 1
90% 4
95% 4
98% 4
99% 4
100% 4 (longest request)
…むむ、遅い?
平均応答時間が500ms?
あ。デバッグ版だからですね。
ビルドのデフォルトはデバッグ版なのか、注意しないと。
リリース版をビルド。
# cargo build --release
cargo run --release # build & run
Running `target/release/nickel-helloworld`
Listening on http://127.0.0.1:6767
Ctrl-C to shutdown server
$ ab -n 10 -c 1 http://localhost:6767/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: Nickel
Server Hostname: localhost
Server Port: 6767
Document Path: /
Document Length: 23 bytes
Concurrency Level: 1
Time taken for tests: 0.001 seconds
Complete requests: 10
Failed requests: 0
Total transferred: 1670 bytes
HTML transferred: 230 bytes
Requests per second: 9433.96 [#/sec] (mean)
Time per request: 0.106 [ms] (mean)
Time per request: 0.106 [ms] (mean, across all concurrent requests)
Transfer rate: 1538.55 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 0.0 0 0
Waiting: 0 0 0.0 0 0
Total: 0 0 0.0 0 0
Percentage of the requests served within a certain time (ms)
50% 0
66% 0
75% 0
80% 0
90% 0
95% 0
98% 0
99% 0
100% 0 (longest request)
応答時間が平均0.1秒と、デバッグ版の5倍にスピードアップ。良いです。
おわりに
RustでのWeb開発、「余りRustっぽさが生きない分野かも?」と思って、始めは少し消極的でしたが、
実際にやってみると、とても良い印象を持ちました。
- シンプル
- Cargoを使ったビルド・依存関係解決・テストが便利
- コンパイル・実行速度もそこそこ速く快適
- 厳格なコンパイル時の型チェック
プロジェクトのメタ情報・依存パッケージはCargo.tomlに書きますが、書式はシンプルですっきりしています。
cargoコマンドの実行も早いです。
Scaffoldも出来るし、cargoでビルド・実行・テスト可能。
規模の大きなWebアプリ開発をするのには時期が少し早いかもしれませんが、
個人的には、Node.jsで作っていた、DBに読み書きするバッチスクリプトやそのWebインターフェース等を、
rustで置き換えてみようかな、と思いました。
ライブラリを始めとしたエコシステムが成長すれば、更に利点が増える訳で、これからが楽しみです。
ところで、今回のようなHello Worldプログラムでも、マクロやムーブセマンティクス等をはっきり理解しないとスムーズに先に進めないことがわかりました。
ハンドラでは文字列返せばとりあえず表示されるだろ、的な考えでサンプルを改造しようとしたところ、コンパイルエラーとなり、その時内容の理解が結構難しかったです。
感覚的で申し訳ないですが、C++のテンプレート関連のエラーのような感じというか、エラーの内容が複雑で一筋縄では行きませんでした。
nickelを拡張する際は、middlewareをマスターする必要があるようですが、これがなかなか手ごわそうな雰囲気が。
…という訳で学習に戻ります。道は長い。