【劉文彬】【精解】EOS TPS 多維實測

原文鏈接:醒者呆的博客園店读,https://www.cnblogs.com/Evsward/p/eostps.html

本文主要研究EOS的tps表現(xiàn),會從插件、cleos、EOSBenchTool以及eosjs四種方式進行分析研究绞蹦。

關鍵字:eos, tps, cleos, txn_test_gen_plugin, EOSBenchTool, qt, eosjs, C++源碼分析

身心準備

  • tps: Transaction per Second. 每秒事務處理量
  • 鏈環(huán)境部署使用Python3腳本 bios-boot-tutorial,使用方法請參考boot-sequence腳本
  • 測試機器的硬件配置:雙核cpu + 8G內(nèi)存
  • eos中一個transaction的結構榜旦,展示如下:
{
    "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"
        ]
    }
}

cleos方式

由于我們在研究eos階段幽七,大量使用到cleos,因此使用cleos來測試tps是我們第一個能想到的手段溅呢。這一節(jié)我們將加深理解tps的意義澡屡,tps的計算方法,討論單節(jié)點與多節(jié)點環(huán)境對tps的影響藕届。

單節(jié)點環(huán)境

單節(jié)點的搭建這里不再贅述,直接使用腳本執(zhí)行亭饵,

./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X

注意參數(shù)的順序不能變休偶。

執(zhí)行成功以后,我們將得到一個擁有1000個stake賬戶(簡單理解為已抵押完可直接投票的賬戶)的單節(jié)點eos環(huán)境辜羊,最后一個參數(shù)-X會讓當前環(huán)境不斷執(zhí)行隨機轉(zhuǎn)賬操作(注意:每一筆轉(zhuǎn)賬都是一個action踏兜,一個action對應一個transaction)

查看日志

修改腳本的stepLog函數(shù)词顾,改為:

def stepLog():
    run('tail -f ' + args.nodes_dir + '00-eosio/stderr')

然后在終端執(zhí)行:

./bios-boot-tutorial.py -l

即可進入同步日志輸出的界面。

一碱妆、shell方式

環(huán)境準備完畢肉盹,我們來測試一下當前正在不斷進行轉(zhuǎn)賬的eos鏈上的tps表現(xiàn)。這里采用的tps計算方式為:

tps = BlockTxs*2

因為eos是半秒出塊疹尾,所以兩個塊的打包交易量之和就是tps上忍,為確保數(shù)值可靠性,每個塊的打包交易量我們要通過大量區(qū)塊取平均值的方式纳本。

基于以上思想窍蓝,可以總結出一個shell命令直接在終端執(zhí)行即可:

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}'

取出區(qū)塊號從200到1200的區(qū)塊,分別計算每個區(qū)塊的打包交易量(通過統(tǒng)計其包含的“executed”即可繁成,因為每個交易對應一個“executed”)吓笙,然后將這些區(qū)塊交易量進行累加除以數(shù)量得到平均值,再乘以2巾腕,輔以可視化備注輸出即可面睛。

最終結果不是很理想,至少距離官方聲稱的幾千tps有很大差距尊搬。

1001 blocks average tps = 39.2727

所以1000個塊統(tǒng)計tps為 39.2727叁鉴。

二、python腳本

由于tps的結果不理想毁嗦,我也有過很多思考亲茅,下面我們換一種計算方式來看:

tps = trxs/time

這里通過一種簡單的方式來計算tps:即統(tǒng)計共發(fā)出了trxs筆交易所耗費的時間,以秒為單位狗准,然后相除即可得到tps克锣。

基于以上思想,由于這部分代碼是無法通過一行shell解決的腔长,所以我通過修改bios腳本來解決袭祟,

  • 增加內(nèi)容:
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)
  • 修改randomTransfer函數(shù),增加參數(shù)t,用來決定循環(huán)次數(shù):
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"),
  • 增加參數(shù):
