cpp-httplib v0.61.0
DRAFT

S18. listen_after_bindで起動順序を制御する

普通はsvr.listen("0.0.0.0", 8080)でbindとlistenをまとめて行いますが、bindした直後に何か処理を差し挟みたいときは、2つを分けて呼べます。

bindとlistenを分ける

httplib::Server svr;

svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });

if (!svr.bind_to_port("0.0.0.0", 8080)) {
  std::cerr << "bind failed" << std::endl;
  return 1;
}

// ここでbindは完了。まだacceptは始まっていない
drop_privileges();
signal_ready_to_parent_process();

svr.listen_after_bind(); // acceptループを開始
httplib::Server svr;

svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });

if (!svr.bind_to_port("0.0.0.0", 8080)) {
  std::cerr << "bind failed" << std::endl;
  return 1;
}

// ここでbindは完了。まだacceptは始まっていない
drop_privileges();
signal_ready_to_parent_process();

svr.listen_after_bind(); // acceptループを開始

bind_to_port()でポートの確保までを行い、listen_after_bind()で実際の待ち受けを開始します。この2段階に分けることで、bindとacceptの間に処理を挟めます。

よくある用途

特権降格: 1023以下のポートにbindするにはroot権限が必要です。bindだけroot権限で行って、その後に一般ユーザーに降格すれば、以降のリクエスト処理は権限が落ちた状態で走ります。

svr.bind_to_port("0.0.0.0", 80);
drop_privileges();
svr.listen_after_bind();
svr.bind_to_port("0.0.0.0", 80);
drop_privileges();
svr.listen_after_bind();

起動完了通知: 親プロセスやsystemdに「準備完了」を通知してからacceptを開始できます。

テストの同期: テストコードで「サーバーがbindされた時点」を確実に捉えてからクライアントを動かせます。

戻り値のチェック

bind_to_port()は失敗するとfalseを返します。ポートにbindする権限が無い場合などです。必ずチェックしてください。

if (!svr.bind_to_port("0.0.0.0", 8080)) {
  std::cerr << "bind failed" << std::endl;
  return 1;
}
if (!svr.bind_to_port("0.0.0.0", 8080)) {
  std::cerr << "bind failed" << std::endl;
  return 1;
}

listen_after_bind()はサーバーが停止するまでブロックし、正常終了ならtrueを返します。

使用中のポートを検出する

実は、デフォルトの設定では、ほかのサーバーが使っているポートにもbindできてしまいます。cpp-httplibがサーバーソケットにSO_REUSEPORT(Linux、macOS)かSO_REUSEADDR(Windows)を設定しているからです。再起動したサーバーはすぐにbindし直せます。その代わり、同じポートで2つ目のサーバーを起動してもエラーにならず、接続が両方に振り分けられます。

使用中のポートでbind_to_port()を失敗させたいときは、set_socket_options()でソケットオプションを差し替えてください。

svr.set_socket_options([](socket_t sock) {
#ifdef _WIN32
  httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
#else
  httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
#endif
});

if (!svr.bind_to_port("0.0.0.0", 8080)) {
  std::cerr << "port already in use" << std::endl;
  return 1;
}
svr.set_socket_options([](socket_t sock) {
#ifdef _WIN32
  httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
#else
  httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
#endif
});

if (!svr.bind_to_port("0.0.0.0", 8080)) {
  std::cerr << "port already in use" << std::endl;
  return 1;
}

set_socket_options()はデフォルトの設定を丸ごと置き換えます。Linux、macOSでSO_REUSEADDRを設定しているのは、再起動したサーバーがすぐにbindし直せるようにするためです。

Note: WindowsではSO_REUSEADDRだけでは足りません。お互いにSO_REUSEADDRを設定したソケット同士は、同じポートにbindできてしまいます。SO_EXCLUSIVEADDRUSEを使ってください。

Note: 空いているポートを自動で選びたいときはS17. ポートを動的に割り当てるを参照してください。こちらも内部ではbind_to_any_port() + listen_after_bind()の組み合わせです。

ESC