C ++の非同期HTTPリクエスト:RESTinioを介して着信、libcurlを介して発信。 パート1

前文



私たちのチームは、 REStinioと呼ばれる、最新のC ++用の小さくて使いやすい、組み込みの非同期HTTPサーバーを開発しています。 着信HTTPリクエストの正確な非同期処理が必要だったため、彼らはそれを始めましたが、私たちが好む準備ができていませんでした。 人生が示すように、C ++アプリケーションでのHTTPリクエストの非同期処理は私たちだけでなく必要です。 最近、同じ会社の開発者は、 libcurlを介した非同期送信リクエストの発行により、RESTinioでの受信リクエストの非同期処理とどういうわけか友だちになることができるかどうかという質問に連絡しました。



状況を明らかにしたとき、この会社は私たち自身が対処しなければならない状況に直面しており、そのためRESTinioの開発を開始しました。 一番下の行は、C ++アプリケーションが着信HTTP要求を受け入れるということです。 リクエストを処理する過程で、アプリケーションはサードパーティのサーバーにアクセスする必要があります。 このサーバーはかなりの時間応答できます。 10秒と言います(ただし、10秒で十分です)。 サードパーティのサーバーに同期要求を行うと、HTTP要求が行われる作業スレッドがブロックされます。 そして、これにより、アプリケーションが処理できる同時リクエストの数が制限され始めます。



解決策は、アプリケーションがすべての要求(着信と発信の両方)を非同期で処理できることです。 次に、限られた作業スレッドのプール(または単一の作業スレッド)で、1つの要求の処理時間が数十秒であっても、数万の要求を同時に処理することが可能になります。



秘Theは、発信HTTP要求アプリケーションがすでにlibcurlを使用していたことです。 しかし、 curl_easyの形式、つまり すべてのリクエストは同期的に実行されました。 RESTinioとcurl_multiを組み合わせることは可能ですか? 私たち自身の質問は面白かったです、なぜなら curl_multiの形式のlibcurlの前は、適用する必要はありませんでした。 したがって、このトピックに没頭することは興味深いものでした。



落ちた。 たくさんの印象を受けました。 読者と共有することにしました。 誰かがcurl_multiでどのように暮らすことができるかに興味があるかもしれません。 なぜなら、実践が示しているように、生きることは可能であるからです。 ただし、慎重に...;)上記の状況の単純なシミュレーションを、ゆっくりと応答するサードパーティサービスで実装した経験に基づいて、一連の記事で説明します。



必要な免責事項



コメント内の無用で非建設的な炎を防ぐために( 前の記事で起こったことのように)、いくつかの警告を出したい:





一般に、上記の条件のいずれかが気に入らない場合は、ご了承ください。 さらに読むことは意味がありません。 さて、これらの警告があなたを怖がらせないのであれば、安心してください。 面白いと思います。



開発されたシミュレーションの本質は何ですか?



デモンストレーションのために、RESTINIOとlibcurlを使用していくつかのアプリケーションを作成しました。 最も簡単なのは、遅延の遅いサーバーであるdelay_serverのシミュレーターです。 シミュレーションを開始するには、必要なパラメーターセット(アドレス、ポート、応答に必要な遅延時間)を指定してdelay_serverを実行する必要があります。



また、シミュレーションにはbridge_server_ *と呼ばれるいくつかの「フロント」が含まれます。 ユーザーからリクエストを受け取り、リクエストをdelay_serverに転送するのはbridge_serverです。 ユーザーは最初にdelay_serverを起動し、次にbridge_serverの1つを起動し、その後、便利な方法でbridge_serverの「シェル」を開始すると想定されています。 たとえば、curl / wgetまたはab / wrkなどのユーティリティを使用します。



シミュレーションには、3つのbridge_server実装が含まれます。





delay_serverの実装の説明からこのシリーズを始めましょう。 幸いなことに、これは最も単純で、おそらく最も理解しやすい部分です。 Bridge_serverの実装はずっと難しくなります。