parser.add_argument('--num-tps', metavar='', help="Number of tps test trx", type=int, default=1000)
  • 執(zhí)行A:

    注意己肮,在執(zhí)行前甚带,我們要先停掉單節(jié)點環(huán)境,將-X去掉胆绊,而采用我們的-A來執(zhí)行隨機轉(zhuǎn)賬。

./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X

  • 執(zhí)行B:

./bios-boot-tutorial.py -A --num-tps 2000

  • 發(fā)起2000筆交易欧募,然后使用腳本函數(shù)stepTPS進行測試压状。

  • 結果:

Time used: 26.172592401504517 s tps= 38.20790790072884

結果與shell方式差不多,都是不到40的tps表現(xiàn)。

多節(jié)點環(huán)境

tps的結果不盡人意种冬,我又轉(zhuǎn)念想到了是否因為單節(jié)點出塊的原因镣丑。因此我搭建了多節(jié)點出塊加全節(jié)點的環(huán)境,搭建環(huán)境的方法可以參考《【精解】EOS多節(jié)點組網(wǎng):商業(yè)場景分析以及節(jié)點啟動時序》

我仍舊通過以上兩種方式娱两,分別是shell方式和Python腳本的方式去測試莺匠,最后結果是并無改變,這也證實了eos不具備多線程處理事務的能力十兢。

插曲:我將python腳本的修改提交了EOSIO/eos的官方pr趣竣,結果被拒絕合并,原因是“unrelated change”纪挎,轉(zhuǎn)念一想期贫,如果合并至源碼,用戶可以通過這種方式直白地得到eos的tps就是幾十個的結論异袄,那絕對是很不好的通砍。

txn_test_gen_plugin插件測試

我對eos的高tps有了深深地懷疑,于是找來了官方的tps測試插件烤蜕,要親自感受一下tps的“洗禮”封孙。插件的使用方式很簡單,按照官方文檔的步驟執(zhí)行即可讽营,最后我調(diào)整參數(shù):

curl --data-binary '[""30, 50]' http:/ /localhost:8888/v1/txn_test_gen/start_generationn

鏈上日志結果:

pic1.png

通過trxs一列可以看出虎忌,每個區(qū)塊打包的交易量大大提升了,平均tps在2000左右橱鹏。

插件的測試方法也是bm所推崇的膜蠢,他說通過cleos無法發(fā)揮出真正的eos的性能。那么具體是為什么莉兰,我們通過插件的源碼txn_test_gen_plugin.cpp進行分析挑围,我將這一部分內(nèi)容單獨成文,請閱讀《【源碼解讀】EOS測試插件:txn_test_gen_plugin.cpp》

EOSBenchTool方式

EOSBenchTool來自于OracleChain的貢獻糖荒,雖然他們的節(jié)點oraclegogogo沒競選上bp杉辙,但我認為bp的競選更多是市場行為,不是技術實力的“成績單”捶朵,在所有bp中蜘矢,目前我也僅看到了OracleChain做出的技術方面的貢獻,包括對EOSIO/eos的pr综看,都是OracleChain自身技術氣質(zhì)的體現(xiàn)品腹。多余夸獎的話不多講了,下面來研究這套工具內(nèi)容红碑。

EOSBenchTool的思想與以上的cleos有很大不同舞吭,與插件的方式(打包交易)比較相似,但它的實現(xiàn)方式卻是獨具一格的,他并不是像插件那樣直接在“服務器端”自我模擬交易來測試tps镣典。他們敢于直接使用C++ 來編寫客戶端請求主網(wǎng)來打包、發(fā)起請求唾琼,最終測試得到一個非常不錯的結果兄春,大約可以到200到300,這個結果也是我在眾多壓測手段中得到的比較理想的結果锡溯,包括下面要介紹到的eosjs的方式赶舆,都不及EOSBenchTool的測試結果。

