RESTinio es un servidor HTTP asíncrono. Asincrónico

Hace un par de años publicamos RESTinio , nuestro pequeño marco OpenSource C ++ para incrustar un servidor HTTP en aplicaciones C ++. RESTinio no se volvió mega popular durante este tiempo, pero no se perdió . Alguien lo elige por el soporte "nativo" para Windows, alguien por algunas características individuales (como el soporte de sendfile), alguien por la proporción de características, facilidad de uso y personalización. Pero, creo, inicialmente muchos RESTinio se sienten atraídos por este lacónico "Hola, Mundo":


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

Esto es realmente todo lo que se necesita para ejecutar el servidor HTTP dentro de una aplicación C ++.


Y aunque siempre tratamos de decir que la característica clave para la que generalmente participamos en RESTinio era el procesamiento asincrónico de las solicitudes entrantes, todavía ocasionalmente encontramos preguntas sobre qué hacer si dentro de request_handler tiene que realizar operaciones largas.


Y dado que dicha pregunta es relevante, puede volver a hablar sobre ella y dar un par de pequeños ejemplos.


Una pequeña referencia a los orígenes.


Decidimos hacer que nuestro servidor HTTP incorporable después de varias veces seguidas enfrentara tareas muy similares: era necesario organizar una entrada HTTP para una aplicación C ++ existente o era necesario escribir un microservicio en el que era necesario reutilizar el C ++ "pesado" ya existente código ny Una característica común de estas tareas era que el procesamiento de la solicitud de la solicitud podía extenderse por decenas de segundos.


En términos generales, durante un milisegundo, el servidor HTTP estaba clasificando una nueva solicitud HTTP, pero para emitir una respuesta HTTP, era necesario recurrir a otros servicios o realizar algunos cálculos largos. Si procesa solicitudes HTTP en modo síncrono, entonces el servidor HTTP necesitará un conjunto de miles de hilos de trabajo, lo que difícilmente puede considerarse una buena idea incluso en condiciones modernas.


Es mucho más conveniente cuando el servidor HTTP puede funcionar en un solo subproceso de trabajo, en el que se realiza la E / S y se llama a los manejadores de solicitudes. El controlador de solicitudes simplemente delega el procesamiento real de algún otro hilo de trabajo y devuelve el control al servidor HTTP. Cuando, mucho más tarde, en algún lugar de otro hilo de trabajo, la información está lista para responder a la solicitud, simplemente se genera una respuesta HTTP que recoge automáticamente el servidor HTTP y envía esta respuesta al cliente apropiado.


Como nunca encontramos una versión lista para usar que fuera simple y conveniente de usar, era multiplataforma y admitía Windows como plataforma "nativa", proporcionaría un rendimiento más o menos decente y, lo más importante, se mejoraría específicamente para asincrónico trabajo, luego a principios de 2017 comenzamos a desarrollar RESTinio.


Queríamos hacer un servidor HTTP embebido asíncrono, fácil de usar, que liberara al usuario de algunas preocupaciones de rutina, mientras era más o menos productivo, multiplataforma y permitía una configuración flexible para diferentes condiciones. Parece estar funcionando, pero dejemos que los usuarios juzguen ...


Por lo tanto, hay una solicitud entrante que requiere mucho tiempo de procesamiento. Que hacer


Hilos de trabajo RESTinio / Asio


A veces, los usuarios de RESTinio no piensan qué hilos de trabajo y cómo utiliza RESTinio exactamente. Por ejemplo, alguien podría considerar que cuando RESTinio se inicia en un hilo de trabajo (usando run(on_this_thread(...)) , como en el ejemplo anterior), en este hilo de trabajo RESTinio solo llama a los manejadores de solicitudes. Mientras que para I / O RESTinio crea un hilo separado debajo del capó. Y este hilo separado continúa sirviendo nuevas conexiones cuando el hilo principal de trabajo está ocupado por request_handler.


