我们正在RESTinio中构建C ++模板的第四层。 为什么以及如何?

RESTinio是一个相对较小的项目,它是内置在C ++应用程序中的异步HTTP服务器。 可以说,它的特征是C ++模板的广泛使用。 无论是在实施中还是在公共API中。


RESTinio中的C ++模板得到了如此积极的使用,以至于第一篇在Habr上谈论RESTinio的文章被称为“ 在具有人脸的嵌入式异步HTTP服务器的实现中的三层C ++模板 ”。


三层模板。 总体而言,这不是一个比喻。


最近,我们再次更新了RESTinio,并将新功能添加到版本0.5.1中,我们不得不使模板的“层数”更高。 因此,在某些地方,RESTinio中的C ++模板已经是四层楼了。



如果有人想知道为什么我们需要它以及我们如何使用模板,然后与我们呆在一起,那么削减中会有一些细节。 熟练的C ++专家不太可能为自己找到任何新东西,但是较不高级的C ++昵称将能够看到如何使用模板来插入/删除功能。 几乎在野外。


连接状态监听器


创建版本0.5.1的主要功能是能够通知用户与HTTP服务器的连接状态已更改的功能。 例如,客户端“下降”,这使得不必处理仍在排队等待的来自该客户端的请求。


有时我们被问到这个功能,现在我们已经实现了它。 但是因为 并不是每个人都问这个功能,有人认为它应该是可选的:如果某些用户需要它,那么让它明确包含它,其余所有在RESTinio中都不需付出任何代价。


并且由于RESTinio中HTTP服务器的主要特征是通过“特征”设置的,因此决定通过服务器属性启用/禁用侦听连接状态。


用户如何为连接状态设置自己的侦听器?


为了将您的侦听器设置为连接状态,用户必须执行三个步骤。


步骤#1:定义您自己的类,该类应具有以下形式的非静态state_changed方法:


void state_changed( const restinio::connection_state::notice_t & notice) noexcept; 

例如,可能是这样的:


 class my_state_listener { std::mutex lock_; ... public: void state_changed(const restinio::connection_state::notice_t & notice) noexcept { std::lock_guard<std::mutex> l{lock_}; .... } ... }; 

步骤2:在服务器属性中,您需要定义一个名为connection_state_listener_t的typedef,它应引用在步骤1中创建的类型的名称:


 struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; }; 

因此,在启动HTTP服务器时应使用以下属性:


 restinio::run(restinio::on_thread_pool<my_traits>(8)...); 

步骤#3:用户必须创建其侦听器的实例,并将此指针通过服务器参数中的shared_ptr传递:


 restinio::run( restinio::on_thread_pool<my_traits>(8) .port(8080) .address("localhost") .request_handler(...) .connection_state_listener(std::make_shared<my_state_listener>(...)) ) ); 

如果用户未调用connection_state_listener方法,则在启动HTTP服务器时将引发异常:如果用户希望使用状态侦听器但未指定此侦听器,则North无法工作。


如果不设置connection_state_listener_t?


如果用户在服务器属性中设置connection_state_listener_t名称,则他必须调用connection_state_listener方法来设置服务器参数。 但是,如果用户未指定connection_state_listener_t


在这种情况下, connection_state_listener_t名称仍将出现在服务器属性中,但是该名称将指向特殊类型restinio::connection_state::noop_listener_t


实际上,会发生以下情况:在RESTinio中,定义常规特征时,将设置值connection_state_listener_t 。 类似于:


 namespace restinio { struct default_traits_t { using time_manager_t = asio_time_manager_t; using logger_t = null_logger_t; ... using connection_state_listener_t = connection_state::noop_listener_t; }; } /* namespace restinio */ 

并且当用户从restinio::default_traits_t继承时, restinio::default_traits_t的标准定义也connection_state_listener_t被继承。 但是,如果在后续类中定义了新名称connection_state_listener_t


 struct my_traits : public restinio::default_traits_t { using connection_state_listener_t = my_state_listener; ... }; 

然后新名称将隐藏对connection_state_listener_t的继承定义。 如果没有新定义,则旧定义仍然可见。


因此,如果用户未为connection_state_listener_t定义自己的值,则RESTinio将使用默认值noop_listener_t ,该值由RESTinio以特殊方式处理。 例如:


  • 在这种情况下,RESTinio根本不为connection_state_listener_t存储shared_ptr。 因此,禁止调用connection_state_listener方法(这样的调用将导致编译时错误)。
  • RESTinio不会进行与更改连接状态有关的任何其他调用。

以及关于如何实现所有这些的内容,将在下面进行讨论。


如何在RESTinio中实现?


