RESTinio ist ein asynchroner HTTP-Server. Asynchron

Vor einigen Jahren haben wir RESTinio veröffentlicht , unser kleines OpenSource C ++ - Framework zum Einbetten eines HTTP-Servers in C ++ - Anwendungen. RESTinio wurde in dieser Zeit nicht besonders beliebt, ging aber nicht verloren . Jemand wählt es für die "native" Unterstützung für Windows, jemand für einige einzelne Funktionen (wie Sendfile-Unterstützung), jemand für das Verhältnis von Funktionen, Benutzerfreundlichkeit und Anpassung. Aber ich denke, anfangs sind viele RESTinio von diesem lakonischen "Hallo Welt" angezogen:


#include <restinio/all.hpp> int main() { restinio::run( restinio::on_this_thread() .port(8080) .address("localhost") .request_handler([](auto req) { return req->create_response().set_body("Hello, World!").done(); })); return 0; } 

Dies ist wirklich alles, was benötigt wird, um den HTTP-Server in einer C ++ - Anwendung auszuführen.


Und obwohl wir immer versuchen zu sagen, dass die Hauptfunktion, für die wir RESTinio im Allgemeinen übernommen haben, die asynchrone Verarbeitung eingehender Anforderungen war, stoßen wir gelegentlich auf Fragen, was zu tun ist, wenn Sie in request_handler längere Vorgänge ausführen müssen.


Und da eine solche Frage relevant ist, können Sie noch einmal darüber sprechen und ein paar kleine Beispiele nennen.


Ein kleiner Hinweis auf die Ursprünge


Wir haben uns entschlossen, unseren einbettbaren HTTP-Server nach mehreren aufeinanderfolgenden Aufgaben sehr ähnlich zu gestalten: Es war erforderlich, eine HTTP-Eingabe für eine vorhandene C ++ - Anwendung zu organisieren, oder es musste ein Mikrodienst geschrieben werden, in dem das bereits vorhandene "schwere" C ++ wiederverwendet werden musste ny Code. Ein gemeinsames Merkmal dieser Aufgaben war, dass sich die Anwendungsverarbeitung der Anforderung über mehrere zehn Sekunden erstrecken konnte.


Grob gesagt sortierte der HTTP-Server eine Millisekunde lang eine neue HTTP-Anforderung aus, aber um eine HTTP-Antwort auszugeben, musste er sich an einige andere Dienste wenden oder einige langwierige Berechnungen durchführen. Wenn Sie HTTP-Anforderungen im synchronen Modus verarbeiten, benötigt der HTTP-Server einen Pool von Tausenden von Arbeitsthreads, was selbst unter modernen Bedingungen kaum als gute Idee angesehen werden kann.


Es ist viel praktischer, wenn der HTTP-Server nur mit einem Arbeitsthread arbeiten kann, für den E / A ausgeführt wird und Anforderungshandler aufgerufen werden. Der Anforderungshandler delegiert einfach die eigentliche Verarbeitung eines anderen Arbeitsthreads und gibt die Steuerung an den HTTP-Server zurück. Wenn viel später irgendwo in einem anderen Arbeitsthread Informationen bereit sind, auf die Anforderung zu antworten, wird einfach eine HTTP-Antwort generiert, die den HTTP-Server automatisch aufnimmt und diese Antwort an den entsprechenden Client sendet.


Da wir nie eine vorgefertigte Version gefunden haben, die einfach und bequem zu verwenden ist, war sie plattformübergreifend und unterstützte Windows als „native“ Plattform, bot mehr oder weniger anständige Leistung und wurde vor allem speziell für asynchrone Anwendungen geschärft Arbeit, dann haben wir Anfang 2017 begonnen, RESTinio zu entwickeln.


Wir wollten einen asynchronen eingebetteten HTTP-Server erstellen, der einfach zu bedienen ist, den Benutzer von einigen Routineproblemen befreit, mehr oder weniger produktiv ist, plattformübergreifend und eine flexible Konfiguration für verschiedene Bedingungen ermöglicht. Es scheint zu funktionieren, aber überlassen wir es den Benutzern, zu beurteilen ...


Es gibt also eine eingehende Anfrage, die viel Verarbeitungszeit erfordert. Was zu tun ist?


Arbeitsthreads RESTinio / Asio