De hecho, todos los hilos que el usuario asigna a RESTinio se usan tanto para realizar operaciones de E / S como para llamar a request_handlers. Por lo tanto, si inició el servidor RESTinio a través de run(on_this_thread(...)) , luego dentro de run() en el hilo actual, se realizarán los manejadores de E / S y de solicitud.


En términos generales, RESTinio lanza un bucle de eventos Asio, dentro del cual procesa nuevas conexiones, lee y analiza datos de conexiones existentes, escribe datos listos para enviar, maneja el cierre de conexiones, etc. Entre otras cosas, después de leer y analizar completamente la solicitud entrante desde la siguiente conexión, se llama al request_handler especificado por el usuario para procesar esta solicitud.


En consecuencia, si request_handler bloquea la operación del subproceso actual, también se bloquea el bucle de eventos Asio-action que trabaja en el mismo subproceso. Todo es simple


Si RESTinio se inicia en un grupo de subprocesos de trabajo (es decir, mediante run(on_thread_pool(...)) , como en este ejemplo ), sucede casi lo mismo: se inicia un bucle de evento de evento Asio en cada subproceso del grupo. Por lo tanto, si algún request_handler comienza a multiplicar matrices grandes, esto bloqueará el subproceso de trabajo en el grupo y las operaciones de E / S ya no se servirán en este subproceso.


Por lo tanto, cuando se usa RESTinio, la tarea del desarrollador es completar sus request_handlers en un tiempo razonable y, preferiblemente, no muy largo.


¿Necesita un grupo de flujo de trabajo para RESTinio / Asio?


Entonces, cuando el request_handler especificado por el usuario bloquea el hilo de trabajo en el que se lo llama durante mucho tiempo, este hilo pierde la capacidad de procesar operaciones de E / S. Pero, ¿qué pasa si request_handler necesita mucho tiempo para formar una respuesta? Supongamos que realiza algún tipo de operación informática pesada, cuyo tiempo, en principio, no puede acortarse a unos pocos milisegundos.


Uno de los usuarios podría pensar que, dado que RESTinio puede trabajar en un grupo de subprocesos de trabajo, simplemente especifique el tamaño de grupo más grande y listo.


Desafortunadamente, esto solo funcionará en casos simples cuando tenga pocas conexiones paralelas. Y la intensidad de la consulta es baja. Si el recuento de consultas paralelas llega a miles (al menos solo unos pocos cientos), es fácil obtener una situación en la que todos los subprocesos de trabajo del grupo estarán ocupados procesando solicitudes ya aceptadas. Y no habrá más subprocesos para realizar operaciones de E / S. Como resultado, el servidor perderá su capacidad de respuesta. La inclusión de RESTinio perderá la capacidad de procesar tiempos de espera que RESTinio cuenta automáticamente cuando recibe nuevas conexiones y al procesar solicitudes.


Por lo tanto, si necesita realizar operaciones de bloqueo prolongadas para atender las solicitudes entrantes, es mejor asignar solo un hilo de trabajo para RESTinio, pero asignar un gran grupo de flujos de trabajo para realizar estas mismas operaciones. El controlador de solicitudes solo colocará la siguiente solicitud en alguna cola, desde donde la solicitud se recuperará y se enviará para su procesamiento.


Observamos un ejemplo de este esquema en detalle cuando hablamos sobre nuestro proyecto de demostración de camarones en este artículo: " Camarones: escale y distribuya imágenes HTTP en C ++ moderno usando ImageMagic ++, SObjectizer y RESTinio ".


Ejemplos de delegación de procesamiento de solicitudes a subprocesos de trabajo individuales


Arriba, traté de explicar por qué no es necesario realizar un procesamiento prolongado dentro del request_handler. ¿De dónde viene el resultado obvio? El procesamiento de solicitudes prolongado debe delegarse en algún otro hilo de trabajo. Veamos cómo podría verse esto.


En los dos ejemplos a continuación, necesitamos un único subproceso de trabajo para ejecutar RESTinio y otro subproceso de trabajo para simular un procesamiento de solicitud prolongado. Y también necesitamos algún tipo de cola de mensajes para transferir solicitudes del hilo RESTinio a un hilo de trabajo separado.


