RESTinio est un serveur HTTP asynchrone. Asynchrone

Il y a quelques années, nous avons publié RESTinio , notre petit framework OpenSource C ++ pour intégrer un serveur HTTP dans des applications C ++. RESTinio n'est pas devenu très populaire pendant cette période, mais il n'a pas été perdu . Quelqu'un le choisit pour la prise en charge «native» de Windows, quelqu'un pour certaines fonctionnalités individuelles (comme la prise en charge de sendfile), quelqu'un pour le rapport des fonctionnalités, la facilité d'utilisation et la personnalisation. Mais je pense qu'au départ, de nombreux RESTinio sont attirés par ce laconique "Hello, World":


#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; } 

C'est vraiment tout ce qui est nécessaire pour exécuter le serveur HTTP dans une application C ++.


Et bien que nous essayions toujours de dire que la fonctionnalité clé pour laquelle nous nous sommes généralement engagés dans RESTinio était le traitement asynchrone des demandes entrantes, nous rencontrons toujours occasionnellement des questions sur ce qu'il faut faire si à l'intérieur de request_handler vous devez effectuer de longues opérations.


Et comme une telle question est pertinente, vous pouvez en reparler et donner quelques petits exemples.


Une petite référence aux origines


Nous avons décidé de rendre notre serveur HTTP intégrable après plusieurs fois d'affilée face à des tâches très similaires: il fallait organiser une entrée HTTP pour une application C ++ existante ou il fallait écrire un microservice dans lequel il fallait réutiliser le C ++ "lourd" déjà existant ny code. Une caractéristique commune de ces tâches était que le traitement de la demande par l'application pouvait s'étendre sur des dizaines de secondes.


En gros, pendant une milliseconde, le serveur HTTP a trié une nouvelle requête HTTP, mais pour émettre une réponse HTTP, il a fallu se tourner vers d'autres services ou effectuer de longs calculs. Si vous traitez les requêtes HTTP en mode synchrone, le serveur HTTP aura besoin d'un pool de milliers de threads de travail, ce qui peut difficilement être considéré comme une bonne idée même dans des conditions modernes.


C'est beaucoup plus pratique lorsque le serveur HTTP ne peut travailler que sur un seul thread de travail, sur lequel des E / S sont effectuées et des gestionnaires de requêtes sont appelés. Le gestionnaire de demandes délègue simplement le traitement réel d'un autre thread de travail et renvoie le contrôle au serveur HTTP. Lorsque, bien plus tard, quelque part sur un autre thread de travail, les informations sont prêtes à répondre à la demande, une réponse HTTP est simplement générée qui récupère automatiquement le serveur HTTP et envoie cette réponse au client approprié.


Comme nous n'avons jamais trouvé de version prête à l'emploi qui soit simple et pratique à utiliser, elle était multiplateforme et supportait Windows en tant que plate-forme «native», fournirait des performances plus ou moins décentes et, plus important encore, serait affinée spécifiquement pour les applications asynchrones. travail, puis début 2017 nous avons commencé à développer RESTinio.


Nous voulions créer un serveur HTTP intégrable asynchrone, facile à utiliser, libérant l'utilisateur de certains soucis de routine, tout en étant plus ou moins productif, multiplateforme et permettant une configuration flexible pour différentes conditions. Cela semble fonctionner, mais laissons aux utilisateurs le soin de juger ...


Il y a donc une demande entrante qui nécessite beaucoup de temps de traitement. Que faire


Fils de travail RESTinio / Asio


