steemd 源码分析1 JSON RPC机制

2018/03/07 c++ steem blockchain

steemd通过JSON-RPC 2.0对外提供API调用, 本篇主要分析steemd通过何种机制将各个类中的方法变成JSON-RPC的api的。

注意: steemdJSON-RPC 2.0做了一个特殊转换 (可能是兼容历史API?):如果method字段为"call"时,将params列表的第一个值作为method使用。

{
    "jsonrpc":"2.0",
    "id":0,
    "method":"call",
    "params":[
        "database_api",
        "get_content",
        [
            "htcptc",
            "it-s-a-great-date-8f5a62660a36f"
        ]
    ]
}

steemd下主要的API插件都位于源码的libraries/plugins/apis/ 目录下。

API类的声明

下面是一个API类的基本声明:

class account_by_key_api
{
   public:
      account_by_key_api();
      ~account_by_key_api();

      DECLARE_API( (get_key_references) )

   private:
      std::unique_ptr< detail::account_by_key_api_impl > my;
};

steemd自己用宏实现一套API声明注册的框架,每个API,对应一个class,如account_by_key_api。这个class并 不实现任何API的逻辑,可以为是这个框架必须的一个容器,每个API下的方法对应的实现,有存储在class内的my指 针上,该指针必须使用这个名称。然后需要对外暴露哪些方法,则使用DECLARE_API/DEFINE_READ_APIS来声明、 实现这个代理层,然后其生成的方法会调用my指针指向的实现类里对应的方法。

该类中每个方法通过DECLARE_API 来宏来声明,支持一次声明多个方法。例如:

  DECLARE_API( (push_block) (push_transaction) )

该宏的展开是:

# 对于每个method都声明这么对应一个函数:
${method}_return ${method}(const ${method}_args & arg, bool lock = false );
...

# 然后再添加一个模板
template< typename Lambda >
void for_each_api( Lambda&& callback )
{
  # 对于每个method都声明这么对应的一个代码块
  {
    typedef std::remove_pointer<decltype(this)>::type this_type;

    callback(*this, "${method}", &this_type::${method},
      static_cast< ${method}_args *>(nullptr),
      static_cast< ${method}_return *>(nullptr)
    );
  }
  ...
}

拿上面的account_by_key_api来实例展开就是:

class account_by_key_api
{
   public:
      account_by_key_api();
      ~account_by_key_api();

      get_key_references_return get_key_references(const get_key_references_args & arg, bool lock = false );

      template< typename Lambda >
      void for_each_api( Lambda&& callback )
      {
         {
            typedef std::remove_pointer<decltype(this)>::type this_type;

            callback(*this, "get_key_references", &this_type::get_key_references,
               static_cast< get_key_references_args *>(nullptr),
               static_cast< get_key_references_return *>(nullptr)
            );
         }
      }

   private:
      std::unique_ptr< detail::account_by_key_api_impl > my;
};

其中for_each_api中的代码块其实还能再进行一次“展开”:

class account_by_key_api
{
   public:
      account_by_key_api();
      ~account_by_key_api();

      get_key_references_return get_key_references(const get_key_references_args & arg, bool lock = false );

      template< typename Lambda >
      void for_each_api( Lambda&& callback )
      {
            callback(*this, "get_key_references", &account_by_key_api::get_key_references,
               static_cast< get_key_references_args *>(nullptr),
               static_cast< get_key_references_return *>(nullptr)
            );
      }

   private:
      std::unique_ptr< detail::account_by_key_api_impl > my;
};

当然,get_key_references_argsget_key_references_return这些类还是要靠手动声明的。

struct get_key_references_args
{
   std::vector< steem::protocol::public_key_type > keys;
};

struct get_key_references_return
{
   std::vector< std::vector< steem::protocol::account_name_type > > accounts;
};

FC_REFLECT( steem::plugins::account_by_key::get_key_references_args, (keys) )
FC_REFLECT( steem::plugins::account_by_key::get_key_references_return, (accounts) )