No fue fácil para mí hacer una nueva implementación de la cola de mensajes segura para subprocesos en mi rodilla para estos dos ejemplos, así que utilicé mi SObjectizer nativo y sus mchains, que son canales CSP. Puede leer más sobre mchain aquí: "¿ Intercambio de información entre hilos de trabajo sin problemas? Canales CSP para ayudarnos" .


Guardar objeto request_handle


La técnica básica sobre la cual se construye la delegación del procesamiento de solicitudes es la transferencia del objeto request_handle_t alguna parte.


Cuando RESTinio llama al request_handler especificado por el usuario para procesar una solicitud entrante, un objeto de tipo request_handle_t se pasa a este request_handle_t . Este tipo no es más que un puntero inteligente a los parámetros de la solicitud recibida. Entonces, si es conveniente que alguien piense que shared_ptr es shared_ptr , entonces puede pensarlo con seguridad. Este shared_ptr es.


Y dado que shared_ptr es shared_ptr , podemos pasar con seguridad este puntero inteligente a alguna parte. Lo que haremos en los ejemplos a continuación.


Por lo tanto, necesitamos un hilo de trabajo y un canal separados para comunicarnos con él. Vamos a crearlo todo:


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

El cuerpo del subproceso de trabajo en sí se encuentra dentro de la función processing_thread_func() , que discutiremos más adelante.


Ahora ya tenemos un hilo de trabajo separado y un canal para la comunicación con él. Puede iniciar el servidor 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 lógica de este servidor es muy simple. Si ha llegado una solicitud GET para '/', delegamos el procesamiento de la solicitud de un solo hilo. Para hacer esto, realizamos dos operaciones importantes:


  • envíe el objeto request_handle_t al canal CSP. Mientras este objeto se almacena dentro del canal CSP o en otro lugar, RESTinio sabe que la solicitud aún está activa;
  • devolvemos el valor restinio::request_accepted() del manejador de solicitudes. Esto hace que RESTinio comprenda que la solicitud ha sido aceptada para su procesamiento y que la conexión con el cliente no se puede cerrar.

El hecho de que request_handler no haya generado inmediatamente una respuesta RESTinio no molesta. Una vez que restinio::request_accepted() fue devuelto, el usuario asumió la responsabilidad de procesar la solicitud y algún día se generará la respuesta a la solicitud.


Si el controlador de solicitudes devolvió restinio::request_rejected() , RESTinio comprende que la solicitud no se procesará y devolverá un error 501 al cliente.


Entonces, arreglamos el resultado preliminar: la instancia request_handle_t se puede pasar a algún lado, ya que de hecho es std::shared_ptr . Mientras esta instancia está activa, RESTinio considera que la solicitud se está procesando. Si el controlador de solicitudes devolvió restinio::request_accepted() , RESTinio no se preocupará de que la respuesta a la solicitud no se haya generado en este momento.


Ahora podemos ver la implementación de este hilo muy separado:


 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 lógica aquí es muy simple: recibimos la solicitud inicial en forma de un mensaje handle_request y la handle_request a nosotros mismos en forma de un mensaje timeout_elapsed retrasado por un tiempo aleatorio. Realizamos el procesamiento real de la solicitud solo después de recibir timeout_elapsed .


Upd. Cuando se llama al método done() en un hilo de trabajo separado, RESTinio es notificado de que ha aparecido una respuesta preparada que debe escribirse en la conexión TCP. RESTinio inicia la operación de escritura, pero la operación de E / S en sí no se ejecutará donde done() llama done() , sino donde RESTinio realiza la E / S y llama a request_handlers. Es decir en este ejemplo, done() se llama en un subproceso de trabajo separado, y la operación de escritura se realizará en el subproceso principal, donde funciona restinio::run() .


Los mensajes en sí son los siguientes:


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

