はじめに
最近業務でactix-webによるWeb Application Server開発を実施する機会がありましたので、備忘録を兼ねて記事にしようと思います。
英語になりますが公式ドキュメントには詳細な説明が記載されていますので、必要に応じてそちらを参照下さい。
actix-webについて
actix-webとは現在非常に活発に開発が進められているRustのWeb application frameworkです。
公式からの引用になりますが、主な特徴としては以下の通りとなります。
- Supported HTTP/1.x and HTTP/2.0 protocols
- Streaming and pipelining
- Keep-alive and slow requests handling
- Client/server WebSockets support
- Transparent content compression/decompression (br, gzip, deflate)
- Configurable request routing
- Graceful server shutdown
- Multipart streams
- Static assets
- SSL support with OpenSSL or native-tls
- Middlewares (Logger, Session, Redis sessions, DefaultHeaders, CORS, CSRF)
- Includes an asynchronous HTTP client
- Built on top of Actix actor framework
タイプセーフかつ新しいFrameworkであるためモダンな記述が可能となっており、同期、非同期処理に加えてactixを使用したアクターモデルでの開発が可能な上、こちらにある通りパフォーマンスが優れているのが特徴です。
前提
- Rust 1.26
- actix-web 0.6.10
本題
サンプルコード
公式ページのExampleより転記します。
こちらをベースに解説していきます。
[package]
name = "qiita"
version = "0.1.0"
authors = ["test"]
[dependencies]
actix = "0.5"
actix-web = "0.6"
futures = "0.1"
serde = "1"
serde_derive = "1"
tokio-threadpool = "0.1"
extern crate actix_web;
use actix_web::{http, server, App, Path};
fn index(info: Path<(u32, String)>) -> String {
format!("Hello {}! id:{}", info.1, info.0)
}
fn main() {
server::new(
|| App::new()
.route("/{id}/{name}/index.html", http::Method::GET, index))
.bind("127.0.0.1:8080").unwrap()
.run();
}
$ curl http://localhost:8080/100/test/index.html
Hello test! id:100
HttpServer
actix-webではTop levelにHttpServerが必要になります。
サンプルコードではserver::newにより作成されています。
HttpServerでは主に以下の設定を行います。
- App factory
- hostname
- worker thread
- keep-alive
- backlog
- bind address
- SSL
actix-webはactixというアクターモデルライブラリの上に構築されています。
詳細は記述しませんが、サンプルコードのrunメソッドではactix周りの初期化と実行、処理のブロックを行っています。
actixによるアクターモデルを使用した開発を行いたい場合は、以下のようにactixのSystem生成を自力で実施する必要があります。
extern crate actix;
extern crate actix_web;
use actix::prelude::*;
use actix_web::{http, server, App, Path};
fn index(info: Path<(u32, String)>) -> String {
format!("Hello {}! id:{}", info.1, info.0)
}
fn main() {
let sys = System::new("system");
server::new(
|| App::new()
.route("/{id}/{name}/index.html", http::Method::GET, index))
.bind("127.0.0.1:8080").unwrap()
.start();
let _ = sys.run();
}
尚、HttpServerでは以下のようにAppを複数関連付けて、prefixによって使用するAppを変更することも出来ます。
use actix_web::{server, App, HttpResponse};
fn main() {
server::new(|| vec![
App::new()
.prefix("/app1")
.resource("/", |r| r.f(|r| HttpResponse::Ok())),
App::new()
.prefix("/app2")
.resource("/", |r| r.f(|r| HttpResponse::Ok())),
App::new()
.resource("/", |r| r.f(|r| HttpResponse::Ok())),
]);
}
その他設定例を以下に記載しておきます。
extern crate actix_web;
use actix_web::{http, server, App, HttpRequest};
fn main() {
server::new(
|| App::new()
.route("/", http::Method::GET, |_: HttpRequest| "test"))
.workers(100)
.backlog(100)
.keep_alive(server::KeepAlive::Timeout(75))
.server_hostname("test".to_string())
.bind("127.0.0.1:8080").unwrap()
.run();
}
App
AppはHttpServerにおける1つのworkerが保持するアプリケーションインスタンスです。
こちらでは主に以下の設定を行います。
- Routing
- State
- middleware
Appは主にHttpServer作成時にnewメソッドの引数としてAppを作成するFactoryを渡し、HttpServerが各worker threadを立ち上げる際に渡されたFactoryを実行することで作成されます。
特筆すべき点としては、各worker threadで動作するAppは全て別のインスタンスであるということです。
こちらは後述するStateの共有を行う際に意識する必要があります。
Routing
Routingはroute又はresourceメソッドを使用して構築します。
それぞれのメソッドはAppを返却するため、メソッドチェインして必要な分を設定していくことになります。
簡単な説明としては、routeは一つのHTTP Methodに対して一つのHandlerを設定し、resourceはResourceHandlerを引数にとるFnを設定し、その中でrouteよりも細かに設定をすることが出来ます。
例えば、HTTP Methodは問わずに全て同じHandlerで処理を行いたい場合や、HTTP Headerの値でHandlerを分ける場合等に使用できます。
route及びresourceでは第一引数としてpathを設定します。
ここで設定されたpathに合致した場合に、同時に設定されたHandlerが実行されることとなります。
pathには{}を使用してHandler内で値を抽出することも可能です。サンプルコードでは"/{id}/{name}/index.html"のように設定されていますが、こちらの{id}及び{name}部分を設定された名前で抽出しています。
また、"/{id}/{name}/{tail:.*}"のtailのように、:の後に正規表現を設定することで、デフォルト正規表現である[^{}/]+を変更して柔軟に抽出することも可能です。
Routing設定例を以下に記載します。
extern crate actix_web;
use actix_web::{http, server, pred, App, HttpRequest};
fn index(_: HttpRequest) -> &'static str {
"index"
}
fn register(_: HttpRequest) -> &'static str {
"register"
}
fn list(_: HttpRequest) -> &'static str {
"list"
}
fn login(_: HttpRequest) -> &'static str {
"login"
}
fn main() {
server::new(
|| App::new()
// Getのみ
.route("/", http::Method::GET, index)
// Postのみ
.route("/register", http::Method::POST, register)
// 全HTTP Method
.resource("/list", |r| r.f(list))
// Get/PostかつContent-Typeが"text/html"のみ
.resource("/login", |r| r.route()
.filter(pred::Any(pred::Get()).or(pred::Post()))
.filter(pred::Header("Content-Type", "text/html"))
.f(login)))
.bind("127.0.0.1:8080").unwrap()
.run();
}
Handlerについては後述します。
State
Config情報やConnection pool、Cache map等といったApp全体で共有したい情報はApp#new()の代わりにApp#with_state(state: S)を使用してAppを作成することでStateを共有することが可能です。
設定されたStateはHandlerの中で取得して参照することができます。
尚、前述の通りApp自体はworker threadの単位で作成されるため、全て異なるインスタンスになります。
with_stateで設定するStateも全て異なるインスタンスである必要があるため、インスタンスを個別に作成するか、インスタンスを共有するためにArcを使用してcloneする必要があります。
また、Stateの値を変更出来るようにしたい場合は、MutexやRwLock等を使用してInternal Mutabilityを持たせる必要があります。
State使用例を以下に記載します。
extern crate actix_web;
use std::{
collections::HashSet,
sync::{
Arc, RwLock,
atomic::{AtomicUsize, Ordering}
}
};
use actix_web::{http, server, App, HttpRequest};
struct ApplicationState {
share_info: String,
access_count: AtomicUsize,
access_ips: RwLock<HashSet<String>>
}
fn index(req: HttpRequest<Arc<ApplicationState>>) -> String {
let state = req.state();
let first_ip = if let Some(ip) = req.connection_info().remote() {
let ip = ip.split(":").next().unwrap();
let access_ips = state.access_ips.read().unwrap();
if !access_ips.contains(ip) {
drop(access_ips);
let mut access_ips = state.access_ips.write().unwrap();
if !access_ips.contains(ip) {
access_ips.insert(ip.to_string());
true
} else {
false
}
} else {
false
}
} else {
false
};
let count = state.access_count.fetch_add(1, Ordering::SeqCst) + 1;
format!("Share info: {}, Access count: {}, First IP: {}", state.share_info, count, first_ip)
}
fn main() {
let app_state = Arc::new(ApplicationState {
share_info: "share_info".to_string(),
access_count: AtomicUsize::new(0),
access_ips: RwLock::new(HashSet::new())
});
server::new(
move || App::with_state(app_state.clone())
.route("/", http::Method::GET, index))
.bind("127.0.0.1:8080").unwrap()
.run();
}
middleware
middlewareを使用すると以下の時点で共通の処理を行うことができます。
- リクエスト開始時 (
Handler処理前) - レスポンス返却前 (
Handler処理後) - レスポンス返却後
actix-webで用意されているmiddlewareには次のようなものがあります。
- actix_web::middleware::DefaultHeaders
- 該当headerがResponse headerに設定されていない場合に当middlewareで指定したheaderを設定する
- actix_web::middleware::ErrorHandlers
- 処理した
Handlerが特定status codeが返却された場合に、指定されたHandlerを実行する
- 処理した
- actix_web::middleware::Logger
- Access logを標準出力に出力する
- actix_web::middleware::cors
- CORS設定を行う
- actix_web::middleware::csrf
- CSRF対応を行う
- GET, HEAD, OPTIONS以外の場合に、Origin headerがある場合はOrigin headerの値、無ければReferer内のOriginが設定された値と合致するかどうかで判定する
- CSRF対応を行う
- actix_web::middleware::identity
- ログイン状態管理を行う
- デフォルトで用意されているものはCookieに暗号化したデータを保持する
- actix_web::middleware::session
- セッション管理を行う
- デフォルトで用意されているものはCookieに暗号化したデータを保持する
また、Middleware traitを実装することで自身で作成したり、外部crateで提供されているMiddlewareを使用することも可能です。
Middleware trait実装サンプルを以下に記載します。
extern crate actix_web; [74/219]
use std::time::SystemTime;
use actix_web::{
http, error, server, App, HttpRequest, HttpResponse,
middleware::{ Middleware, Started, Response }
};
struct ResponseTime;
impl<S> Middleware<S> for ResponseTime {
fn start(&self, req: &mut HttpRequest<S>) -> error::Result<Started> {
let _ = req.extensions_mut().insert(SystemTime::now());
Ok(Started::Done)
}
fn response(&self, req: &mut HttpRequest<S>, res: HttpResponse) -> error::Result<Response> {
if let Some(start_time) = req.extensions().get::<SystemTime>() {
if let Ok(duration) = start_time.elapsed() {
println!("Process time: {}.{:09}", duration.as_secs(), duration.subsec_nanos());
}
}
Ok(Response::Done(res))
}
}
fn main() {
server::new(|| App::new()
.middleware(ResponseTime)
.route("/", http::Method::GET, |_: HttpRequest| "Hello"))
.bind("127.0.0.1:8080").unwrap()
.workers(100)
.run();
}
Handler
HandlerはRoutingに紐付けられて、リクエストを処理しレスポンスを返却するWeb Applicationにおける業務処理を記述する箇所となります。
尚、Handler traitというものがあり、そちらを実装したstructureもHandlerとなりますが、それ以外にもリクエストをハンドリングする形があるため、当記事ではそれらも含めてリクエストをハンドリングする処理を全てHandlerと呼んでいます。
簡易的ではありますが、Handlerには大きく4つの形があります。
FromRequestを受けて`Responderを返却するタイプ
Fn(T) -> R + 'static
R: Responder + 'static
T: FromRequest<S> + 'static
通常使用する場合はこの形となります。
後述するExtractorを使用してリクエスト情報を引数に受けて、レスポンスを生成します。
対象メソッド
App#route()Route#with()
HttpRequest<S>を受けて`Responderを返却するタイプ
Fn(HttpRequest<S>) -> R + 'static
R: Responder + 'static
こちらは1つ目の形と似ていますが、Extractorは使用せずにHttpRequest<S>を引数に受けてレスポンスを生成します。
動的パスやクエリのハンドリングをExtractorを使用せず、自力で制御する形となります。
一般的なWeb Application Frameworkに近い形です。
対象メソッド
Route#f()Route#h()
FromRequestを受けてFuture<Item = I, Error = E>を返却するタイプ
Fn(T) -> R + 'static
R: Future<Item = I, Error = E> + 'static
I: Responder + 'static
E: Into<Error> + 'static
T: FromRequest<S> + 'static
1つ目のパターンにおいて、非同期処理としてレスポンスを返却する形となります。
但し、Responderの実装の中にもBox<Future<Item = I, Error = E>> (I: Responder, E: Into<Error>)が存在するため、1つ目のパターンでも非同期処理を行うことが出来るため、このパターンを使用する場合についてはよく解っていません。
利点などご存知の方がいればご教示頂ければ幸いです。
対象メソッド
Route#with_async()
HttpRequest<S>を受けてFuture<Item = R, Error = E>を返却するタイプ
Fn(HttpRequest<S>) -> F + 'static
F: Future<Item = R, Error = E> + 'static
R: Responder + 'static
E: Into<Error> + 'static
3つ目の形においてExtractorを使用せずにHttpRequest<S>を引数に受けるタイプです。
対象メソッド
Route#a()
Extractor (FromRequest)
Handlerでは基本的にHttpRequestから全てのリクエスト情報及びState情報を取得することが出来ます。
しかしそれ以外にもURLパス文字(Path)やURL parameter (Query)等、FromRequestを実装しているものに対しては直接Handlerの引数として受けることが可能です。
それらを使用した場合、パラメータ名や型等が合致しない場合にはHandler自体が実行されず、HTTP status 400 (Bad request)が返却されることとなり、パラメータチェックをある程度自動化することが可能になります。
Handlerの引数としては1つのパラメータしか受け付けられませんが、FromRequestは9個までのタプルにも対応しているため、複数のExtractorを使用することができます。
また、HttpRequest自体もFromRequestを実装しているため、HTTPヘッダー情報などが必要な場合はHttpRequestを含めて、URL parameterはQueryを使用する等といった使用方法も可能になっています。
尚、Query等で必須ではないパラメータに対してはOptionを定義することで受容することも可能です。
Optionが定義されていない場合は必須パラメータとなります。
現時点では以下に対してFromRequestが実装されています。
- Bytes
- Request Payload
- String
- Request Payload
- Path
- URL path parameter
- Query
- URL query parameter
- Form
- Request Payload
- State
- HttpRequest
- Json
- Request Payload
- Session
サンプルコードは以下となります。
extern crate actix_web;
# [macro_use]
extern crate serde_derive;
use actix_web::{http, server, App, Path, Query};
# [derive(Debug, Deserialize)]
struct IndexQuery {
need: u32,
option: Option<String>
}
fn index(input: (Path<(u32, String)>, Query<IndexQuery>)) -> String {
let (info, query) = input;
format!("Hello {}! id:{}. Query: {:?}", info.1, info.0, query.into_inner())
}
fn main() {
server::new(
|| App::new()
.route("/{id}/{name}/index.html", http::Method::GET, index))
.bind("127.0.0.1:8080").unwrap()
.run();
}
Responder
Responderはレスポンスの具体的な内容を表します。
こちらもExtractorと同様に、HttpResponseが全てを柔軟に扱える一方で、簡易的な返却に関しては直接Stringを返却したりすることも可能になっています。
ただし、HttpResponse以外のResponderを使用する場合はContent Type等は決められた固定値になりますので、柔軟なコントロールが必要な際は自力でHttpResponseを構築して返却することになります。
HttpResponse以外のResponderについて一部を以下に記載します。
他の実装についてはこちらを参照ください。
尚、rust 1.26から使用可能なimpl Traitを使用して、impl Responderとして戻り値を記載することも可能です。
| Type | Note |
|---|---|
| <T: Responder, E: Into<Error>> Result<T, E> |
エラー発生時に特定のステータスコードを返却したい場合等に使用することができます |
| <I: Responder + 'static, E: Into + 'static> Box<Future<Item = I, Error = E>> |
非同期処理を行う場合に使用します 又、 FutureResponse<I, E>というType定義も存在します。 |
| &'static str | 文字列を返却する際に使用します。 Content Type: "text/plain; charset=utf-8" |
| String | 文字列を返却する際に使用します。 Content Type: "text/plain; charset=utf-8" |
| &'a String | 文字列を返却する際に使用します。 Content Type: "text/plain; charset=utf-8" |
| &'static [u8] | バイナリを返却する際に使用します。 Content Type: "application/octet-stream" |
| Bytes | バイナリを返却する際に使用します。 Content Type: "application/octet-stream" |
| BytesMut | バイナリを返却する際に使用します。 Content Type: "application/octet-stream" |
| <T: Serialize> Json<T> |
JSONを返却する際に使用します。 Content Type: "application/json" |
| NamedFile | 静的ファイルを返却する際に使用します。 Content Typeはファイル拡張子から算出されます。 |
HttpResponse生成
Http status codeやResponse Header、Content Typeの設定やChunked Streaming Response等、より詳細なレスポンスを行う際はHttpResponseを生成して返却します。
HttpResponseの生成は主にHttpResponseのOK()やCreated()等を使用してHttpResponseBuilderを取得し、各種設定を行って最後にHttpResponseを生成して返却する形となります。
各種設定メソッドの詳細は公式ドキュメントを参照ください。
公式からの引用ですが、サンプルコードはこちらとなります。
fn index(req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.content_encoding(ContentEncoding::Br)
.content_type("plain/text")
.header("X-Hdr", "sample")
.body("data")
}
非同期処理
actix-webでは非同期処理を扱うことができます。
非同期処理を行う際は、Handlerの戻り値としてBox<Future<Item = I, Error = E>>を返却する形になります。
actix-webではworkersで指定された(無指定の場合はCPU数)単位で個別のイベントループが実行されており、処理を全て非同期で記述した場合はnode.jsのシングルスレッドがworkersの数だけマルチスレッドで動作しているようなイメージになります。
実際に内部的に非同期処理となっているかどうかは実装者に委ねられることとなりますので、Futureを返却したとしてもその中でブロックする処理が含まれていれば結局ブロックされてしまうため気を付けて実装する必要があります。
また、現バージョンのactix(及びactix-web)はtokio-coreをベースにして作られていますので、tokio-coreベースの外部crate等を使用することが可能です。
※最新のtokioベースではないため、外部crateを使用する際はtokioベースかtokio-coreベースかを確認して使用ください。
外部crateが非同期に対応していない場合や別スレッドで動作させたいような場合は、アクセス数が少なければ都度threadを生成しても良いですが、RustのthreadはOS native threadでGolang等のGreen threadよりも生成コストが高いため、tokio-threadpool等のスレッドプールをStateで共有して使用するか、アクターモデルで作成することをお勧めします。
非同期処理の中でThreadPoolを使用して同期処理を行うサンプルを以下に記載します。
extern crate actix_web;
extern crate futures;
extern crate tokio_threadpool;
use std::{sync::Arc, time::Duration, thread};
use actix_web::{server, AsyncResponder, App, State, Responder};
use futures::{future::lazy, sync::oneshot::channel};
use tokio_threadpool::{ThreadPool, Builder};
fn index(pool: State<Arc<ThreadPool>>) -> impl Responder {
println!("Start index.");
let (tx, rx) = channel();
pool.spawn(lazy(|| {
println!("Start async process.");
thread::sleep(Duration::from_secs(1));
println!("End async process.");
tx.send("Hello").unwrap();
Ok(())
}));
rx.responder()
}
fn main() {
let pool = Arc::new(Builder::new()
.pool_size(4)
.build());
server::new(
move || App::with_state(pool.clone())
.resource("/", |r| r.with(index)))
.bind("127.0.0.1:8080").unwrap()
.run();
}
最後に
本当はもう少し実践的な内容を記載しようと思っていたのですが、機能紹介だけで結構な分量となったため、当記事はここまでで一旦終わりとします。
また別の記事にて実践的な内容を記載したいと思います。
RustのWeb Application Frameworkとして、執筆時点で非同期処理を行えるものは現状hyperとactix-web位しか無いように思われます。(他にもあればご教示下さい)
そしてhyperよりも簡潔に記述することができ、処理速度も悪くないためこれからRustでWeb Applicationを作成するのであればactix-webは良い選択だと思います。
尚、hyperが0.12になり、actix-webに近い文法で記述出来るようになったようです。
あまり詳しく見ていませんが、気になる方はチェックしてみてはいかがでしょうか。
そもそもRustでWeb Applicationを作成するという選択はあまり無いかもしれませんが、タイプセーフなモダン言語であり、メモリリークや並列処理におけるバグをコンパイル時に発見することができ、C++並の速度を出せることから、パフォーマンスが重要な場合には悪くない選択だと考えています。
個人的に好きな言語であり流行ってくれたら嬉しいので、当記事が参考になりましたら幸いです。
参考
- actix-web document
- crates.io
- other