用户实现这2个类时,需要满足3个要求:

  1. 这2个类必须调用FC_REFLECT进行反射
  2. 调用FC_REFLECT进行反射时,暴露的类成员必须是public的。
  3. 使用FC_REFLECT反射暴露的每个成员的类型如果不是buildin类型,则也必须使用FC_REFLECT进行声明。

这3点一定需要记住,否则在注册时,构造fc::variant时,会实例化失败。

其中FC_REFLECT是一个宏,用模板函数特化和TypeTraits来实现静态的反射,具体的实现,可以参考:1

然后我们可以使用以下”反射”函数:

const char* fc::get_typename<TYPE>::name(); # 返回类型TYPE的名称
                                            # fc::get_typename<steem::plugins::account_by_key::get_key_references_args>返回字符串"steem::plugins::account_by_key::get_key_references_args"
fc::reflector<TYPE>::is_defined::value      # 表示TYPE是否定义过,当TYPE使用该类宏声明过,value为true,否则为false
                                            # fc::reflector<steem::plugins::account_by_key::get_key_references_args>::is_defined::value == true
fc::reflector<TYPE>::is_enum::value         # 表示TYPE是否是enum
                                            # fc::reflector<steem::plugins::account_by_key::get_key_references_args>::is_enum::value == false
fc::reflector<TYPE>::local_member_count     # 是一个enum,其值为当时TYPE使用FC_REFLECT声明时,其后面声明了几个成员,注意并不是TYPE实际拥有的成员数
                                            # fc::reflector<steem::plugins::account_by_key::get_key_references_args>::local_member_count == 1
fc::reflector<TYPE>::total_member_count     # 使用FC_REFLECT声明的TYPE的total_member_count == local_member_count,其他方式声明的不一定
fc::reflector<TYPE>::visit( Visitor )       # 使用一个Visitor()遍历TYPE注册的成员类型

API类的实现

API类方法的实现使用宏DEFINE_READ_APIS/DEFINE_WRITE_APIS/DEFINE_LOCKLESS_APIS 来生成。例如:

DEFINE_READ_APIS( account_by_key_api, (get_key_references) )

其展开为:

${method}_return ${api_class}::${method} (const ${method}_args & args, bool lock)
{
  if (lock)
  {
    return my->_db.with_read_lock( [&args, this](){ return my->${method}(args); } );
  }
  else
  {
    return my->${method}(args);
  }
}

另外2个宏展开的函数类似,区别在于my->_db锁上的处理。

拿上面的account_by_key_api来实例展开就是:

get_key_references_return account_by_key_api::get_key_references(
		const get_key_references_args & args, bool lock)
{
  if (lock)
  {
    return my->_db.with_read_lock( [&args, this](){ return my->get_key_references(args); } );
  }
  else
  {
    return my->get_key_references(args);
  }
}

其中my->get_key_references就是实际逻辑的实现函数,由用户自己实现。如果用户的逻辑不依赖于chain::database 的话,可以自行实现方法,因此可以使用DEFINE_API_IMPL, 该宏帮助生成方法的函数签名。

API的注册

API类声明、实现好了后,JSON RPC模块并不知该API的存在,因此将API注册到JSON RPC的模块插件里。该注册 时机为API类实例化时,在API类的构造函数中,通过宏JSON_RPC_REGISTER_API将API方法注册到JSON RPC插件里。

account_by_key_api::account_by_key_api(): my( new detail::account_by_key_api_impl() )
{
   JSON_RPC_REGISTER_API( STEEM_ACCOUNT_BY_KEY_API_PLUGIN_NAME );
}

该宏展开后是:

account_by_key_api::account_by_key_api(): my( new detail::account_by_key_api_impl() )
{
   steem::plugins::json_rpc::detail::register_api_method_visitor vtor("account_by_key_api");
   for_each_api(vtor);
}

其调用了前面宏生成的for_each_api模板函数,在该函数中会调用 void steem::plugins::json_rpc::detail::register_api_method_visitor::operator()方法进行注册。其实现为:

template< typename Plugin, typename Method, typename Args, typename Ret >
void operator()( Plugin& plugin, const std::string& method_name, Method method, Args* args, Ret* ret )
{
  _json_rpc_plugin.add_api_method( _api_name, method_name,
    [&plugin,method]( const fc::variant& args ) -> fc::variant
    {
      return fc::variant( (plugin.*method)( args.as< Args >(), true ) );
    },
    api_method_signature{ fc::variant( Args() ), fc::variant( Ret() ) }
  );
}

其中的_json_rpc_plugin是JSON RPC插件的一个引用,通过appbase::app().get_plugin< steem::plugins::json_rpc::json_rpc_plugin >() 获得。fc::variant 是一个类型容器,可以存储一些任意类型。其主要使用的是枚举方法实现,与std::variantboost::variant 完全不同的。

这个注册过程就是一次将上面生成的每个API方法的的名称、输入参数、输出参数传递给JSON RPC插件的 add_api_method方法。

拿前面的account_by_key_api举例就是:

  // 该函数的几个输入参数为:
  // API类名称
  // 要注册的方法函数名称
  // 一个lambda表达式,该表达式返回一个存储该API类的该方法返回对象的fc::variant
  // struct api_method_signature 对象,该对象有2个成员,分别是存储要注册方法输入和输出对象的fc::variant
  _json_rpc_plugin.add_api_method("account_by_key_api", "get_key_references"
    [&account_by_key_api/*该类的引用*/, account_by_key_api::*get_key_references/*成员函数指针*/]( const fc::variant & args) -> fc::variant
    {
      // 调用该类的成员函数
      return fc::variant( account_by_key_api.*get_key_references(args.as<get_key_references_args *>(), true) );
    },
    api_method_signature{fc::variant( get_key_references_args() ), fc::variant( get_key_references_return() )}
  );

注意:使用非build类型构造fc::variant时,该类型必须之前使用FC_REFLECT之类的宏构建一系列反射所需的声明。 因为他们在构造fc::variant需要使用到void fc::to_variant( const T& o, variant& v ), 里面会调用到反射的一些东西。最终会构造成构造成存储了一个fc::mutable_variant_objectfc::variant

// mutable_variant_object 中 std::vector< entry > 放置的是先前该类使用FC_REFLECT声明反射时暴露
// 的每个成员名称,和成员类型值
variant::variant( mutable_variant_object obj)
{
   *reinterpret_cast<variant_object**>(this)  = new variant_object(fc::move(obj));
   set_variant_type(this,  object_type );
}

接下来我们需要看JSON RPC插件的add_api_method这个方法的内部逻辑。其最终会调用 steem::plugins::json_rpc::detail::json_rpc_plugin_impl::add_api_method这个方法。 steem::plugins::json_rpc::detail::json_rpc_plugin_impl类内部有3个成员用来存储add_api_method被调用时 传入的参数,即API类名,方法名,方法输入/输出参数签名。

  std::map< string, api_description >                          _registered_apis;
  std::vector< string >                                        _methods;
  std::map< string, std::map< string, api_method_signature > > _method_sigs;

此时,JSON RPC插件就知道了各个API类注册的API类、方法名、参数签名等信息。

已注册API的调用

接下来,我们需要了解JSON RPC插件是如何利用这些注册信息来进行对应的方法调用。JSON RPC插件并不负责 HTTP通讯,HTTP通讯由webserver插件负责,这个模块留在以后再进行分析。

当一个HTTP请求到达服务器时,会通过webserver插件进行处理,然后调用JSON RPC插件的string json_rpc_plugin::call( const string& message ) 方法。

该方法先将获得的数据解析成JSON,存储在fc::variant中,然后调用json_rpc_response json_rpc_plugin_impl::rpc(const fc::variant& message)。 经过一些细节处理,然后调用void json_rpc_plugin_impl::rpc_jsonrpc( const fc::variant_object& request, json_rpc_response& response )。 该方法校验解析得到的JSON数据是否符合JSON-RPC 2.0 的规范,然后从数据中取出调用参数和方法指针,此处获得的方法指针 就是_json_rpc_plugin.add_api_method注册的那个lambda表达式。然后再调用该lambda表达式, 即完成API的调用。