Parfois, les utilisateurs de RESTinio ne réfléchissent pas aux threads de travail et à la façon exacte d'utiliser RESTinio. Par exemple, quelqu'un pourrait considérer que lorsque RESTinio est lancé sur un thread de travail (en utilisant run(on_this_thread(...)) , comme dans l'exemple ci-dessus), alors sur ce thread de travail, RESTinio n'appelle que les gestionnaires de demandes. Alors que pour les E / S, RESTinio crée un filetage séparé sous le capot. Et ce thread séparé continue de servir de nouvelles connexions lorsque le thread de travail principal est occupé par request_handler.


En fait, tous les threads que l'utilisateur alloue à RESTinio sont utilisés à la fois pour effectuer des opérations d'E / S et pour appeler des gestionnaires de requêtes. Par conséquent, si vous avez démarré le serveur RESTinio via run(on_this_thread(...)) , puis à l'intérieur de run() sur le thread actuel, les E / S et les gestionnaires de requêtes seront exécutés.


En gros, RESTinio lance une boucle d'événements Asio, à l'intérieur de laquelle il traite les nouvelles connexions, lit et analyse les données des connexions existantes, écrit les données prêtes pour l'envoi, gère la fermeture des connexions, etc. Entre autres choses, après que la requête entrante a été lue et complètement analysée à partir de la connexion suivante, le gestionnaire de requêtes spécifié par l'utilisateur est appelé pour traiter cette requête.


Par conséquent, si request_handler bloque le fonctionnement du thread actuel, la boucle d'événements Asio-action travaillant sur le même thread est également bloquée. Tout est simple.


Si RESTinio est démarré sur un pool de threads de travail (c'est-à-dire au moyen de run(on_thread_pool(...)) , comme dans cet exemple ), alors presque la même chose se produit: une boucle d'événement Asio-event est lancée sur chaque thread du pool. Par conséquent, si certains request_handler commencent à multiplier les grandes matrices, cela bloquera le thread de travail dans le pool et les opérations d'E / S ne seront plus servies sur ce thread.


Par conséquent, lorsque vous utilisez RESTinio, la tâche du développeur est de terminer ses gestionnaires de requêtes dans un délai raisonnable et, de préférence, pas très long.


Avez-vous besoin d'un pool de workflows pour RESTinio / Asio?


Ainsi, lorsque le request_handler spécifié par l'utilisateur bloque le thread de travail sur lequel il est appelé pendant une longue période, ce thread perd la capacité de traiter les opérations d'E / S. Mais que se passe-t-il si request_handler a besoin de beaucoup de temps pour former une réponse? Supposons qu'il effectue une sorte d'opération informatique lourde, dont le temps, en principe, ne peut pas être raccourci à quelques millisecondes?


L'un des utilisateurs pourrait penser que, puisque RESTinio peut fonctionner sur un pool de threads de travail, il suffit de spécifier la plus grande taille de pool et c'est tout.


Malheureusement, cela ne fonctionnera que dans des cas simples lorsque vous avez peu de connexions parallèles. Et l'intensité des requêtes est faible. Si le nombre de requêtes parallèles atteint des milliers (au moins quelques centaines), il est facile d'obtenir une situation où tous les threads de travail du pool seront occupés à traiter des demandes déjà acceptées. Et il ne restera plus de threads pour effectuer des opérations d'E / S. En conséquence, le serveur perdra sa réactivité. L'inclusion de RESTinio perdra la capacité de traiter les délais d'attente que RESTinio compte automatiquement lorsqu'il reçoit de nouvelles connexions et lors du traitement des demandes.


Par conséquent, si vous devez effectuer de longues opérations de blocage pour traiter les demandes entrantes, il est préférable d'allouer un seul thread de travail pour RESTinio, mais d'affecter un grand pool de flux de travail pour effectuer ces mêmes opérations. Le gestionnaire de demande mettra simplement la demande suivante dans une file d'attente, d'où la demande sera récupérée et soumise pour traitement.


Nous avons examiné un exemple de ce schéma en détail lorsque nous avons parlé de notre projet de démonstration Shrimp dans cet article: " Shrimp: redimensionner et partager des images HTTP en C ++ moderne en utilisant ImageMagic ++, SObjectizer et RESTinio ."


Exemples de délégation du traitement des demandes à des threads de travail individuels


Ci-dessus, j'ai essayé d'expliquer pourquoi il n'est pas nécessaire d'effectuer un traitement long directement dans le request_handler. D'où vient le résultat évident: le long traitement des demandes doit être délégué à un autre fil de travail. Voyons à quoi cela pourrait ressembler.


Dans les deux exemples ci-dessous, nous avons besoin d'un seul thread de travail pour exécuter RESTinio et d'un autre thread de travail pour simuler un long traitement des demandes. Et nous avons également besoin d'une sorte de file d'attente de messages pour transférer les demandes du thread RESTinio vers un thread de travail distinct.


Il n'a pas été facile pour moi de créer une nouvelle implémentation de file d'attente de messages thread-safe sur mes genoux pour ces deux exemples, j'ai donc utilisé mon SObjectizer natif et ses mchains, qui sont des canaux CSP. Vous pouvez en savoir plus sur mchain ici: " Échange d'informations entre les threads de travail sans douleur? Canaux CSP pour nous aider ."


Enregistrement de l'objet request_handle


La technique de base sur laquelle repose la délégation du traitement des demandes est le transfert de l'objet request_handle_t quelque part.


Lorsque RESTinio appelle le request_handler spécifié par l'utilisateur pour traiter une demande entrante, un objet de type request_handle_t est passé à ce request_handle_t . Ce type n'est rien de plus qu'un pointeur intelligent vers les paramètres de la demande reçue. Donc, s'il est pratique pour quelqu'un de penser que request_handle_t est shared_ptr , vous pouvez le penser en toute sécurité. Ce shared_ptr est.


Et puisque request_handle_t est shared_ptr , nous pouvons passer ce pointeur intelligent quelque part en toute sécurité. Ce que nous ferons dans les exemples ci-dessous.


Nous avons donc besoin d'un thread de travail et d'un canal distincts pour communiquer avec lui. Créons tout cela:


 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 }; 

Le corps du thread de travail lui-même est situé à l'intérieur de la fonction processing_thread_func() , dont nous parlerons un peu plus tard.


Maintenant, nous avons déjà un thread de travail séparé et un canal de communication avec lui. Vous pouvez démarrer le serveur RESTinio:


  // ,     . 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); })); 

La logique de ce serveur est très simple. Si une demande GET est arrivée pour '/', nous déléguons le traitement de la demande d'un seul thread. Pour ce faire, nous effectuons deux opérations importantes:


  • envoyer l'objet request_handle_t au canal CSP. Bien que cet objet soit stocké à l'intérieur du canal CSP ou ailleurs, RESTinio sait que la demande est toujours en vie;
  • nous restinio::request_accepted() la valeur restinio::request_accepted() du gestionnaire de requêtes. Cela permet à RESTinio de comprendre que la demande a été acceptée pour traitement et que la connexion avec le client ne peut pas être fermée.

Le fait que request_handler n'ait pas généré immédiatement une réponse RESTinio ne dérange pas. Une fois restinio::request_accepted() retourné, l'utilisateur a pris la responsabilité de traiter la demande et un jour la réponse à la demande sera générée.


Si le gestionnaire de demande a renvoyé restinio::request_rejected() , alors RESTinio comprend que la demande ne sera pas traitée et renverra une erreur 501 au client.


Donc, nous fixons le résultat préliminaire: l'instance request_handle_t peut être passée quelque part, car il s'agit en fait de std::shared_ptr . Tant que cette instance est vivante, RESTinio considère que la demande est en cours de traitement. Si le gestionnaire de demande a renvoyé restinio::request_accepted() , alors RESTinio ne s'inquiétera pas que la réponse à la demande n'ait pas été générée pour l'instant.


Maintenant, nous pouvons regarder la mise en œuvre de ce fil très distinct:


 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(); }) ); } 

