以太坊中web3調(diào)用邏輯剖析

寫在篇頭

本篇要介紹的內(nèi)容大概是回答以下幾個問題。

  1. geth是怎樣或者使用何種技術(shù)在終端中實現(xiàn)了一個javascript的運行環(huán)境的匀谣。
  2. 在終端中輸入的一個命令是如何調(diào)到以太坊的底層函數(shù)杆麸,從而拿到想要的結(jié)果的瓶您。
  3. 如何增加自己的web3接口衙荐,需要進行哪些修改。

1. JSRE(javascript runtime environment)

以太坊實現(xiàn)了一個javascript的運行環(huán)境即JSRE滩届,可以在console這種交互模式或者script這種非交互模式中使用。詳細(xì)使用見JavaScript Console

  • 交互模式被啼,其可以通過console和attach子命令來啟動帜消。console是在啟動geth的時候在啟動完節(jié)點后打開終端棠枉。attach是在一個運行中的geth實例上開啟一個終端。
$geth console
$geth attach
  • 非交互模式泡挺,是指使用JSRE來執(zhí)行文件术健。console和attach子命令都可以接受--exec的參數(shù)。
$geth --exec "eth.blockNumber" attach
$geth --exec 'loadScript("/tmp/checkbalances.js")' attach

以太坊(這里特指go-ethereum)中使用Otto JS VM來實現(xiàn)JSRE粘衬,即可以在go語言中調(diào)用javascript荞估,也可以獲取javascript中變量的值等,甚至在javascript中也可以調(diào)用go語言中定義的函數(shù)稚新。

下面的是Otto JS VM的一段代碼示例

package main

import (
    "fmt"
    "github.com/robertkrimen/otto"
)

func main() {
    vm := otto.New()
    // 使用vm在go語言中運行一段簡單的javascript代碼
    vm.Run(`
    abc = 2 + 2;
    console.log("The value of abc is " + abc); // 4
    `)
    // 獲取javascript中的變量的值
    if value, err := vm.Get("abc"); err == nil {
        if value_int, err := value.ToInteger(); err == nil {
            fmt.Println(value_int, err) //4 <nil>
        }
    }

    // 定義了一個sayHello的javascript方法勘伺,其調(diào)用的是一段go語言代碼。
    vm.Set("sayHello", func(call otto.FunctionCall) otto.Value {
        fmt.Printf("Hello, %s.\n", call.Argument(0).String())
        return otto.Value{}
    })
    result, _ = vm.Run(`
    sayHello("Xyzzy");      // Hello, Xyzzy.
    `)
}

2. 終端中調(diào)用web3方法的執(zhí)行流程

終端中調(diào)用web3方法的執(zhí)行流程如下圖褂删,大致流程為:


web3命令的調(diào)用流程
  • 終端部分獲取用戶的請求(這部分交互模式與非交互模式略有不同)飞醉。
  • 獲取到用戶請求后,調(diào)用JSRE的方法屯阀,將用戶請求放入到JSRE的隊列當(dāng)中缅帘。
  • JSRE從自己的隊列中獲取請求,然后使用Otto JS VM难衰,調(diào)用相應(yīng)的JS方法钦无。
  • JS代碼中會調(diào)用其privider的Send方法。
  • Send方法中盖袭,調(diào)用rpc client的Call方法失暂。
  • rpc client端向rpc服務(wù)端發(fā)送請求。
  • rpc server端接收到請求鳄虱,調(diào)用相應(yīng)的底層API弟塞,然后將結(jié)果返回給client端。
  • client端逐級返回結(jié)果拙已,最終由終端輸出顯示决记。

使用非交互模式調(diào)用web3方法的流程與該流程類似。不同的部分主要是

  • 交互模式下倍踪,用戶的請求是通過終端協(xié)程獲取用戶輸入得到的系宫,其得到用戶輸入后調(diào)用JSRE的Evaluate方法。Evaluate方法將用戶的請求放入evalQueue中惭适。