获取全部注册的API方法

JSON RPC插件对外暴露了2个方法get_methodsget_signature 用于让用户查看正在运行的服务器上注册的API方法和每个方法的签名。

我们可以通过以下命令来调用:

> curl -s http://xx.xx.xx.xx:8090/rpc -d '{"jsonrpc": "2.0", "method": "jsonrpc.get_methods", "params": {}, "id": 11}' | python -mjson.tool
> curl -s http://xx.xx.xx.xx:8090/rpc -d '{"jsonrpc": "2.0", "method": "jsonrpc.get_signature", "params": {"method":"block_api.get_block"}, "id": 11}' |python -mjson.tool

实现自己的插件和API

我们可以参照上面讲解的机制来实现一个自己的API插件,并且注册自己的API。我们将这个插件命名为 demo_api,放置在libraries/plugins/apis/目录下。

注意,不能按官方文档中说的,放置在项目的external_plugins 目录下,因为这个模板不再CMake中插件模板生成器的调用路径中。

首先我们创建该插件的整体的文件结构为:

> tree libraries/plugins/apis/demo_api/
libraries/plugins/apis/demo_api/
├── CMakeLists.txt
├── demo_api.cpp
├── include
│   └── steem
│       └── plugins
│           └── demo_api
│               ├── demo_api.hpp
│               └── demo_api_plugin.hpp
└── plugin.json

然后,我们开始填充这些文件,前面的原理分析中有很多微小的细节没有明说,这些微小的细节会在注释中说明:

  • libraries/plugins/apis/demo_api/include/steem/plugins/demo_api/demo_api.hpp文件
#pragma once
#include <steem/plugins/json_rpc/utility.hpp>
#include <steem/protocol/types.hpp>
#include <fc/optional.hpp>
#include <fc/variant.hpp>
#include <fc/vector.hpp>

namespace steem {
namespace plugins {
namespace demo {

namespace detail {
class demo_api_impl;
}

// get_sum方法的输入参数
struct get_sum_args {
  std::vector<int64_t> nums;
};

// get_sum方法的输出参数
struct get_sum_return {
  int64_t sum;
};

class demo_api {
 public:
  demo_api();
  ~demo_api();

  DECLARE_API((get_sum))

 private:
  std::unique_ptr<detail::demo_api_impl> my;
};
}
}
}

// 将方法输入、输出参数进行反射
FC_REFLECT( steem::plugins::demo::get_sum_args, (nums) )
FC_REFLECT( steem::plugins::demo::get_sum_return, (sum) )
  • libraries/plugins/apis/demo_api/include/steem/plugins/demo_api/demo_api_plugin.hpp文件
// 这是插件类声明的头文件,该文件名必须与plugin.json中的plugin_project字段和该插件目录
// 中CMakeLists.txt的add_library声明的库名相同,如果3者不相同的话,在编译时,插件模板
// 生成的文件中会无法正确匹配到该头文件,从而编译错误。

#pragma once
#include <steem/plugins/json_rpc/json_rpc_plugin.hpp>
#include <appbase/application.hpp>

#define STEEM_DEMO_API_PLUGIN_NAME "demo_api"

namespace steem {
namespace plugins {
namespace demo {
class demo_api_plugin : public appbase::plugin<demo_api_plugin> {
 public:
  demo_api_plugin() {};
  virtual ~demo_api_plugin() {};

  // 用以声明该插件依赖哪些插件
  APPBASE_PLUGIN_REQUIRES((steem::plugins::json_rpc::json_rpc_plugin))
  // 必须拥有的一个方法name,注册时用以唯一标识该插件
  static const std::string &name() {
    static std::string name = STEEM_DEMO_API_PLUGIN_NAME;
    return name;
  }

  virtual void set_program_options(appbase::options_description &cli, appbase::options_description &cfg) override {};

  virtual void plugin_initialize(const appbase::variables_map &options) override;
  virtual void plugin_startup() override {};
  virtual void plugin_shutdown() override {};