Manchmal denken RESTinio-Benutzer nicht darüber nach, welche Arbeitsthreads und wie genau RESTinio verwendet wird. Beispielsweise könnte jemand in Betracht ziehen, dass RESTinio beim Starten von RESTinio in einem Arbeitsthread (unter Verwendung von run(on_this_thread(...)) wie im obigen Beispiel) in diesem Arbeitsthread nur Anforderungshandler aufruft. Während für E / A RESTinio einen separaten Faden unter der Haube erzeugt. Und dieser separate Thread bedient weiterhin neue Verbindungen, wenn der Hauptarbeitsthread von request_handler belegt ist.


Tatsächlich werden alle Threads, die der Benutzer RESTinio zuweist, sowohl zum Ausführen von E / A-Operationen als auch zum Aufrufen von request_handlers verwendet. Wenn Sie den RESTinio-Server über run(on_this_thread(...)) und dann über run() im aktuellen Thread gestartet haben, werden sowohl E / A- als auch Anforderungshandler ausgeführt.


Grob gesagt startet RESTinio eine Asio-Ereignisschleife, in der neue Verbindungen verarbeitet, Daten von vorhandenen Verbindungen gelesen und analysiert, sendfertige Daten geschrieben, Verbindungen geschlossen usw. werden. Nachdem die eingehende Anforderung gelesen und von der nächsten Verbindung vollständig analysiert wurde, wird unter anderem der vom Benutzer angegebene request_handler aufgerufen, um diese Anforderung zu verarbeiten.


Wenn request_handler den Betrieb des aktuellen Threads blockiert, wird dementsprechend auch die Asio-Action-Ereignisschleife blockiert, die an demselben Thread arbeitet. Alles ist einfach.


Wenn RESTinio für einen Pool von Arbeitsthreads gestartet wird (d. H. Durch run(on_thread_pool(...)) wie in diesem Beispiel ), geschieht fast dasselbe: Für jeden Thread aus dem Pool wird eine Asio-Ereignisereignisschleife gestartet. Wenn ein request_handler beginnt, große Matrizen zu multiplizieren, blockiert dies den Arbeitsthread im Pool und E / A-Operationen werden nicht mehr für diesen Thread bereitgestellt.


Daher besteht die Aufgabe des Entwicklers bei der Verwendung von RESTinio darin, seine request_handlers in angemessener und vorzugsweise nicht sehr langer Zeit abzuschließen.


Benötigen Sie einen Workflow-Pool für RESTinio / Asio?


Wenn der vom Benutzer angegebene request_handler den Arbeitsthread, für den er aufgerufen wird, für längere Zeit blockiert, verliert dieser Thread die Fähigkeit, E / A-Operationen zu verarbeiten. Was aber, wenn request_handler viel Zeit benötigt, um eine Antwort zu erstellen? Angenommen, er führt eine schwere Rechenoperation aus, deren Zeit im Prinzip nicht auf einige Millisekunden verkürzt werden kann?


Einer der Benutzer könnte denken, da RESTinio an einem Pool von Arbeitsthreads arbeiten kann, geben Sie einfach die größere Poolgröße an und fertig.


Leider funktioniert dies nur in einfachen Fällen, wenn Sie nur wenige parallele Verbindungen haben. Und die Abfrageintensität ist gering. Wenn die Anzahl der parallelen Abfragen Tausende (mindestens nur einige Hundert) beträgt, kann es leicht zu einer Situation kommen, in der alle Arbeitsthreads des Pools damit beschäftigt sind, bereits akzeptierte Anforderungen zu verarbeiten. Und es werden keine Threads mehr zum Ausführen von E / A-Operationen übrig sein. Infolgedessen verliert der Server seine Reaktionsfähigkeit. Durch das Einbeziehen von RESTinio wird die Fähigkeit verloren, Zeitüberschreitungen zu verarbeiten, die RESTinio automatisch zählt, wenn neue Verbindungen empfangen und Anforderungen verarbeitet werden.


Wenn Sie längere Blockierungsvorgänge ausführen müssen, um eingehende Anforderungen zu bearbeiten, ist es daher besser, nur einen Arbeitsthread für RESTinio zuzuweisen, aber einen großen Pool von Arbeitsabläufen zuzuweisen, um dieselben Vorgänge auszuführen. Der Anforderungshandler stellt die nächste Anforderung nur in eine Warteschlange, von der aus die Anforderung abgerufen und zur Verarbeitung gesendet wird.


