EOS通信机制分析
客户端和服务器端的通信采用RESTful软件架构风格,服务器端的每个资源对应一个唯一的URL地址,客户端将URL地址封装成http请求发送到服务器端,请求对应的资源或者执行相应操作。
客户端发送消息流程
以转账为例,说明EOS消息处理流程。通过cleos客户端发起转账命令,在main函数中,解析transfer命令,通过create_transfer函数将交易发送者、交易接收者、token数量等信息封装成mutable_variant_object对象,然后调用send_action函数,将交易信息发送到服务器端打包进区块链。
# ./cleos transfer sender recipient amount memo
programs/cleos/main.cpp
main()
{
…
send_actions({create_transfer(sender, recipient, amount, memo)});
…
}
void send_actions {
auto result = push_actions( move(actions), extra_kcpu, compression);
…
}
fc::variant push_actions {
signed_transaction trx;
trx.actions = std::forward<decltype(actions)>(actions);
return push_transaction(trx, extra_kcpu, compression);
}
fc::variant push_transaction{
trx.set_reference_block(ref_block_id);
// 发送 ”/V1/chain/push_transaction” URL地址到服务器端
if (!tx_dont_broadcast) {
return call(push_txn_func, packed_transaction(trx, compression));
}
}
fc::variant call{
try {
return eosio::client::http::do_http_call( url, path, fc::variant(v) );
}
}
fc::variant do_http_call {
// 将请求的URL封装成http包
request_stream << “POST ” << path_prefix + path << ” HTTP/1.0\r\n”;
request_stream << “Host: ” << server << “\r\n”;
request_stream << “content-length: ” << postjson.size() << “\r\n”;
request_stream << “Accept: */*\r\n”;
request_stream << “Connection: close\r\n\r\n”;
request_stream << postjson;
// 和服务器建立连接
do_connect(socket, server, port);
// 发送http报文,并获取返回结果
re = do_txrx(socket, request, status_code);
}
服务器接收消息流程
nodeos服务器先通过http_plugin插件接收客户端发过来的http请求报文,然后解析出请求的URL地址和数据信息,然后调用对应的回调函数处理,并将结果返回给cleos客户端。
# ./nodeos -e -p eosio HTTP消息处理流程
在nodeos的main函数中启动http_plugin插件,注册处理http请求的回调函数(handle_http_request),然后监听socket通信端口,等待建立客户端远程连接。
void http_plugin::plugin_startup() {
// 注册http请求处理函数
my->create_server_for_endpoint(*my->https_listen_endpoint, my->https_server);
// 监听socket通信端口
my->https_server.listen(*my->https_listen_endpoint);
// 等待建立客户端远程连接
my->https_server.start_accept();
}
void create_server_for_endpoint{
ws.set_http_handler([&](connection_hdl hdl) {
handle_http_request<T>(ws.get_con_from_hdl(hdl));
});
}
http请求处理函数从http报文中解析出URL地址(resource)、消息内容(body),然后在url_handlers集合中查找URL对应的回调函数,最后通过handler_itr->second调用处理函数。
void handle_http_request {
…
auto body = con->get_request_body();
auto resource = con->get_uri()->get_resource();
auto handler_itr = url_handlers.find(resource);
if(handler_itr != url_handlers.end()) {
handler_itr->second(resource, body, [con](int code, string body) {
con->set_body(body);
con->set_status(websocketpp::http::status_code::value(code));
});
}
…
}
注册URL处理函数
url_handlers是一个URL和处理函数的键值对map集合,由class http_plugin_impl管理,其它插件模块通过add_api函数注册URL回调函数。
plugins/http_plugin/http_plugin.cpp
class http_plugin_impl {
map<string,url_handler> url_handlers;
…
}
void add_api(const api_description& api) {
for (const auto& call : api)
add_handler(call.first, call.second);
}
void http_plugin::add_handler {
…
my->url_handlers.insert(std::make_pair(url,handler);
}
例如,chain_api_plugin插件在启动函数中注册了以下URL回调函数,包括查询区块信息、处理交易数据:
void chain_api_plugin::plugin_startup() {
app().get_plugin<http_plugin>().add_api({
CHAIN_RO_CALL(get_info, 200),
CHAIN_RO_CALL(get_block, 200),
…
CHAIN_RW_CALL(push_transaction, 202),
CHAIN_RW_CALL(push_transactions, 202)
});
}
生产区块流程
客户端发送 ”/V1/chain/push_transaction” URL地址和交易信息到服务器端,然后服务器调用URL对应的回调函数push_transaction将交易信息写入到一个待打包的区块(_pending_block)中。
chain_controller::push_transaction {
if( !_pending_block ) {
_start_pending_block();
}
…
return _push_transaction(trx);
}
chain_controller::_push_transaction {
_pending_block->input_transactions.emplace_back(packed_trx);
}
一个区块可以包含很多个transaction,通过input_transactions将这些transaction管理起来,随后由producer_plugin插件将区块打包进区块链中,然后向其它nodeos节点广播区块信息。
struct signed_block : public signed_block_summary {
…
vector<packed_transaction> input_transactions;
}
producer_plugin插件启动后,通过schedule_production_loop函数循环生产区块。EOS采用DPoS(委托股权证明)算法,先由EOS持有者(股东)选出21个区块生产者(董事会成员),区块通过这21个生产者轮流产生,每3秒出一个区块,类似操作系统的时间片概念,每个时间片对应一个唯一的生产者,当时间片到来时才能打包区块。
DPoS算法和比特币的POW算法有很大区别,在POW算法中,矿工只要发现交易信息就开始打包区块,而且需要消耗巨大的算力,而且交易确认时间很长。而DPoS算法则通过提前选举出可信节点,避免了信任证明的开销,同时生产者数量的减少(21个)也极大提升了交易确认效率,防止性能差的节点拖慢整个区块链生产速度。DPoS的时间片机制能够保证可信区块链的长度始终比恶意分叉的区块链长(恶意节点数量不大于1/3总节点数量),例如,节点B想自己构造一个分叉链,但是由于每9秒才能产生一个区块,所以始终没有主链长。
void producer_plugin::plugin_startup()
{
…
my->schedule_production_loop();
…
}
计算出现在距离下一个区块时间片的时间间隔time_to_next_block_time,然后启动一个定时器,当下一个时间片到来时调用block_production_loop函数生产区块。
void producer_plugin_impl::schedule_production_loop() {
int64_t time_to_next_block_time = (config::block_interval_us) – (now.time_since_epoch().count() % (config::block_interval_us) );
_timer.expires_from_now( boost::posix_time::microseconds(time_to_next_block_time) );
_timer.async_wait( [&](const boost::system::error_code&){ block_production_loop(); } );
}
调用maybe_produce_block函数生产区块,从函数名的maybe可知,不一定能够生产出区块,只是进行尝试,然后处理结果,最后递归调用schedule_production_loop函数进入下一次循环。
producer_plugin_impl::block_production_loop() {
result = maybe_produce_block(capture);
…
schedule_production_loop();
return result;
}
获取当前时间对应的生产者,然后调用chain.generate_block函数生产区块,完成后通过broadcast_block函数向其它节点广播区块信息。
producer_plugin_impl::maybe_produce_block {
uint32_t slot = chain.get_slot_at_time( now );
…
auto scheduled_producer = chain.get_scheduled_producer( slot );
…
auto block = chain.generate_block(
scheduled_time,
scheduled_producer,
private_key_itr->second,
_production_skip_flags
);
app().get_plugin<net_plugin>().broadcast_block(block);
return block_production_condition::produced;
}
chain_controller::generate_block(
…
return _generate_block( when, producer, block_signing_private_key );
}
将生产者信息更新到之前的待打包区块_pending_block中,例如,区块时间戳、区块编号、生产者状态等等,最后将区块写入本地区块链中。
chain_controller::_generate_block {
_pending_block->timestamp = when;
_pending_block->producer = producer_obj.owner;
_pending_block->previous = head_block_id();
…
if( !(skip & skip_producer_signature) )
_pending_block->sign( block_signing_key );
_finalize_block( *_pending_block_trace, producer_obj );
}
至此,一次完整的区块处理流程就完成了,后面不断重复打包过程,随着时间推移,形成一个不可逆转的区块链。
【相关阅读】
EOS代码架构及分析(一)
本文由专栏作者极简主义首发于资讯,未经允许不得转载。