La logique ici est très simple: nous obtenons la demande initiale sous la forme d'un message handle_request et nous la transmettons sous la forme d'un message timeout_elapsed retardé pendant un certain temps aléatoire. Nous n'effectuons le traitement réel de la demande qu'à réception de timeout_elapsed .


Upd. Lorsque la méthode done() est appelée sur un thread de travail distinct, RESTinio est averti qu'une réponse toute prête est apparue et doit être écrite sur la connexion TCP. RESTinio lance l'opération d'écriture, mais l'opération d'E / S elle-même ne sera pas exécutée lorsque done() appelée, mais lorsque RESTinio effectue les E / S et appelle request_handlers. C'est-à-dire dans cet exemple, done() est appelée sur un thread de travail séparé et l'opération d'écriture sera effectuée sur le thread principal, où restinio::run() fonctionne.


Les messages eux-mêmes sont les suivants:


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

C'est-à-dire un thread de travail séparé prend request_handle_t et l'enregistre jusqu'à ce que l'occasion se présente de former une réponse complète. Et lorsque cette opportunité se présente, create_response() est appelée sur l'objet de demande enregistré et la réponse est renvoyée à RESTinio. Ensuite, RESTinio déjà dans son contexte de travail écrit la réponse en relation avec le client correspondant.