Wir haben uns ein Beispiel für dieses Schema im Detail angesehen, als wir in diesem Artikel über unser Shrimp-Demo-Projekt sprachen: " Shrimp: Skalieren und Freigeben von HTTP-Bildern in modernem C ++ mit ImageMagic ++, SObjectizer und RESTinio ."


Beispiele für das Delegieren der Anforderungsverarbeitung an einzelne Arbeitsthreads


Oben habe ich versucht zu erklären, warum es nicht notwendig ist, eine lange Verarbeitung direkt im request_handler durchzuführen. Woher kommt das offensichtliche Ergebnis: Eine lange Anforderungsverarbeitung muss an einen anderen Arbeitsthread delegiert werden. Schauen wir uns an, wie das aussehen könnte.


In den beiden folgenden Beispielen benötigen wir einen einzelnen Arbeitsthread, um RESTinio auszuführen, und einen anderen Arbeitsthread, um eine lange Anforderungsverarbeitung zu simulieren. Außerdem benötigen wir eine Art Nachrichtenwarteschlange, um Anforderungen vom RESTinio-Thread an einen separaten Arbeitsthread zu übertragen.


Für diese beiden Beispiele konnte ich keine neue Implementierung der thread-sicheren Nachrichtenwarteschlange auf dem Knie durchführen. Daher habe ich meinen nativen SObjectizer und seine Ketten verwendet, bei denen es sich um CSP-Kanäle handelt. Weitere Informationen zu mchain finden Sie hier: " Informationsaustausch zwischen Arbeitsthreads ohne Schmerzen? CSP-Kanäle, die uns helfen ."


Anforderungshandle-Objekt speichern


Die grundlegende Technik, auf der die Delegierung der Anforderungsverarbeitung basiert, ist die Übertragung des Objekts request_handle_t irgendwo.


Wenn RESTinio den vom Benutzer angegebenen request_handler aufruft, um eine eingehende Anforderung zu verarbeiten, wird ein Objekt vom Typ request_handle_t an diesen request_handle_t . Dieser Typ ist nichts anderes als ein intelligenter Zeiger auf die Parameter der empfangenen Anforderung. Wenn es für jemanden request_handle_t ist zu glauben, dass request_handle_t shared_ptr , können Sie dies sicher glauben. Dieser shared_ptr ist.


Und da request_handle_t shared_ptr , können wir diesen intelligenten Zeiger sicher irgendwo übergeben. Was wir in den folgenden Beispielen tun werden.