EOSBenchTool既能不犧牲在真實場景中的模擬祭饭,又能通過技術手段優(yōu)化交易通訊芜茵,可以說他的tps結果是比較具備真實性、業(yè)務可行性倡蝙,以及他的技術實現(xiàn)手段也是非常值得業(yè)務方來學習并嘗試使用的九串。

EOSBenchTool的使用

官方文檔的介紹比較技術范兒,就是不太親民寺鸥。這里我給他填點肉猪钮,希望層級嘗試使用EOSBenchTool卻失敗的朋友能夠在這里找到答案。

準備

一胆建、EOS主網(wǎng)環(huán)境

首先烤低,要準備EOS主網(wǎng)環(huán)境,可以通過腳本快速獲得:python3 ./bios-boot-tutorial.py -k -w -b -s -c -t (不部署system合約笆载,因為部署后無法使用create account創(chuàng)建賬戶扑馁。)

二、獲取代碼凉驻,QT工具腻要,編譯代碼

  • 源碼位置:EOSBenchTool

  • QT去官網(wǎng)下載community版本即可,注意:QT在安裝時要同時勾選安裝 QCreator 和 QT source 以及 QT prebuild tool(這里我選擇的是mingw)

  • 打開QCreator沿侈,一般情況下闯第,上面的步驟準備妥當以后,QCreator會自動檢測一套構建套件(Kit)缀拭,構建套件依賴于Qt Version咳短、編譯器、Debuggers蛛淋,Cmakes咙好,這些工具也都是可以自動檢測到的,如果無法檢測到褐荷,一定是某個工具未安裝勾效,請檢查相應的工具,并重新下載安裝(一般來講,所有這些工具在QT安裝包都會包含层宫,只需再次打開QT安裝包杨伙,選擇更新,重新勾選缺乏的工具安裝即可萌腿。)最終我的構建套件(Kit) 截圖如下:

pic2.png
  • QCreator中限匣,Open Project 導入項目源碼中的文件 src/EOSBenchTool.pro,點擊左下角小鋤頭構建項目

啟動EOSBenchTool

以上工作都順利完成以后毁菱,在QCreator中米死,點擊左下角三角按鈕運行啟動EOSBenchTool工具。建議將UI最大化贮庞,可以更方便地查看日志峦筒。填寫好setting內(nèi)容,如下:

pic3.png

關于幾個參數(shù):

  • Thread number:會創(chuàng)建對應的賬戶數(shù)量窗慎。
  • Transaction pool size:總共發(fā)送的測試交易筆數(shù)
  • Transaction batch size:打包時每個包內(nèi)包含的交易筆數(shù)

其他參數(shù)不多介紹物喷。設置好參數(shù)以后,點擊OK保存遮斥,然后切換到 Benchmark Testing 點擊Prepare:創(chuàng)建測試賬戶脯丝、給測試賬戶轉(zhuǎn)賬、每個測試賬戶發(fā)起測試交易并打包伏伐。

等待Prepare結束宠进,1萬筆測試交易大約兩到三分鐘,視客戶端機器本地性能藐翎。然后點擊Start材蹬,得到tps結果,這里由于界面都是可視化的吝镣,我不再贅述堤器。

源碼分析

這部分我們將一起通過源碼學習EOSBenchTool打包交易的原理。

  • 整個EOSBenchTool工具末贾,我們從main.cpp入口闸溃,然后轉(zhuǎn)到主要文件mainwindow.cpp,這里面包含了UI界面配置拱撵,傳參辉川,以及按鈕事件,這里面我們主要關注按鈕事件拴测,總共有三個:
    • on_pushButtonOK_clicked乓旗,這是對應界面 setting 中的ok按鈕,這是負責傳參的集索,這里不做介紹屿愚。
    • on_pushButtonInitialize_clicked汇跨,這是對應界面 Benchmark Testing 中的Prepare按鈕,稍后主要分析妆距。
  • on_pushButtonRun_clicked穷遂,這是對應界面 Benchmark Testing 中的Start按鈕,稍后主要分析娱据。

