原文連接:醒者呆的博客園,www.cnblogs.com/Evsward/p/e…html
本文主要研究EOS的tps表現,會從插件、cleos、EOSBenchTool以及eosjs四種方式進行分析研究。node
關鍵字:eos, tps, cleos, txn_test_gen_plugin, EOSBenchTool, qt, eosjs, C++源碼分析python
{
"transaction_id": "7943f613f8cde71bc37d76daf3581ceb62ae6d481fa9b3a11ba73d19d909c666",
"broadcast": false,
"transaction": {
"compression": "none",
"transaction": {
"expiration": "2018-07-12T09:51:14",
"ref_block_num": 526,
"ref_block_prefix": 52869816,
"net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [
{
"account": "eosio.token",
"name": "transfer",
"authorization": [
{
"actor": "eosiotestay",
"permission": "active"
}
],
"data": "00bcc95865ea305500fcc95865ea3055010000000000000004535953000000000c7061636b696e672074657374"
}
],
"transaction_extensions": []
},
"signatures": [
"SIG_K1_KB6ENT2Ns3QmaPSfvxqCkgZTjK5RUDRFwkZ7p9Jv6p1GpnD67jhMUsw1Spfp7yw4hChsubPeiTc2HSt5hc6YdMH5rk5Kfz"
]
}
}
複製代碼
因爲咱們在研究eos階段,大量使用到cleos,所以使用cleos來測試tps是咱們第一個能想到的手段。這一節咱們將加深理解tps的意義,tps的計算方法,討論單節點與多節點環境對tps的影響。ios
單節點的搭建這裏再也不贅述,直接使用腳本執行,git
./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
es6
注意參數的順序不能變。
複製代碼
執行成功之後,咱們將獲得一個擁有1000個stake帳戶(簡單理解爲已抵押完可直接投票的帳戶)的單節點eos環境,最後一個參數-X會讓當前環境不斷執行隨機轉帳操做(注意:每一筆轉帳都是一個action,一個action對應一個transaction)github
查看日誌shell
修改腳本的stepLog函數,改成:json
def stepLog():
run('tail -f ' + args.nodes_dir + '00-eosio/stderr')
複製代碼
而後在終端執行:api
./bios-boot-tutorial.py -l
便可進入同步日誌輸出的界面。
環境準備完畢,咱們來測試一下當前正在不斷進行轉帳的eos鏈上的tps表現。這裏採用的tps計算方式爲:
tps = BlockTxs*2
由於eos是半秒出塊,因此兩個塊的打包交易量之和就是tps,爲確保數值可靠性,每一個塊的打包交易量咱們要經過大量區塊取平均值的方式。
複製代碼
基於以上思想,能夠總結出一個shell命令直接在終端執行便可:
for (( i = 12638; i <= 13638; i++ )); do cleos --wallet-url http://localhost:6666 --url http://localhost:8000 get block $i | grep "executed" | wc -l; done | awk '{sum+=$1} END {print NR,"blocks average tps =", sum/NR*2}'
取出區塊號從200到1200的區塊,分別計算每一個區塊的打包交易量(經過統計其包含的「executed」便可,由於每一個交易對應一個「executed」),而後將這些區塊交易量進行累加除以數量獲得平均值,再乘以2,輔以可視化備註輸出便可。
複製代碼
最終結果不是很理想,至少距離官方聲稱的幾千tps有很大差距。
1001 blocks average tps = 39.2727
因此1000個塊統計tps爲 39.2727。
因爲tps的結果不理想,我也有過不少思考,下面咱們換一種計算方式來看:
tps = trxs/time
這裏經過一種簡單的方式來計算tps:即統計共發出了trxs筆交易所耗費的時間,以秒爲單位,而後相除便可獲得tps。
複製代碼
基於以上思想,因爲這部分代碼是沒法經過一行shell解決的,因此我經過修改bios腳原本解決,
def stepTPS():
start = time.time()
numtps = args.num_tps
i = 0
while i < numtps :
print ("on: ",i)
randomTransfer(0, args.num_senders,1)
i=i+1
elapsed = (time.time() - start)
print ("Time used:",elapsed,"s tps=",numtps/elapsed)
複製代碼
def randomTransfer(b, e, t):
for j in range(t):
src = accounts[random.randint(b, e - 1)]['name']
dest = src
while dest == src:
dest = accounts[random.randint(b, e - 1)]['name']
run(args.cleos + 'transfer -f ' + src + ' ' + dest + ' "0.0001 ' + args.symbol + '"' + ' || true')
複製代碼
('A', 'tps', stepTPS, False, "calculate the tps"),
複製代碼
parser.add_argument('--num-tps', metavar='', help="Number of tps test trx", type=int, default=1000)
複製代碼
執行A:
注意,在執行前,咱們要先停掉單節點環境,將-X去掉,而採用咱們的-A來執行隨機轉帳。
./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
./bios-boot-tutorial.py -A --num-tps 2000
發起2000筆交易,而後使用腳本函數stepTPS進行測試。
結果:
Time used: 26.172592401504517 s tps= 38.20790790072884
結果與shell方式差很少,都是不到40的tps表現。
tps的結果不盡人意,我又轉念想到了是否由於單節點出塊的緣由。所以我搭建了多節點出塊加全節點的環境,搭建環境的方法能夠參考《【精解】EOS多節點組網:商業場景分析以及節點啓動時序》
我仍舊經過以上兩種方式,分別是shell方式和Python腳本的方式去測試,最後結果是並沒有改變,這也證明了eos不具有多線程處理事務的能力。
插曲:我將python腳本的修改提交了EOSIO/eos的官方pr,結果被拒絕合併,緣由是「unrelated change」,轉念一想,若是合併至源碼,用戶能夠經過這種方式直白地獲得eos的tps就是幾十個的結論,那絕對是很很差的。
複製代碼
我對eos的高tps有了深深地懷疑,因而找來了官方的tps測試插件,要親自感覺一下tps的「洗禮」。插件的使用方式很簡單,按照官方文檔的步驟執行便可,最後我調整參數:
curl --data-binary '[""30, 50]' http:/ /localhost:8888/v1/txn_test_gen/start_generationn
複製代碼
鏈上日誌結果:
經過trxs一列能夠看出,每一個區塊打包的交易量大大提高了,平均tps在2000左右。
插件的測試方法也是bm所推崇的,他說經過cleos沒法發揮出真正的eos的性能。那麼具體是爲何,咱們經過插件的源碼txn_test_gen_plugin.cpp進行分析,我將這一部份內容單獨成文,請閱讀《【源碼解讀】EOS測試插件:txn_test_gen_plugin.cpp》
複製代碼
EOSBenchTool來自於OracleChain的貢獻,雖然他們的節點oraclegogogo沒競選上bp,但我認爲bp的競選更可能是市場行爲,不是技術實力的「成績單」,在全部bp中,目前我也僅看到了OracleChain作出的技術方面的貢獻,包括對EOSIO/eos的pr,都是OracleChain自身技術氣質的體現。多餘誇獎的話很少講了,下面來研究這套工具內容。
EOSBenchTool的思想與以上的cleos有很大不一樣,與插件的方式(打包交易)比較類似,但它的實現方式倒是獨具一格的,他並非像插件那樣直接在「服務器端」自我模擬交易來測試tps。他們勇於直接使用C++ 來編寫客戶端請求主網來打包、發起請求,最終測試獲得一個很是不錯的結果,大約能夠到200到300,這個結果也是我在衆多壓測手段中獲得的比較理想的結果,包括下面要介紹到的eosjs的方式,都不及EOSBenchTool的測試結果。
EOSBenchTool既能不犧牲在真實場景中的模擬,又能經過技術手段優化交易通信,能夠說他的tps結果是比較具有真實性、業務可行性,以及他的技術實現手段也是很是值得業務方來學習並嘗試使用的。
複製代碼
官方文檔的介紹比較技術範兒,就是不太親民。這裏我給他填點肉,但願層級嘗試使用EOSBenchTool卻失敗的朋友可以在這裏找到答案。
1、EOS主網環境
首先,要準備EOS主網環境,能夠經過腳本快速得到:python3 ./bios-boot-tutorial.py -k -w -b -s -c -t (不部署system合約,由於部署後沒法使用create account建立帳戶。)
2、獲取代碼,QT工具,編譯代碼
源碼位置:EOSBenchTool
QT去官網下載community版本便可,注意:QT在安裝時要同時勾選安裝 QCreator 和 QT source 以及 QT prebuild tool(這裏我選擇的是mingw)
打開QCreator,通常狀況下,上面的步驟準備穩當之後,QCreator會自動檢測一套構建套件(Kit),構建套件依賴於Qt Version、編譯器、Debuggers,Cmakes,這些工具也都是能夠自動檢測到的,若是沒法檢測到,必定是某個工具未安裝,請檢查相應的工具,並從新下載安裝(通常來說,全部這些工具在QT安裝包都會包含,只需再次打開QT安裝包,選擇更新,從新勾選缺少的工具安裝便可。)最終個人構建套件(Kit) 截圖以下:
以上工做都順利完成之後,在QCreator中,點擊左下角三角按鈕運行啓動EOSBenchTool工具。建議將UI最大化,能夠更方便地查看日誌。填寫好setting內容,以下:
關於幾個參數:
其餘參數很少介紹。設置好參數之後,點擊OK保存,而後切換到 Benchmark Testing 點擊Prepare:建立測試帳戶、給測試帳戶轉帳、每一個測試帳戶發起測試交易並打包。
等待Prepare結束,1萬筆測試交易大約兩到三分鐘,視客戶端機器本地性能。而後點擊Start,獲得tps結果,這裏因爲界面都是可視化的,我再也不贅述。
這部分咱們將一塊兒經過源碼學習EOSBenchTool打包交易的原理。
Prepare階段,正如上面在EOSBenchTool使用中介紹到的那樣,包括建立帳戶,轉帳,打包。
建立帳戶
下面先來看建立帳戶的源碼:
CreateAccount createAccount;
int count = createAccount.create(thread_num, [=](const QString& name, bool res) { // lambda格式的回調函數:打印日誌
commonOutput(QString("Create %1 %2.").arg(name).arg(res ? "succeed" : "failed"));
});
複製代碼
進入createaccount.cpp文件,查看create函數:
int CreateAccount::create(int threadNum, const create_account_callback& func)
{
if (threadNum <= 0) { // 根據threadNum個數建立對應數量的帳戶。
return 0;
}
// 清空其餘帳戶
AccountManager::instance().removeAll();
for (int i = 0; i < threadNum; ++i) {
eos_key owner, active;
keys.clear(); // 頭文件中的 QVector<eos_key> keys;
keys.push_back(owner); // 添加owner和active權限到keys對象
keys.push_back(active);
newAccountName = createNewName();
bool res = false;
QEventLoop loop;
// WINSOCK_API_LINKAGE int PASCAL connect (SOCKET, const struct sockaddr *, int);
// 經過connect開啓一個socket通道
connect(this, &CreateAccount::oneRoundFinished, &loop, &QEventLoop::quit);
if (httpc) { // httpc(new HttpClient)
httpc->request(FunctionID::get_info); // 經過http請求get info
// 以上的get_info回調函數,實際功能函數:get_info_returned,由connect開啓socket訪問進去。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
}
loop.exec();
// 返回執行結果res,成功爲true,失敗爲false
res = !(AccountManager::instance().listKeys(newAccountName).first.empty());
// 執行回調函數:打印日誌
func(newAccountName, res);
}
return AccountManager::instance().count() - 1; // 除了super account之外的集合中的帳戶個數
}
複製代碼
查看一下AccountManager的源碼:
class AccountManager
{
public:
AccountManager();
static AccountManager& instance();
void addAccounts(const QString& name, const QPair<std::string, std::string>& keypairs);
void removeAll();
QPair<std::string, std::string> listKeys(const QString& account);
QVector<std::string> listAccounts();
int count() const;
private: // 私有屬性,QMap集合對象 accounts
QMap<QString, QPair<std::string, std::string>> accounts;
};
複製代碼
removeAll的實現方法:
void AccountManager::removeAll()
{
QPair<std::string, std::string> superKey = accounts[super_account];
accounts.clear();
accounts.insert(super_account, superKey);
}
複製代碼
super_account和superKey是全局變量,在mainwindow.cpp前面標明:
QString super_account = "eosio";
實際上,是對QMap集合對象 accounts的操做。接着,帳戶名的生成方式:
QString CreateAccount::createNewName()
{
// eos的命名規則
static const char *char_map = "12345abcdefghijklmnopqrstuvwxyz";
int map_size = strlen(char_map);
QString newName;
for (int i = 0; i < 5; ++i) {
int r = rand() % map_size; // 隨機選出char_map的下標位置
newName += char_map[r];
} // 返回的是一個五位的名字
return newName;
}
複製代碼
AccountManager的實例也是個static的單例
AccountManager &AccountManager::instance()
{
static AccountManager manager;
return manager;
}
複製代碼
get_info_returned函數,
void CreateAccount::get_info_returned(const QByteArray &data)
{
//先關閉進來的socket通道
disconnect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
getInfoData.clear();
getInfoData = data;
QByteArray param = packGetRequiredKeysParam();
if (param.isNull()) {
emit oneRoundFinished();
return;
}
if (httpc) {
// 經過http請求鏈的get_required_keys接口,傳入對應事務的json格式做爲入參。
httpc->request(FunctionID::get_required_keys, param);
// get_required_keys的回調函數,經過socket創建通道去訪問get_required_keys_returned函數。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
}
}
複製代碼
轉到函數packGetRequiredKeysParam(),該函數是建立帳戶的實際生效函數:
QByteArray CreateAccount::packGetRequiredKeysParam()
{
if (getInfoData.isEmpty()) {
return QByteArray();
}
// 組裝了newAccount的請求數據
EOSNewAccount newAccount(EOS_SYSTEM_ACCOUNT, newAccountName.toStdString(),
keys.at(0).get_eos_public_key(), keys.at(1).get_eos_public_key(),
EOS_SYSTEM_ACCOUNT);
std::vector<unsigned char> hexData = newAccount.dataAsHex(); // 將data對象轉爲十六進制
// 經過ChainManager建立事務,是建立帳戶的事務。
signedTxn = ChainManager::createTransaction(EOS_SYSTEM_ACCOUNT, newAccount.getActionName(), std::string(hexData.begin(), hexData.end()),
ChainManager::getActivePermission(EOS_SYSTEM_ACCOUNT), getInfoData);
QJsonObject txnObj = signedTxn.toJson().toObject();
QJsonArray avaibleKeys;
std::string pub = eos_key::get_eos_public_key_by_wif(super_private_key.toStdString());// 經過私鑰得到公鑰
avaibleKeys.append(QJsonValue(QString::fromStdString(pub)));
QJsonObject obj;
obj.insert("available_keys", avaibleKeys);
obj.insert("transaction", txnObj);
return QJsonDocument(obj).toJson();// 最終得到json格式的建立帳戶的事務對象
}
複製代碼
進入get_required_keys_returned函數,
void CreateAccount::get_required_keys_returned(const QByteArray &data)
{
disconnect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
getRequiredKeysData.clear();
getRequiredKeysData = data;
QByteArray param = packPushTransactionParam();
if (param.isNull()) {
emit oneRoundFinished();
return;
}
if (httpc) {
// 相同的套路,經過packPushTransactionParam()函數組裝好的推送交易接口的入參param,而後經過http發起請求。
httpc->request(FunctionID::push_transaction, param);
// 經過connect創建socket鏈接訪問push_transaction的回調函數push_transaction_returned,繼續處理。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::push_transaction_returned);
}
}
複製代碼
packPushTransactionParam(),開始組裝push transaction的參數,因爲代碼中對於數據的處理較多,這裏只展現結果的部分:
// 給上面由函數packGetRequiredKeysParam()組裝的交易signedTxn簽名。
signedTxn.sign(pri, TypeChainId::fromHex(info.value("chain_id").toString().toStdString()));
PackedTransaction packedTxn(signedTxn, "none");
QJsonObject obj = packedTxn.toJson().toObject();
return QJsonDocument(obj).toJson(); // 得到簽名後的交易數據
複製代碼
push_transaction_returned,咱們通過大量的組合校驗,與鏈上的信息進行同步組裝得到了合法的簽名交易對象,而後經過http接口請求了push_transaction接口將簽名交易對象推送到鏈上執行,執行結果經過回調函數處理,回調函數的主要做用是將處理結果 -> 成功建立了的這個帳戶,存入集合accounts中,因爲accounts是私有屬性,因此經過方法AccountManager::instance().addAccounts執行。
客戶端本地保存了一個對象accounts用來同步本身建立過的帳戶。大部分代碼是對accounts的處理。
複製代碼
帳戶轉帳
在上一個建立帳戶的部分,咱們詳細解讀了通信的過程,仍舊是經過http去發起請求,經過每一個請求的回調函數進行處理,組裝,維護了本地的集合accounts。因爲篇幅過大,在以後的介紹中,不會再過多介紹,而專一於實現方式的核心代碼。轉帳的核心代碼:
QVector<std::string> accounts = AccountManager::instance().listAccounts(); // 經過accounts得到測試帳戶們
int accountSize = accounts.size();
int balance = total_tokens / accountSize; // 平均分配測試用幣
for (int i = 0; i < accountSize; ++i) {
PushManager push;
QString quantity = QString("%1.0000 %2").arg(balance).arg(token_name); // 拼串,轉帳額度
QString to = QString::fromStdString(accounts.at(i)); // 遍歷接收轉帳的帳戶
commonOutput(QString("Transfering %1 to %2 ...").arg(quantity).arg(to)); // 日誌
bool ret = push.transferToken(super_account, to, quantity); // 核心生效代碼,是PushManager的transferToken函數。
commonOutput(ret ? "Succeed." : "Failed.");
}
複製代碼
PushManager的transferToken函數是本地組裝了標準的轉帳請求參數,json字符串格式的from, to, quality以及memo信息。而後跳轉到make_push函數。make_push函數須要經過http請求接口abi_json_to_bin,而針對該接口的入參,都須要在這個函數處理獲取到,入參包括action,code以及args。code就是對應的合約的code,例如咱們使用帳戶eosio部署了合約eosio.system,那麼eosio.system的code就能夠經過get code eosio得到。action就是轉帳:transfer。args就是上面PushManager的transferToken函數組裝的參數對象。http請求成功之後,經過回調函數abi_json_to_bin_returned處理響應結果。
if (httpc) {
httpc->request(FunctionID::abi_json_to_bin, QJsonDocument(obj).toJson());
connect(httpc, &HttpClient::responseData, this, &PushManager::abi_json_to_bin_returned);
}
複製代碼
接口abi_json_to_bin:序列化json數據爲二進制數據。這個結果的數據一般用在push_transaction的data字段。
複製代碼
action.setData(hexData); // action的hexData字段就是以上接口**abi\_json\_to\_bin**得到的結果。
剩餘部分與上面介紹「建立帳戶」相同,get_info -> get_required_keys -> push_transaction 的流程。
總結一下,轉帳因爲涉及到合約,因此多了一步abi_json_to_bin,而建立帳戶不須要這一步,但建立帳戶須要本地的集合對象同步存儲。
複製代碼
打包交易
首先說明,打包的交易是測試交易,不是以上的建立帳戶和帳戶轉帳。先看源碼部分:
trxpool = new TransactionPool; // 建立交易池
trxpool->setTargetSize(trx_size); // 設置交易池的大小
// packedTrxTransferFinished,打包測試交易發送鏈所有結束
connect(trxpool, &TransactionPool::finished, this, &MainWindow::packedTrxTransferFinished);
// packedTrxReady,prepare階段完成,能夠點擊start
connect(trxpool, &TransactionPool::packedTrxPoolFulfilled, this, &MainWindow::packedTrxReady);
enablePacker(true);// 核心打包內容
複製代碼
enablePacker(),觸發打包流程
QVector<std::string> accounts = AccountManager::instance().listAccounts();
for (int i = 0; i < accounts.size(); ++i) {
Packer *p = new Packer;
connect(p, &Packer::finished, p, &QObject::deleteLater); // auto delete
// A:稍後重點講
connect(p, &Packer::newPackedTrx, trxpool, &TransactionPool::incomingPackedTrxs);
// 爲Packer的對象設置屬性的值
p->setAccountName(QString::fromStdString(accounts.at(i)));
p->setCallback([=] (const QString& msg) {
commonOutput(msg);
});
p->start(); // 執行Packer
packers.push_back(p);
}
複製代碼
進入incomingPackedTrxs函數,
void TransactionPool::incomingPackedTrxs(const QByteArray &data)
{
// 上鎖,data推入packedTransactions,QVector<QByteArray> packedTransactions;
QMutexLocker locker(&mutex);
packedTransactions.push_back(data);
if (packedTransactions.size() >= targetSize) { // 經過咱們設置的交易池的大小來控制總測試交易量
emit packedTrxPoolFulfilled();
}
}
複製代碼
Packer開始執行,
void Packer::run()
{
while(!needStop) {
PushManager push(false);
// 這是一個包含lambda爲回調函數的connect語句
connect(&push, &PushManager::trxPacked, this, [&](const QByteArray& data){
emit newPackedTrx(data); // emit 發送signal給newPackedTrx B:稍後重點講
func(QString("PACKED: %1 to %2.").arg(accountName).arg(super_account));// 打印日誌
});
// 如下部分與帳戶轉帳接口一致,後續內容均同上。
push.transferToken(accountName, super_account, QString("0.0001 %1").arg(token_name));
}
}
複製代碼
當Packer開始run的時候,它是一個無線循環,直到灌滿trxPool爲止,而其中,咱們注意觀察,這一connect翻譯過來就是:我先註冊一個signals trxPacked在這,等待某處代碼將該信號發射,會被這裏捕捉到,將它傳入回調函數,就是這個lambda回調函數的參數data中,這個lambda回調函數咱們先放一放,來說這個signals trxPacked:
signals 對應的觸發是 emit
複製代碼
trxPacked 做爲一個signals 是在PushManager::get_required_keys_returned中被髮射emit的(注意這個是與上面講到的CreateAccount::get_required_keys_returned是不一樣的。)
QByteArray param = packPushTransactionParam();
emit trxPacked(param);
...
httpc->request(FunctionID::push_transaction, param);
複製代碼
這個emit發送的param是僅在push_transaction發送以前的transaction,會將這個對象傳入回調函數。下面來看一下lambda回調函數的內部,獲取到transaction數據對象之後,會將該對象再次emit到一個signals newPackedTrx,咱們去找一下這個signals的註冊位置:MainWindow::enablePacker,就是上面展現過的代碼,我註釋爲「A:稍後重點講」,所以相同的原理,這個data又被傳入了incomingPackedTrxs函數,最終被打包進packedTransactions集合中。
關於QT的signals emit slot connect 的具體語法介紹的內容能夠查看這篇文章咱們沒有QT開發的需求,因此不必在此過多介紹語法內容,只須要捋清楚業務邏輯便可。
複製代碼
packedTransactions的內容是屬於TransactionPool的,它會在TransactionPool被啓動時(也就是start按鈕被按下時)使用,而這個對象是在prepare階段被儲存。(聽說這個時間只有5分鐘,機器性能不太好的不要將trxPool設置地過高,不然執行不完,打包好的packedTransactions並未作持久化,就會消失掉,最終致使測試結果失真)
這個按鈕點擊事件的內容看上去比較簡單,只有一個enableTrxpool(true)是生效代碼,其餘都是一些日誌。下面直接進入enableTrxpool函數,不張貼了,直接轉到核心代碼trxpool->start(); 那麼咱們進入到transactionpool.cpp,start對應run函數,源碼以下:
void TransactionPool::run()
{
DataManager::instance().setBeginBlockNum(get_block_info());// get_block_info()是經過http請求鏈獲取的
HttpClient httpc;
int sz = packedTransactions.size();
for (int i = 0; i < sz && !needStop; i += batch_size) {
QEventLoop loop;
connect(&httpc, &HttpClient::responseData, &loop, &QEventLoop::quit);
QJsonArray array;
int range = sz - i > batch_size ? batch_size : sz - i;
for (int j = 0; j < range; ++j) {
QJsonObject val = QJsonDocument::fromJson(packedTransactions.at(i+j)).object();
array.append(val);
}
// http請求push_transactions接口,推送打包交易到鏈
httpc.request(FunctionID::push_transactions, QJsonDocument(array).toJson());
loop.exec();
}
DataManager::instance().setEndBlockNum(get_block_info());
packedTransactions.clear();
}
複製代碼
這段代碼就是上面提到的對 packedTransactions 的「消費」,核心代碼是按照設置的打包(後稱小包)大小來逐漸「消費」packedTransactions,而後經過http的push_transactions接口,將這些「小包」推送到鏈執行。
沒想到EOSBenchTool的源碼解讀一會兒搞了這麼長的篇幅,我沒控制住,讀者又要吃力了。其實到這裏咱們來總結一下,EOSBenchTool主要是使用了QT的界面系統,同時也用到了QT的signals,emit,connect等專有語法,不懂qt的同窗看起來有些吃力。然而,拋開這些語言或者類庫的語法來說,咱們專一於代碼邏輯,EOSBenchTool的實現是容易被人理解的:
上面咱們介紹了:
Way | Business | TPS | memo |
---|---|---|---|
cleos | 可直接使用 | 70-80 | (單節點、多節點)shell方式,python腳本 |
txn_test_gen_plugin | 不可以使用 | 1500-2000 | 官方用來測試的一種方式,這個插件純粹是爲了測tps而設的 |
EOSBenchTool | 可修改使用 | 200-300 | C++門檻較高且無對外封裝接口 |
經過以上總結,咱們能夠推論出,若是有一種方式,支持:
那麼它對於業務方來說,是徹底能夠接受並享受基於eos的區塊鏈帶來的紅利的。
下面就到了引出eosjs的時刻了,eosjs是官方EOSIO組織認可的客戶端調用技術,它不只僅是對rpc協議的封裝,更多的還有大量的eos自己的特性,這些特性均可以作到在客戶端本地實現,例如本地簽名,本地生成交易id等等,這些技術可讓咱們在業務方的客戶端角度充分挖掘需求,自定義接口,上乘業務方,下啓公有鏈eos環境,這種目前爲止最爲合適的承上啓下的技術就是eosjs。
eos環境,可經過腳本快速搭建:
python3 ./bios-boot-tutorial.py -k -w -b -s -c -t
繼續調用
python3 ./bios-boot-tutorial.py -l
將終端界面的輸出內容保持鏈日誌的同步輸出。
eosjs是使用JavaScript語言,nodejs框架構成。
nodejs框架天生可讓咱們便攜地封裝導出以及依賴導入某個「組件」,監於這種特性,咱們也能夠爲業務方開發本身的sdk。
複製代碼
const Eos = require('../src')
const ecc = require('eosjs-ecc')
複製代碼
const keyProvider = [
"5K463ynhZoCDDa4RDcr63cUwWLTnKqmdcoTKTHBjqoKfv4u5V7p",
ecc.seedPrivate('test-tps')
]
const eos = Eos({
httpEndpoint: 'http://39.107.152.239:8000',
chainId: '1c6ae7719a2a3b4ecb19584a30ff510ba1b6ded86e1fd8b8fc22f1179c622a32',
keyProvider: keyProvider,
expireInSeconds: 60,
broadcast: false,
verbose: true
})
複製代碼
eos對象的能力:
{ getCurrencyBalance: [Function],
getCurrencyStats: [Function],
getProducers: [Function],
getInfo: [Function],
getBlock: [Function],
getAccount: [Function],
getCode: [Function],
getTableRows: [Function],
getAbi: [Function],
abiJsonToBin: [Function],
abiBinToJson: [Function],
getRequiredKeys: [Function],
pushBlock: [Function],
pushTransaction: [Function],
pushTransactions: [Function],
getActions: [Function],
getControlledAccounts: [Function],
getKeyAccounts: [Function],
getTransaction: [Function],
createTransaction: [Function],
api: { createTransaction: [Function: createTransaction] },
transaction: [AsyncFunction],
nonce: [Function],
bidname: [Function],
buyram: [Function],
buyrambytes: [Function],
canceldelay: [Function],
claimrewards: [Function],
delegatebw: [Function],
deleteauth: [Function],
linkauth: [Function],
newaccount: [Function],
onerror: [Function],
refund: [Function],
regproducer: [Function],
regproxy: [Function],
reqauth: [Function],
rmvproducer: [Function],
sellram: [Function],
setalimits: [Function],
setglimits: [Function],
setprods: [Function],
setabi: [Function],
setcode: [Function],
setparams: [Function],
setpriv: [Function],
setram: [Function],
undelegatebw: [Function],
unlinkauth: [Function],
unregprod: [Function],
updateauth: [Function],
voteproducer: [Function],
create: [Function],
issue: [Function],
transfer: [Function],
contract: [Function],
fc:
{ structs:
{ extensions_type: [Object],
transaction_header: [Object],
transaction: [Object],
signed_transaction: [Object],
field_def: [Object],
producer_key: [Object],
producer_schedule: [Object],
chain_config: [Object],
type_def: [Object],
struct_def: [Object],
clause_pair: [Object],
error_message: [Object],
abi_def: [Object],
table_def: [Object],
action: [Object],
action_def: [Object],
block_header: [Object],
packed_transaction: [Object],
nonce: [Object],
authority: [Object],
bidname: [Object],
blockchain_parameters: [Object],
buyram: [Object],
buyrambytes: [Object],
canceldelay: [Object],
claimrewards: [Object],
connector: [Object],
delegatebw: [Object],
delegated_bandwidth: [Object],
deleteauth: [Object],
eosio_global_state: [Object],
exchange_state: [Object],
key_weight: [Object],
linkauth: [Object],
namebid_info: [Object],
newaccount: [Object],
onerror: [Object],
permission_level: [Object],
permission_level_weight: [Object],
producer_info: [Object],
refund: [Object],
refund_request: [Object],
regproducer: [Object],
regproxy: [Object],
require_auth: [Object],
rmvproducer: [Object],
sellram: [Object],
set_account_limits: [Object],
set_global_limits: [Object],
set_producers: [Object],
setabi: [Object],
setcode: [Object],
setparams: [Object],
setpriv: [Object],
setram: [Object],
total_resources: [Object],
undelegatebw: [Object],
unlinkauth: [Object],
unregprod: [Object],
updateauth: [Object],
user_resources: [Object],
voteproducer: [Object],
voter_info: [Object],
wait_weight: [Object],
account: [Object],
create: [Object],
currency_stats: [Object],
issue: [Object],
transfer: [Object],
fields: [Object] },
types:
{ bytes: [Function],
string: [Function],
vector: [Function],
optional: [Function],
time: [Function],
map: [Function],
static_variant: [Function],
fixed_string16: [Function],
fixed_string32: [Function],
fixed_bytes16: [Function],
fixed_bytes20: [Function],
fixed_bytes28: [Function],
fixed_bytes32: [Function],
fixed_bytes33: [Function],
fixed_bytes64: [Function],
fixed_bytes65: [Function],
uint8: [Function],
uint16: [Function],
uint32: [Function],
uint64: [Function],
uint128: [Function],
uint224: [Function],
uint256: [Function],
uint512: [Function],
varuint32: [Function],
int8: [Function],
int16: [Function],
int32: [Function],
int64: [Function],
int128: [Function],
int224: [Function],
int256: [Function],
int512: [Function],
varint32: [Function],
float64: [Function],
name: [Function],
public_key: [Function],
symbol: [Function],
extended_symbol: [Function],
asset: [Function],
extended_asset: [Function],
signature: [Function],
config: [Object],
checksum160: [Function],
checksum256: [Function],
checksum512: [Function],
message_type: [Function],
symbol_code: [Function],
field_name: [Function],
account_name: [Function],
permission_name: [Function],
type_name: [Function],
token_name: [Function],
table_name: [Function],
scope_name: [Function],
action_name: [Function],
time_point: [Function],
time_point_sec: [Function],
timestamp: [Function],
block_timestamp_type: [Function],
block_id: [Function],
checksum_type: [Function],
checksum256_type: [Function],
checksum512_type: [Function],
checksum160_type: [Function],
sha256: [Function],
sha512: [Function],
sha160: [Function],
weight_type: [Function],
block_num_type: [Function],
share_type: [Function],
digest_type: [Function],
context_free_type: [Function],
unsigned_int: [Function],
bool: [Function],
transaction_id_type: [Function] },
fromBuffer: [Function],
toBuffer: [Function],
abiCache: { abiAsync: [Function: abiAsync], abi: [Function: abi] } },
modules:
{ format:
{ ULong: [Function: ULong],
isName: [Function: isName],
encodeName: [Function: encodeName],
decodeName: [Function: decodeName],
encodeNameHex: [Function: encodeNameHex],
decodeNameHex: [Function: decodeNameHex],
DecimalString: [Function: DecimalString],
DecimalPad: [Function: DecimalPad],
DecimalImply: [Function: DecimalImply],
DecimalUnimply: [Function: DecimalUnimply],
printAsset: [Function: printAsset],
parseAsset: [Function: parseAsset] } } }
複製代碼
經過以上列出的eos對象的提供的這些功能,咱們能夠知足大部分業務方的需求,這裏展現一個建立用戶的代碼實例:
const nameRule = "12345abcdefghijklmnopqrstuvwxyz"
const config = {
trx_pool_size: 10,
optBCST: {expireInSeconds: 120, broadcast: true},
opts: {expireInSeconds: 60, broadcast: false},
ok: true,
no: false
}
function createAccount(account, publicKey, callback) {
eos.transaction(tr => {
tr.newaccount({
creator: 'eosio',
name: account,
owner: publicKey,
active: publicKey
})
tr.buyrambytes({
payer: 'eosio',
receiver: account,
bytes: 4096
})
tr.delegatebw({
from: 'eosio',
receiver: account,
stake_net_quantity: '0.0002 SYS',
stake_cpu_quantity: '0.0002 SYS',
transfer: 0
})
}).then(callback)
}
function generateAccounts(nameroot) {
for (i = 0; i < 31; i++) {
let accountname = nameroot + nameRule.charAt(i)
console.log("create account: ", accountname)
createAccount(accountname, ecc.privateToPublic(keyProvider[1]), asset => {
eos.transfer("eosio", accountname, "40.0000 SYS", "initial distribution", config.optBCST)
})
}
}
複製代碼
function getAccountsBalance(nameroot) {
for (i = 0; i < 31; i++) {
let accountname = nameroot + nameRule.charAt(i)
eos.getCurrencyBalance("eosio.token", accountname, "SYS").then(tx => {
console.log(accountname + " balance: " + tx[0])
})
}
}
複製代碼
打包交易接口目前我還未封裝完畢,這篇文章更適合做爲學習研究而不是代碼段粘貼,所以對於打包交易的功能,研究好以上內容的朋友能夠有本身的想法,這裏我簡單說一下個人實現思路:
每筆transaction是能夠包含多個action的,在上面介紹過的插件的實現中,也是它的實現思路。另外push_transactions接口是鏈提供的http接口,咱們打包多筆transaction成一個transactions對象請求這個接口,正如插件和EOSBenchTool的實現方式。而後中間要通過大量的優化,這其中較爲重要的是咱們的本地交易池,這個概念在EOSBenchTool中也研究過,那裏的內存對象最多存活5分鐘,而咱們這裏要如何設計呢?是否採用內存變量?仍是引入隊列?這都是架構師的工做,也是根據不一樣的業務場景大有所爲的地方。
複製代碼
更新添加打包交易時序圖:
更新打包交易源碼: Templar
本篇文章全面而詳細地分析了EOS中關於tps的一切手段,包括了cleos,插件,EOSBenchTool,eosjs的方式,這其中,咱們仔細研究了EOSBenchTool的源碼,過程當中也涉及到了qt的部分語法,對比了這幾種方式的利弊,討論了tps的計算方式,tps的現實意義,插件的「做弊」行爲,EOSBenchTool的良好思路和貢獻,eosjs的最終確型,以及針對transaction,action等內部元素的深刻理解與研究。最後也思考了將來eos商業實現的架構設想:經過eosjs做爲承上啓下的sdk。
圓方圓學院聚集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。
公開課地址:ke.qq.com/course/3451…