博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【刘文彬】RPC的基础:调研EOS插件http_plugin
阅读量:5862 次
发布时间:2019-06-19

本文共 16575 字,大约阅读时间需要 55 分钟。

原文链接:

区块链的应用是基于http服务,这种能力在EOS中是依靠http_plugin插件赋予的。

关键字:通讯模式,add_api,http server,https server,unix server,io_service,socket,connection

通讯模式

EOS中,一个插件的使用要先获取其实例,例如http_plugin获取实例的语句是:

auto& _http_plugin = app().get_plugin
();

其他插件的获取方式与此相同。目前为止,包括前文介绍到的method、channel、信号槽、信号量,跨模块的交互方式可以总结为五种:

  • method,插件之间的调用,一个插件A将其函数按key注册到method池中,其他任意数量的插件B、C、D均可通过key去method池中找到该函数并调用。这种通讯模式是一个由调用者主动发起的过程。
  • channel,插件之间的调用,一个插件A按key找到频道并向频道publish一个动作,其他任意数量的插件B、C、D,甚至在不同节点上的插件B、C、D,只要是按key订阅了该channel并绑定了他们各自本地的一个notify function,就会被触发执行。这种通讯模式是基于发布订阅模式,或者说是更高级的观察者模式,是由发布者的行为交由channel来触发所有订阅者绑定的本地通知函数的过程。
  • 信号槽,插件与controller的交互过程。controller下启基于chainbase的状态数据库,上承信号的管理,通过信号来与外部进行交互,controller会根据链的行为emit一个对应的信号出来,其他插件如果有处理该信号的需求会连接connect该信号并绑定函数实现。有时候一个信号会被多个插件所连接,例如accepted_block_header信号,是承认区块头的信号,会被net_plugin捕捉并处理,同时该信号也会被chain_plugin所捕捉,触发广播。
  • 信号量,一般是应用程序与操作系统发生的交互,在EOS中,应用程序的实例是application,它与操作系统发生的交互都是通过信号量来完成,首先声明一个信号,然后通过async_wait触发信号完成与操作系统的交互。
  • 实例调用,对比以上四种松散的方式,这种模式是强关联,正如我们刚刚学习编程时喜欢使用new/create而不考虑对象的垃圾处理以及实例管理,后来会采用解耦的松散的统一实例管理框架,或者采用单例而不是每次都要new/create。但这种方式并不是完全不被推荐的,当实例的某个成员直接被需要时,可以直接通过该方式获取到,而不是通过以上四种方式来使用。

目前总结出来的五种跨模块交互方式,前四种更注重通讯,最后一种更注重其他模块的内容。更注重通讯的前四种是基于同一底层通讯机制(socket),但适用于不同场景的设计实现。

add_api函数

从chain_api_plugin过来,http_plugin的使用方式是:

_http_plugin.add_api({      CHAIN_RO_CALL(get_info, 200l),      ...   });

那么,就从add_api入手研究http_plugin。add_api函数声明在http_plugin头文件中,说明该函数的内容很少或很具备通用性。

void add_api(const api_description& api) {   for (const auto& call : api)      add_handler(call.first, call.second);}

从前面的调用代码可以看出,add_api函数的参数是一个对象集合,它们总体是一个api_description类型的常量引用。

using api_description = std::map
;

api_description根据源码可知是一个map,key为string类型的url路径地址,值为url_handler是具体实现API功能的处理函数。在add_api的调用部分,宏CHAIN_RO_CALL调用了另一个宏CALL,CALL组装了map的这两个数:

#define CALL(api_name, api_handle, api_namespace, call_name) \{std::string("/v1/" #api_name "/" #call_name), \   [api_handle](string, string body, url_response_callback cb) mutable { \          try { \             if (body.empty()) body = "{}"; \             auto result = api_handle.call_name(fc::json::from_string(body).as
()); \ cb(200, fc::json::to_string(result)); \ } catch (...) { \ http_plugin::handle_exception(#api_name, #call_name, body, cb); \ } \ }}

CALL宏体包含两个数据,以逗号隔开,前面部分为url路径地址,后面部分为api_handler,此处实际上是一个匿名内部函数。回到add_api函数的声明,遍历整个api,逐一执行add_handler为url和api处理函数添加相互绑定的关系。

add_handler函数

直接进入函数实现的代码:

void http_plugin::add_handler(const string& url, const url_handler& handler) {  ilog( "add api url: ${c}", ("c",url) ); // 输出日志  app().get_io_service().post([=](){    my->url_handlers.insert(std::make_pair(url,handler));  });}

app()前文讲到了,是用来获取application实例的,其包含一个public权限的成员函数get_io_service:

boost::asio::io_service& get_io_service() { return *io_serv; }

返回的是基于boost::asio::io_service库的共享指针类型,application的私有成员io_serv的指针。

io_service是asio框架中的调度器,用来调度异步事件,application实例要保存一个io_service对象,用于保存当前实例的所有待调度的异步事件。

io_service的两个重要方法:

  • post,用于发布一个异步事件,依赖asio库进行自动调度,不需要显式调用函数。
  • run,显式调用,同步执行回调函数。

当appbase.exec()执行时,io_service会同步启动,如果一个插件需要IO或其他异步操作,可以通过以下方式进行分发:

app().get_io_service().post( lambda )

那么,这种分发方式,除了在http_plugin的add_handler函数中使用到,EOSIO/eos中在bnet_plugin插件中有大量使用到,缘于bnet_plugin对异步事件发布的需求。回到add_handler函数,post后面跟随的是lambda表达式,[=]代表捕获所有以值访问的局部名字。lambda体是将url和handler作为二元组插入到http_plugin_impl对象的唯一指针my的共有成员url_handlers集合中,数据类型与上面的api_description一致。

url_handlers集合

url_handlers集合的数据源是其他插件通过add_api函数传入组装好的url和handler的对象。该集合作为api的异步处理器集合,在http_plugin中消费该集合数据的是handle_http_request函数。该函数处理外部请求,根据请求url在url_handlers集合中查找数据,找到handler以后,传入外部参数数据并执行handler对应的处理函数。

handle_http_request函数

/** * 处理一个http请求(http_plugin) * @tparam T socket type * @param con 连接对象 */template
void handle_http_request(typename websocketpp::server
::connection_ptr con) { try { auto& req = con->get_request(); // 获得请求对象req。 if(!allow_host
(req, con))// 检查host地址是否有效 return; // 根据config.ini中http_plugin相关的连接配置项进行设置。 if( !access_control_allow_origin.empty()) { con->append_header( "Access-Control-Allow-Origin", access_control_allow_origin ); } if( !access_control_allow_headers.empty()) { con->append_header( "Access-Control-Allow-Headers", access_control_allow_headers ); } if( !access_control_max_age.empty()) { con->append_header( "Access-Control-Max-Age", access_control_max_age ); } if( access_control_allow_credentials ) { con->append_header( "Access-Control-Allow-Credentials", "true" ); } if(req.get_method() == "OPTIONS") { // HTTP method包含:`GET` `HEAD` `POST` `OPTIONS` `PUT` `DELETE` `TRACE` `CONNECT` con->set_status(websocketpp::http::status_code::ok); return;// OPTIONS不能缓存,未能获取到请求的资源。 } con->append_header( "Content-type", "application/json" );// 增加请求头。 auto body = con->get_request_body(); // 获得请求体(请求参数) auto resource = con->get_uri()->get_resource(); // 获得请求的路径(url) auto handler_itr = url_handlers.find( resource ); // 在url_handlers集合中找到对应的handler if( handler_itr != url_handlers.end()) { con->defer_http_response();// 延时响应 // 调用handler,传入参数、url,回调函数是lambda表达式,用于将接收到的结果code和响应body赋值给连接。 handler_itr->second( resource, body, [con]( auto code, auto&& body ) { con->set_body( std::move( body )); // 接收到的响应body赋值给连接。 con->set_status( websocketpp::http::status_code::value( code )); // 接收到的code赋值给连接。 con->send_http_response();// 发送http响应 } ); } else { dlog( "404 - not found: ${ep}", ("ep", resource)); // 未在url_handlers集合中找到 // 针对失败的情况,设置http的响应对象数据。 error_results results{websocketpp::http::status_code::not_found, "Not Found", error_results::error_info(fc::exception( FC_LOG_MESSAGE( error, "Unknown Endpoint" )), verbose_http_errors )}; con->set_body( fc::json::to_string( results )); con->set_status( websocketpp::http::status_code::not_found ); } } catch( ... ) { handle_exception
( con ); }}

下面来看该函数handle_http_request的使用位置。有两处,均在http_plugin内部:

  • create_server_for_endpoint函数,为websocket对象ws设置http处理函数,是一个lambda表达式,lambda体为handle_http_request函数的调用,传入连接对象con,由hdl转换而来。另外,create_server_for_endpoint函数在http_plugin::plugin_startup中也有两处调用。
  • http_plugin::plugin_startup,插件的启动阶段,下面将分析该插件的生命周期。

http_plugin的生命周期

正如研究其他的插件一样,学习路线离不开插件的生命周期。

插件一般都是在程序入口(例如nodeos,keosd)进行生命周期的控制的,一般不做区分,由于插件有共同基类,程序入口做统一控制。

下面依次介绍http_plugin的生命周期。

http_plugin::set_defaults

仅属于http_plugin插件的生命周期。设置默认值,默认值仅包含三项:

struct http_plugin_defaults {  // 如果不为空,该项的值将在被监听的地址生效。作为不同配置项的前缀。  string address_config_prefix;  // 如果为空,unix socket支持将被完全禁用。如果不为空,值为data目录的相对路径,作为默认路径启用unix socket支持。  string default_unix_socket_path;  // 如果不是0,HTTP将被启用于默认给出的端口号。如果是0,HTTP将不被默认启用。  uint16_t default_http_port{0};};

nodeos的set_defaults语句为:

http_plugin::set_defaults({    .address_config_prefix = "",    .default_unix_socket_path = "",    .default_http_port = 8888});

keosd的set_defaults语句为:

http_plugin::set_defaults({    .address_config_prefix = "",    // key_store_executable_name = "keosd";    .default_unix_socket_path = keosd::config::key_store_executable_name + ".sock", // 默认unix socket路径为keosd.sock    .default_http_port = 0});

http_plugin::set_program_options

设置http_plugin插件的参数,构建属于http_plugin的配置选项,将与其他插件的配置共同组成配置文件config.ini,在此基础上添加--help等参数构建程序(例如nodeos)的CLI命令行参数。同时设置参数被设置以后的处理方案。

/** * 生命周期 http_plugin::set_program_options * @param cfg 命令行和配置文件的手动配置项的并集,交集以命令行配置为准的配置对象。 */void http_plugin::set_program_options(options_description&, options_description& cfg) {   // 处理默认set_defaults配置项。  my->mangle_option_names();  if(current_http_plugin_defaults.default_unix_socket_path.length())// 默认unix socket 路径     cfg.add_options()        (my->unix_socket_path_option_name.c_str(), bpo::value
()->default_value(current_http_plugin_defaults.default_unix_socket_path), "The filename (relative to data-dir) to create a unix socket for HTTP RPC; set blank to disable."); if(current_http_plugin_defaults.default_http_port)// 设置默认http端口 cfg.add_options() (my->http_server_address_option_name.c_str(), bpo::value
()->default_value("127.0.0.1:" + std::to_string(current_http_plugin_defaults.default_http_port)), "The local IP and port to listen for incoming http connections; set blank to disable."); else cfg.add_options() (my->http_server_address_option_name.c_str(), bpo::value
(), "The local IP and port to listen for incoming http connections; leave blank to disable.");// 端口配置为空的话禁用http // 根据手动配置项来设置 cfg.add_options() (my->https_server_address_option_name.c_str(), bpo::value
(), "The local IP and port to listen for incoming https connections; leave blank to disable.")// 端口配置为空的话禁用http ("https-certificate-chain-file", bpo::value
(),// https的配置,证书链文件 "Filename with the certificate chain to present on https connections. PEM format. Required for https.") ("https-private-key-file", bpo::value
(),// https的配置,私钥文件 "Filename with https private key in PEM format. Required for https") ("access-control-allow-origin", bpo::value
()->notifier([this](const string& v) {// 跨域问题,控制访问源 my->access_control_allow_origin = v; ilog("configured http with Access-Control-Allow-Origin: ${o}", ("o", my->access_control_allow_origin)); }), "Specify the Access-Control-Allow-Origin to be returned on each request.") ("access-control-allow-headers", bpo::value
()->notifier([this](const string& v) {// 控制允许访问的http头 my->access_control_allow_headers = v; ilog("configured http with Access-Control-Allow-Headers : ${o}", ("o", my->access_control_allow_headers)); }), "Specify the Access-Control-Allow-Headers to be returned on each request.") ("access-control-max-age", bpo::value
()->notifier([this](const string& v) {// 控制访问的最大缓存age my->access_control_max_age = v; ilog("configured http with Access-Control-Max-Age : ${o}", ("o", my->access_control_max_age)); }), "Specify the Access-Control-Max-Age to be returned on each request.") ("access-control-allow-credentials", bpo::bool_switch()->notifier([this](bool v) { my->access_control_allow_credentials = v; if (v) ilog("configured http with Access-Control-Allow-Credentials: true"); })->default_value(false), // 控制访问允许的证书 "Specify if Access-Control-Allow-Credentials: true should be returned on each request.") // 最大请求体的大小,默认为1MB。 ("max-body-size", bpo::value
()->default_value(1024*1024), "The maximum body size in bytes allowed for incoming RPC requests") // 打印http详细的错误信息到日志,默认为false,不打印。 ("verbose-http-errors", bpo::bool_switch()->default_value(false), "Append the error log to HTTP responses") // 校验host,如果设置为false,任意host均为有效。默认为true,要校验host。 ("http-validate-host", boost::program_options::value
()->default_value(true), "If set to false, then any incoming \"Host\" header is considered valid") // 别名。另外可接受的host头 ("http-alias", bpo::value
>()->composing(), "Additionaly acceptable values for the \"Host\" header of incoming HTTP requests, can be specified multiple times. Includes http/s_server_address by default.");}

http_plugin::plugin_initialize

插件初始化的操作。读取配置并做出处理。

实际上,在set_option_program阶段也做了对配置值的读取及转储处理。原因是一些默认参数,即用户不经常配置的选项,就不需要读取用户配置的选项,可以在set_option_program阶段做出处理,而那些需要用户来配置的选项则需要在初始化阶段读入并处理。

初始化阶段读入的配置项包含:

  • validate_host,是否校验host,bool类型的值。
  • valid_hosts,添加alias别名作为有效host。
  • listen_endpoint,根据在set_option_program阶段赋值的my成员http_server_address_option_name,重组处理得到监听点,同时添加至valid_hosts。
  • unix_endpoint,同样根据my成员unix_socket_path_option_name处理,得到绝对路径赋值给unix_endpoint。
  • 对set_option_program阶段赋值的my成员https_server_address_option_name的值的处理,https的两个配置的处理,最终重组处理,分别赋值给my成员https_listen_endpoint,https_cert_chain,https_key,以及valid_hosts。
  • max_body_size,直接赋值。

    当然在初始化阶段仍旧可以配置set_option_program阶段已做出处理的配置项,以用户配置为准。

http_plugin::plugin_startup

在插件中,启动阶段都是非常重要的生命周期。它往往代码很简单甚至简略,但功能性很强。下面来看http_plugin的启动阶段的内容,g共分为三部分:

  • listen_endpoint,本地节点的http监听路径,例如127.0.0.1:8888。
  • unix_endpoint,如果为空,unix socket支持将被完全禁用。如果不为空,值为data目录的相对路径,作为默认路径启用unix socket支持。
  • https_listen_endpoint,https版本的本地节点http监听路径,一般不设置,对应的是配置中的https_server_address选项。

对于以上三种情况,启动阶段分别做了三种对应的处理,首先来看最标准最常见的情况,就是基于http的本地监听路径listen_endpoint:

if(my->listen_endpoint) {    try {        my->create_server_for_endpoint(*my->listen_endpoint, my->server); // 创建http服务(上面介绍到的函数)。内部调用了http请求处理函数。        ilog("start listening for http requests");        my->server.listen(*my->listen_endpoint);// 手动监听设置端点。使用设置绑定内部接收器。        my->server.start_accept();// 启动服务器的异步连接,开始监听:无限循环接收器。启动服务器连接无限循环接收器。监听后必须调用。在底层io_service开始运行之前,此方法不会有任何效果。它可以在io_service已经运行之后被调用。有关如何停止此验收循环的说明,请参阅传输策略的文档。    } catch ( const fc::exception& e ){        elog( "http service failed to start: ${e}", ("e",e.to_detail_string()));        throw;    } catch ( const std::exception& e ){        elog( "http service failed to start: ${e}", ("e",e.what()));        throw;    } catch (...) {        elog("error thrown from http io service");        throw;    }}

主要是启动http服务的流程,包括客户端和服务端,endpoint和server_endpoint两个角色的启动。下面来看基于unix socket的情况unix_endpoint:

if(my->unix_endpoint) {    try {        my->unix_server.clear_access_channels(websocketpp::log::alevel::all);// 清除所有登陆的频道        my->unix_server.init_asio(&app().get_io_service());// 初始化io_service对象,io_service就是上面分析过的application的io_service对象,传入asio初始化函数初始化asio传输策略。在使用asio transport之前必须要init asio。        my->unix_server.set_max_http_body_size(my->max_body_size); // 设置HTTP消息体大小的最大值,该值决定了如果超过这个值的消息体将导致连接断开。        my->unix_server.listen(*my->unix_endpoint); // 手动设置本地socket监听路径。        my->unix_server.set_http_handler([&](connection_hdl hdl) {// 设置http请求处理函数(注意此处不再通过create_server_for_endpoint函数来调用,因为不再需要websocket的包装)。           my->handle_http_request
( my->unix_server.get_con_from_hdl(hdl)); }); my->unix_server.start_accept();// 同上,启动server端的无限循环接收器。 } catch ( const fc::exception& e ){ elog( "unix socket service failed to start: ${e}", ("e",e.to_detail_string())); throw; } catch ( const std::exception& e ){ elog( "unix socket service failed to start: ${e}", ("e",e.what())); throw; } catch (...) { elog("error thrown from unix socket io service"); throw; }}

下面来看基于https的本地监听路径https_listen_endpointd的处理:

if(my->https_listen_endpoint) {    try {        my->create_server_for_endpoint(*my->https_listen_endpoint, my->https_server); // 同上http的原理,只是参数换为https的值。        // 设置TLS初始化处理器。当请求一个TLS上下文使用时,将调用该TLS初始化处理器。该处理器必须返回一个有效TLS上下文,以支持当前端点能够初始化TLS连接。        // connection_hdl,一个连接的唯一标识。它是实现了一个弱引用智能指针weak_ptr指向连接对象。线程安全。通过函数endpoint::get_con_from_hdl()可以转化为一个完整的共享指针。        my->https_server.set_tls_init_handler([this](websocketpp::connection_hdl hdl) -> ssl_context_ptr{           return my->on_tls_init(hdl);         });        ilog("start listening for https requests");        my->https_server.listen(*my->https_listen_endpoint);// 同上http的原理,监听地址。        my->https_server.start_accept();// 同上http的原理,启动服务。    } catch ( const fc::exception& e ){        elog( "https service failed to start: ${e}", ("e",e.to_detail_string()));        throw;    } catch ( const std::exception& e ){        elog( "https service failed to start: ${e}", ("e",e.what()));        throw;    } catch (...) {        elog("error thrown from https io service");        throw;    }}

unix server与server的底层实现是一致的,只是外部的包裹处理不同,https_server的类型再加上这个ssl上下文的类型指针ssl_context_ptr。他们的声明分别是:

using websocket_server_type = websocketpp::server
>; // http serverusing websocket_local_server_type = websocketpp::server
; // unix serverusing websocket_server_tls_type = websocketpp::server
>; // https serverusing ssl_context_ptr = websocketpp::lib::shared_ptr
; // https ssl_context_ptr
HTTPS = HTTP over TLS。TLS的前身是SSL。

从上面的声明可以看出,http和https最大的不同是,前者是basic_socket,后者是tls_socket,socket类型不同,http是基础socket,https是包裹了tls的socket。

http_plugin::plugin_shutdown

关闭是插件的最后一个生命周期,代码很少,主要执行的是资源释放工作。

void http_plugin::plugin_shutdown() {  if(my->server.is_listening())     my->server.stop_listening();  if(my->https_server.is_listening())     my->https_server.stop_listening();}

此处没有unix_server的处理。http和https都是socket,需要手动停止监听,启动无限循环接收器。unix server是通过io_service来异步处理,底层实现逻辑相同,也启动了无限循环接收器。

总结

本文首先以外部使用http_plugin的方式:add_api函数为研究入口,逐层深入分析。接着从整体上研究了http_plugin的生命周期,进一步加深了对http_plugin的http/https/unix三种server的认识。

转载于:https://blog.51cto.com/13625500/2327890

你可能感兴趣的文章
No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=arm64, VALID_ARCHS=armv7 armv7s)
查看>>
Exchange Server 2016 独立部署/共存部署 (七)—— DAG功能测试
查看>>
Linq==数据访问层?
查看>>
对RTMP视频流进行BitmapData.draw()出错的解决办法
查看>>
Linux 进程中 Stop, Park, Freeze【转】
查看>>
Vertica的这些事<十>—— vertica中group by 和join 语句的优化
查看>>
Spark修炼之道(基础篇)——Linux大数据开发基础:第九节:Shell编程入门(一)...
查看>>
MySQL中如何启用InnoDB数据引擎
查看>>
Duplicate Symbol链接错误的原因总结和解决方法[转]
查看>>
适配器模式
查看>>
刨根问底区块链 —— 基础篇
查看>>
php 直接调用svn命令
查看>>
建立低权限的ftp帐号
查看>>
htpasswd
查看>>
Android窗口机制(三)Window和WindowManager的创建与Activity
查看>>
Android 编译出错解决
查看>>
iOS--The request was denied by service delegate (SBMainWorkspace) for reason:
查看>>
Android 打开WIFI并快速获取WIFI的信息
查看>>
Spring boot 入门篇
查看>>
【IOS开发】GDataXML解析XML
查看>>