Ici, l'instance timeout_elapsed est stockée dans un message différé timeout_elapsed , car il n'y a pas de traitement réel dans cet exemple primitif. Dans une application réelle, request_handle_t peut être stocké dans une sorte de file d'attente ou à l'intérieur d'un objet créé pour traiter la demande.


Le code complet de cet exemple se trouve parmi les exemples réguliers de RESTinio .


Quelques petites notes de code


Cette construction définit les propriétés RESTinio qu'un serveur RESTinio devrait avoir:


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

Pour cet exemple, j'ai besoin de RESTinio pour enregistrer ses actions de traitement des demandes. Par conséquent, j'ai défini logger_t être différent du null_logger_t par défaut. Mais depuis RESTinio fonctionnera, en fait, sur plusieurs threads (RESTinio traite les demandes entrantes sur le thread principal, mais les réponses viennent d'un thread de travail séparé), alors vous avez besoin d'un enregistreur thread-safe, qui est shared_ostream_logger_t .


À l'intérieur de processing_thread_func() , la fonction SObjectizer select() , ce qui est quelque peu similaire à la construction de sélection Go-shn: vous pouvez lire et traiter les messages de plusieurs canaux à la fois. La fonction select() fonctionne jusqu'à ce que tous les canaux qui lui sont passés soient fermés. Ou jusqu'à ce qu'on lui dise de force qu'il est temps de mettre fin.


Dans le même temps, si le canal de communication avec le serveur RESTinio est fermé, il est inutile de poursuivre le travail. Par conséquent, dans select() , la réponse à la fermeture de l'un des canaux est déterminée: dès qu'un canal est fermé, le drapeau d'arrêt est levé. Et cela conduira à l'achèvement de select() et à la sortie de processing_thread_func() .


Enregistrement de l'objet response_builder


Dans l'exemple précédent, nous avons considéré un cas simple où il est possible d'enregistrer request_handle_t jusqu'à ce que nous puissions immédiatement donner la réponse entière à la demande.


Mais il peut y avoir des scénarios plus complexes lorsque, par exemple, vous devez donner une réponse en plusieurs parties. Autrement dit, nous recevons une demande, nous ne pouvons immédiatement former que la première partie de la réponse. Nous le formons. Ensuite, après un certain temps, nous avons la possibilité de former la deuxième partie de la réponse. Ensuite, après un peu plus de temps, nous pouvons former la partie suivante, etc.


De plus, il peut être souhaitable pour nous que toutes ces parties disparaissent au fur et à mesure que nous les formons. C'est-à-dire Tout d'abord, la première partie de la réponse pour que le client puisse la soustraire, puis la deuxième, puis la troisième, etc.


RESTinio vous permet de le faire en raison de différents types de responce_builders . En particulier, des types tels que user_controlled_output et chunked_output .


Dans ce cas, il ne suffit pas d'enregistrer request_handle_t , car request_handle_t ne sera utile que jusqu'au premier appel à create_reponse() . Ensuite, nous devons travailler avec response_builder. Et bien ...


Eh bien, ça va. Response_builder est un type mobile, quelque peu similaire à unique_ptr. Nous pouvons donc le conserver aussi longtemps que nous en avons besoin. Et pour montrer à quoi ça ressemble, on refait légèrement l'exemple ci-dessus. Laissez la fonction processing_thread_func() former la réponse en plusieurs parties.


Ce n'est pas du tout difficile.


Nous devons d'abord décider des types dont le nouveau processing_thread_func() aura besoin:


 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; }; 

Le message handle_request reste inchangé. Mais dans le message timeout_elapsed nous stockons maintenant non request_handle_t , mais response_builder du type dont nous avons besoin. Plus un compteur des pièces restantes. Dès que ce compteur est réinitialisé, le service de demande se termine.


Maintenant, nous pouvons regarder une nouvelle version de la fonction processing_thread_func() :


 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(); }) ); } 

C'est-à-dire , . . .


Upd. flush() , done() : RESTinio , I/O- , flush() , , RESTinio - request_handler-. C'est-à-dire 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- ( ).


Conclusion


, , 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/fr451728/


All Articles