« C++ テンプレートで(いまさら)FizzBuzz | メイン | データベースの差分バックアップとウェブサービスのお引っ越し »

2008年04月18日

C++ テンプレートを使って高速な高機能サーバを書く方法

 「C++ のメンバ関数ポインタって何のためにあるの」という質問を耳にすることがあります。実際は、たとえばステートマシンを書くのに便利なのですが、ちょうどサイボウズ・ラボの C++ 熱が盛り上がっていることもあり、昔の作ったサーバフレームワークを再実装してみました。ちなみにもともとは、1990年代に東京大学駒場キャンパスで使われていた friends というサービスのバックエンドだった、finger プロキシ用に書いたコードです。ソースコードは /lang/cplusplus/friends_framework - CodeRepos::Share - Trac においてあります。特徴は以下のとおり:

  • シングルスレッド、select(2) ベースによる実装
  • C++ テンプレートとメンバ関数ポインタによるステートマシン化
  • タイムアウト処理の容易な組み込み

 実際の処理をどのように書くかは、test/proxy.cpp をご覧いただければいいと思います。これは TCP の L4 Proxy を実装したものですが、関数ポインタとフラグの切り替えによって、転送処理とタイムアウト処理を実装しています。

struct ProxyConn {
  CoopRunnable<CoopSocketReader, ProxyConn> clientRead;
  CoopRunnable<CoopSocketWriter, ProxyConn> clientWrite;
  CoopRunnable<CoopSocketReader, ProxyConn> serverRead;
  CoopRunnable<CoopSocketWriter, ProxyConn> serverWrite;
  CoopRunnable<CoopSleeper, ProxyConn> timeout;
  CoopBuffer dataToServer;
  CoopBuffer dataToClient;
  ProxyConn(int clientSock, sockaddr *, socklen_t);
  ~ProxyConn() {
    close(clientRead.sock);
    close(serverRead.sock);
  }
  ProxyConn *Init();
  void ResetTimeout() {
    timeout.SetTimeout(10);
  }
  void OnTimeout(CoopSleeper *) {
    delete this;
  }
  void OnServerConnect(CoopSocketWriter *) {
    ResetTimeout();
    clientRead.isActive = true;
    serverRead.isActive = true;
    serverWrite.func = &ProxyConn::OnServerWrite;
  }
  void OnClientRead(CoopSocketReader *) {
    ResetTimeout();
    if (! CoopTransfer::OnRead(&clientRead, &serverWrite, dataToServer))
      delete this;
  }
  void OnServerWrite(CoopSocketWriter *) {
    ResetTimeout();
    if (! CoopTransfer::OnWrite(&clientRead, &serverWrite, dataToServer))
      delete this;
  }
  void OnServerRead(CoopSocketReader *) {
    ResetTimeout();
    if (! CoopTransfer::OnRead(&serverRead, &clientWrite, dataToClient))
      delete this;
  }
  void OnClientWrite(CoopSocketWriter *) {
    ResetTimeout();
    if (! CoopTransfer::OnWrite(&serverRead, &clientWrite, dataToClient))
      delete this;
  }
};

ProxyConn::ProxyConn(int clientSock, sockaddr *, socklen_t)
: clientRead(this, &ProxyConn::OnClientRead),
  clientWrite(this, &ProxyConn::OnClientWrite),
  serverRead(this, &ProxyConn::OnServerRead),
  serverWrite(this, &ProxyConn::OnServerConnect),
  timeout(this, &ProxyConn::OnTimeout)
{
  clientRead.sock = clientWrite.sock = clientSock;
  serverRead.sock = serverWrite.sock = ::socket(AF_INET, SOCK_STREAM, 0);
  assert(serverRead.sock != -1);
}

 もちろん現代的なサーバプログラムでは、CPU のマルチコア化を背景にマルチスレッドのプログラムを書くことが多いです。しかし、例えばチャットサーバのようにコネクション間の情報交換が多い場合は、今でもこのようなシングルスレッドによるステートマシンによる実装の方が現実的な場合も多いでしょう。

 また、上の ProxyConn クラスでは、OnClientRead と OnServerWrite、OnServerRead と OnClientWrite という2つのメンバ関数の組み合わせで、双方向のストリームを実装していますが、このようなフレームワークではストリームの本数を増やすことも容易です。たとえばx本のサーバ接続を重畳して1本のクライアントに接続する、といったことも簡単にできます。

 そして、そのようなモデルの高速なサーバを容易に、かつ型安全に書ける、という点において、C++ は優れた言語であると思います。

4月24日追記: 上記コードは簡潔さを重視して再実装したので、最適化という観点からはまだまだできることがあります。思いつく範囲で 高速なサーバの書き方補遺 - id:kazuhookuのメモ置き場 に箇条書きにしておいたので、あわせてご覧ください。

投稿者 kazuho : 2008年04月18日 16:39 このエントリーを含むはてなブックマーク このエントリーを含むはてなブックマーク

トラックバック

このエントリーのトラックバックURL:
http://labs.cybozu.co.jp/cgi-bin/mt-admin/mt-tbp.cgi/1864