delay_server



delay_serverは何をしますか?



delay_serverは、/ YYYY / MM / DDの形式のURLに対するHTTP GET要求を受け入れます。ここで、YYYY、MM、およびDDは数値です。 他のすべてのリクエストの場合、delay_serverは404コードで応答します。



HTTP GETリクエストが/ YYYY / MM / DDの形式のURLに到着すると、delay_serverは一時停止し、「Hello、World」という挨拶と一時停止の長さを含む小さなテキストで応答します。 たとえば、パラメータを指定してdelay_serverを実行する場合:



  delay_server -a localhost -p 4040 -m 1500 -M 4000 


つまり 彼はlocalhost:4040でリッスンし、1.5秒から4.0秒の間の応答を一時停止します。 次に実行する場合:



  curl -4 http:// localhost:4040/2018/02/22 


次に取得します:



 こんにちは世界!
一時停止:2347ms。 




さて、またはあなたは何が起こっているかのトレースを有効にすることができます。 サーバーの場合、これは次のとおりです。



  delayed_server -a localhost -p 4040 -m 1500 -M 4000 -t 


カールの場合、これは次のとおりです。



  curl -4 -v http:// localhost:4040/2018/02/22 


delay_serverの場合、次のようなものが表示されます。



 [2018-02-22 16:47:54.441]トレース:127.0.0.1:4040でサーバーを起動[2018-02-22 16:47:54.441]情報:init accept#0 [2018-02-22 16:47: 54.441]情報:サーバーは127.0.0.1:4040で起動しました[2018-02-22 16:47:57.040]トレース:ソケット#0で127.0.0.1haps8468からの接続を受け入れます[2018-02-22 16:47:57.041] TRACE:[接続:1] 127.0.0.1haps8468との接続を開始[2018-02-22 16:47:57.041] TRACE:[接続:1]リクエストの待機を開始[2018-02-22 16:47:57.041] TRACE:[接続:1]要求の読み取りを続行[2018-02-22 16:47:57.041] TRACE:[接続:1]受信88バイト[2018-02-22 16:47:57.041] TRACE:[接続:1 ]要求の受信(#0):GET / 2018/02/22 [2018-02-22 16:47:59.401] TRACE:[接続:1]追加応答(#0)、フラグ:{final_parts、connection_keepalive}、bufsカウント:2 [2018-02-22 16:47:59.401]トレース:[接続:1]応答データの送信、bufカウント:2 [2018-02-22 16:47:59.402]トレース:[接続:1]発信データが送信された:206バイト[2018-02-22 16:47:59.402]トレース:[接続:1]を保持する必要があります  p alive [2018-02-22 16:47:59.402] TRACE:[接続:1]要求の待機を開始[2018-02-22 16:47:59.402] TRACE:[接続:1]要求の読み取りを続行[2018- 02-22 16:47:59.403]トレース:[接続:1] EOFおよびリクエストなし、接続を閉じる[2018-02-22 16:47:59.403]トレース:[接続:1]閉じる[2018-02-22 16 :47:59.403]トレース:[接続:1]デストラクタが呼び出されました 


