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(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(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 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().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 input_transactions;
}
producer_plugin 插件啟動後,通過 schedule_production_loop 函數循環生產區塊。EOS 採用 DPoS (委託股權證明)算法,先由 EOS 持有者(股東)選出 21 個區塊生產者(董事會成員),區塊通過這 21 個生產者輪流產生,每 3 秒出一個區塊,類似操作系統的時間片概念,每個時間片對應一個唯一的生產者,當時間片到來時才能打包區塊。![DPoS](http://cdn.8btc.com
/wp-content/uploads/2018/05/201805111325146196.png)
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( &{
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().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 );
}
至此,一次完整的區塊處理流程就完成了,後面不斷重複打包過程,隨著時間推移,形成一個不可逆轉的區塊鏈。