「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くらいまでは普通に行くと思う。ちゃんとした性能テストはマシンを調達してからやる。