カールの場合:



  * 127.0.0.1を試す...
 * TCP_NODELAYセット
 * localhost(127.0.0.1)ポート4040(#0)に接続
 > GET / 2018/02/22 HTTP / 1.1
 >ホスト:localhost:4040
 >ユーザーエージェント:curl / 7.58.0
 >受け入れる:* / *
 >
 <HTTP / 1.1 200 OK
 <接続:キープアライブ
 <コンテンツの長さ:28
 <サーバー:RESTinio hello worldサーバー
 <日付:2018年2月22日(木)13:47:59 GMT
 <Content-Type:text / plain; 文字セット= utf-8
 <
こんにちは世界!
一時停止:2360ms。
 *ホストlocalhostへの接続#0はそのまま 


delay_serverはどのようにこれを行いますか?



delay_serverは、単純なシングルスレッドC ++アプリケーションです。 組み込みHTTPサーバーがメインスレッドで起動され、適切なURLへのリクエストを受信すると、ユーザーが割り当てたコールバックをプルします。 このコールバックはAsio-chnyタイマーを作成し、作成されたタイマーをランダムに選択された一時停止まで上げます(一時停止はdelay_serverの開始時に設定された制限内に入るように選択されます)。 その後、コールバックはHTTPサーバーに制御を返し、サーバーが次の要求を受け入れて処理できるようにします。 コックされたタイマーがコールバックによってトリガーされると、以前に受信したHTTP要求に対する応答が生成されて送信されます。



delay_serverの実装の解析



メイン関数()



delay_server実装の分析は、main()関数を使用してすぐに分析を開始し、main()の内部と外部で何が起こっているかを徐々に説明します。



したがって、メイン()コードは次のようになります。



int main(int argc, char ** argv) { try { const auto cfg = parse_cmd_line_args(argc, argv); if(cfg.help_requested_) return 1; //    io_context  ,      //     . restinio::asio_ns::io_context ioctx; //          . pauses_generator_t generator{cfg.config_.min_pause_, cfg.config_.max_pause_}; //    ,    //    ,       // (    ). auto actual_handler = [&ioctx, &generator](auto req, auto /*params*/) { return handler(ioctx, generator, std::move(req)); }; //     ,   //    . if(cfg.config_.tracing_) { run_server<traceable_server_traits_t>( ioctx, cfg.config_, std::move(actual_handler)); } else { //   ,    . run_server<non_traceable_server_traits_t>( ioctx, cfg.config_, std::move(actual_handler)); } // ,     . } catch( const std::exception & ex ) { std::cerr << "Error: " << ex.what() << std::endl; return 2; } return 0; }
      
      





ここで何が起こっていますか?



まず、コマンドライン引数を解析し、delay_serverの構成オブジェクトを取得します。



次に、必要ないくつかのオブジェクトを作成します。





第三に、HTTPサーバーを起動しています。 ただし、起動は、ユーザーがサーバー操作のトレースを表示するかどうかを考慮して行われます。 ここで、小さなテンプレートマジックが登場します。これはRESTinioで積極的に使用しており、先ほど少し説明しました



ここでは、実際にはすべてのdelay_server :)



しかし、いつものように、悪魔は詳細にあります。 したがって、これらの単純なアクションの背後に何が隠れているのかを考えてみましょう。



コマンドライン設定と解析



Delay_serverは、非常に単純な構造を使用してサーバー構成を記述します。



 // ,   . struct config_t { // ,       . std::string address_{"localhost"}; // ,    . std::uint16_t port_{8090}; //      . milliseconds min_pause_{4000}; //      . milliseconds max_pause_{6000}; //    ? bool tracing_{false}; };
      
      





コマンドラインの解析は非常に膨大であるため、あまり詳しく説明しません。 しかし、希望する人は、ネタバレを見て、何が起こっているのかを印象づけることができます。



コマンドライン引数の解析の詳細
 //    . //     . auto parse_cmd_line_args(int argc, char ** argv) { struct result_t { bool help_requested_{false}; config_t config_; }; result_t result; long min_pause{result.config_.min_pause_.count()}; long max_pause{result.config_.max_pause_.count()}; //     . using namespace clara; auto cli = Opt(result.config_.address_, "address")["-a"]["--address"] ("address to listen (default: localhost)") | Opt(result.config_.port_, "port")["-p"]["--port"] ("port to listen (default: 8090)") | Opt(min_pause, "minimal pause")["-m"]["--min-pause"] ("minimal pause before response, milliseconds") | Opt(max_pause, "maximum pause")["-M"]["--max-pause"] ("maximal pause before response, milliseconds") | Opt(result.config_.tracing_)["-t"]["--tracing"] ("turn server tracing ON (default: OFF)") | Help(result.help_requested_); //  ... auto parse_result = cli.parse(Args(argc, argv)); // ...      . if(!parse_result) throw std::runtime_error("Invalid command line: " + parse_result.errorMessage()); if(result.help_requested_) std::cout << cli << std::endl; else { //      . if(min_pause <= 0) throw std::runtime_error("minimal pause can't be less or equal to 0"); if(max_pause <= 0) throw std::runtime_error("maximal pause can't be less or equal to 0"); if(max_pause < min_pause) throw std::runtime_error("minimal pause can't be less than " "maximum pause"); result.config_.min_pause_ = milliseconds{min_pause}; result.config_.max_pause_ = milliseconds{max_pause}; } return result; }
      
      





分析のために、C ++でのユニットテストに、ライブラリの作成者が提供する新しいClaraライブラリを使用しようとしました。



一般に、1つのトリックを除いて複雑なことはありません。parse_cmd_line_args関数は、ローカルに定義された構造のインスタンスを返します。 良いことには、次のようなものが返されます:



 struct help_requested_t {}; using cmd_line_args_parsing_result_t = variant<config_t, help_requested_t>;
      
      





しかし、C ++ 14にはstd :: variantはありませんが、variantの実装をサードパーティライブラリからドラッグしたり、std :: Experimental :: variantの存在に依存したりしませんでした。 したがって、彼らはこのようにしました。 もちろん、コードはスマックですが、ひざの上でシミュレートする場合はシミュレーションを行います。



ランダム遅延発生器



ここではすべてが単純であり、原則として、議論することは何もありません。 したがって、単なるコードです。 存在するために。



Pauses_generator_tの実装
 //      . class pauses_generator_t { std::mt19937 generator_{std::random_device{}()}; std::uniform_int_distribution<long> distrib_; const milliseconds minimal_; public: pauses_generator_t(milliseconds min, milliseconds max) : distrib_{0, (max - min).count()} , minimal_{min} {} auto next() { return minimal_ + milliseconds{distrib_(generator_)}; } };
      
      





必要な場合にのみnext()メソッドをプルする必要があり、範囲[min、max]のランダムな値が返されます。



ハンドラー()関数



delay_server実装の重要な要素の1つは、小さなHTTPハンドラー()関数です。この関数内では、着信HTTP要求の処理が行われます。 この関数の完全なコードは次のとおりです。



 //   . restinio::request_handling_status_t handler( restinio::asio_ns::io_context & ioctx, pauses_generator_t & generator, restinio::request_handle_t req) { //      (   ). const auto pause = generator.next(); //     Asio-. auto timer = std::make_shared<restinio::asio_ns::steady_timer>(ioctx); timer->expires_after(pause); timer->async_wait([timer, req, pause](const auto & ec) { if(!ec) { //   ,   . req->create_response() .append_header(restinio::http_field::server, "RESTinio hello world server") .append_header_date_field() .append_header(restinio::http_field::content_type, "text/plain; charset=utf-8") .set_body( fmt::format("Hello world!\nPause: {}ms.\n", pause.count())) .done(); } } ); // ,         - //   . return restinio::request_accepted(); }
      
      





この関数(main()-eで作成されたラムダ経由)は、HTTPサーバーが目的のURLへのGETリクエストを受信するたびに呼び出されます。 着信HTTP要求自体は、restinio :: request_handle_t型のreqパラメーターで渡されます。



この非常にrestinio :: request_handle_tは、HTTPリクエストのコンテンツを持つオブジェクトへのスマートポインターです。 これにより、req値を保存して後で使用できます。 これは、まさにRESTinioの非同期性の基礎の1つです。REStinioはユーザーが提供するコールバックをプルし、request_handle_tインスタンスをこのコールバックに渡します。 ユーザーは、コールバック内でHTTP応答をすぐに生成することができ(その後、単純な同期処理になります)、reqを自分自身に保存するか、reqを他のスレッドに渡すことができます。 次に、制御をRESTinioに戻します。 後で答えを定式化するために、適切なタイミングが来たら。



この場合、asio :: steady_timerインスタンスが作成され、reqはタイマーのasync_waitに渡されるラムダ関数に保存されます。 したがって、HTTP要求オブジェクトは、タイマーがオフになるまで保存されます。



ハンドラーの非常に重要なポイント()-eは返される値です。 RESTinioは、戻り値によって、ユーザーが要求に対する応答を生成する責任を負ったかどうかを理解します。 この場合、request_accepted値が返されます。これは、ユーザーがRESTinioに、後で着信HTTP要求に対する応答を生成することを約束したことを意味します。



ただし、handler()がrequest_rejected()などを返した場合、RESTINIOはリクエストの処理を終了し、ユーザーにコード501で応答します。



したがって、ハンドラー()は、着信HTTP要求が目的のURLに到着すると呼び出されます(これについては以下で説明します)。 ハンドラーは、応答の遅延量を計算します。 次に、タイマーが作成され、コックされます。 タイマーが切れると、リクエストに対する応答が生成されます。 さて、ハンドラー()はRESTinioにrequest_acceptedを返すことで要求への応答を生成することを約束します。



実際、それがすべてです。 小さな些細なこと: fmtlibは 、応答本文を形成するために使用されます。 原則として、ここではそれなしで行うことができます。 しかし、まず、fmtlibが本当に好きで、できる限りfmtlibを使用します。 2番目に、bridge_serverにfmtlibが必要だったため、delay_serverで拒否する理由はありませんでした。



関数run_server()



run_server()関数は、HTTPサーバーの構成と起動を担当します。 HTTPサーバーが処理する要求と、HTTPサーバーが他のすべての要求に応答する方法を決定します。



また、run_server()はHTTPサーバーが動作する場所を決定します。 delay_serverの場合、これがアプリケーションのメインスレッドになります。



最初にrun_server()コードを見てから、まだ話していないいくつかの重要なポイントを見てみましょう。



コードは次のとおりです。



 template<typename Server_Traits, typename Handler> void run_server( restinio::asio_ns::io_context & ioctx, const config_t & config, Handler && handler) { //      express-. auto router = std::make_unique<express_router_t>(); //   URL   . router->http_get( R"(/:year(\d{4})/:month(\d{2})/:day(\d{2}))", std::forward<Handler>(handler)); //      404. router->non_matched_request_handler([](auto req) { return req->create_response(404, "Not found") .append_header_date_field() .connection_close() .done(); }); restinio::run(ioctx, restinio::on_this_thread<Server_Traits>() .address(config.address_) .port(config.port_) .handle_request_timeout(config.max_pause_) .request_handler(std::move(router))); }
      
      





それで何が起こっているのか、なぜこのように起こっているのですか?



まず、 expressjsリクエストルーティングシステムと同様の遅延がdelay_serverに使用されます 。 RESTinioでは、これはエクスプレスルーターと呼ばれます



正規表現に基づくクエリのルーティングを担当するオブジェクトのインスタンスを作成する必要があります。 次に、このオブジェクトにルートのリストを配置し、各ハンドラーにルートを設定する必要があります。 私たちがすること。 ハンドラーを作成します。



 auto router = std::make_unique<express_router_t>();
      
      





興味のあるルートを示します:



 router->http_get( R"(/:year(\d{4})/:month(\d{2})/:day(\d{2}))", std::forward<Handler>(handler));
      
      





次に、他のすべてのリクエストのハンドラーも設定します。 404コードで単純に応答します:



 router->non_matched_request_handler([](auto req) { return req->create_response(404, "Not found") .append_header_date_field() .connection_close() .done(); });
      
      





これで、必要なExpressルーターの準備が完了しました。



次に、run()を呼び出すとき、HTTPサーバーが指定されたio_contextを使用し、run()呼び出しが行われたスレッドで動作する必要があることを示します。 さらに、構成のパラメーターがサーバーに設定されます(IPアドレスとポート、要求処理の最大許容時間、およびプロセッサー自体のため):



 restinio::run(ioctx, restinio::on_this_thread<Server_Traits>() .address(config.address_) .port(config.port_) .handle_request_timeout(config.max_pause_) .request_handler(std::move(router)));
      
      





ここで、on_this_threadを使用すると、同じスレッドのコンテキストでRESTinioがHTTPサーバーを起動するだけです。



run_server()がテンプレートなのはなぜですか?



run_server()関数は、2つのパラメーターに依存するテンプレート関数です。



 template<typename Server_Traits, typename Handler> void run_server( restinio::asio_ns::io_context & ioctx, const config_t & config, Handler && handler);
      
      





なぜそうなのかを説明するために、2番目のテンプレートパラメーター-ハンドルから始めます。



main()内で、ラムダ関数の形式で実際の要求ハンドラーを作成します。 コンパイラーだけがこのラムダの実際の型を知っています。 したがって、ラムダハンドラをrun_server()に渡すには、テンプレートパラメータHandleが必要です。 これにより、コンパイラはrun_server()内の目的のタイプのハンドラー引数を推測します。



ただし、Server_Traitsパラメーターを使用すると、状況はもう少し複雑になります。 実際、REStinioのHTTPサーバーは、サーバーの動作と実装のさまざまな側面を決定する一連のプロパティを設定する必要があります。 たとえば、サーバーがマルチスレッドモードで動作するように適合されるかどうか。 サーバーは実行する操作などをログに記録しますか これらはすべて、restinio :: http_server_tクラスのTraitsテンプレートパラメーターによって設定されます。 この例では、このクラスは表示されません。 http_server_tのインスタンスがrun()内に作成されます。 しかし、まだ特性を設定する必要があります。 run_server()関数のテンプレートパラメータServer_Traitsだけで、http_server_tのTraitsを設定します。



delay_serverでは、2つの異なるタイプの特性を定義する必要がありました。



 //    express-router.     //   . using express_router_t = restinio::router::express_router_t<>; //          http-. //    ,     . struct non_traceable_server_traits_t : public restinio::default_single_thread_traits_t { using request_handler_t = express_router_t; }; //    ,    . struct traceable_server_traits_t : public restinio::default_single_thread_traits_t { using request_handler_t = express_router_t; using logger_t = restinio::single_threaded_ostream_logger_t; };
      
      





最初のタイプ、non_traceable_server_traits_tは、サーバーがアクションをログに記録する必要がない場合に使用されます。 2番目のタイプであるtraceable_server_traits_tは、ロギングが必要な場合に使用されます。



したがって、main()関数内では、「-t」キーの有無に応じて、run_server()関数がnon_traceable_server_traits_tまたはtraceable_server_traits_tで呼び出されます。



 //     ,   //    . if(cfg.config_.tracing_) { run_server<traceable_server_traits_t>( ioctx, cfg.config_, std::move(actual_handler)); } else { //   ,    . run_server<non_traceable_server_traits_t>( ioctx, cfg.config_, std::move(actual_handler)); }
      
      





したがって、必要なプロパティをHTTPサーバーに割り当てることが、run_server()がテンプレート関数であるもう1つの理由です。



restinio :: http_server_tのTraitsトピックについては、以前のRESTinioに関する記事で詳しく説明しています



前半のまとめ



実際、これがRESTinioに基づくdelay_serverの実装について言えることのすべてです。 説明した資料が明確であることを願っています。 そうでない場合は、コメントで質問にお答えします。



後続の記事では、bridge_server_1とbridge_server_2の実装を解析するRESTinioとcurl_multiの統合例について説明します。 そこで、特にRESTinioに関連する部分は、この記事で示したものよりもボリュームが大きく複雑ではありません。 そして、大部分のコードと主な複雑さはcurl_multiに起因します。 しかし、これは全く異なる話です...



継続する



All Articles