Express.jsライクな簡単なWebアプリケーションフレームワークを作った話

リポジトリはここ(GitHub)です。ライセンスはApache License 2.0です。
これを使って何か起きても責任は負いません。

Express.jsはNode.jsで動作するWebアプリケーションフレームワークで、MEANスタックのEです。JavaScriptで簡単に書けて便利なフレームワークです。
これっぽいものをC++で実装したのがこれです。

使い方はできるだけ本家に近づけましたが、足りない機能がかなりたくさんあります。(1ヘッダだから許して...)
とりあえず、C++でRESTful APIを作りたいという人(いるのかわからないですが)におすすめです!

使い方

test.cppを見ていただければ分かると思いますが、とりあえず書いておきます。
  1. express::Expressのインスタンスを作成します。
  2. get, post, put, del関数を使ってハンドラを登録します。
  3. listen関数を呼んで実行を開始します。
パスの書き方は/abcみたいにパスにパラメータがないものと/abc/:idみたいにパスにパラメータがあるものが書けます。
:idの部分がパスパラメータになります。パスパラメータの値はreq.params("id")のような形で取得できます。
#include "express.hpp"
#include <iostream>

int main() {
    express::Express app;
    app.get("/", [](const express::Request &req, express::Response &res) {
        std::cout << "ok /" << std::endl;
        res.end("ok");
    });

    app.get("/parameters/:id/others/:other",
            [](const express::Request &req, express::Response &res) {
                std::cout << "ok /parameters/" << req.params("id") << "/others/"
                          << req.params("other") << std::endl;
                res.end("ok");
            });

    app.post("/post", [](const express::Request &req, express::Response &res) {
        std::cout << "ok /port " << req.body() << std::endl;
        res.end("ok post");
    });

    app.listen();
    return 0;
}

技術的説明

  • 全ての機能がC++11でコンパイルできるレベルになっています
  • Linuxでしか動きません(epollがある環境ならLinuxじゃなくても動くかも)
  • GET, POST, PUT, DELETEのみに対応
  • 標準C++ライブラリ以外に依存がありません
  • ヘッダひとつだけで動きます
  • HTTP/1.1しか動きません
  • 絶対にconnection: closeします
  • コメント空行込みで600行!!
処理の流れは次のとおりです。
  1. ソケット作成
  2. bind
  3. listen
  4. epollでaccept待ち
  5. accept
  6. HTTPヘッダを読み込み(\r\n\r\nまでを読み込み)
  7. ヘッダをパース
  8. メソッドがPOST, PUTの場合はContent-Lengthを読んでbodyを読み込み
  9. マッチするハンドラを検索
  10. ハンドラにreq, resを渡す
  11. 4に戻る

ハンドラの登録

ハンドラの登録にはメソッド、パス、ハンドラ本体の関数が必要です。
登録前にパスを正規表現の形に変換します。先に正規表現に直しておくことで、リクエストを受けるときに、いちいち文字列操作をしなくて済むようにしています。

パスの変換はExpress::_pathToRegex関数で行っています。

ヘッダのパース

ヘッダそのもののパースはdetail::RequestHeader::Parse関数で行っています。
やっていることは超絶単純で、ほとんど文字列の分割しかしていません。

まず、ヘッダ文字列を\r\nで分割します。
一行目をスペースで分割しメソッド名、パスを取ります。(HTTPのバージョンは見てません)
二行目以降は:で分割しキーとバリューに分けます。
こんだけです。

ヘッダのキーは見つけやすいよう全て小文字に変換されています。
キーからバリューを取るときも小文字に変換されて検索されるので大文字小文字は使う側からも関係ありません。

残念なところ

非同期じゃない

リクエストごとにスレッドで処理していて非同期じゃないのはともかく、1スレッドで非同期じゃないのはだいぶ致命的ですね。
できたら非同期にします。

絶対にConnection: close

構造を単純にするため、リクエストを処理したら即closeされます。
リソースの確保・破棄を繰り返すので、まあ良くないですよね。
まだ、そこまで致命的ではないのですぐには対応しないと思います。

Linuxしか使えない

これは、残念なんだろうかとは思いますが、おそらくLinuxしか動かないです。
Windowsは確実に動かないですが、FreeBSDとかでも動かないはずです。
まあ、当分困らんだろうということでだいぶ放置されると思います。

JSONが使えない

JSONパーサーを入れる余裕(コード量と心の余裕)がないので、ここはpicojsonとかの外部JSONパーサーがある場合のみ使えるようにするかもしれないです。
JSONがあればAPIが作りやすいので、これは入れたいとは思っています。

今後

とりあえず残念なところとバグの修正をすると思います。
気が向いたら更新するみたいな感じになると思うので、「こいつ全然更新しねーじゃん」みたいになると思いますが、PRを出してもらえれば気づいたときに確認するので気が向いたらよろしくです。

コメント

このブログの人気の投稿

初めの挨拶

C++11の機能 (型推論, 範囲for)

C++11の機能 (関数のdefault・delete宣言, overrideとfinal指定子, 移譲コンストラクタ, 継承コンストラクタ)