2.1 終端

終端中有協(xié)程監(jiān)控著用戶的輸入笙瑟,當(dāng)用戶輸入結(jié)束的時候(一行,或者多行結(jié)束了)癞志。交由另一個協(xié)程處理往枷,該協(xié)程調(diào)用jsre的Evaluate來對用戶輸入的請求進行處理。

// Interactive starts an interactive user session, where input is propted from
// the configured user prompter.
func (c *Console) Interactive() {
    var (
        prompt    = c.prompt          // Current prompt line (used for multi-line inputs)
        indents   = 0                 // Current number of input indents (used for multi-line inputs)
        input     = ""                // Current user input
        scheduler = make(chan string) // Channel to send the next prompt on and receive the input
    )
    // Start a goroutine to listen for promt requests and send back inputs
    go func() {
        for {
            // 獲得用戶的輸入
            // Read the next user input
            line, err := c.prompter.PromptInput(<-scheduler)
            if err != nil {
                // In case of an error, either clear the prompt or fail
                if err == liner.ErrPromptAborted { // ctrl-C
                    prompt, indents, input = c.prompt, 0, ""
                    scheduler <- ""
                    continue
                }
                close(scheduler)
                return
            }
            // User input retrieved, send for interpretation and loop
            scheduler <- line
        }
    }()
    // Monitor Ctrl-C too in case the input is empty and we need to bail
    abort := make(chan os.Signal, 1)
    signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)

    // Start sending prompts to the user and reading back inputs
    for {
        // Send the next prompt, triggering an input read and process the result
        scheduler <- prompt
        select {
        case <-abort:
            // User forcefully quite the console
            fmt.Fprintln(c.printer, "caught interrupt, exiting")
            return

        case line, ok := <-scheduler:
            // User input was returned by the prompter, handle special cases
            // ... 省略掉部分對輸入進行格式化即合法性判斷的處理
            // If all the needed lines are present, save the command and run
            if indents <= 0 {
                if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
                    if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
                        c.history = append(c.history, command)
                        if c.prompter != nil {
                            c.prompter.AppendHistory(command)
                        }
                    }
                }
                // 拿到用戶輸入后調(diào)用Evaluate評估
                c.Evaluate(input)
                input = ""
            }
        }
    }
}
// Evaluate executes code and pretty prints the result to the specified output stream.
func (c *Console) Evaluate(statement string) error {
    defer func() {
        if r := recover(); r != nil {
            fmt.Fprintf(c.printer, "[native] error: %v\n", r)
        }
    }()
    // 調(diào)用jsre的Evaluate來執(zhí)行請求
    return c.jsre.Evaluate(statement, c.printer)
}

2.2 JSRE中的調(diào)用

  • Evaluate定義了請求的方法,方法是調(diào)用vm.Run,并調(diào)用Do方法错洁。
  • Do方法將用戶請求放入到JSRE的隊列evalQueue中秉宿。
  • runEventLoop為JSRE的協(xié)程,從evalQueue中拿到請求后屯碴,調(diào)用相應(yīng)的方法描睦。在Evaluate的情況下即是調(diào)用vm.Run方法。
// Evaluate executes code and pretty prints the result to the specified output stream.
func (re *JSRE) Evaluate(code string, w io.Writer) error {
    var fail error
    // 定義了請求的處理方法导而,即調(diào)用vm.Run忱叭,并將結(jié)果進行相應(yīng)的輸出。
    re.Do(func(vm *otto.Otto) {
        val, err := vm.Run(code)
        if err != nil {
            prettyError(vm, err, w)
        } else {
            prettyPrint(vm, val, w)
        }
        fmt.Fprintln(w)
    })
    return fail
}
// Do executes the given function on the JS event loop.
func (re *JSRE) Do(fn func(*otto.Otto)) {
    done := make(chan bool)
    req := &evalReq{fn, done}
    re.evalQueue <- req
    <-done
}
// This function runs the main event loop from a goroutine that is started
// when JSRE is created. Use Stop() before exiting to properly stop it.
// The event loop processes vm access requests from the evalQueue in a
// serialized way and calls timer callback functions at the appropriate time.

