50行のC++コードでWebサーバを実装する

ID: 94
creation date: 2010/09/19 11:09
modification date: 2010/09/19 11:09
owner: mikio

「Kyoto Tycoonの設計 その四」改め、50行でWebサーバを書く方法を解説する。前回実装した「多重I/Oマルチスレッド汎用TCPサーバ」の上にHTTPの処理を行う層をつけて、「多重I/Oマルチスレッド汎用HTTPサーバ」を司るクラスを実装してみたので、それを使ってちょちょいとやる。

URLクラス

HTTPと言えばURLが使えないと始まらない。URLは単なる文字列として扱ってもよいのだが、様々なシーンで分解や加工が必要になり、その処理はなにげに複雑で面倒なので、予めクラスとして導出しておいた方がよいだろう。

class URL {
public:
  // 文字列のURLを解析して内部構造を作る
  void set_expression(const std::string& expr);
  // スキーム要素を設定する
  void set_scheme(const std::string& scheme);
  // ホスト名要素を設定する
  void set_host(const std::string& host);
  // ポート番号を設定する
  void set_port(uint32_t port);
  // 認証情報を設定する
  void set_authority(const std::string& authority);
  // パス情報を設定する
  void set_path(const std::string& path);
  // クエリ文字列情報を設定する
  void set_query(const std::string& query);
  // フラグメント文字列を設定する
  void set_fragment(const std::string& fragment);
  // 内部構造を再編して文字列として返す
  std::string expression();
  // パスとクエリ文字列を結合してHTTPリクエスト用文字列を作る
  std::string path_query();
  // その他、各要素のアクセサ
};

HTTPクライアントクラス

HTTPサーバのテストをするためにはHTTPクライアントが必要なので、それを先に書いた。一撃でURLに対応するリソースのデータを取得することもできるし、持続接続でいくつかのリソースのデータを連続して取得することもできる。

class HTTPClient {
public:
  // 持続接続を始める
  bool open(const std::string& host, uint32_t port = 80, double timeout = -1);
  // 持続接続を終える
  bool close();
  // 持続接続でリソースを取得する
  int32_t fetch(const std::string& pathquery, Method method = MGET,
                std::string* resbody = NULL,
                std::map<std::string, std::string>* resheads = NULL,
                const std::string* reqbody = NULL,
                const std::map<std::string, std::string>* reqheads = NULL);
  // 持続接続せずに一撃でデータを取得する
  static int32_t fetch_once(const std::string& url, Method method = MGET,
                            std::string* resbody = NULL,
                            std::map<std::string, std::string>* resheads = NULL,
                            const std::string* reqbody = NULL,
                            const std::map<std::string, std::string>* reqheads = NULL,
                            double timeout = -1);
};

このクラスは後でKTのクライアントAPI(get/set/removeなどなど)を作るために活躍するわけだが、それ以外の用途でWeb上のリソースを参照するのにも便利である。特にfetch_onceは数行でデータを取得できるから素敵だ。fallabs.comのトップページが取得したいなら以下のコードを書くだけでいいのだ。

std::string data;
if (HTTPClient::fetch_once("http://fallabs.com/", HTTPClient::MGET, &data) == 200) {
  // dataに内容が入っている
}

簡単に使えるわりに、GETだけでなくHEADもPOSTもPUTもDELETEも発行でき、ヘッダなども自由に設定できるのが素晴らしい。HTTPSが喋れないのが弱点だが。

HTTPサーバ

いよいよHTTPサーバである。前回実装したThreadedServerを内包して、そのWorkerをHTTP用にアダプタで書き換えたものである。

class HTTPServer {
public:
  // ワーカ用内部クラス
  class Worker {
  public:
    // 個々のリクエストを処理する
    virtual int32_t process(HTTPServer* serv, Session* sess,
                            const std::string& path, HTTPClient::Method method,
                            const std::map<std::string, std::string>& reqheads,
                            const std::string& reqbody,
                            std::map<std::string, std::string>& resheads,
                            std::string& resbody,
                            const std::map<std::string, std::string>& misc) = 0;
  };
  // サーバのアドレス表現とセッションのタイムアウトとサーバ名を指定
  void set_network(const std::string& expr, double timeout = -1,
                   const std::string& name = "");
  // ワーカスレッドの実装とスレッド数を指定
  void set_worker(Worker* worker, size_t thnum = 1);
  // サーバを開始する。停止するまでブロックする。
  bool start();
  // サーバに停止指示を出す
  bool stop();
  // サーバの終了処理を行う
  bool finish();
};

ワーカのprocessメソッドがちょっと複雑化している以外には、TCPサーバとほぼ同じインターフェイスになっていて簡単に使えると思う。詳細な仕様はAPI文書に書いてある。

