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

前の記事で、着信HTTP要求の非同期処理を実装する方法について話し始めました。その中で、非同期HTTP発信要求を実行する必要があります。 C ++およびRESTinioで実装されたサードパーティのサーバーの模倣を調査しました。これは、長い間HTTPリクエストに応答してきました。 次に、 curl_multi_performを使用して、このサーバーへの非同期送信HTTPリクエストの発行を実装する方法について説明します。



curl_multiの使用方法に関するいくつかの言葉



libcurlライブラリーは 、CおよびC ++の世界で広く知られています。 しかし、おそらく、それはいわゆる curl_easy 。 curl_easyの使用は簡単です。最初にcurl_easy_initを呼び出し、次にcurl_easy_setoptを数回呼び出し、次にcurl_easy_performを1回呼び出します。 そして、一般に、それですべてです。



curl_easyを使用したスト​​ーリーのコンテキストでは、悪いのは同期インターフェイスであることです。 つまり curl_easy_performへの各呼び出しは、要求が完了するまで、それを引き起こした作業スレッドをブロックします。 カテゴリー的に私たちに合わないのは、なぜなら 遅いサードパーティのサーバーが私たちに答えようとしている間、私たちはワークスレッドをブロックしたくありません。 libcurlから、HTTPリクエストとの非同期作業が必要です。



また、libcurlを使用すると、いわゆるHTTPリクエストを介してHTTPリクエストを非同期に処理できます。 curl_multi curl_multiを使用する場合、プログラマーはcurl_easy_initおよびcurl_easy_setoptを呼び出して各HTTP要求を準備します。 ただし、curl_easy_performは呼び出しません。 代わりに、ユーザーはcurl_multi_initを呼び出してcurl_multiのインスタンスを作成します。 次に、準備されたcurl_easyインスタンスをcurl_multi_add_handleおよび...



そして今後、curl_multiはプログラマーに選択肢を与えます。





その後、完了したリクエストを判断するためにcurl_multi_info_readを呼び出します。



両方のアプローチの使用を示します。 この記事では、curl_multi_performの操作について説明し、シリーズの最終記事ではcurl_multi_socket_actionの操作について説明します。



今日は何について話しますか?



前回の記事では、delay_serverと複数のbridge_serverで構成される小さなデモの概要を示し、delay_serverの実装を詳細に検討しました。 今日はbridge_server_1についてお話します。これはcurl_multi_performを介してdelay_serverへのリクエストを実行します。



bridge_server_1



bridge_server_1は何をしますか?



bridge_server_1は、/ data?年の形式のURLに対するHTTP GET要求を受け入れます?年= YYYY&月= MM&日= DD。 受信した各リクエストは、delay_serverへのHTTP GETリクエストに変換されます。 delay_serverから応答を受信すると、この応答はそれに応じて元のHTTP GET要求に対する応答に変換されます。



delay_serverを最初に実行した場合:



  delay_server -p 4040 


次にbridge_server_1を実行します。



  bridge_server_1 -p 8080 -P 4040 