on_pushButtonInitialize_clicked

Prepare階段塞颁,正如上面在EOSBenchTool使用中介紹到的那樣,包括創(chuàng)建賬戶吸耿,轉(zhuǎn)賬,打包酷窥。

  • 通過CreateAccount對象創(chuàng)建測試賬戶
  • 通過PushManager來轉(zhuǎn)賬
  • 通過Packer來打包交易

創(chuàng)建賬戶

下面先來看創(chuàng)建賬戶的源碼:

CreateAccount createAccount;
int count = createAccount.create(thread_num, [=](const QString& name, bool res) { // lambda格式的回調(diào)函數(shù):打印日志
    commonOutput(QString("Create %1 %2.").arg(name).arg(res ? "succeed" : "failed"));
});

進入createaccount.cpp文件咽安,查看create函數(shù):

int CreateAccount::create(int threadNum, const create_account_callback& func)
{
    if (threadNum <= 0) { // 根據(jù)threadNum個數(shù)創(chuàng)建對應數(shù)量的賬戶。
        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回調(diào)函數(shù)蓬推,實際功能函數(shù):get_info_returned妆棒,由connect開啟socket訪問進去。
            connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
        }
    
        loop.exec();
    
        // 返回執(zhí)行結果res沸伏,成功為true糕珊,失敗為false
        res = !(AccountManager::instance().listKeys(newAccountName).first.empty());
    
        // 執(zhí)行回調(diào)函數(shù):打印日志
        func(newAccountName, res);
    }
    
    return AccountManager::instance().count() - 1;  // 除了super account以外的集合中的賬戶個數(shù)
}

查看一下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的實現(xiàn)方法:

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的命名規(guī)則
    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函數(shù)喇肋,

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格式作為入?yún)ⅰ?        httpc->request(FunctionID::get_required_keys, param);
        // get_required_keys的回調(diào)函數(shù)迹辐,通過socket建立通道去訪問get_required_keys_returned函數(shù)蝶防。
        connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
    }
}

轉(zhuǎn)到函數(shù)packGetRequiredKeysParam(),該函數(shù)是創(chuàng)建賬戶的實際生效函數(shù):

QByteArray CreateAccount::packGetRequiredKeysParam()
{
    if (getInfoData.isEmpty()) {
        return QByteArray();
    }
    
    // 組裝了newAccount的請求數(shù)據(jù)
    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對象轉(zhuǎn)為十六進制
    // 通過ChainManager創(chuàng)建事務明吩,是創(chuàng)建賬戶的事務间学。
    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格式的創(chuàng)建賬戶的事務對象
}

進入get_required_keys_returned函數(shù),

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()函數(shù)組裝好的推送交易接口的入?yún)aram低葫,然后通過http發(fā)起請求。
        httpc->request(FunctionID::push_transaction, param);
        // 通過connect建立socket連接訪問push_transaction的回調(diào)函數(shù)push_transaction_returned仍律,繼續(xù)處理氮采。
        connect(httpc, &HttpClient::responseData, this, &CreateAccount::push_transaction_returned);
    }
}

packPushTransactionParam(),開始組裝push transaction的參數(shù)染苛,由于代碼中對于數(shù)據(jù)的處理較多鹊漠,這里只展示結果的部分:

// 給上面由函數(shù)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(); // 獲得簽名后的交易數(shù)據(jù)

push_transaction_returned,我們經(jīng)過大量的組合校驗躯概,與鏈上的信息進行同步組裝獲得了合法的簽名交易對象登钥,然后通過http接口請求了push_transaction接口將簽名交易對象推送到鏈上執(zhí)行,執(zhí)行結果通過回調(diào)函數(shù)處理娶靡,回調(diào)函數(shù)的主要作用是將處理結果 -> 成功創(chuàng)建了的這個賬戶牧牢,存入集合accounts中,由于accounts是私有屬性姿锭,所以通過方法AccountManager::instance().addAccounts執(zhí)行塔鳍。

