IronでHTTPサーバーを立てる

  • 42
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

RustとHTTP

Module std::net というのがありますが、これはプリミティブなTCP/UDP通信を提供するものなので、これをそのまま使ってHTTPを扱うのは少し大変です。
Are we web yet? Getting there を読んで、今回はIronというフレームワークを使ってみました。

バージョンは以下のとおりです。

  • rust: 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)
  • iron: 0.1.17

Iron

Iron はミドルウェア指向で拡張性の高いHTTPサーバーのフレームワークです。Iron自体のソースコードは小さく、これに必要なミドルウェアやプラグインを足すスタイルになっています。

コアとなるモジュールはあらかじめバンドルされています。

Middleware:

Plugins:

Both:

Railsを置き換えるようなものではなく、単機能の一部のサーバーに使うことを想定しているので、小さい部品を組み合わせて書ける方が都合が良いと思いました。

Handler

基本的には以下のようにHandlerを渡してサーバーを起動します。

Iron::new(handler).http("localhost:3000").unwrap();

Handlerは受け取ったリクエストからレスポンスを作ります。Handlerは以下のようなトレイトになっています。

pub trait Handler: Send + Sync + Any {
    /// Produce a `Response` from a Request, with the possibility of error.
    fn handle(&self, &mut Request) -> IronResult<Response>;
}

Request を受け取って IronResult<Response> を返すものがHandlerということになります。なのでこのようにクロージャを渡しても良いですし、

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

もちろんメソッドを渡すことも可能です。

fn main() {
    fn handler(req: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Hello world!")))
    }

    Iron::new(handler).http("localhost:3000").unwrap();
}

Ok(...) という値を返していますが、これはIronResultが Module std::result の型シノニムとして定義されているためです。

enum Result<T, E> {
   Ok(T),
   Err(E)
}

pub type IronResult<T> = Result<T, IronError>;

このサンプルでは Ok(T) を返しているということです。

Router

パスで分岐した処理を書きやすくするために Router というモジュールがあります。

let mut router = Router::new();
router.get("/", |_: &mut Request| {
    ...
});

RouterもHandlerなので、Handlerトレイトを実装しています。そのためRouterもIronのコンストラクタに渡すことができます。

Iron::new(router).http("localhost:3000").unwrap();

MiddlewareとChain

IronではMiddlewareをChainに登録してスタックの玉ねぎを作ります。

ChainもHandlerトレイトを実装しています。

let mut chain = Chain::new(router);

chain.link_before(MyBeforeMiddleware);
chain.link_after(MyAfterMmiddleware);

Iron::new(chain).http("localhost:3000").unwrap();

定義はこのようになります。

struct MyBeforeMiddleware;

impl BeforeMiddleware for MyBeforeMiddleware {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        ...
        Ok(())
    }
}

struct MyAfterMiddleware;

impl AfterMiddleware for MyAfterMiddleware {
    fn after(&self, req: &mut Request, res: Response) -> IronResult<Response> {
        ...
        Ok(res)
    }
}

req.extensions

Middlewareでリクエストに値を追加するには req.extensions を使います。extensionsは TypeMap です。TypeMapはAnyMapのようですが、キーと対応するバリューの型を強制することができるモジュールです。
これを使ってBeforeMiddlewareからAfterMiddlewareに値を渡すこともできます。

struct MyBeforeMiddleware;

impl Key for BeforeMiddleware { type Value = String; }

impl BeforeMiddleware for MyBeforeMiddleware {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        let message = "This message from MyBeforeMiddleware".to_string();
        req.extensions.insert::<MyBeforeMiddleware>(message);
        Ok(())
    }
}

struct MyAfterMiddleware;

impl AfterMiddleware for MyAfterMiddleware {
    fn after(&self, req: &mut Request, res: Response) -> IronResult<Response> {
        let message = req.extensions.get::<MyBeforeMiddleware>().unwrap();
        println!("{:?}", message); // => This message from MyBeforeMiddleware
        Ok(res)
    }
}

レスポンスタイムを測る

ArroundMiddlewareを使うとHandlerの前後に処理をすることができます。ここではHandlerの前後で時間を計測してLoggerで出力しています。

struct LoggerHandler<H: Handler> { logger: Logger, handler: H }

impl<H: Handler> Handler for LoggerHandler<H> {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        let entry = ::time::precise_time_ns();
        let res = self.handler.handle(req);
        let time = ::time::precise_time_ns() - entry;
        self.logger.log(req, res.as_ref(), time);
        res
    }
}

struct Logger;

impl Logger {
    fn log(&self, req: &Request, res: Result<&Response, &IronError>, time: u64) {
        println!("Request: {:?}\nResponse: {:?}\nResponse-Time: {:?}", req, res, time)
    }
}

impl AroundMiddleware for Logger {
    fn around(self, handler: Box<Handler>) -> Box<Handler> {
        Box::new(LoggerHandler {
            logger: self,
            handler: handler
        }) as Box<Handler>
    }
}

fn main() {
    let mut chain = Chain::new(router);
    chain.around(Logger);

    Iron::new(chain).http("localhost:3000").unwrap();
}

リクエストを送ると以下のようなログが出力されます。

Request: Request {
    url: Url { scheme: "http", host: Domain("localhost"), port: 3000, path: [""], username: None, password: None, query: None, fragment: None }
    method: Get
    remote_addr: V6([::1]:52567)
    local_addr: V6([::1]:3000)
}
Response: Ok(HTTP/1.1 200 OK OK
Content-Length: 12

)
Response-Time: 7247

ちなみに cargo run にreleaseフラグを付けて実行したら10倍以上速くなりました。

その他

今回はIronを触ってみましたが Nickel というフレームワークも気になっています。後発だけあってインタフェースが洗練されていて使いやすそうなので、別の機会に試してみたいと思います。
Rustを実際にプロダクションで使うかと言われると、Rust自体が先日バージョン1になったくらいなので、世の中のライブラリは当然バージョン0.xという状態なので、すぐには使わないと思います。
しかし高い抽象度でありながら安全でC++に匹敵する速度が出るらしいRustには期待しています。