はじめに
最近業務で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