因此,在RESTinio代码中,您需要检查connection_state_listener_t的定义在服务器属性中connection_state_listener_t什么值,并且取决于此值:


  • 存储或不存储connecton_state_listener_t类型的对象的shared_ptr实例;
  • 允许或禁止调用connection_state_listener方法来设置HTTP服务器参数;
  • 在开始HTTP服务器操作之前,检查或不检查是否存在指向connection_state_listener_t类型的对象的当前指针;
  • 当与客户端的连接状态更改时,对state_changed方法进行或不进行调用。

RESTinio仍在作为C ++ 14库开发的边界条件上也添加了它,因此,您不能在实现中使用C ++ 17的功能(如果使用constexpr,则相同)。


所有这些都是通过简单的技巧实现的:模板类及其对restinio::connection_state::noop_listener_t类型的restinio::connection_state::noop_listener_t 。 例如,这是服务器参数中类型为connection_state_listener_t的对象的shared_ptr存储方式。 第一部分:


 template< typename Listener > struct connection_state_listener_holder_t { ... //  compile-time . std::shared_ptr< Listener > m_connection_state_listener; static constexpr bool has_actual_connection_state_listener = true; void check_valid_connection_state_listener_pointer() const { if( !m_connection_state_listener ) throw exception_t{ "connection state listener is not specified" }; } }; template<> struct connection_state_listener_holder_t< connection_state::noop_listener_t > { static constexpr bool has_actual_connection_state_listener = false; void check_valid_connection_state_listener_pointer() const { // Nothing to do. } }; 

此处定义的模板结构具有有用的内容或没有有用的内容。 仅对于类型noop_listener_t ,它没有有用的内容。


第二部分:


 template<typename Derived, typename Traits> class basic_server_settings_t : public socket_type_dependent_settings_t< Derived, typename Traits::stream_socket_t > , protected connection_state_listener_holder_t< typename Traits::connection_state_listener_t > , protected ip_blocker_holder_t< typename Traits::ip_blocker_t > { ... }; 

包含HTTP服务器参数的类是从connection_state_listener_holder_t继承的。 因此,服务器参数要么显示connection_state_listener_t类型的对象的shared_ptr,要么不显示。


我必须说在参数中存储或不存储shared_ptr都是鲜花。 但是,只有当connection_state_listener_tnoop_listener_t不同时,尝试使basic_server_settings_t用于状态侦听器的方法可用时,浆果才noop_listener_t


理想情况下,我想使编译器“根本看不到”。 但是我很痛苦地为std::enable_if编写条件以隐藏这些方法。 因此,只限于添加static_asser:


 Derived & connection_state_listener( std::shared_ptr< typename Traits::connection_state_listener_t > listener ) & { static_assert( has_actual_connection_state_listener, "connection_state_listener(listener) can't be used " "for the default connection_state::noop_listener_t" ); this->m_connection_state_listener = std::move(listener); return reference_to_derived(); } Derived && connection_state_listener( std::shared_ptr< typename Traits::connection_state_listener_t > listener ) && { return std::move(this->connection_state_listener(std::move(listener))); } const std::shared_ptr< typename Traits::connection_state_listener_t > & connection_state_listener() const noexcept { static_assert( has_actual_connection_state_listener, "connection_state_listener() can't be used " "for the default connection_state::noop_listener_t" ); return this->m_connection_state_listener; } void ensure_valid_connection_state_listener() { this->check_valid_connection_state_listener_pointer(); } 

只是有一瞬间,我碰巧后悔在C ++中,如果constexpr与在D中的static不同,那么感到很遗憾。 通常在C ++ 14中没有类似的东西:(


在这里,您还可以查看ensure_valid_connection_state_listener方法的可用性。 在http_server_t构造函数中调用此方法,以验证服务器参数是否包含所有必需的值:


 template<typename D> http_server_t( io_context_holder_t io_context, basic_server_settings_t< D, Traits > && settings ) : m_io_context{ io_context.giveaway_context() } , m_cleanup_functor{ settings.giveaway_cleanup_func() } { // Since v.0.5.1 the presence of custom connection state // listener should be checked before the start of HTTP server. settings.ensure_valid_connection_state_listener(); ... 

同时,在ensure_valid_connection_state_listener内部ensure_valid_connection_state_listener了从connection_state_listener_holder_t继承ensure_valid_connection_state_listener方法,由于connection_state_listener_holder_t专门化,因此该方法要么进行实际检查,要么不执行任何操作。


如果用户要使用状态侦听器,则使用类似的技巧来调用当前的state_changed ,否则不调用任何其他方法。


首先,我们需要另一个state_listener_holder_t选项:


 namespace connection_settings_details { template< typename Listener > struct state_listener_holder_t { std::shared_ptr< Listener > m_connection_state_listener; template< typename Settings > state_listener_holder_t( const Settings & settings ) : m_connection_state_listener{ settings.connection_state_listener() } {} template< typename Lambda > void call_state_listener( Lambda && lambda ) const noexcept { m_connection_state_listener->state_changed( lambda() ); } }; template<> struct state_listener_holder_t< connection_state::noop_listener_t > { template< typename Settings > state_listener_holder_t( const Settings & ) { /* nothing to do */ } template< typename Lambda > void call_state_listener( Lambda && /*lambda*/ ) const noexcept { /* nothing to do */ } }; } /* namespace connection_settings_details */ 

与之前显示的用来在整个服务器的参数中(即在basic_server_settings_t类型的对象中)存储连接状态侦听器的connection_state_listener_holder_t不同,该state_listener_holder_t将用于类似目的,而不是用于整个服务器的参数,而是用于单独的服务器连接:


 template < typename Traits > struct connection_settings_t final : public std::enable_shared_from_this< connection_settings_t< Traits > > , public connection_settings_details::state_listener_holder_t< typename Traits::connection_state_listener_t > { using connection_state_listener_holder_t = connection_settings_details::state_listener_holder_t< typename Traits::connection_state_listener_t >; ... 

这里有两个功能。


首先,初始化state_listener_holder_t 。 是否需要它。 但是只有state_listener_holder_t知道这一点。 因此,正如他们所说,为state_listener_holder_tconnection_settings_t构造函数只是简单地“拉” state_listener_holder_t构造函数:


 template < typename Settings > connection_settings_t( Settings && settings, http_parser_settings parser_settings, timer_manager_handle_t timer_manager ) : connection_state_listener_holder_t{ settings } , m_request_handler{ settings.request_handler() } 

而且state_listener_holder_t构造函数state_listener_holder_t要么执行必要的操作,要么什么都不做(在后一种情况下,或多或少明智的编译器将根本不生成任何代码来初始化state_listener_holder_t )。


其次,是state_listner_holder_t::call_state_listener ,它对状态侦听器进行state_changed调用。 如果没有状态侦听器,则为否。 在RESTinio诊断连接状态更改的地方call_state_listenercall_state_listener 。 例如,当检测到连接已关闭时:


 void close() { m_logger.trace( [&]{ return fmt::format( "[connection:{}] close", connection_id() ); } ); ... // Inform state listener if it used. m_settings->call_state_listener( [this]() noexcept { return connection_state::notice_t{ this->connection_id(), this->m_remote_endpoint, connection_state::cause_t::closed }; } ); } 

call_state_listener传递给call_state_listener ,从该call_state_listener返回带有连接状态信息的notice_t对象。 如果存在实际的侦听器,则确实会调用此lambda,并将其返回的值传递给state_changed


但是,如果没有侦听器,则call_state_listener将为空,因此不会调用任何lambda。 实际上,普通的编译器只是将所有调用都抛出一个空的call_state_listener 。 在这种情况下,在生成的代码中,根本没有任何内容与侦听器正在访问的连接状态有关。


也是IP拦截器


在RESTinio-0.5.1中,除了连接状态侦听器之外,还添加了IP阻止程序 。 即 用户可以为每个新的传入连接指定RESTinio将“拉”的对象。 如果IP阻止程序说您可以使用该连接,则RESTinio将开始对新连接的常规维护(它将读取和解析请求,调用请求处理程序,控制超时等)。 但是,如果IP阻止程序禁止使用该连接,则RESTinio会愚蠢地关闭此连接,并且不再执行任何操作。


像状态侦听器一样,IP阻止程序是一项可选功能。 要使用IP阻止程序,必须明确启用它。 通过HTTP服务器的属性。 就像连接状态监听器一样。 RESTinio中IP阻止程序支持的实现使用了上面已经描述的相同技术。 因此,我们将不讨论在RESTinio中如何使用IP阻止程序。 而是考虑一个IP阻止程序和状态侦听器都是同一对象的示例。


分析标准示例ip_blocker


在版本0.5.1中,标准RESTinio示例中包括另一个示例: ip_blocker 。 本示例演示如何限制从单个IP地址到服务器的并发连接数。


这不仅需要一个IP阻止程序,它可以允许或禁止接受连接。 而且还是连接状态的监听器。 需要一个侦听器以跟踪创建和关闭连接的时刻。


同时,IP阻止程序和侦听器将需要相同的数据集。 因此,最简单的解决方案是使IP阻止程序和侦听器成为同一对象。


没问题,我们可以轻松做到这一点:


 class blocker_t { std::mutex m_lock; using connections_t = std::map< restinio::asio_ns::ip::address, std::vector< restinio::connection_id_t > >; connections_t m_connections; public: //   IP-blocker-. restinio::ip_blocker::inspection_result_t inspect( const restinio::ip_blocker::incoming_info_t & info ) noexcept {...} //     . void state_changed( const restinio::connection_state::notice_t & notice ) noexcept {...} }; 

在这里,我们没有任何接口的继承或继承的虚方法的替代。 侦听器的唯一要求是state_changed方法的存在。 满足此要求。


同样,对于IP阻止程序的唯一要求是:是否存在带有所需签名的inspect方法? 有! 所以一切都很好。


然后剩下的就是确定HTTP服务器的正确属性:


 struct my_traits_t : public restinio::default_traits_t { using logger_t = restinio::shared_ostream_logger_t; //      . using connection_state_listener_t = blocker_t; using ip_blocker_t = blocker_t; }; 

之后,仅保留创建blocker_t的实例并将其作为参数传递给HTTP服务器:


 auto blocker = std::make_shared<blocker_t>(); restinio::run( ioctx, restinio::on_thread_pool<my_traits_t>( std::thread::hardware_concurrency() ) .port( 8080 ) .address( "localhost" ) .connection_state_listener( blocker ) .ip_blocker( blocker ) .max_pipelined_requests( 4 ) .handle_request_timeout( std::chrono::seconds{20} ) .request_handler( [&ioctx](auto req) { return handler( ioctx, std::move(req) ); } ) ); 

结论


关于C ++模板


在我看来,C ++模板就是所谓的“大炮”。 即 如此强大的功能,您不由自主地必须考虑如何合理地使用它。 因此,现代C ++社区似乎被划分为多个交战阵营。


其中之一的代表希望远离模板。 因为模板很复杂,所以它们会生成无法读取的错误消息表的长度,从而大大增加了编译时间。 更不用说关于膨胀代码和降低性能的城市传说了。


另一个阵营的代表(如我)相信模板是C ++最强大的方面之一。 模板甚至有可能是C ++在现代世界中为数不多的最重要的竞争优势之一。 因此,我认为C ++的未来正是模板。 随着时间的流逝,与模板广泛使用相关的一些当前不便(例如冗长而耗费资源的编译或无信息的错误消息)将被消除。


因此,在我个人看来,在RESTinio的实现过程中选择的方法(即模板的广泛使用和通过属性设置HTTP服务器的特征)仍然会奏效。 因此,我们可以针对特定需求进行良好的自定义。 同时,从字面上看,我们不为不使用的商品付费。


但是,另一方面,似乎C ++模板中的编程仍然不合理地复杂。 当您不必经常编程时,而是在不同活动之间进行切换时,您会特别感到。 您将因编码而分心数周,然后您将返回并开始公开地公开表示自己(如果有必要的话),然后使用SFINAE隐藏某种方法,或者检查对象上是否存在具有特定签名的方法。


因此,最好使用C ++。 如果使它们达到这样的状态,甚至像我这样的初学者也可以轻松地使用C ++模板,而不必每隔10-15分钟研究cppreference和stackoverflow,那就更好了。


关于RESTinio的当前状态和RESTinio的未来功能。 不仅是RESTinio


目前,RESTinio正在基于“有时间有心愿单”的原则进行开发。 例如,在2018年秋季和2019年冬季,我们没有太多时间来开发RESTinio。 他们回答了用户的问题,进行了较小的更改,但是对于更多的事情,我们的资源还不够。


但是在2019年春末,有时间使用RESTinio,我们首先将RESTinio设置为0.5.0 ,然后是0.5.1 。 同时,我们和其他人的愿望清单的供应已用尽。 即 我们自己希望在RESTinio中看到的内容以及用户告诉我们的内容已经在RESTinio中。


显然,RESTinio可以填充更多内容。 但是到底是什么呢?


答案很简单:只有我们被要求进入RESTinio。 因此,如果您想在RESTinio中看到自己需要的东西,那么花点时间告诉我们(例如,通过GitHubBitBucket上的问题,或者通过Google小组 ,或者直接在Habré上的评论中) 。 你什么也不会说-你什么也不会收到;)


实际上,其他项目,尤其是SObjectizer ,情况也是如此 。 他们的新版本将在收到可理解的愿望清单后发布。


好吧,最后,我想向尚未尝试过RESTinio的所有人提供:尝试一下 免费的 不伤人。 突然喜欢上它。 如果您不喜欢它,请准确分享。 这将帮助我们使RESTinio更加方便和实用。

Source: https://habr.com/ru/post/zh-CN456632/


All Articles