Es decir un hilo de trabajo separado toma request_handle_t y lo guarda hasta que surja la oportunidad de formar una respuesta completa. Y cuando surge esta oportunidad, se llama a create_response() en el objeto de solicitud guardado y la respuesta se devuelve a RESTinio. Entonces RESTinio ya en su contexto de trabajo escribe la respuesta en relación con el cliente correspondiente.


Aquí, la instancia timeout_elapsed se almacena en un mensaje retrasado timeout_elapsed , ya que no hay un procesamiento real en este ejemplo primitivo. En una aplicación real, request_handle_t puede almacenarse en algún tipo de cola o dentro de algún objeto creado para procesar la solicitud.


El código completo para este ejemplo se puede encontrar entre los ejemplos regulares de RESTinio .


Algunas notas de código pequeño


Esta construcción establece las propiedades RESTinio que debe tener un servidor 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>() 

Para este ejemplo, necesito RESTinio para registrar sus acciones de procesamiento de solicitudes. Por lo tanto, configuro logger_t sea ​​diferente del null_logger_t predeterminado. Pero desde RESTinio funcionará, de hecho, en varios subprocesos (RESTinio procesa las solicitudes entrantes en el subproceso principal, pero las respuestas provienen de un subproceso de trabajo separado), entonces necesita un registrador seguro para subprocesos, que es shared_ostream_logger_t .


Dentro de processing_thread_func() , se utiliza la función SObjectizer select() , que es algo similar a la construcción Go select: puede leer y procesar mensajes de varios canales a la vez. La función select() funciona hasta que todos los canales que se le pasan estén cerrados. O hasta que le digan a la fuerza que es hora de terminar.


Al mismo tiempo, si el canal para la comunicación con el servidor RESTinio está cerrado, entonces no tiene sentido continuar el trabajo. Por lo tanto, en select() , se determina la respuesta al cierre de cualquiera de los canales: tan pronto como se cierra un canal, se levanta el indicador de detención. Y esto llevará a la finalización de select() y a salir de processing_thread_func() .


Guardar el objeto response_builder


En el ejemplo anterior, consideramos un caso simple cuando es posible guardar request_handle_t hasta que podamos dar inmediatamente la respuesta completa a la solicitud.


Pero puede haber escenarios más complejos cuando, por ejemplo, necesita dar una respuesta en partes. Es decir, recibimos una solicitud, podemos formar inmediatamente solo la primera parte de la respuesta. Nosotros lo formamos. Luego, después de un tiempo, tenemos la oportunidad de formar la segunda parte de la respuesta. Luego, después de más tiempo, podemos formar la siguiente parte, etc.


Además, puede ser deseable para nosotros que todas estas partes desaparezcan a medida que las formemos. Es decir Primero, la primera parte de la respuesta para que el cliente pueda restarla, luego la segunda, luego la tercera, etc.


RESTinio le permite hacer esto debido a los diferentes tipos de Responce_builders . En particular, tipos como user_controlled_output y chunked_output .


En este caso, no es suficiente guardar request_handle_t , porque request_handle_t será útil hasta la primera llamada a create_reponse() . Luego tenemos que trabajar con response_builder. Bueno ...


Bueno, esta bien. Response_builder es un tipo móvil, algo similar a unique_ptr. Por lo tanto, también podemos conservarlo mientras lo necesitemos. Y para mostrar cómo se ve, rehacemos ligeramente el ejemplo anterior. Deje que la función processing_thread_func() forme la respuesta en partes.


Esto no es del todo difícil.


Primero necesitamos decidir sobre los tipos que necesitará new processing_thread_func() :


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

El mensaje handle_request permanece sin cambios. Pero en el mensaje timeout_elapsed ahora almacenamos no request_handle_t , sino response_builder del tipo que necesitamos. Además de un contador de las partes restantes. Tan pronto como se restablezca este contador, finaliza el servicio de solicitud.


Ahora podemos ver una nueva versión de la función 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(); }) ); } 

Es decir , . . .


Upd. flush() , done() : RESTinio , I/O- , flush() , , RESTinio - request_handler-. Es decir 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- ( ).


Conclusión


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


All Articles