客戶端本地保存了一個對象accounts用來同步自己創(chuàng)建過的賬戶。大部分代碼是對accounts的處理呻此。

賬戶轉(zhuǎn)賬

在上一個創(chuàng)建賬戶的部分轮纫,我們詳細解讀了通訊的過程,仍舊是通過http去發(fā)起請求焚鲜,通過每個請求的回調(diào)函數(shù)進行處理掌唾,組裝,維護了本地的集合accounts忿磅。由于篇幅過大糯彬,在之后的介紹中,不會再過多介紹葱她,而專注于實現(xiàn)方式的核心代碼撩扒。轉(zhuǎn)賬的核心代碼:

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); // 拼串,轉(zhuǎn)賬額度
    QString to = QString::fromStdString(accounts.at(i)); // 遍歷接收轉(zhuǎn)賬的賬戶
    commonOutput(QString("Transfering %1 to %2 ...").arg(quantity).arg(to)); // 日志
    bool ret = push.transferToken(super_account, to, quantity); // 核心生效代碼吨些,是PushManager的transferToken函數(shù)却舀。
    commonOutput(ret ? "Succeed." : "Failed.");
}

PushManager的transferToken函數(shù)是本地組裝了標準的轉(zhuǎn)賬請求參數(shù),json字符串格式的from, to, quality以及memo信息锤灿。然后跳轉(zhuǎn)到make_push函數(shù)挽拔。make_push函數(shù)需要通過http請求接口abi_json_to_bin,而針對該接口的入?yún)⒌#夹枰谶@個函數(shù)處理獲取到螃诅,入?yún)╝ction,code以及args状囱。code就是對應的合約的code术裸,例如我們使用賬戶eosio部署了合約eosio.system,那么eosio.system的code就可以通過get code eosio獲得亭枷。action就是轉(zhuǎn)賬:transfer袭艺。args就是上面PushManager的transferToken函數(shù)組裝的參數(shù)對象。http請求成功以后叨粘,通過回調(diào)函數(shù)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數(shù)據(jù)為二進制數(shù)據(jù)瘤睹。這個結果的數(shù)據(jù)通常用在push_transaction的data字段。

action.setData(hexData); // action的hexData字段就是以上接口**abi\_json\_to\_bin**獲得的結果答倡。

剩余部分與上面介紹“創(chuàng)建賬戶”相同轰传,get_info -> get_required_keys -> push_transaction 的流程。

總結一下瘪撇,轉(zhuǎn)賬由于涉及到合約获茬,所以多了一步abi_json_to_bin,而創(chuàng)建賬戶不需要這一步倔既,但創(chuàng)建賬戶需要本地的集合對象同步存儲恕曲。

打包交易

首先說明,打包的交易是測試交易渤涌,不是以上的創(chuàng)建賬戶和賬戶轉(zhuǎn)賬佩谣。先看源碼部分:

trxpool = new TransactionPool; // 創(chuàng)建交易池
trxpool->setTargetSize(trx_size); // 設置交易池的大小
// packedTrxTransferFinished,打包測試交易發(fā)送鏈全部結束
connect(trxpool, &TransactionPool::finished, this, &MainWindow::packedTrxTransferFinished); 
// packedTrxReady歼捏,prepare階段完成,可以點擊start
connect(trxpool, &TransactionPool::packedTrxPoolFulfilled, this, &MainWindow::packedTrxReady);

enablePacker(true);// 核心打包內(nèi)容

enablePacker()笨篷,觸發(fā)打包流程

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(); // 執(zhí)行Packer

    packers.push_back(p);
}

進入incomingPackedTrxs函數(shù)瞳秽,

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開始執(zhí)行练俐,