そして、bridge_server_1へのリクエストを実行すると、次を取得できます。



  curl -4 -v "http:// localhost:8080 / data?年= 2018&月= 02&日= 25"
 * 127.0.0.1を試す...
 * TCP_NODELAYセット
 * localhost(127.0.0.1)ポート8080(#0)に接続
 > GET /データ?年= 2018&月= 02&日= 25 HTTP / 1.1
 >ホスト:localhost:8080
 >ユーザーエージェント:curl / 7.58.0
 >受け入れる:* / *
 >
 <HTTP / 1.1 200 OK
 <接続:キープアライブ
 <コンテンツの長さ:111
 <サーバー:RESTinio hello worldサーバー
 <日付:2018年2月24日(土)10:15:41 GMT
 <Content-Type:text / plain; 文字セット= utf-8
 <
リクエストが処理されました。
パス:/データ
クエリ:年= 2018&月= 02&日= 25
応答:
 ===
こんにちは世界!
一時停止:4376ms。

 ===
 *ホストlocalhostへの接続#0はそのまま 


bridge_server_1は、URLから年、月、日のパラメーターの値を取得し、それらを変更せずにdelay_serverに渡します。 したがって、パラメータの1つの値が正しく設定されていない場合、bridge_server_1はこの誤った値をdelay_serverに渡し、初期リクエストへの応答に結果が表示されます。



  curl -4 -v "http:// localhost:8080 / data?年= 2018&月= 2月&日= 25"
 * 127.0.0.1を試す...
 * TCP_NODELAYセット
 * localhost(127.0.0.1)ポート8080(#0)に接続
 > GET /データ?年= 2018&月= 2月&日= 25 HTTP / 1.1
 >ホスト:localhost:8080
 >ユーザーエージェント:curl / 7.58.0
 >受け入れる:* / *
 >
 <HTTP / 1.1 200 OK
 <接続:キープアライブ
 <コンテンツの長さ:81
 <サーバー:RESTinio hello worldサーバー
 <日付:2018年2月24日(土)10:19:55 GMT
 <Content-Type:text / plain; 文字セット= utf-8
 <
リクエストに失敗しました。
パス:/データ
クエリ:年= 2018&月= 2月&日= 25
応答コード:404
 *ホストlocalhostへの接続#0はそのまま 


bridge_server_1は、HTTP GET要求のみを受け入れ、URL /データに対してのみ受け入れます。 Bridge_server_1は、他のすべての要求を拒否します。



bridge_server_1はどのように機能しますか?



bridge_server_1は、2つのスレッドで作業が実行されるC ++アプリケーションです。 RESTinioはメインスレッドで実行されます(つまり、組み込みHTTPサーバーはメインスレッドで実行されます)。 メイン()関数から起動される2番目のスレッドでは、curl_multiを使用した操作が実行されます(このスレッドはcurlスレッドと呼ばれます)。 情報は、単純な自家製のスレッドセーフコンテナを介して、メインスレッドから動作中のカールスレッドに転送されます。



RESTinioが新しいHTTPリクエストを受信すると、このリクエストはRESTinioの開始時に指定されたコールバックに送信されます。 そこで、リクエストのURLがチェックされ、それが私たちにとって関心のあるリクエストである場合、受信したリクエストの説明を含むオブジェクトが作成されます。 作成されたオブジェクトはスレッドセーフコンテナにプッシュされ、そこからこのオブジェクトは動作中のcurlスレッドによって抽出されます。



動作中のcurlスレッドは、スレッドセーフコンテナから受信したリクエストの説明とともにオブジェクトを定期的に取得します。 受信したリクエストごとに、対応するcurl_easyインスタンスがこの作業スレッド上に作成されます。 このインスタンスは、curl_multiインスタンスに登録されます。



動作中のcurlスレッドは、curl_multi_performの定期的な呼び出しを通じて処理を実行します。

curl_multi_waitおよびcurl_multi_info_read。ただし、これについては以下で詳しく説明します。 curlスレッドが次の要求が処理されたことを検出すると(つまり、応答がdelay_serverから受信された場合)、元の着信HTTP要求に対する応答が生成されます。 つまり 着信HTTP要求はアプリケーションのメインスレッドで受信され、その後、受信した着信HTTP要求への応答が形成されるcurlスレッドに送信されます。



bridge_server_1コードの解析



bride_server_1のコードは次のように解析されます。





たとえば、REStinioやコマンドライン引数の解析に関連する多くのポイントについては、前の記事で説明したため、ここでは詳しく説明しません。



メイン関数()



bridge_server_1のメイン()関数コード全体を次に示します。



int main(int argc, char ** argv) { try { const auto cfg = parse_cmd_line_args(argc, argv); if(cfg.help_requested_) return 1; //        //  . request_info_queue_t queue; //    HTTP-. auto actual_handler = [&cfg, &queue](auto req) { return handler(cfg.config_, queue, std::move(req)); }; //    ,     //      curl_multi_perform. std::thread curl_thread{[&queue]{ curl_multi_work_thread(queue); }}; //         //    . auto curl_thread_stopper = cpp_util_3::at_scope_exit([&] { queue.close(); curl_thread.join(); }); //     HTTP-. //     ,   //    . if(cfg.config_.tracing_) { //  ,    ,   //     . struct traceable_server_traits_t : public restinio::default_single_thread_traits_t { //     . using logger_t = restinio::single_threaded_ostream_logger_t; }; //         . run_server<traceable_server_traits_t>( cfg.config_, std::move(actual_handler)); } else { //   ,     . run_server<restinio::default_single_thread_traits_t>( cfg.config_, std::move(actual_handler)); } // ,     . } catch( const std::exception & ex ) { std::cerr << "Error: " << ex.what() << std::endl; return 2; } return 0; }
      
      





main()の重要な部分-ただし、前の記事で説明したdelay_serverのmain()を繰り返します。 コマンドライン引数の同じ解析。 実際のHTTP要求ハンドラーへの呼び出しでラムダ関数を格納するための同じactual_handler変数。 HTTPサーバートレースを使用するかどうかに応じて、特定のタイプの特性を選択した同じrun_server呼び出し。



しかし、いくつかの違いがあります。



まず、メインスレッドからcurlスレッドに受信したリクエストに関する情報を送信するためのスレッドセーフコンテナが必要です。 タイプrequest_info_queue_tのキュー変数がこのコンテナとして使用されます。 コンテナの実装については、以下でさらに詳しく検討します。



次に、追加の作業スレッドを実行する必要があります。このスレッドでcurl_multiを使用します。 また、main()を終了するときにこの追加の作業スレッドを停止し、終了する必要があります。 これはすべて、次の行で発生します。



 //    ,     //      curl_multi_perform. std::thread curl_thread{[&queue]{ curl_multi_work_thread(queue); }}; //         //    . auto curl_thread_stopper = cpp_util_3::at_scope_exit([&] { queue.close(); curl_thread.join(); });
      
      





スレッドを開始するためのコードが質問を引き起こさないことを願っています。 作業スレッドを完了するには、2つのアクションを実行する必要があります。



1.作業スレッドに作業を完了するよう信号を送ります。 これは、操作queue.close()を介して行われます。

2.作業スレッドの完了を待ちます。 これはcurl_thread.join()によるものです。



ラムダ形式のこれらのアクションは両方とも、 ユーティリティライブラリからat_scope_exit()ヘルパー関数に渡されます 。 このat_scope_exit()は、BoostからBOOST_SCOPE_EXIT、Goから延期、Dからscope(exit)などのよく知られたものの単純な類似物です。at_scope_exit()のおかげで、curlスレッドは自動的に終了しますmain()を終了する理由は何ですか。



コマンドライン引数の構成と解析



興味のある方は、以下でbridge_server_1の構成がどのように見えるかを確認できます。 そして、コマンドライン引数を解析した結果、この構成がどのように形成されるか。 すべてがdelay_serverで行った方法と非常に似ているため、注意をそらさないように詳細はスポイラーの下に隠されています。



config_t構造体とparse_cmd_line_args()関数
 // ,   . struct config_t { // ,       . std::string address_{"localhost"}; // ,    . std::uint16_t port_{8080}; // ,      . std::string target_address_{"localhost"}; // ,      . std::uint16_t target_port_{8090}; //    ? 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; //     . using namespace clara; auto cli = Opt(result.config_.address_, "address")["-a"]["--address"] ("address to listen (default: localhost)") | Opt(result.config_.port_, "port")["-p"]["--port"] (fmt::format("port to listen (default: {})", result.config_.port_)) | Opt(result.config_.target_address_, "target address")["-T"]["--target-address"] (fmt::format("target address (default: {})", result.config_.target_address_)) | Opt(result.config_.target_port_, "target port")["-P"]["--target-port"] (fmt::format("target port (default: {})", result.config_.target_port_)) | 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; return result; }
      
      







RESTinioとcurlパーツ間の相互作用の詳細



受信した着信HTTP要求に関する情報は、bridge_server_1のRESTinio部分からこの構造のインスタンスを介してcurl部分に送信されます。



 // ,        curl_multi_perform //  ,      . struct request_info_t { // URL,     . const std::string url_; // ,         . restinio::request_handle_t original_req_; //     curl-. CURLcode curl_code_{CURLE_OK}; //    . //       . long response_code_{0}; //  ,      . std::string reply_data_; request_info_t(std::string url, restinio::request_handle_t req) : url_{std::move(url)}, original_req_{std::move(req)} {} };
      
      





最初は、url_とreq_の2つのフィールドのみが入力されます。 しかし、リクエストがcurlスレッドによって処理された後、残りのフィールドは埋められます。 これは主にcurl_code_フィールドです。 CURLE_OKが表示される場合、response_code_フィールドとreply_data_フィールドも値を受け取ります。



作業スレッド間でrequest_info_tインスタンスを転送するために、次の自家製のスレッドセーフコンテナが使用されます。



 //   thread-safe     //    . //           //   .       //    . template<typename T> class thread_safe_queue_t { using unique_ptr_t = std::unique_ptr<T>; std::mutex lock_; std::queue<unique_ptr_t> content_; bool closed_{false}; public: enum class status_t { extracted, empty_queue, closed }; void push(unique_ptr_t what) { std::lock_guard<std::mutex> l{lock_}; content_.emplace(std::move(what)); } //  pop  -,     //     ,    . //      mutex-,  , //         ,  pop() //    . template<typename Acceptor> status_t pop(Acceptor && acceptor) { std::lock_guard<std::mutex> l{lock_}; if(closed_) { return status_t::closed; } else if(content_.empty()) { return status_t::empty_queue; } else { while(!content_.empty()) { acceptor(std::move(content_.front())); content_.pop(); } return status_t::extracted; } } void close() { std::lock_guard<std::mutex> l{lock_}; closed_ = true; } }; //        . using request_info_queue_t = thread_safe_queue_t<request_info_t>;
      
      





基本的に、thread_safe_queue_tはテンプレートである必要はありませんでした。 しかし、たまたま最初にテンプレートクラスthread_safe_queue_tが作成され、後になってrequest_info_t型でのみ使用されることが判明しました。 しかし、テンプレートから通常のクラスへの実装のリメイクは開始しませんでした。



bridge_server_1のRESTinio部分



bridge_server_1コードには、RESTinioと対話する3つの関数しかありません。 まず、これはテンプレート関数run_server()であり、アプリケーションのメインスレッドのコンテキストでHTTPサーバーを起動します。



 //  ,       . template<typename Server_Traits, typename Handler> void run_server( const config_t & config, Handler && handler) { restinio::run( restinio::on_this_thread<Server_Traits>() .address(config.address_) .port(config.port_) .request_handler(std::forward<Handler>(handler))); }
      
      





bridge_server_1では、delay_serverよりもさらに簡単です。 そして、一般的に言えば、それなしでもできます。 main()でrestinio :: run()を直接呼び出すことができます-e。 ただし、別のrun_server()を用意することをお勧めします。必要に応じて、起動したHTTPサーバーの設定を変更し、1か所で変更する必要があります



第二に、これはハンドラー()関数であり、これはHTTP要求ハンドラーです。 delay_serverの対応物よりも少し複雑ですが、理解に困難をもたらす可能性はほとんどありません。



 //   . restinio::request_handling_status_t handler( const config_t & config, request_info_queue_t & queue, restinio::request_handle_t req) { if(restinio::http_method_get() == req->header().method() && "/data" == req->header().path()) { //    . const auto qp = restinio::parse_query(req->header().query()); //          //      curl_multi. auto url = fmt::format("http://{}:{}/{}/{}/{}", config.target_address_, config.target_port_, qp["year"], qp["month"], qp["day"]); auto info = std::make_unique<request_info_t>( std::move(url), std::move(req)); queue.push(std::move(info)); // ,         - //   . return restinio::request_accepted(); } //       . return restinio::request_rejected(); }
      
      





ここでは、最初に、受信したリクエストのタイプとそこからのURLを手動でチェックします。 これが/ dataのHTTP GETでない場合、リクエストの処理を拒否します。 bridge_server_1では、この確認を手動で行う必要がありますが、delay_serverでは、エクスプレスルーターを使用しているため、この必要はありませんでした。



さらに、これが予想されるリクエストである場合、クエリ文字列をそのコンポーネントに解析し、独自の発信リクエスト用のdelay_serverにURLを形成します。 その後、request_info_tオブジェクトを作成します。このオブジェクトには、生成されたURLと受信した受信リクエストへのスマートリンクを保存します。 そして、このrequest_info_tをcurlスレッドの処理に渡します(スレッドセーフコンテナに保存します)。



第三に、受信したHTTPリクエストに応答するcomplete_request_processing()関数です。



 //       . // curl_multi    .   http-response, //        http-request. void complete_request_processing(request_info_t & info) { auto response = info.original_req_->create_response(); response.append_header(restinio::http_field::server, "RESTinio hello world server"); response.append_header_date_field(); response.append_header(restinio::http_field::content_type, "text/plain; charset=utf-8"); if(CURLE_OK == info.curl_code_) { if(200 == info.response_code_) response.set_body( fmt::format("Request processed.\nPath: {}\nQuery: {}\n" "Response:\n===\n{}\n===\n", info.original_req_->header().path(), info.original_req_->header().query(), info.reply_data_)); else response.set_body( fmt::format("Request failed.\nPath: {}\nQuery: {}\n" "Response code: {}\n", info.original_req_->header().path(), info.original_req_->header().query(), info.response_code_)); } else response.set_body("Target service unavailable\n"); response.done(); }
      
      





ここでは、request_info_t :: original_req_フィールドに保存された元の着信要求を使用します。 restinio :: request_t :: create_response()メソッドは、HTTPレスポンスの生成に使用するオブジェクトを返します。 このオブジェクトを応答変数に保存します。 この変数の型が明示的に記述されていないという事実は偶然ではありません。 実際、create_response()はさまざまなタイプのオブジェクトを返すことができます(詳細はこちらをご覧ください )。 この場合、create_response()が返す最も単純な形式が何であるかは重要ではありません。



次に、delay_serverへのHTTPリクエストの終了方法に応じて、HTTPレスポンスを入力します。 そして、HTTP応答が完全に形成されたら、response.done()を呼び出してHTTPクライアントに応答を送信するようRESTinioに指示します。



complete_request_processing()関数に関して、非常に重要なことを強調する必要があります。それはcurlスレッドのコンテキストで呼び出されます。 ただし、response.done()を呼び出すと、生成された応答の配信は、HTTPサーバーが実行されているアプリケーションのメインスレッドに自動的に委任されます。



bridge_server_1のcurl部分



bridge_server_1のcurl部分には、curl_multiおよびcurl_easyで機能するいくつかの関数が含まれています。 この部分をメイン関数curl_multi_work_thread()で解析し始め、curl_multi_work_thread()から直接または間接的に呼び出される残りの関数を検討します。



しかし、最初に、デモでC ++ラッパーを使用せずに「裸の」libcurlを使用した理由について少し説明します。 理由は散文的です: 何を考え、振って、適切なラッパーを検索し、このラッパーが何をどのように行うかを調べることに時間を費やしたくありませんでした。 かつてlibcurlの使用経験があるにもかかわらず、ネイティブC-th APIのレベルでlibcurlとやり取りする方法を想像していました。 libcurl機能の最小限のセットのみが必要でした。 そして同時に、すべてを完全に管理したいと考えました。 そのため、libcurlではサードパーティのC ++アドオンを使用しないことにしました。



そして、カールコードを解析する前にもう1つの重要な免責事項を実行する必要があります。 デモアプリケーションのコードを可能な限り単純化および削減するために、エラー制御は一切行いませんでした。 curl関数の戻りコードを適切に制御すると、コードは3倍に膨れ上がり、理解が著しく失われますが、機能的には何も勝ちません。 したがって、デモンストレーションでは、libcurl呼び出しが常に成功することを期待しています。 これは、この特定の実験に対する私たちの意識的な決定ですが、実際の製品コードではそうしなかったでしょう。



さて、今、すべての必要な説明の後、curl_multi_performが非同期の発信HTTPリクエストの処理をどのように整理できるかを調べてみましょう。



Curl_multi_work_thread()関数



bridge_server_1の別のcurlスレッドで実行されるメイン関数のコードは次のとおりです。



 //   ,      // curl_multi_perform. void curl_multi_work_thread(request_info_queue_t & queue) { using namespace cpp_util_3; //   curl. curl_global_init(CURL_GLOBAL_ALL); auto curl_global_deinitializer = at_scope_exit([]{ curl_global_cleanup(); }); //   curl_multi,      //    . auto curlm = curl_multi_init(); auto curlm_destroyer = at_scope_exit([&]{ curl_multi_cleanup(curlm); }); //   . int still_running{ 0 }; while(true) { //     .     , //     . auto status = try_extract_new_requests(queue, curlm); if(request_info_queue_t::status_t::closed == status) //   . // ,      . return; //   -      , //   curl_multi_perform. if(0 != still_running || request_info_queue_t::status_t::extracted == status) { curl_multi_perform(curlm, &still_running); //  ,   - . check_curl_op_completion(curlm); } //    ,   curl_multi_wait, //    -. if(0 != still_running) { curl_multi_wait(curlm, nullptr, 0, 50 /*ms*/, nullptr); } else { //   ,   ,    // ,     . std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } }
      
      





2つの部分に分割できます。最初の部分では、必要なlibcurlが初期化され、curl_multiのインスタンスが作成されます。2番目の部分では、発信HTTP要求を処理するためのメインサイクルが実行されます。



最初の部分は非常に簡単です。 libcurlを初期化するには、curl_global_init()を呼び出してから、作業の最後にcurl_global_cleanup()を呼び出す必要があります。 at_scope_exitで既に説明したフォーカスを使用して行います。 同様の手法を使用して、curl_multiのインスタンスを作成/削除します。 このコードが簡単であることを願っています。



しかし、2番目の部分はより複雑です。 アイデアはこれです:





大まかに言って、カールスレッドはタックフリーで動作します。 各メジャーの開始時に、新しいクエリが取得され、アクティブなクエリの結果がチェックされます。 その後、カールスレッドは、IO操作の準備ができるまで、または50ミリ秒の一時停止の期限が切れる前にスリープ状態になります。 同時に、IO操作の準備完了の待機も50ミリ秒間隔によって制限されます。



スキームは非常に単純です。 しかし、いくつかの欠点があります。 状況によっては、これらの欠点は致命的かもしれませんが、まったく欠点ではないかもしれません。



1. curl_multi_info_read()関数は、curl_multi_perform()を呼び出すたびに呼び出されます。 原則として、curl_multi_performは現在処理中のリクエストの数を返します。 そして、この値の変更に基づいて、リクエスト数が減少する瞬間を、curl_multi_info_readを呼び出した後にのみ決定することができます。 ただし、実行中の要求の総数は同じままで、1つの要求が完了して新しい要求が1つ追加される状況に煩わされないように、作業の最も原始的なバージョンを使用します。



2.次のリクエストの処理のレイテンシが増加しています。 そのため、現在アクティブな要求がなく、新しい着信HTTP要求が到着した場合、curlスレッドは、this_thread :: sleep_for()への次の呼び出しを終了した後にのみ、それに関する情報を受け取ります。 curl_multi_work_thread()のクロックサイクルが50ミリ秒の場合、これはリクエスト処理のレイテンシ(最悪の場合)に+ 50ミリ秒を意味します。 bridge_server_1では、これは気にしません。 しかし、bridge_server_1_pipeの実装では、curlスレッドの通知に追加のパイプを使用して、この欠点を取り除くことを試みました。 最初はbridge_server_1_pipeを詳細に解析するつもりはありませんでしたが、誰かがそのような分析を確認したい場合は、コメントを登録解除してください。 このような要望がある場合、分析を含む追加記事を作成します。



したがって、一般的な用語では、例のbridge_server_1のcurlスレッドは機能します。 まだ質問がある場合は、コメントで質問してください。回答を試みます。 それまでの間、bridge_server_1のcurl部分に関連する残りの関数の分析に移りましょう。



新しい着信HTTPリクエストを受信するための関数



curl_multi_work_thread()内のメインループの各反復の開始時に、すべての新しい着信HTTPリクエストをスレッドセーフコンテナから取得し、curl_easyインスタンスに変換し、これらの新しいcurl_easyインスタンスをcurl_multiインスタンスに追加しようとします。 これはすべて、いくつかのヘルパー関数を使用して行われます。



1つ目はtry_extract_new_requests()関数です。



 //    ,    . //   status_t::closed,     // . auto try_extract_new_requests(request_info_queue_t & queue, CURLM * curlm) { return queue.pop([curlm](auto info) { introduce_new_request_to_curl_multi(curlm, std::move(info)); }); }
      
      





実際、彼女の仕事は、スレッドセーフコンテナのpop()メソッドを呼び出し、必要なラムダ関数をpop()に渡すことです。 概して、これらはすべてcurl_multi_work_thread()内に記述できますが、最初はtry_extract_new_requests()がより大量でした。 そして、その存在はcurl_multi_work_thread()コードを単純化します。



第二に、これは関数describe_new_request_to_curl_multi()で​​あり、実際にはすべての主要な作業が行われます。 すなわち:



 //  curl_easy    ,    //        curl_easy  curl_multi. void introduce_new_request_to_curl_multi( CURLM * curlm, std::unique_ptr<request_info_t> info) { //    curl_easy    . CURL * h = curl_easy_init(); curl_easy_setopt(h, CURLOPT_URL, info->url_.c_str()); curl_easy_setopt(h, CURLOPT_PRIVATE, info.get()); curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(h, CURLOPT_WRITEDATA, info.get()); //  curl_easy ,     curl_multi. curl_multi_add_handle(curlm, h); // unique_ptr       . //        . info.release(); }
      
      





curl_easyを使用した場合、ここでは新しいものは表示されません。 curl_multi_add_handle()の呼び出しを除きます。 これは、curl_multiインスタンスへの個別のHTTP要求の実行に対する制御の転送が実行される方法とまったく同じです。 以前にcurl_easyを使用したことがない場合は、curl_easy_setopt()が何を要求し、どのような効果をもたらすかを理解するために、公式ドキュメントに精通する必要があります。



Introduction_new_request_to_curl_multi()のキーポイントは、request_info_tインスタンスのライフタイムの制御に関連しています。実際、request_info_tはunique_ptrを使用してワーカースレッド間で渡されます。また、introduce_new_request_to_curl_multi()で​​は、unique_ptrと同じようになります。したがって、特別なアクションを実行しないと、introduce_new_request_to_curl_multi()を終了するとrequest_info_tインスタンスが破棄されます。ただし、libcurlがこのリクエストの処理を完了するまでrequest_info_tを保存する必要があります。



したがって、request_info_tへのポインターをcurl_easyインスタンス内のプライベートデータとして保存します。そして、unique_ptrがrelease()を呼び出して、unique_ptrがオブジェクトのライフタイムの制御を停止します。リクエストの処理が完了すると、curl_easyインスタンスからプライベートデータを手動で抽出し、自分でrequest_info_tオブジェクトを破棄します(これは、後述のcheck_curl_op_completion()関数内で確認できます)。



ちなみに、これはデモアプリケーションで気を取られなかった別の点に関連していますが、誰が本番コードを書くのに時間をかけなければならないでしょうか? -インスタンスは削除されません。つまりcurl_multi_work_thread()のメインループを終了するとき、curl_easyの残りのライブインスタンスを通過せず、自分でrequest_info_tをクリーンアップしません。良いことには、これを行う必要があります。



そして、3番目に、write_callback関数は、curl_easyインスタンスに格納するポインターであるHTTPリクエストを準備します。



 //     curl     //   .       // CURLOPT_WRITEFUNCTION. std::size_t write_callback( char *ptr, size_t size, size_t nmemb, void *userdata) { auto info = reinterpret_cast<request_info_t *>(userdata); const auto total_size = size * nmemb; info->reply_data_.append(ptr, total_size); return total_size; }
      
      





この関数は、発信リクエストへの応答としてリモートサーバーがデータを送信するときにlibcurlによって呼び出されます。このデータは、request_info_t :: reply_data_フィールドに蓄積します。また、request_info_tインスタンスへのポインターがcurl_easyインスタンス内のプライベートデータとして格納されるという事実も使用します。



関数check_curl_op_completion()



最後に、bridge_server_1のcurl部分の主要な機能の1つを検討します。これは、完了したHTTP要求を見つけて処理を完了する役割を果たします。



一番下の行は、curl_multiインスタンス内に、curl_multiの操作中にlibcurlによって生成された特定のメッセージのキューがあることです。 curl_multiがcurl_multi_perform内の次のリクエストの処理を終了すると、特別なステータスCURLMSG_DONEのメッセージがこのメッセージキューに置かれます。このメッセージには、処理された要求に関する情報が含まれています。私たちのタスクは、このキューを調べて、そこにあるすべてのCURLMSG_DONEメッセージを処理することです。



次のようになります。



 //    ,      //  curl_multi. void check_curl_op_completion(CURLM * curlm) { CURLMsg * msg; int messages_left{0}; //       curl_multi   //   CURLMSG_DONE. while(nullptr != (msg = curl_multi_info_read(curlm, &messages_left))) { if(CURLMSG_DONE == msg->msg) { //  ,   . //     unique_ptr,     // curl_easy_cleanup. std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> easy_handle{ msg->easy_handle, &curl_easy_cleanup}; //    curl_multi    . curl_multi_remove_handle(curlm, easy_handle.get()); //    ,     //  . request_info_t * info_raw_ptr{nullptr}; curl_easy_getinfo(easy_handle.get(), CURLINFO_PRIVATE, &info_raw_ptr); //    unique_ptr,   . std::unique_ptr<request_info_t> info{info_raw_ptr}; info->curl_code_ = msg->data.result; if(CURLE_OK == info->curl_code_) { //   ,     . curl_easy_getinfo( easy_handle.get(), CURLINFO_RESPONSE_CODE, &info->response_code_); } //     . complete_request_processing(*info); } } }
      
      





キューに何かがある限りcurl_multi_info_read()をループでプルします。CURLMSG_DONEタイプのメッセージを抽出する場合、メッセージからcurl_easyのインスタンスを取得し、次のことを行います。





したがって、この時点ですでに完了しているすべてのリクエストに対して。



第二部の結論



ストーリーのこの部分では、単一のスレッドで着信HTTP要求を受信し、その処理をcurl_multi_performを使用して発信HTTP要求が実行される2番目の作業スレッドに転送する方法を調べました。記事の本文の主要なポイントを強調するようにしました。しかし、もし何かが理解できないままであるならば、質問をしてください、そして、我々はコメントでそれらに答えようとします。



また、通知パイプを使用するbridge_server_1_pipeの実装の分析に興味がある場合は、お知らせください。次に、このテーマに関する記事を作成します。



さて、よりトリッキーなcurl_multi_socket_actionメカニズムが使用されるbridge_server_2を検討することは残っています。そこではすべてがもっと楽しくなります。少なくともそのように私たちはこの非常にcurl_multi_socket_actionを扱っている間に見えた:)



続行する ...



All Articles