HTTPサーバなのでもちろんHTTPを喋るのだが、リソースをどう管理するかはプログラマが指定できる必要がある。リソースがファイルでなくてもよいということだ(ファイルをリソースとして扱いたいならApacheを使えばいい)。そして、KTではファイルでなくDBMの中にリソースを入れることで、HTTPサーバだけどKVSとして使える新感覚ソリューションが実現するわけだ。

サンプルコード

KVS化の話はひとまず置いておいて、Apache的に、ファイルシステム上の個々のファイルをリソースとするWebサーバを実装してみようではないか。上述のクラス群は全て kthttp.h に記述されているので、それをincludeするだけで使えるようになる。

#include <kthttp.h>

namespace kc = kyotocabinet;
namespace kt = kyototycoon;

kt::HTTPServer *g_server;

// 停止用シグナルハンドラ
static void killserver(int signum) {
  // サーバソケットの処理を「上品」に停止
  if (g_server) {
    g_server->stop();
    g_server = NULL;
  }
}

// メインルーチン
int main(int argc, char** argv) {
  // シグナルハンドラを登録する
  kt::setkillsignalhandler(killserver);
  // HTTPサーバを作る
  kt::HTTPServer serv;
  // ワーカの処理を定義する
  class Worker : public kt::HTTPServer::Worker {
  private:
    int32_t process(kt::HTTPServer* serv, kt::HTTPServer::Session* sess,
                    const std::string& path,
                    kt::HTTPClient::Method method,
                    const std::map<std::string, std::string>& reqheads,
                    const std::string& reqbody,
                    std::map<std::string, std::string>& resheads,
                    std::string& resbody,
                    const std::map<std::string, std::string>& misc) {
      int32_t code = -1;
      // URLのパス情報をローカルファイルシステムのパスに変換
      const std::string& lpath = kt::HTTPServer::localize_path(path);
      std::string apath = std::string("./") + lpath;
      // GETメソッドのみに対応
      if (method == kt::HTTPClient::MGET) {
        // ファイルを読み込む
        int64_t size;
        char* buf = kc::File::read_file(apath, &size, 256LL << 20);
        if (buf) {
          // ステータスコードを指定
          code = 200;
          // MIMEタイプを拡張子から判別して指定
          const char* type = kt::HTTPServer::media_type(path);
          if (type) resheads["content-type"] = type;
          // レスポンスのエンティティボディを設定
          resbody.append(buf, size);
          // リソース開放
          delete[] buf;
        } else {
          // 読めなかった場合は404 Not Foundを返す
          code = 404;
          resheads["content-type"] = "text/plain";
          kc::strprintf(&resbody, "%s\n", kt::HTTPServer::status_name(code));
        }
      }
      // ステータスコードを返す
      return code;
    }
  };
  // ワーカオブジェクトを作る
  Worker worker;
  // 全ネットワークインターフェイスの1978番に接続するように設定
  serv.set_network(":1978");
  // 上述のワーカスレッドを4スレッドで動かすように設定
  serv.set_worker(&worker, 4);
  // シグナルハンドラ用にサーバを大域化
  g_server = &serv;
  // サーバを動かす
  serv.start();
  // サーバの終了処理を行う
  serv.finish();
  return 0;
}

つーことで、コメントを抜けば約50行である。たったこれだけだが、多重I/Oで待ち受けてスレッドプールで並列処理してサービスを提供できる。Webブラウザで「http://localhost:1978/xxxx」を開くとちゃんと表示されるだろう。xxxxはカレントディレクトリからのパスである。KTのプロトタイプ最新版を入れれば、ビルドするためのライブラリは全て同梱されている。

また、上記の実装よりももうちょい作り込んで、ロギングやディレクトリのインデックス表示などもサポートしたサーバのコマンドも同梱されていて、以下のように起動できる。「Apacheを入れるまでもないけど、ちょっとの間だけ、とあるディレクトリをHTTPで公開したい」といった用途で活用いただけると思う。

ktutilserv http /path/to/basedir

なお、終了するには、端末にCtrl-Cを入力してSIGINTを送るかkillコマンドでSIGTERMを送ればよい。デーモン起動モードは実装していなので、daemontoolsとかを使っていただきたい。

まとめ

俺が使う範囲では過不足ないWebサーバフレームワークができあがった。これさえあれば、HTTP経由でRPC的なことをするサーバが本当に簡単に書ける。性能もそこそこで、持続接続すればクライアント同居の状態で2万qpsとか行くので、クライアントを分散させれば10万qpsくらいまでは普通に行くと思う。ちゃんとした性能テストはマシンを調達してからやる。

1 comments
an : good article !! (2013/11/20 13:19)
riddle for guest comment authorization:
Where is the capital city of Japan? ...