Wir brauchen also einen separaten Arbeitsthread und Kanal, um mit ihm zu kommunizieren. Lassen Sie uns alles erstellen:


 int main() { //  SObjectizer. so_5::wrapped_env_t sobj; //  std::thread    . std::thread processing_thread; //    main      join. //    RAII. auto processing_thread_joiner = so_5::auto_join(processing_thread); //      . auto req_ch = so_5::create_mchain(sobj); //       main. //    RAII. auto ch_closer = so_5::auto_close_drop_content(req_ch); //     . //      main()  - , //     ,      join(). processing_thread = std::thread{ processing_thread_func, req_ch }; 

Der Hauptteil des Arbeitsthreads selbst befindet sich in der Funktion processing_thread_func() , auf die wir später noch eingehen werden.


Jetzt haben wir bereits einen separaten Arbeitsthread und einen Kanal für die Kommunikation damit. Sie können den RESTinio-Server starten:


  // ,     . struct traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t; }; restinio::run( restinio::on_this_thread<traits_t>() .port(8080) .address("localhost") .request_handler([req_ch](auto req) { //   GET-   . if(restinio::http_method_t::http_get == req->header().method() && "/" == req->header().path()) { //    . so_5::send<handle_request>(req_ch, req); return restinio::request_accepted(); } else return restinio::request_rejected(); }) .cleanup_func([&] { //      . //    , ..  req_ch //          //     . so_5::close_drop_content(req_ch); })); 

Die Logik für diesen Server ist sehr einfach. Wenn eine GET-Anfrage für '/' eingetroffen ist, delegieren wir die Anforderungsverarbeitung eines einzelnen Threads. Dazu führen wir zwei wichtige Operationen durch:


  • Senden Sie das Objekt request_handle_t an den CSP-Kanal. Während dieses Objekt im CSP-Kanal oder anderswo gespeichert ist, weiß RESTinio, dass die Anforderung noch aktiv ist.
  • Wir geben den Wert restinio::request_accepted() vom Request-Handler zurück. Dadurch wird RESTinio klar, dass die Anforderung zur Verarbeitung angenommen wurde und die Verbindung zum Client nicht geschlossen werden kann.

Die Tatsache, dass request_handler nicht sofort eine RESTinio-Antwort generiert hat, stört nicht. Sobald restinio::request_accepted() zurückgegeben wurde, übernahm der Benutzer die Verantwortung für die Verarbeitung der Anfrage und eines Tages wird die Antwort auf die Anfrage generiert.


Wenn der Anforderungshandler restinio::request_rejected() , versteht RESTinio, dass die Anforderung nicht verarbeitet wird, und gibt einen 501-Fehler an den Client zurück.


Also request_handle_t wir das vorläufige Ergebnis: Die request_handle_t Instanz kann irgendwo übergeben werden, da es sich tatsächlich um std::shared_ptr . Während diese Instanz aktiv ist, geht RESTinio davon aus, dass die Anforderung verarbeitet wird. Wenn der Anforderungshandler restinio::request_accepted() , macht sich RESTinio keine Sorgen darüber, dass die Antwort auf die Anforderung gerade nicht generiert wurde.


Jetzt können wir uns die Implementierung dieses sehr separaten Threads ansehen:


 void processing_thread_func(so_5::mchain_t req_ch) { //       //    . std::random_device rd; std::mt19937 generator{rd()}; std::uniform_int_distribution<> pause_generator{350, 3500}; //      timeout_elapsed. auto delayed_ch = so_5::create_mchain(req_ch->environment()); //     -  . bool stop = false; select( so_5::from_all() //      . .on_close([&stop](const auto &) { stop = true; }) //     select(). //  select()     . .stop_on([&stop]{ return stop; }), //   handle_request     RESTinio. case_(req_ch, [&](handle_request cmd) { //     . const std::chrono::milliseconds pause{pause_generator(generator)}; //     . so_5::send_delayed<timeout_elapsed>(delayed_ch, //    timeout_elapsed. pause, //      timeout_elapsed. cmd.m_req, pause); }), //   timeout_elapsed. case_(delayed_ch, [](timeout_elapsed cmd) { //     . cmd.m_req->create_response() .set_body("Hello, World! (pause:" + std::to_string(cmd.m_pause.count()) + "ms)") .done(); }) ); } 

Die Logik hier ist sehr einfach: Wir erhalten die erste Anforderung in Form einer handle_request Nachricht und leiten sie in Form einer timeout_elapsed Nachricht, die um eine zufällige Zeit verzögert ist, an uns weiter. Die eigentliche Bearbeitung der Anfrage erfolgt erst nach Erhalt von timeout_elapsed .


Upd. Wenn die Methode done() in einem separaten Arbeitsthread aufgerufen wird, wird RESTinio benachrichtigt, dass eine vorgefertigte Antwort angezeigt wurde, die in die TCP-Verbindung geschrieben werden muss. RESTinio initiiert die Schreiboperation, aber die E / A-Operation selbst wird nicht ausgeführt, wenn done() aufgerufen wird, sondern wenn RESTinio die E / A ausführt und request_handlers aufruft. Das heißt, In diesem Beispiel wird done() für einen separaten Arbeitsthread aufgerufen, und die Schreiboperation wird für den Hauptthread ausgeführt, in dem restinio::run() funktioniert.


Die Nachrichten selbst lauten wie folgt:


 struct handle_request { restinio::request_handle_t m_req; }; struct timeout_elapsed { restinio::request_handle_t m_req; std::chrono::milliseconds m_pause; }; 

Das heißt, Ein separater Arbeitsthread nimmt request_handle_t und speichert es, bis sich die Gelegenheit ergibt, eine vollständige Antwort zu bilden. Wenn sich diese Gelegenheit ergibt, wird create_response() für das gespeicherte Anforderungsobjekt aufgerufen und die Antwort an RESTinio zurückgegeben. Dann schreibt RESTinio bereits in seinem Arbeitskontext die Antwort in Verbindung mit dem entsprechenden Client.


Hier wird die request_handle_t Instanz in einer verzögerten timeout_elapsed Nachricht gespeichert, da in diesem primitiven Beispiel keine echte Verarbeitung erfolgt. In einer realen Anwendung kann request_handle_t in einer Warteschlange oder in einem Objekt gespeichert werden, das zur Verarbeitung der Anforderung erstellt wurde.


Der vollständige Code für dieses Beispiel befindet sich unter den regulären RESTinio-Beispielen .


Einige kleine Code-Notizen


Diese Konstruktion legt die RESTinio-Eigenschaften fest, die ein RESTinio-Server haben sollte:


  // ,     . struct traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t; }; restinio::run( restinio::on_this_thread<traits_t>() 

In diesem Beispiel benötige ich RESTinio, um die Anforderungsverarbeitungsaktionen zu protokollieren. Daher habe ich logger_t dass es sich vom Standardwert null_logger_t . Aber seitdem RESTinio funktioniert tatsächlich mit mehreren Threads (RESTinio verarbeitet eingehende Anforderungen im Hauptthread, die Antworten stammen jedoch von einem separaten Arbeitsthread). Dann benötigen Sie einen threadsicheren Logger, shared_ostream_logger_t .


In process_thread_func processing_thread_func() wird die SObjectizer-Funktion select() , die dem Go select-Konstrukt etwas ähnelt: Sie können Nachrichten von mehreren Kanälen gleichzeitig lesen und verarbeiten. Die Funktion select() funktioniert so lange, bis alle an sie übergebenen Kanäle geschlossen sind. Oder bis ihr gewaltsam gesagt wird, dass es Zeit ist zu enden.


Wenn der Kanal für die Kommunikation mit dem RESTinio-Server geschlossen ist, macht es keinen Sinn, die Arbeit fortzusetzen. Daher wird in select() die Reaktion auf das Schließen eines der Kanäle bestimmt: Sobald ein Kanal geschlossen wird, wird das Stoppflag gehisst. Dies führt zum Abschluss von select() und zum Beenden von processing_thread_func() .


Speichern des response_builder-Objekts


Im vorherigen Beispiel haben wir einen einfachen Fall betrachtet, in dem es möglich ist, request_handle_t zu speichern, bis wir sofort die gesamte Antwort auf die Anforderung geben können.


Es kann jedoch komplexere Szenarien geben, in denen Sie beispielsweise teilweise eine Antwort geben müssen. Das heißt, wir erhalten eine Anfrage, wir können sofort nur den ersten Teil der Antwort bilden. Wir bilden es. Nach einiger Zeit haben wir dann die Möglichkeit, den zweiten Teil der Antwort zu bilden. Dann, nach einiger Zeit, können wir den nächsten Teil usw. bilden.


Darüber hinaus kann es für uns wünschenswert sein, dass alle diese Teile verschwinden, wenn wir sie bilden. Das heißt, Zuerst der erste Teil der Antwort, damit der Client sie subtrahieren kann, dann der zweite, dann der dritte usw.


Mit RESTinio können Sie dies aufgrund verschiedener Arten von responce_builders tun . Insbesondere Typen wie user_controlled_output und chunked_output .


In diesem Fall reicht es nicht aus, request_handle_t zu speichern, da request_handle_t nur bis zum ersten Aufruf von create_reponse() nützlich ist. Als nächstes müssen wir mit response_builder arbeiten. Na und...


Nun, das ist okay. Response_builder ist ein beweglicher Typ, der unique_ptr ähnelt. So können wir es auch so lange behalten, wie wir es brauchen. Und um zu zeigen, wie es aussieht, wiederholen wir das obige Beispiel leicht. Lassen Sie die Funktion processing_thread_func() die Antwort in Teilen bilden.


Das ist überhaupt nicht schwierig.


Zuerst müssen wir uns für die Typen entscheiden, die new processing_thread_func() benötigt:


 struct handle_request { restinio::request_handle_t m_req; }; //     . using output_t = restinio::chunked_output_t; //   reponse_builder-   . using response_t = restinio::response_builder_t<output_t>; //     . struct timeout_elapsed { response_t m_resp; int m_counter; }; 

Die Nachricht handle_request bleibt unverändert. In der Nachricht timeout_elapsed speichern wir jetzt nicht request_handle_t , sondern response_builder des Typs, den wir benötigen. Plus einen Zähler der restlichen Teile. Sobald dieser Zähler zurückgesetzt wird, endet der Anforderungsdienst.


Jetzt können wir uns eine neue Version der Funktion processing_thread_func() ansehen:


 void processing_thread_func(so_5::mchain_t req_ch) { std::random_device rd; std::mt19937 generator{rd()}; std::uniform_int_distribution<> pause_generator{350, 3500}; auto delayed_ch = so_5::create_mchain(req_ch->environment()); bool stop = false; select( so_5::from_all() .on_close([&stop](const auto &) { stop = true; }) .stop_on([&stop]{ return stop; }), case_(req_ch, [&](handle_request cmd) { //    ,    . auto resp = cmd.m_req->create_response<output_t>(); resp.append_header( restinio::http_field::server, "RESTinio" ) .append_header_date_field() .append_header( restinio::http_field::content_type, "text/plain; charset=utf-8" ); //    ,  RESTinio //   . resp.flush(); //       . so_5::send_delayed<so_5::mutable_msg<timeout_elapsed>>(delayed_ch, //     . std::chrono::milliseconds{pause_generator(generator)}, //    timeout_elapsed. //     response_builder-  . std::move(resp), 3); }), case_(delayed_ch, [&](so_5::mutable_mhood_t<timeout_elapsed> cmd) { //      . cmd->m_resp.append_chunk( "this is the next part of the response\n" ); //  RESTinio   . cmd->m_resp.flush(); cmd->m_counter -= 1; if( 0 != cmd->m_counter ) { //        . so_5::send_delayed( delayed_ch, std::chrono::milliseconds{pause_generator(generator)}, std::move(cmd)); } else // ,   . cmd->m_resp.done(); }) ); } 

Das heißt, , . . .


Upd. flush() , done() : RESTinio , I/O- , flush() , , RESTinio - request_handler-. Das heißt, flush() , , , restinio::run() .


, RESTinio :


 [2019-05-13 15:02:35.106] TRACE: starting server on 127.0.0.1:8080 [2019-05-13 15:02:35.106] INFO: init accept #0 [2019-05-13 15:02:35.106] INFO: server started on 127.0.0.1:8080 [2019-05-13 15:02:39.050] TRACE: accept connection from 127.0.0.1:49280 on socket #0 [2019-05-13 15:02:39.050] TRACE: [connection:1] start connection with 127.0.0.1:49280 [2019-05-13 15:02:39.050] TRACE: [connection:1] start waiting for request [2019-05-13 15:02:39.050] TRACE: [connection:1] continue reading request [2019-05-13 15:02:39.050] TRACE: [connection:1] received 78 bytes [2019-05-13 15:02:39.050] TRACE: [connection:1] request received (#0): GET / [2019-05-13 15:02:39.050] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 1 [2019-05-13 15:02:39.050] TRACE: [connection:1] start next write group for response (#0), size: 1 [2019-05-13 15:02:39.050] TRACE: [connection:1] start response (#0): HTTP/1.1 200 OK [2019-05-13 15:02:39.050] TRACE: [connection:1] sending resp data, buf count: 1, total size: 167 [2019-05-13 15:02:39.050] TRACE: [connection:1] outgoing data was sent: 167 bytes [2019-05-13 15:02:39.050] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:39.050] TRACE: [connection:1] should keep alive [2019-05-13 15:02:40.190] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 3 [2019-05-13 15:02:40.190] TRACE: [connection:1] start next write group for response (#0), size: 3 [2019-05-13 15:02:40.190] TRACE: [connection:1] sending resp data, buf count: 3, total size: 42 [2019-05-13 15:02:40.190] TRACE: [connection:1] outgoing data was sent: 42 bytes [2019-05-13 15:02:40.190] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:40.190] TRACE: [connection:1] should keep alive [2019-05-13 15:02:43.542] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 3 [2019-05-13 15:02:43.542] TRACE: [connection:1] start next write group for response (#0), size: 3 [2019-05-13 15:02:43.542] TRACE: [connection:1] sending resp data, buf count: 3, total size: 42 [2019-05-13 15:02:43.542] TRACE: [connection:1] outgoing data was sent: 42 bytes [2019-05-13 15:02:43.542] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:43.542] TRACE: [connection:1] should keep alive [2019-05-13 15:02:46.297] TRACE: [connection:1] append response (#0), flags: { not_final_parts, connection_keepalive }, write group size: 3 [2019-05-13 15:02:46.297] TRACE: [connection:1] start next write group for response (#0), size: 3 [2019-05-13 15:02:46.297] TRACE: [connection:1] sending resp data, buf count: 3, total size: 42 [2019-05-13 15:02:46.297] TRACE: [connection:1] append response (#0), flags: { final_parts, connection_keepalive }, write group size: 1 [2019-05-13 15:02:46.297] TRACE: [connection:1] outgoing data was sent: 42 bytes [2019-05-13 15:02:46.298] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:46.298] TRACE: [connection:1] should keep alive [2019-05-13 15:02:46.298] TRACE: [connection:1] start next write group for response (#0), size: 1 [2019-05-13 15:02:46.298] TRACE: [connection:1] sending resp data, buf count: 1, total size: 5 [2019-05-13 15:02:46.298] TRACE: [connection:1] outgoing data was sent: 5 bytes [2019-05-13 15:02:46.298] TRACE: [connection:1] finishing current write group [2019-05-13 15:02:46.298] TRACE: [connection:1] should keep alive [2019-05-13 15:02:46.298] TRACE: [connection:1] start waiting for request [2019-05-13 15:02:46.298] TRACE: [connection:1] continue reading request [2019-05-13 15:02:46.298] TRACE: [connection:1] EOF and no request, close connection [2019-05-13 15:02:46.298] TRACE: [connection:1] close [2019-05-13 15:02:46.298] TRACE: [connection:1] close: close socket [2019-05-13 15:02:46.298] TRACE: [connection:1] close: timer canceled [2019-05-13 15:02:46.298] TRACE: [connection:1] close: reset responses data [2019-05-13 15:02:46.298] TRACE: [connection:1] destructor called 

, RESTinio 167 . , , RESTinio .


, RESTinio - response_builder , .


. , , . response_builder . , responce_builder , ..


.


, ?


, request_handler- - . , , ?


RESTinio , - request_handler-. - , , RESTinio . , . , :


 [2019-05-13 15:32:23.618] TRACE: starting server on 127.0.0.1:8080 [2019-05-13 15:32:23.618] INFO: init accept #0 [2019-05-13 15:32:23.618] INFO: server started on 127.0.0.1:8080 [2019-05-13 15:32:26.768] TRACE: accept connection from 127.0.0.1:49502 on socket #0 [2019-05-13 15:32:26.768] TRACE: [connection:1] start connection with 127.0.0.1:49502 [2019-05-13 15:32:26.768] TRACE: [connection:1] start waiting for request [2019-05-13 15:32:26.768] TRACE: [connection:1] continue reading request [2019-05-13 15:32:26.768] TRACE: [connection:1] received 78 bytes [2019-05-13 15:32:26.768] TRACE: [connection:1] request received (#0): GET / [2019-05-13 15:32:30.768] TRACE: [connection:1] handle request timed out [2019-05-13 15:32:30.768] TRACE: [connection:1] close [2019-05-13 15:32:30.768] TRACE: [connection:1] close: close socket [2019-05-13 15:32:30.768] TRACE: [connection:1] close: timer canceled [2019-05-13 15:32:30.768] TRACE: [connection:1] close: reset responses data [2019-05-13 15:32:31.768] WARN: [connection:1] try to write response, while socket is closed [2019-05-13 15:32:31.768] TRACE: [connection:1] destructor called 

- . , , RESTinio , .. .


- handle_request_timeout , RESTinio- ( ).


Fazit


, , RESTinio — , . , RESTinio, , RESTinio, .


RESTinio , , , : ? - ? - ? - - ?


PS. RESTinio , SObjectizer, . , - RESTinio , : " C++ HTTP- ", " HTTP- C++: RESTinio, libcurl. 1 ", " Shrimp: HTTP C++ ImageMagic++, SObjectizer RESTinio "

Source: https://habr.com/ru/post/de451728/


All Articles