// Exported functions always access the vm through the event queue. You can
// call the functions of the otto vm directly to circumvent the queue. These
// functions should be used if and only if running a routine that was already
// called from JS through an RPC call.
func (re *JSRE) runEventLoop() {
    defer close(re.closed)

    vm := otto.New()
    r := randomSource()
    vm.SetRandomSource(r.Float64)

    // 省略掉部分其他信息

loop:
    for {
        select {
        case req := <-re.evalQueue:
            // run the code, send the result back
            // 調(diào)用相應(yīng)的方法
            req.fn(vm)
            close(req.done)
            if waitForCallbacks && (len(registry) == 0) {
                break loop
            }
        // 省略掉其他情況
    }
}

2.3 JS中的方法

簡單看下web3.js今艺,可以發(fā)現(xiàn)大致是定義方法的名字name韵丑,方法要調(diào)用的方法名eth_getBalance(表示調(diào)用eth服務(wù)的GetBalance方法),參數(shù)個數(shù)params,輸入?yún)?shù)的格式化inputFormatter虚缎,輸出參數(shù)的格式化outputFormatter撵彻。

var methods = function () {
    // web3.js中的getBalance方法
    var getBalance = new Method({
        name: 'getBalance',
        call: 'eth_getBalance',
        params: 2,
        inputFormatter: [formatters.inputAddressFormatter, formatters.inputDefaultBlockNumberFormatter],
        outputFormatter: formatters.outputBigNumberFormatter
    });
    // ...
};

真正執(zhí)行的時候會怎么去像流程圖中畫的調(diào)用provider的send方法,這個代碼在web3.js中实牡,js我不是太懂陌僵,有些東西沒整明白。把我的理解簡單貼下创坞,有錯誤再改碗短。

  • 如上的getBalance是一個Method對象(另外還有一種Property和Method略有差別,這里只分析Method)摆霉。
  • js中對method和properties都分別進行了attachToObject和setRequestManager的操作豪椿。
  • attachToObject中調(diào)用了buildCall方法奔坟,buildcall方法定義了send方法携栋,方法中調(diào)用requestMananger的相應(yīng)方法。
  • requestMananger中調(diào)用的是provider的send或者sendAsync咳秉。
// 這里只給出了Eth婉支,其他的也都類似
function Eth(web3) {
    this._requestManager = web3._requestManager;

    var self = this;

    methods().forEach(function(method) {
        method.attachToObject(self);
        method.setRequestManager(self._requestManager);
    });

    properties().forEach(function(p) {
        p.attachToObject(self);
        p.setRequestManager(self._requestManager);
    });


    this.iban = Iban;
    this.sendIBANTransaction = transfer.bind(null, this);
}

var Method = function (options) {
    this.name = options.name;
    this.call = options.call;
    this.params = options.params || 0;
    this.inputFormatter = options.inputFormatter;
    this.outputFormatter = options.outputFormatter;
    this.requestManager = null;
};

Method.prototype.setRequestManager = function (rm) {
    this.requestManager = rm;
};

Method.prototype.attachToObject = function (obj) {
    var func = this.buildCall();
    func.call = this.call; // TODO!!! that's ugly. filter.js uses it
    var name = this.name.split('.');
    if (name.length > 1) {
        obj[name[0]] = obj[name[0]] || {};
        obj[name[0]][name[1]] = func;
    } else {
        obj[name[0]] = func; 
    }
};

Method.prototype.buildCall = function() {
    var method = this;
    var send = function () {
        var payload = method.toPayload(Array.prototype.slice.call(arguments));
        if (payload.callback) {
            // 調(diào)用requestManager的sendAsync
            return method.requestManager.sendAsync(payload, function (err, result) {
                payload.callback(err, method.formatOutput(result));
            });
        }
        // 調(diào)用requestManager的send
        return method.formatOutput(method.requestManager.send(payload));
    };
    send.request = this.request.bind(this);
    return send;
};