  std::shared_ptr<class demo_api> api;
};
}
}
}
  • libraries/plugins/apis/demo_api/demo_api.cpp文件
#include <steem/plugins/demo_api/demo_api.hpp>
#include <steem/plugins/demo_api/demo_api_plugin.hpp>

namespace steem {
namespace plugins {
namespace demo {

namespace detail {

class demo_api_impl {
 public:
  demo_api_impl() {}
  ~demo_api_impl() {}

  // get_sum 就是我们提供的一个API方法,将输入的数组进行求和
  get_sum_return get_sum(const get_sum_args &args) const {
    get_sum_return final{0};
    for (auto num : args.nums) {
      final.sum += num;
    }
    return final;
  }
};
}

demo_api::demo_api() : my(new detail::demo_api_impl()) {
  JSON_RPC_REGISTER_API(STEEM_DEMO_API_PLUGIN_NAME);
}

demo_api::~demo_api() {}

// 需要注意创建demo_api的时机,因为demo_api的构造函数中会调用JSON RPC插件去注册API,因此
// 需要等JSON RPC先初始化好,plugin_initialize被调用时,会先注册demo_api_plugin的依赖
// 模块,因此可以确保此时JSON RPC插件此时已经注册完毕。
void demo_api_plugin::plugin_initialize(const appbase::variables_map &options) {
  api = std::make_shared<demo_api>();
}

DEFINE_LOCKLESS_APIS( demo_api, (get_sum) )
}
}
}
  • libraries/plugins/apis/demo_api/CMakeLists.txt文件
file(GLOB HEADERS "include/steem/plugins/demo_api/*.hpp")
add_library( demo_api_plugin
        demo_api.cpp
        )

# 当该模块调用了其他模块的方法时,target_link_libraries需要将这些被调用的模块添加进来。
# 下面的例子基本上是最小模块
target_link_libraries( demo_api_plugin json_rpc_plugin steem_protocol appbase fc )
target_include_directories( demo_api_plugin PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" )


if( CLANG_TIDY_EXE )
    set_target_properties(
            demo_api_plugin PROPERTIES
            CXX_CLANG_TIDY "${DO_CLANG_TIDY}"
    )
endif( CLANG_TIDY_EXE )

install( TARGETS
        demo_api_plugin

        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib
        )
  • libraries/plugins/apis/demo_api/plugin.json文件
{
  "plugin_name": "demo_api",
  "plugin_namespace": "demo",
  "plugin_project": "demo_api_plugin"
}

编译

# 由于项目使用cmake,我们可以在任意地方编译源码
> mkdir build && cd build
> cmake -DBOOST_ROOT="$BOOST_ROOT" \
		      -DREADLINE_INCLUDE_DIR="/usr/local/Cellar/readline/7.0.3_1/include" \
		      -DOPENSSL_ROOT_DIR="/usr/local/opt/openssl" \
		      -DBUILD_STEEM_TESTNET=ON
		      ../steem # 源码路径

运行

首先,我们先运行一次编译好的程序,它会在当前目录创建一个默认配置。

> ./programs/steemd/steemd

然后我们打开默认配置witness_node_data_dir/config.ini将第一次启动输出的initminer private key:后的 的字符串填到配置的private-key配置项,然后在配置的plugin填上我们自己的插件demo_api,还有其他一些 配置:

plugin = demo_api
webserver-http-endpoint = 127.0.0.1:8090
witness = "initminer"
enable-stale-production = true
private-key = 5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n

然后再次启动:

> ./programs/steemd/steemd

# 测试我们的API是否注册成功
> curl -s http://127.0.0.1:8090/rpc -d '{"jsonrpc": "2.0", "method": "jsonrpc.get_methods", "params": {}, "id": 11}'

# 调用我们的API
> curl -s http://127.0.0.1:8090/rpc -d '{"jsonrpc": "2.0", "method": "demo_api.get_sum", "params": {"nums":[1,2,3,4,5]}, "id": 11}'
# 得到:{"jsonrpc":"2.0","result":{"sum":15},"id":11}

验证完毕。

Search

    Table of Contents