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()の組み合わせです。