void Packer::run()
{
    while(!needStop) {
        PushManager push(false);
        // 這是一個包含lambda為回調(diào)函數(shù)的connect語句
        connect(&push, &PushManager::trxPacked, this, [&](const QByteArray& data){
            emit newPackedTrx(data); // emit 發(fā)送signal給newPackedTrx B:稍后重點講
            func(QString("PACKED: %1 to %2.").arg(accountName).arg(super_account));// 打印日志
        });
        // 以下部分與賬戶轉(zhuǎn)賬接口一致,后續(xù)內(nèi)容均同上冕臭。
        push.transferToken(accountName, super_account, QString("0.0001 %1").arg(token_name));
    }
}

當Packer開始run的時候腺晾,它是一個無線循環(huán),直到灌滿trxPool為止辜贵,而其中悯蝉,我們注意觀察,這一connect翻譯過來就是:我先注冊一個signals trxPacked在這托慨,等待某處代碼將該信號發(fā)射鼻由,會被這里捕捉到,將它傳入回調(diào)函數(shù)厚棵,就是這個lambda回調(diào)函數(shù)的參數(shù)data中蕉世,這個lambda回調(diào)函數(shù)我們先放一放,來講這個signals trxPacked:

signals 對應的觸發(fā)是 emit

trxPacked 作為一個signals 是在PushManager::get_required_keys_returned中被發(fā)射emit的(注意這個是與上面講到的CreateAccount::get_required_keys_returned是不同的婆硬。)

QByteArray param = packPushTransactionParam();
emit trxPacked(param);
...
httpc->request(FunctionID::push_transaction, param);

這個emit發(fā)送的param是僅在push_transaction發(fā)送之前的transaction狠轻,會將這個對象傳入回調(diào)函數(shù)。下面來看一下lambda回調(diào)函數(shù)的內(nèi)部彬犯,獲取到transaction數(shù)據(jù)對象以后向楼,會將該對象再次emit到一個signals newPackedTrx查吊,我們?nèi)フ乙幌逻@個signals的注冊位置:MainWindow::enablePacker,就是上面展示過的代碼蜜自,我注釋為“A:稍后重點講”菩貌,因此相同的原理,這個data又被傳入了incomingPackedTrxs函數(shù)重荠,最終被打包進packedTransactions集合中箭阶。

關于QT的signals emit slot connect 的具體語法介紹的內(nèi)容可以查看這篇文章我們沒有QT開發(fā)的需求,所以沒必要在此過多介紹語法內(nèi)容戈鲁,只需要捋清楚業(yè)務邏輯即可仇参。

packedTransactions的內(nèi)容是屬于TransactionPool的,它會在TransactionPool被啟動時(也就是start按鈕被按下時)使用婆殿,而這個對象是在prepare階段被儲存诈乒。(據(jù)說這個時間只有5分鐘,機器性能不太好的不要將trxPool設置地太高婆芦,否則執(zhí)行不完怕磨,打包好的packedTransactions并未做持久化,就會消失掉消约,最終導致測試結果失真)

on_pushButtonRun_clicked

這個按鈕點擊事件的內(nèi)容看上去比較簡單肠鲫,只有一個enableTrxpool(true)是生效代碼,其他都是一些日志或粮。下面直接進入enableTrxpool函數(shù)导饲,不張貼了,直接轉(zhuǎn)到核心代碼trxpool->start(); 那么我們進入到transactionpool.cpp氯材,start對應run函數(shù)渣锦,源碼如下:

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接口,將這些“小包”推送到鏈執(zhí)行冗尤。

總結