// requestManager的send調(diào)用provider的send方法,
// sendAsync方法也類似澜建,調(diào)用的是provider的sendAsync方法向挖,在此不列出了
RequestManager.prototype.send = function (data) {
    if (!this.provider) {
        console.error(errors.InvalidProvider());
        return null;
    }

    var payload = Jsonrpc.toPayload(data.method, data.params);
    // 重點,這里調(diào)用的是provider的send方法
    var result = this.provider.send(payload);

    if (!Jsonrpc.isValidResponse(result)) {
        throw errors.InvalidResponse(result);
    }

    return result.result;
};

// provider是在最開始的Web3方法中設(shè)置的
function Web3 (provider) {
    // 設(shè)置provider
    this._requestManager = new RequestManager(provider);
    this.currentProvider = provider;
    this.eth = new Eth(this);
    this.db = new DB(this);
    this.shh = new Shh(this);
    this.net = new Net(this);
    this.personal = new Personal(this);
    this.bzz = new Swarm(this);
    this.settings = new Settings();
    this.version = {
        api: version.version
    };
    this.providers = {
        HttpProvider: HttpProvider,
        IpcProvider: IpcProvider
    };
    this._extend = extend(this);
    this._extend({
        properties: properties()
    });
}

2.4 provider的send方法

  • 在console啟動的時候?qū)rovider進行了設(shè)置炕舵,將其設(shè)置為空結(jié)構(gòu)體jeth何之。
  • 將jeth的send方法和sendAsync方法分別設(shè)置為bridge的send方法和sendAsync方法。
// init retrieves the available APIs from the remote RPC provider and initializes
// the console's JavaScript namespaces based on the exposed modules.
func (c *Console) init(preload []string) error {
    // Initialize the JavaScript <-> Go RPC bridge
    bridge := newBridge(c.client, c.prompter, c.printer)
    c.jsre.Set("jeth", struct{}{})

    jethObj, _ := c.jsre.Get("jeth")
    jethObj.Object().Set("send", bridge.Send)
    jethObj.Object().Set("sendAsync", bridge.Send)
    // ...省略其他部分
    if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil {
        return fmt.Errorf("web3 provider: %v", err)
    }
    // ...省略其他部分
}
  • bridge的send方法中(sendAsync方法類似不單獨介紹了)咽筋,首先將request轉(zhuǎn)化為go的值溶推,然后調(diào)用rpc client的Call方法。
  • 根據(jù)rpc client返回的結(jié)果進行相應(yīng)操作。
