几年前,我们发布了RESTinio ,这是我们的小型OpenSource C ++框架,用于将HTTP服务器嵌入C ++应用程序。 RESTinio在这段时间内并没有广受欢迎,但是它并没有丢失 。 有人选择它是为了对Windows的“本机”支持,有人选择了某些单独的功能(例如sendfile支持),有人选择了功能的比例,易于使用和自定义。 但是,我认为,最初,许多RESTinio都被这个简洁的“ 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 ++应用程序中运行HTTP服务器所需的全部。
而且尽管尽管我们总是试图说我们通常使用RESTinio的主要功能是传入请求的异步处理,但是我们仍然偶尔会遇到一些问题,即如果必须在request_handler中执行长时间的操作,该怎么办。
既然这样的问题是相关的,那么您可以再次讨论它,并举几个小例子。
对起源的一点参考
我们决定连续几次让嵌入式HTTP服务器面临非常相似的任务:有必要为现有的C ++应用程序组织HTTP输入,或者有必要编写微服务,其中必须重用已经存在的“繁重” C ++ ny代码。 这些任务的共同特征是请求的应用程序处理可能会延长数十秒。
粗略地说,一毫秒之内,HTTP服务器正在整理一个新的HTTP请求,但是为了发出HTTP响应,有必要求助于某些其他服务或进行一些冗长的计算。 如果以同步方式处理HTTP请求,则HTTP服务器将需要成千上万个工作线程池,即使在现代条件下,也很难认为这是一个好主意。
当HTTP服务器只能在一个工作线程上执行I / O并调用请求处理程序时,这将更加方便。 请求处理程序仅委托其他工作线程的实际处理,并将控制权返回给HTTP服务器。 当很晚以后,在另一个工作线程上的某个地方,信息准备好响应该请求时,就会简单地生成一个HTTP响应,该HTTP响应会自动选择HTTP服务器并将此响应发送到适当的客户端。
由于我们从未找到过易于使用且易于使用的现成版本,因此它是跨平台的,并且支持Windows作为“本机”平台,将或多或少提供不错的性能,最重要的是,将其专门针对异步进行优化。工作,然后在2017年初,我们开始开发RESTinio。
我们希望使异步嵌入式HTTP服务器易于使用,将用户从日常工作中解放出来,同时或多或少地提高生产力,跨平台并允许针对不同条件进行灵活配置。 它似乎正在解决,但让用户来判断...
因此,存在一个传入请求,需要大量处理时间。 怎么办
工作线程RESTinio / Asio
有时RESTinio用户不会考虑什么工作线程以及如何正确使用RESTinio。 例如,某人可能会认为,当在一个工作线程上run(on_this_thread(...))
如上例所示,使用run(on_this_thread(...))
),那么RESTinio仅在该工作线程上调用请求处理程序。 而对于I / O,RESTinio在后台创建了一个单独的线程。 当主工作线程被request_handler占用时,该单独的线程继续为新连接提供服务。
实际上,用户分配给RESTinio的所有线程都用于执行I / O操作和调用request_handlers。 因此,如果通过run(on_this_thread(...))
启动RESTinio服务器,则在当前线程的run()
内部,将执行I / O和请求处理程序。
粗略地说,RESTinio启动一个Asio事件循环,在该循环中它处理新连接,从现有连接中读取和解析数据,写入准备发送的数据,处理关闭连接等。 除其他事项外,在从下一个连接读取并完全解析了传入的请求之后,将调用用户指定的request_handler来处理此请求。
因此,如果request_handler阻止了当前线程的操作,那么也将阻止在同一线程上工作的Asio-action事件循环。 一切都很简单。
如果RESTinio是在工作线程池上启动的(即通过run(on_thread_pool(...))
,如本例中所示 ),则会发生几乎相同的事情:一个Asio-event事件循环在池中的每个线程上启动。 因此,如果某些request_handler开始乘以大矩阵,则这将阻塞池中的工作线程,并且I / O操作将不再在该线程上提供服务。
因此,在使用RESTinio时,开发人员的任务是在合理的时间(最好不是很长时间)内完成他的request_handlers。
您是否需要RESTinio / Asio的工作流池?
因此,当用户指定的request_handler长时间阻塞了对其调用的工作线程时,该线程将失去处理I / O操作的能力。 但是,如果request_handler需要大量时间来形成响应怎么办? 假设他执行某种繁重的计算操作,原则上不能将其时间缩短到几毫秒?
其中一位用户可能会认为,由于RESTinio可以在工作线程池中工作,因此只需指定更大的池大小即可。
不幸的是,这仅在很少并行连接的简单情况下有效。 而且查询强度低。 如果并行查询数达到数千(至少只有几百个),那么很容易出现以下情况:池中的所有工作线程都将忙于处理已接受的请求。 并且将没有更多线程可用于执行I / O操作。 结果,服务器将失去其响应能力。 包括RESTinio将失去处理超时的能力,RESTinio在收到新连接和处理请求时会自动将其超时。
因此,如果您需要执行冗长的阻塞操作来处理传入的请求,最好只为RESTinio分配一个工作线程,而分配大量的工作流来执行这些相同的操作。 请求处理程序会将下一个请求放入一个队列中,从该队列中检索请求并将其提交进行处理。
当我们在本文中讨论Shrimp演示项目时,我们详细研究了该方案的一个示例:“ Shrimp:使用ImageMagic ++,SObjectizer和RESTinio在现代C ++中缩放和共享HTTP图像 。”
将请求处理委派给各个工作线程的示例
上面,我试图解释为什么没必要在request_handler内部执行冗长的处理。 显而易见的结果从何而来:冗长的请求处理必须委托给其他一些工作线程。 让我们看一下它的外观。
在下面的两个示例中,我们需要一个工作线程来运行RESTinio,并需要另一个工作线程来模拟冗长的请求处理。 而且,我们还需要某种消息队列来将请求从RESTinio线程传输到单独的工作线程。
对于这两个示例,我无法在膝盖上进行线程安全消息队列的新实现,因此我使用了本机SObjectizer及其mchain,它们是CSP通道。 您可以在这里阅读有关mchain的更多信息:“ 工作线程之间的信息交换没有麻烦?CSP通道可为我们提供帮助 。”
保存request_handle对象
建立请求处理委托的基本技术是将request_handle_t
对象转移到某个地方。
当RESTinio调用用户指定的request_handler来处理传入请求时,类型request_handle_t
的对象将传递给此request_handle_t
。 此类型仅是指向已接收请求参数的智能指针。 因此,如果有人方便地认为request_handle_t
是shared_ptr
,那么您可以放心地这样做。 这个shared_ptr
是。
并且由于request_handle_t
是shared_ptr
,我们可以在某个地方安全地传递此智能指针。 我们将在以下示例中进行操作。
因此,我们需要一个单独的工作线程和通道与之通信。 让我们全部创建:
int main() {
工作线程的主体本身位于processing_thread_func()
函数内部,稍后我们将进行讨论。
现在我们已经有一个单独的工作线程和与之通信的通道。 您可以启动RESTinio服务器:
该服务器的逻辑非常简单。 如果GET请求已针对“ /”到达,则我们委派单个线程的请求处理。 为此,我们执行两个重要操作:
- 发送
request_handle_t
对象到CSP通道。 当此对象存储在CSP通道内或其他位置时,RESTinio知道该请求仍然有效。 - 我们从请求处理程序中返回值
restinio::request_accepted()
。 这使RESTinio了解请求已被接受进行处理,并且无法关闭与客户端的连接。
request_handler不会立即生成RESTinio响应这一事实不会打扰。 一旦restinio::request_accepted()
,则用户将负责处理请求,并有一天将生成对请求的响应。
如果请求处理程序返回restinio::request_rejected()
,则RESTinio会理解将不会处理该请求,并将向客户端返回501错误。
因此,我们修复了初步结果: request_handle_t
实例可以传递到某个地方,因为实际上它是std::shared_ptr
。 在此实例处于活动状态时,RESTinio认为该请求正在处理中。 如果请求处理程序返回restinio::request_accepted()
,则RESTinio将不担心当前尚未生成对请求的响应。
现在我们来看一下这个非常独立的线程的实现:
void processing_thread_func(so_5::mchain_t req_ch) {
这里的逻辑非常简单:我们以handle_request
消息的形式接收初始请求,并以timeout_elapsed
消息的形式将其转发给自己,该消息会延迟一段时间。 我们仅在收到timeout_elapsed
请求进行实际处理。
更新。 在单独的工作线程上调用done()
方法时,将通知RESTinio已出现现成的响应,需要将其写入TCP连接。 RESTinio启动写操作,但是I / O操作本身不会在调用done()
执行,而是在RESTinio执行I / O并调用request_handlers的位置执行。 即 在此示例中,在单独的工作线程上调用了done()
,并且将在restinio::run()
工作的主线程上执行写操作。
消息本身如下:
struct handle_request { restinio::request_handle_t m_req; }; struct timeout_elapsed { restinio::request_handle_t m_req; std::chrono::milliseconds m_pause; };
即 一个单独的工作线程接受request_handle_t
并保存,直到有机会形成一个完整的响应为止。 并且当机会出现时,在已保存的请求对象上调用create_response()
,并将响应返回给RESTinio。 然后,RESTinio已经在其工作上下文中编写了与相应客户端有关的响应。
这里,由于在此原始示例中没有实际处理,因此request_handle_t
实例存储在timeout_elapsed
延迟消息中。 在实际的应用程序中, request_handle_t
可以存储在某种队列中,也可以存储在为处理请求而创建的某些对象中。
可以在常规RESTinio示例中找到该示例的完整代码。
一些小代码注释
此构造设置RESTinio服务器应具有的RESTinio属性:
对于此示例,我需要RESTinio记录其请求处理操作。 因此,我将logger_t
设置logger_t
与默认的null_logger_t
不同。 但是因为 RESTinio实际上将在多个线程上工作(RESTinio处理主线程上的传入请求,但响应来自单独的工作线程),那么您需要一个线程安全的记录器,即shared_ostream_logger_t
。
在processing_thread_func()
内部,使用了SObjectizer函数select()
,这与select Go-shn构造有点类似:您可以一次从多个通道读取和处理消息。 select()
函数将起作用,直到传递给它的所有通道都关闭。 或直到被迫告诉她该结束了。
同时,如果与RESTinio服务器的通信通道已关闭,则继续工作毫无意义。 因此,在select()
,确定关闭任何通道的响应:一旦关闭通道,停止标志就会升高。 这将导致select()
的完成并从processing_thread_func()
退出。
保存response_builder对象
在前面的示例中,我们考虑了一个简单的情况,即可以保存request_handle_t
直到我们可以立即将整个响应提供给请求。
但是,例如,当您需要分部分给出答案时,可能会有更复杂的场景。 也就是说,我们收到一个请求,我们可以立即仅形成响应的第一部分。 我们形成它。 然后,经过一段时间,我们有机会形成答案的第二部分。 然后,再经过一些时间,我们可以形成下一部分,依此类推。
而且,对我们来说,可能希望所有这些部分在形成它们时都消失。 即 首先是答案的第一部分,以便客户可以减去它,然后是第二部分,然后是第三部分,依此类推。
由于responce_builders的类型不同, RESTinio允许您执行此操作。 特别是诸如user_受控_output和chunked_output之类的类型。
在这种情况下,仅保存request_handle_t
是不够的,因为request_handle_t
仅在第一次调用create_reponse()
之前才有用。 接下来,我们需要使用response_builder。 好吧...
好吧,没关系。 Response_builder是一种可移动类型,有点类似于unique_ptr。 因此,我们也可以在需要时保留它。 为了展示它的外观,我们稍微重做上面的示例。 让processing_thread_func()
函数部分地形成响应。
这一点都不困难。
首先,我们需要确定new processing_thread_func()
将需要的类型:
struct handle_request { restinio::request_handle_t m_req; };
handle_request
消息保持不变。 但是,在timeout_elapsed
消息中timeout_elapsed
我们现在不存储request_handle_t
,而是存储所需类型的response_builder。 加上剩余部分的计数器。 一旦重置此计数器,请求服务就会结束。
现在我们来看一个新版本的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) {
即 , . . .
Upd. flush()
, done()
: RESTinio , I/O- , flush()
, , RESTinio - request_handler-. 即 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- ( ).
结论
, , RESTinio — , . , RESTinio, , RESTinio, .
RESTinio , , , : ? - ? - ? - - ?
PS。 RESTinio , SObjectizer, . , - RESTinio , : " C++ HTTP- ", " HTTP- C++: RESTinio, libcurl. 1 ", " Shrimp: HTTP C++ ImageMagic++, SObjectizer RESTinio "