沒想到EOSBenchTool的源碼解讀一下子搞了這么長的篇幅娄猫,我沒控制住,讀者又要吃力了生闲。其實到這里我們來總結一下媳溺,EOSBenchTool主要是使用了QT的界面系統(tǒng),同時也用到了QT的signals碍讯,emit悬蔽,connect等專有語法,不懂qt的同學看起來有些吃力捉兴。然而蝎困,拋開這些語言或者類庫的語法來講录语,我們專注于代碼邏輯,EOSBenchTool的實現(xiàn)是容易被人理解的:

  • 首先禾乘,可以確定他是一個客戶端澎埠,都是通過我們前面文章介紹過很多遍的最熟悉的那些http接口的請求來與鏈交互的。
  • ++接著始藕,它采用了本地內(nèi)存對象的方式來存儲我們設定好的所有的交易量的集合對象蒲稳。這個部分是可以改善的,畢竟如果測試量過大就會丟失伍派。++
  • 它設計了一個“小包”的概念江耀,相對應的,我們前面打包好的“大包”诉植,我們設置了一個小包的大小祥国,可以按照小包為單位對鏈發(fā)起批量交易的請求。

eosjs方式

上面我們介紹了:

Way Business TPS memo
cleos 可直接使用 70-80 (單節(jié)點晾腔、多節(jié)點)shell方式舌稀,python腳本
txn_test_gen_plugin 不可使用 1500-2000 官方用來測試的一種方式,這個插件純粹是為了測tps而設的
EOSBenchTool 可修改使用 200-300 C++門檻較高且無對外封裝接口

通過以上總結灼擂,我們可以推論出壁查,如果有一種方式,支持:

  • 有對外接口可易于調(diào)用
  • 開發(fā)語言門檻較低
  • 客戶端行為
  • 支持打包請求
  • tps能達到200-300

那么它對于業(yè)務方來講缤至,是完全可以接受并享受基于eos的區(qū)塊鏈帶來的紅利的潮罪。

下面就到了引出eosjs的時刻了康谆,eosjs是官方EOSIO組織承認的客戶端調(diào)用技術领斥,它不僅僅是對rpc協(xié)議的封裝,更多的還有大量的eos本身的特性沃暗,這些特性都可以做到在客戶端本地實現(xiàn)月洛,例如本地簽名,本地生成交易id等等孽锥,這些技術可以讓我們在業(yè)務方的客戶端角度充分挖掘需求嚼黔,自定義接口,上乘業(yè)務方惜辑,下啟公有鏈eos環(huán)境唬涧,這種目前為止最為合適的承上啟下的技術就是eosjs。

源碼位置

準備環(huán)境

eos環(huán)境盛撑,可通過腳本快速搭建:

python3 ./bios-boot-tutorial.py -k -w -b -s -c -t

繼續(xù)調(diào)用

python3 ./bios-boot-tutorial.py -l

將終端界面的輸出內(nèi)容保持鏈日志的同步輸出碎节。

源碼架構

eosjs是使用JavaScript語言,nodejs框架構成抵卫。

nodejs框架天生可以讓我們便攜地封裝導出以及依賴導入某個“組件”狮荔,監(jiān)于這種特性胎撇,我們也可以為業(yè)務方開發(fā)自己的sdk。

常用組件

  • src/index.js 中的 module.exports = EOS殖氏,這是主要組件晚树,通過該組件可創(chuàng)建相應對象
  • eosjs-ecc,可獲得加密工具對象雅采,該對象能夠調(diào)用所有加密相關的動作爵憎,例如簽名,私鑰公鑰等总滩。
const Eos = require('../src')
const ecc = require('eosjs-ecc')

EOS對象

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
})
  • expireInSeconds:過期時間纲堵,該行為如在此過期時間內(nèi)仍未執(zhí)行成功,則會被判定過期而拋棄闰渔。
  • broadcast:這是一個本地行為(false)還是要廣播到遠端鏈上(ture)席函。
  • verbose:是否要打印所有發(fā)生http請求的請求返回結構體。

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] } } }

實例:創(chuàng)建用戶

通過以上列出的eos對象的提供的這些功能冈涧,我們可以滿足大部分業(yè)務方的需求茂附,這里展示一個創(chuàng)建用戶的代碼實例:

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])
        })
    }
}

打包交易