// Send implements the web3 provider "send" method.
func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
    // ...省略掉將request轉(zhuǎn)化為go的值的過程

    // Execute the requests.
    resps, _ := call.Otto.Object("new Array()")
    for _, req := range reqs {
        resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`)
        resp.Set("id", req.Id)
        var result json.RawMessage
        // 調(diào)用rpc client的Call方法蒜危,將request的method和參數(shù)虱痕,以及回調(diào)的result地址傳給Call
        err = b.client.Call(&result, req.Method, req.Params...) 
        switch err := err.(type) {
        //...根據(jù)錯誤情況設(shè)置resp
        }
        resps.Call("push", resp)
    }

    // Return the responses either to the callback (if supplied)
    // or directly as the return value.
    if batch {
        response = resps.Value()
    } else {
        response, _ = resps.Get("0")
    }
    if fn := call.Argument(1); fn.Class() == "Function" {
        fn.Call(otto.NullValue(), otto.NullValue(), response)
        return otto.UndefinedValue()
    }
    return response
}

2.5 rpc Client端邏輯

rpc client端判斷客戶端是http鏈接,還是本地調(diào)用分別發(fā)送sendHTTP和send方法辐赞。并對調(diào)用的返回結(jié)果進行處理部翘。

func (c *Client) Call(result interface{}, method string, args ...interface{}) error {
    ctx := context.Background()
    return c.CallContext(ctx, result, method, args...)
}

func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
    msg, err := c.newMessage(method, args...)
    if err != nil {
        return err
    }
    op := &requestOp{ids: []json.RawMessage{msg.ID}, resp: make(chan *jsonrpcMessage, 1)}

    if c.isHTTP {
        err = c.sendHTTP(ctx, op, msg)
    } else {
        err = c.send(ctx, op, msg)
    }
    if err != nil {
        return err
    }

    // dispatch has accepted the request and will close the channel it when it quits.
    switch resp, err := op.wait(ctx); {
    //...省略對結(jié)果的處理
    }
}

2.6 rpc Server端邏輯

server端主要分成兩部分,一部分是底層API實現(xiàn)响委,另一部分是rpc server端的處理新思。

底層API實現(xiàn),以eth.getBalance為例赘风。

func (s *PublicBlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (*big.Int, error) {
    state, _, err := s.b.StateAndHeaderByNumber(ctx, blockNr)
    if state == nil || err != nil {
        return nil, err
    }
    b := state.GetBalance(address)
    return b, state.Error()
}

rpc server端的處理流程如下圖:

rpc server端處理流程
  • readRequest主要是通過反射的機制將rpc的request表牢,轉(zhuǎn)化為serverRequest晋辆,可以理解為通過反射找到要調(diào)用的服務(wù)名svcname喘漏,要調(diào)用的方法callb灰伟,并將方法需要使用的參數(shù)args也傳遞到serverRequest中筐高。
  • 根據(jù)是否需要批量處理請求翘紊,調(diào)用execBatch和exec乘盖。
  • exec和execBatch調(diào)用handle射赛,并將結(jié)果返回給rpc client端副签。
  • handle中通過讀取serverRequest中的內(nèi)容山析,調(diào)用相應(yīng)服務(wù)的相應(yīng)方法堰燎。
func (s *Server) serveRequest(ctx context.Context, codec ServerCodec, singleShot bool, options CodecOption) error {
    // ...省略其他部分

    // test if the server is ordered to stop
    for atomic.LoadInt32(&s.run) == 1 {
        // 從readRequest中讀出請求
        reqs, batch, err := s.readRequest(codec)
        if err != nil {
            // If a parsing error occurred, send an error
            if err.Error() != "EOF" {
                log.Debug(fmt.Sprintf("read error %v\n", err))
                codec.Write(codec.CreateErrorResponse(nil, err))
            }
            // Error or end of stream, wait for requests and tear down
            pend.Wait()
            return nil
        }

        // ...省略檢查server是否關(guān)閉,關(guān)閉返回錯誤的情況
        
        // 可以看到批量調(diào)用execBatch笋轨,否則調(diào)用exec
        // If a single shot request is executing, run and return immediately
        if singleShot {
            if batch {
                s.execBatch(ctx, codec, reqs)
            } else {
                s.exec(ctx, codec, reqs[0])
            }
            return nil
        }
        // For multi-shot connections, start a goroutine to serve and loop back
        pend.Add(1)

        go func(reqs []*serverRequest, batch bool) {
            defer pend.Done()
            if batch {
                s.execBatch(ctx, codec, reqs)
            } else {
                s.exec(ctx, codec, reqs[0])
            }
        }(reqs, batch)
    }
    return nil
}

3. 增加web3接口

如何在web3中增加新的接口秆剪。比方說想要增加一個eth.getNonce([addr])的接口。

  • API中增加相應(yīng)的接口爵政。
func (s *PublicBlockChainAPI) GetNonce(ctx context.Context, address common.Address, blockNr rpc.BlockNumber) (uint64, error) {
    state, _, err := s.b.StateAndHeaderByNumber(ctx, blockNr)
    if state == nil || err != nil {
        return nil, err
    }
    b := state.GetNonce(address)
    return b, state.Error()
}
  • web3.js中增加接口仅讽。
var methods = function(){
    var getNonce = new Method({
        name: 'getBalance',
        call: 'eth_getNonce',
        params: 2,
        inputFormatter: [formatters.inputAddressFormatter, formatters.inputDefaultBlockNumberFormatter],
        outputFormatter: formatters.outputBigNumberFormatter
    });
    //...
    return [
        getNonce,
        //...
    ];
};
  • deps.go中有g(shù)o:generate語句,用來重新生成bindata.go文件.

生成bindata.go文件钾挟,需要安裝go-bindata洁灵。安裝方法如下。

go get github.com/jteeuwen/go-bindata
go install github.com/jteeuwen/go-bindata/go-bindata

安裝后生成bindata.go文件掺出,可以在IDE中點擊生成徽千,也可以在命令行中執(zhí)行命令生成。

cd $GOPATH/src/github.com/ethereum/go-ethereum/internal/jsre/deps
go-bindata -nometadata -pkg deps -o bindata.go bignumber.js web3.js
gofmt -w -s bindata.go
  • 重編geth汤锨。
make clean
make geth
  • 重新打開console就可以使用getNonce命令了双抽。
> eth.getNonce(eth.accounts[0])
46
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市闲礼,隨后出現(xiàn)的幾起案子牍汹,更是在濱河造成了極大的恐慌琅翻,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柑贞,死亡現(xiàn)場離奇詭異方椎,居然都是意外死亡,警方通過查閱死者的電腦和手機钧嘶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門棠众,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人有决,你說我怎么就攤上這事闸拿。” “怎么了书幕?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵新荤,是天一觀的道長。 經(jīng)常有香客問我台汇,道長苛骨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任苟呐,我火速辦了婚禮痒芝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘牵素。我一直安慰自己严衬,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布笆呆。 她就那樣靜靜地躺著请琳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赠幕。 梳的紋絲不亂的頭發(fā)上俄精,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音劣坊,去河邊找鬼嘀倒。 笑死,一個胖子當(dāng)著我的面吹牛局冰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播灌危,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼康二,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勇蝙?” 一聲冷哼從身側(cè)響起沫勿,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后产雹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诫惭,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年蔓挖,在試婚紗的時候發(fā)現(xiàn)自己被綠了夕土。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡瘟判,死狀恐怖怨绣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拷获,我是刑警寧澤篮撑,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站匆瓜,受9級特大地震影響赢笨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜驮吱,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一质欲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糠馆,春花似錦嘶伟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至毕匀,卻和暖如春铸鹰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背皂岔。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工蹋笼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人躁垛。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓剖毯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親教馆。 傳聞我的和親對象是個殘疾皇子逊谋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

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

  • 1 go語言的RPC機制 RPC(Remote Procedure Call,遠程過程調(diào)用)是一種通過網(wǎng)絡(luò)從遠程計...
    中v中閱讀 2,754評論 0 1
  • 上一篇:以太坊Solidity開發(fā)入門(宗師篇) 第1章: 介紹 Web3.js 完成第五課以后土铺,我們的僵尸 DA...
    那個大螺絲閱讀 2,390評論 1 10
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理胶滋,服務(wù)發(fā)現(xiàn)板鬓,斷路器,智...
    卡卡羅2017閱讀 134,701評論 18 139
  • 打開園長媽媽APP 點擊忘記密碼 分兩步:第一步輸入給孩子幼兒園報名時預(yù)留的手機號碼 第二步點擊獲取驗證碼短信 接...
    胡芳英閱讀 559評論 0 0
  • 在我工作的第三年究恤,開始學(xué)習(xí)英語俭令。真的張嘴就來的說和提筆忘字的寫。一起去上學(xué)的同事部宿,有三妮兒皮特和春天抄腔,同班的三妮兒...
    雪納瑞狗閱讀 347評論 0 0