打包交易接口目前我還未封裝完畢,這篇文章更適合作為學習研究而不是代碼段粘貼督弓,因此對于打包交易的功能营曼,研究好以上內(nèi)容的朋友可以有自己的想法,這里我簡單說一下我的實現(xiàn)思路:

每筆transaction是可以包含多個action的愚隧,在上面介紹過的插件的實現(xiàn)中蒂阱,也是它的實現(xiàn)思路。另外push_transactions接口是鏈提供的http接口狂塘,我們打包多筆transaction成一個transactions對象請求這個接口录煤,正如插件和EOSBenchTool的實現(xiàn)方式。然后中間要經(jīng)過大量的優(yōu)化荞胡,這其中較為重要的是我們的本地交易池妈踊,這個概念在EOSBenchTool中也研究過,那里的內(nèi)存對象最多存活5分鐘泪漂,而我們這里要如何設計呢廊营?是否采用內(nèi)存變量?還是引入隊列萝勤?這都是架構師的工作露筒,也是根據(jù)不同的業(yè)務場景大有所為的地方。

更新添加打包交易時序圖:

pic4.png

更新打包交易源碼: Templar

總結

本篇文章全面而詳細地分析了EOS中關于tps的一切手段敌卓,包括了cleos慎式,插件,EOSBenchTool,eosjs的方式瞬捕,這其中鞍历,我們仔細研究了EOSBenchTool的源碼,過程中也涉及到了qt的部分語法肪虎,對比了這幾種方式的利弊劣砍,討論了tps的計算方式,tps的現(xiàn)實意義扇救,插件的“作弊”行為刑枝,EOSBenchTool的良好思路和貢獻藐唠,eosjs的最終確型沐鼠,以及針對transaction拴曲,action等內(nèi)部元素的深入理解與研究薄腻。最后也思考了未來eos商業(yè)實現(xiàn)的架構設想:通過eosjs作為承上啟下的sdk。

參考資料

  • EOS官方文檔
  • EOSBenchTool源碼
  • eosjs源碼

相關文章和視頻推薦

圓方圓學院匯集大批區(qū)塊鏈名師暗赶,打造精品的區(qū)塊鏈技術課程晌纫。 在各大平臺都長期有優(yōu)質(zhì)免費公開課剂癌,歡迎報名收看锌雀。

公開課地址:https://ke.qq.com/course/345101

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚂夕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子腋逆,更是在濱河造成了極大的恐慌婿牍,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惩歉,死亡現(xiàn)場離奇詭異等脂,居然都是意外死亡,警方通過查閱死者的電腦和手機撑蚌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門上遥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锨并,你說我怎么就攤上這事露该〔桥铮” “怎么了第煮?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抑党。 經(jīng)常有香客問我包警,道長,這世上最難降的妖魔是什么底靠? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任害晦,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘壹瘟。我一直安慰自己鲫剿,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布稻轨。 她就那樣靜靜地躺著灵莲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪殴俱。 梳的紋絲不亂的頭發(fā)上政冻,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音线欲,去河邊找鬼明场。 笑死,一個胖子當著我的面吹牛李丰,可吹牛的內(nèi)容都是我干的苦锨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼趴泌,長吁一口氣:“原來是場噩夢啊……” “哼逆屡!你這毒婦竟也來了?” 一聲冷哼從身側響起踱讨,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤魏蔗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后痹筛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體莺治,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年帚稠,在試婚紗的時候發(fā)現(xiàn)自己被綠了谣旁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡滋早,死狀恐怖榄审,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杆麸,我是刑警寧澤搁进,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站昔头,受9級特大地震影響饼问,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜揭斧,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一莱革、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦盅视、人聲如沸捐名。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桐筏。三九已至,卻和暖如春拇砰,著一層夾襖步出監(jiān)牢的瞬間梅忌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工除破, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留牧氮,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓瑰枫,卻偏偏與公主長得像踱葛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子光坝,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容