【深度知識】RPC原理及以太坊RPC的實現

1.摘要

本文介紹RPC協議的原理和調用流程,同時介紹以太坊RPC的實現機制。

2. 內容

2.1 RPC協議和調用流程

2.1.1 遠程過程調用 (RPC)

Remote Procedure Calls 遠程過程調用 (RPC) 是一種協議,就是從一臺機器(客戶端)上通過參數傳遞的方式調用另一臺機器(服務器)上的一個函數或方法(可以統稱爲服務)並得到返回的結果。
通常的實現有**XML-RPC , JSON-RPC ,**通信方式基本相同, 所不同的只是傳輸數據的格式。

RPC是分佈式架構的核心,按響應方式分如下兩種:
**<1>同步調用:**客戶端調用服務方方法,等待直到服務方返回結果或者超時,再繼續自己的操作;
**<2>異步調用:**客戶端把消息發送給中間件,不再等待服務端返回,直接繼續自己的操作;

同步調用的實現方式有WebService和RMI。
<1>Web Service提供的服務是基於web容器的,底層使用http協議,因而適合不同語言異構系統間的調用。
<2>RMI(Remote Method Invocation,遠程方法調用)實際上是Java語言的RPC實現,允許方法返回 Java 對象以及基本數據類型,適合用於JAVA語言構建的不同系統間的調用。

異步調用的JAVA實現版就是JMS(Java Message Service),目前開源的的JMS中間件有Apache社區的ActiveMQ、Kafka消息中間件,另外有阿里的RocketMQ。

2.1.2 RPC框架

一個完整的RPC架構裏面包含了四個核心的組件,分別是Client,Client Stub,Server以及Server Stub,這個Stub可以理解爲存根。

  • 客戶端(Client),服務的調用方。
  • 客戶端存根(Client Stub),存放服務端的地址消息,再將客戶端的請求參數打包成網絡消息,然後通過網絡遠程發送給服務方。
  • 服務端(Server),真正的服務提供者。
  • 服務端存根(Server Stub),接收客戶端發送過來的消息,將消息解包,並調用本地的方法。

2.1.3 RPC調用流程

具體實現步驟:
1、 服務調用方(client)(客戶端)以本地調用方式調用服務;
2、 client stub接收到調用後負責將方法、參數等組裝成能夠進行網絡傳輸的消息體;在Java裏就是序列化的過程;
3、 client stub找到服務地址,並將消息通過網絡發送到服務端;
4、 server stub收到消息後進行解碼,在Java裏就是反序列化的過程;
5、 server stub根據解碼結果調用本地的服務;
6、 本地服務執行處理邏輯;
7、 本地服務將結果返回給server stub;
8、 server stub將返回結果打包成消息,Java裏的序列化;
9、 server stub將打包後的消息通過網絡併發送至消費方;
10、 client stub接收到消息,並進行解碼, Java裏的反序列化;
11、 服務調用方(client)得到最終結果。

RPC框架的目標就是把2-10步封裝起來,把調用、編碼/解碼的過程封裝起來,讓用戶像調用本地服務一樣的調用遠程服務。要做到對客戶端(調用方)透明化服務, RPC框架需要考慮解決如下問題: 
1、通訊問題 : 主要是通過在客戶端和服務器之間建立TCP連接,遠程過程調用的所有交換的數據都在這個連接裏傳輸。連接可以是按需連接,調用結束後就斷掉,也可以是長連接,多個遠程過程調用共享同一個連接。 
2、尋址問題: A服務器上的應用怎麼告訴底層的RPC框架,如何連接到B服務器(如主機或IP地址)以及特定的端口,方法的名稱是什麼,這樣才能完成調用。比如基於Web服務協議棧的RPC,就要提供一個endpoint URI,或者是從UDDI服務上查找。如果是RMI調用的話,還需要一個RMI Registry來註冊服務的地址。 
3、序列化與反序列化 : 當A服務器上的應用發起遠程過程調用時,方法的參數需要通過底層的網絡協議如TCP傳遞到B服務器,由於網絡協議是基於二進制的,內存中的參數的值要序列化成二進制的形式,也就是序列化(Serialize)或編組(marshal),通過尋址和傳輸將序列化的二進制發送給B服務器。 
同理,B服務器接收參數要將參數反序列化。B服務器應用調用自己的方法處理後返回的結果也要序列化給A服務器,A服務器接收也要經過反序列化的過程。

2.2 以太坊RPC實現

JSON-RPC是區塊鏈外部調用的標配了。以太坊同樣也實現了這個功能。底層支持四種協議:InProc,IPC,HTTP,WEBSOCKED。上層除了常規的方法調用之外還實現了Pub/Sub功能。本文主要分析以太坊是如何支持這些個功能的。

2.2.1 api發佈

api接口分佈在各個模塊,主要分爲兩種

  • 1:直接code在Node中的幾個service(admin,web3j,debug etc)
  • 2: 實現了Service接口的服務結構,已經註冊的服務會調用APIs()方法獲得其中的api。
//file go-ethereum/node/node.go
func (n *Node) startRPC(services map[reflect.Type]Service) error {
	apis := n.apis()
	for _, service := range services {
		apis = append(apis, service.APIs()...)
	}
}

node中寫死的接口

    // node中寫死的接口
    func (n *Node) apis() []rpc.API {
        return []rpc.API{
            {
                Namespace: "admin",
                Version:   "1.0",
                Service:   NewPrivateAdminAPI(n),
            }, {
                Namespace: "admin",
                Version:   "1.0",
                Service:   NewPublicAdminAPI(n),
                Public:    true,
            }, {
                Namespace: "debug",
                Version:   "1.0",
                Service:   debug.Handler,
            }, {
                Namespace: "debug",
                Version:   "1.0",
                Service:   NewPublicDebugAPI(n),
                Public:    true,
            }, {
                Namespace: "web3",
                Version:   "1.0",
                Service:   NewPublicWeb3API(n),
                Public:    true,
            },
        }
    }

Ethereum 服務實現的APIs()接口 類似的還有其他的服務(dashboard,ethstats)

    //Ethereum 服務實現的APIs()接口
    func (s *Ethereum) APIs() []rpc.API {
        apis := ethapi.GetAPIs(s.ApiBackend)

        // Append any APIs exposed explicitly by the consensus engine
        apis = append(apis, s.engine.APIs(s.BlockChain())...)

        // Append all the local APIs and return
        return append(apis, []rpc.API{
            {
                Namespace: "eth",
                Version:   "1.0",
                Service:   NewPublicEthereumAPI(s),
                Public:    true,
            }, {
                Namespace: "eth",
                Version:   "1.0",
                Service:   NewPublicMinerAPI(s),
                Public:    true,
            }, {
                Namespace: "eth",
                Version:   "1.0",
                Service:   downloader.NewPublicDownloaderAPI(s.protocolManager.downloader, s.eventMux),
                Public:    true,
            }, {
                Namespace: "miner",
                Version:   "1.0",
                Service:   NewPrivateMinerAPI(s),
                Public:    false,
            }, {
                Namespace: "eth",
                Version:   "1.0",
                Service:   filters.NewPublicFilterAPI(s.ApiBackend, false),
                Public:    true,
            }, {
                Namespace: "admin",
                Version:   "1.0",
                Service:   NewPrivateAdminAPI(s),
            }, {
                Namespace: "debug",
                Version:   "1.0",
                Service:   NewPublicDebugAPI(s),
                Public:    true,
            }, {
                Namespace: "debug",
                Version:   "1.0",
                Service:   NewPrivateDebugAPI(s.chainConfig, s),
            }, {
                Namespace: "net",
                Version:   "1.0",
                Service:   s.netRPCService,
                Public:    true,
            },
        }...)
    }

這裏的Service只是類型,還要註冊到Server裏面,原理就是反射出結構體裏的類型,解析出函數方法名稱(轉小寫),參數名稱,返回類型等信息,最終每個合格的方法都會生成service實例。

    type service struct {
        name          string        // name for service
        typ           reflect.Type  // receiver type
        callbacks     callbacks     // registered handlers
        subscriptions subscriptions // available subscriptions/notifications
    }

    //反射除Service Api的結構方法
    //file go-ethereum/rpc/utils.go
    func suitableCallbacks(rcvr reflect.Value, typ reflect.Type) (callbacks, subscriptions) {
        callbacks := make(callbacks)
        subscriptions := make(subscriptions)

    METHODS:
        for m := 0; m < typ.NumMethod(); m++ {
            method := typ.Method(m)
            mtype := method.Type
            //轉小寫
            mname := formatName(method.Name)
            if method.PkgPath != "" { // method must be exported
                continue
            }

            var h callback
            //訂閱事件類型判斷 主要根據簽名的入參第二位和返回參數第一位
            h.isSubscribe = isPubSub(mtype)  
            h.rcvr = rcvr
            h.method = method
            h.errPos = -1

            firstArg := 1
            numIn := mtype.NumIn()
            if numIn >= 2 && mtype.In(1) == contextType {
                h.hasCtx = true
                firstArg = 2
            }

            if h.isSubscribe {
                //訂閱類型
                h.argTypes = make([]reflect.Type, numIn-firstArg) // skip rcvr type
                for i := firstArg; i < numIn; i++ {
                    argType := mtype.In(i)
                    if isExportedOrBuiltinType(argType) {
                        h.argTypes[i-firstArg] = argType
                    } else {
                        continue METHODS
                    }
                }

                subscriptions[mname] = &h
                continue METHODS
            }

            // determine method arguments, ignore first arg since it's the receiver type
            // Arguments must be exported or builtin types
            h.argTypes = make([]reflect.Type, numIn-firstArg)
            for i := firstArg; i < numIn; i++ {
                argType := mtype.In(i)
                if !isExportedOrBuiltinType(argType) {
                    continue METHODS
                }
                h.argTypes[i-firstArg] = argType
            }

            // check that all returned values are exported or builtin types
            for i := 0; i < mtype.NumOut(); i++ {
                if !isExportedOrBuiltinType(mtype.Out(i)) {
                    continue METHODS
                }
            }

            // when a method returns an error it must be the last returned value
            h.errPos = -1
            for i := 0; i < mtype.NumOut(); i++ {
                if isErrorType(mtype.Out(i)) {
                    h.errPos = i
                    break
                }
            }

            if h.errPos >= 0 && h.errPos != mtype.NumOut()-1 {
                continue METHODS
            }

            switch mtype.NumOut() {
            case 0, 1, 2:
                if mtype.NumOut() == 2 && h.errPos == -1 { // method must one return value and 1 error
                    continue METHODS
                }
                callbacks[mname] = &h
            }
        }

        return callbacks, subscriptions
    }

2.2.2 底層協議

底層支持了InProc,IPC,HTTP,WEBSOCKED 四種傳輸協議

  • 1 InProc 直接生成RPCService實例,掛在Node上面可以直接調用。
  • 2 IPC 監聽管道,收到消息後解析成ServerCodec對象,扔給Server的ServeCodec方法使用;
    //file ipc.go
    func (srv *Server) ServeListener(l net.Listener) error {
        for {
            conn, err := l.Accept()
            if netutil.IsTemporaryError(err) {
                log.Warn("RPC accept error", "err", err)
                continue
            } else if err != nil {
                return err
            }
            log.Trace("Accepted connection", "addr", conn.RemoteAddr())
            go srv.ServeCodec(NewJSONCodec(conn), OptionMethodInvocation|OptionSubscriptions)
        }
    }

  • 3 HTTP 生成兩個中間件,第二個中間件接收消息生成ServerCOdec,扔給Server的ServeSingleRequest方法
    //file http.go
    func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        // Permit dumb empty requests for remote health-checks (AWS)
        if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
            return
        }
        if code, err := validateRequest(r); err != nil {
            http.Error(w, err.Error(), code)
            return
        }
        // All checks passed, create a codec that reads direct from the request body
        // untilEOF and writes the response to w and order the server to process a
        // single request.
        ctx := context.Background()
        ctx = context.WithValue(ctx, "remote", r.RemoteAddr)
        ctx = context.WithValue(ctx, "scheme", r.Proto)
        ctx = context.WithValue(ctx, "local", r.Host)

        body := io.LimitReader(r.Body, maxRequestContentLength)
        codec := NewJSONCodec(&httpReadWriteNopCloser{body, w})
        defer codec.Close()

        w.Header().Set("content-type", contentType)
        srv.ServeSingleRequest(codec, OptionMethodInvocation, ctx)
    }

  • 1 WEBSOCKED 與Http類型生成WebsocketHandler中間件,到消息後解析成ServerCodec對象,扔給Server的ServeCodec方法使用
    //websocked.go
    func (srv *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
        return websocket.Server{
            Handshake: wsHandshakeValidator(allowedOrigins),
            Handler: func(conn *websocket.Conn) {
                // Create a custom encode/decode pair to enforce payload size and number encoding
                conn.MaxPayloadBytes = maxRequestContentLength

                encoder := func(v interface{}) error {
                    return websocketJSONCodec.Send(conn, v)
                }
                decoder := func(v interface{}) error {
                    return websocketJSONCodec.Receive(conn, v)
                }
                srv.ServeCodec(NewCodec(conn, encoder, decoder), OptionMethodInvocation|OptionSubscriptions)
            },
        }
    }

2.2.3 rpc響應

上面四種協議再拿到ServerCodec對象後,會把這個對象傳遞到service的響應請數裏面去。最終都是調到handle函數裏面,handle裏面再根據不同的類型進行響應。

    func (s *Server) handle(ctx context.Context, codec ServerCodec, req *serverRequest) (interface{}, func()) {
        if req.err != nil {
            return codec.CreateErrorResponse(&req.id, req.err), nil
        }

        if req.isUnsubscribe { 
            //取消訂閱功能
            if len(req.args) >= 1 && req.args[0].Kind() == reflect.String {
                notifier, supported := NotifierFromContext(ctx)  //獲取notifier對象
                if !supported { // interface doesn't support subscriptions (e.g. http)
                    return codec.CreateErrorResponse(&req.id, &callbackError{ErrNotificationsUnsupported.Error()}), nil
                }

                //取消訂閱
                subid := ID(req.args[0].String())
                if err := notifier.unsubscribe(subid); err != nil {
                    return codec.CreateErrorResponse(&req.id, &callbackError{err.Error()}), nil
                }

                return codec.CreateResponse(req.id, true), nil
            }
            return codec.CreateErrorResponse(&req.id, &invalidParamsError{"Expected subscription id as first argument"}), nil
        }

        if req.callb.isSubscribe {
            //訂閱功能  

            subid, err := s.createSubscription(ctx, codec, req)
            if err != nil {
                return codec.CreateErrorResponse(&req.id, &callbackError{err.Error()}), nil
            }

            // active the subscription after the sub id was successfully sent to the client
            activateSub := func() {
                notifier, _ := NotifierFromContext(ctx)  //獲取notifier對象
                notifier.activate(subid, req.svcname)    //訂閱事件
            }

            return codec.CreateResponse(req.id, subid), activateSub
        }

        // regular RPC call, prepare arguments
        //參數生成
        if len(req.args) != len(req.callb.argTypes) {
            rpcErr := &invalidParamsError{fmt.Sprintf("%s%s%s expects %d parameters, got %d",
                req.svcname, serviceMethodSeparator, req.callb.method.Name,
                len(req.callb.argTypes), len(req.args))}
            return codec.CreateErrorResponse(&req.id, rpcErr), nil
        }

        arguments := []reflect.Value{req.callb.rcvr}
        if req.callb.hasCtx {
            arguments = append(arguments, reflect.ValueOf(ctx))
        }
        if len(req.args) > 0 {
            arguments = append(arguments, req.args...)
        }

        // execute RPC method and return result
        //執行對應的函數
        reply := req.callb.method.Func.Call(arguments)
        if len(reply) == 0 {
            return codec.CreateResponse(req.id, nil), nil
        }
        //校驗結果
        if req.callb.errPos >= 0 { // test if method returned an error
            if !reply[req.callb.errPos].IsNil() {
                e := reply[req.callb.errPos].Interface().(error)
                res := codec.CreateErrorResponse(&req.id, &callbackError{e.Error()})
                return res, nil
            }
        }
        return codec.CreateResponse(req.id, reply[0].Interface()), nil
    }

2.2.4 Pub/sub 實現

底層在context綁定一個notifier對象

	if options&OptionSubscriptions == OptionSubscriptions {
		ctx = context.WithValue(ctx, notifierKey{}, newNotifier(codec))
	}

sub/unsub的時候會通過context.Value中拿notifier對象,調用上面的方法

註冊或者取消註冊
    func NotifierFromContext(ctx context.Context) (*Notifier, bool) {
        n, ok := ctx.Value(notifierKey{}).(*Notifier)
        return n, ok
    }

註冊
    func (n *Notifier) activate(id ID, namespace string) {
        n.subMu.Lock()
        defer n.subMu.Unlock()
        if sub, found := n.inactive[id]; found {
            sub.namespace = namespace
            n.active[id] = sub
            delete(n.inactive, id)
        }
    }

註銷
    func (n *Notifier) unsubscribe(id ID) error {
        n.subMu.Lock()
        defer n.subMu.Unlock()
        if s, found := n.active[id]; found {
            close(s.err)
            delete(n.active, id)
            return nil
        }
        return ErrSubscriptionNotFound
    }

消息事件觸發
    func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
        // Make sure the server is running, fail otherwise
        server := api.node.Server()
        if server == nil {
            return nil, ErrNodeStopped
        }

        // Create the subscription
        //獲取notifier對象
        notifier, supported := rpc.NotifierFromContext(ctx)
        if !supported {
            return nil, rpc.ErrNotificationsUnsupported
        }
        //生成標識
        rpcSub := notifier.CreateSubscription()

        go func() {
            events := make(chan *p2p.PeerEvent)
            sub := server.SubscribeEvents(events)
            defer sub.Unsubscribe()

            for {
                select {
                case event := <-events:
                    //觸發事件,發送通知消息
                    notifier.Notify(rpcSub.ID, event)
                case <-sub.Err():
                    return
                case <-rpcSub.Err():
                    return
                case <-notifier.Closed():
                    return
                }
            }
        }()

        return rpcSub, nil
    }

2.2.5 rpc client調用

以太坊提供了RPC服務,可以在geth啓動時通過參數設置

2.2.5.1 geth啓動選項參數

--rpc  啓動HTTP-RPC服務(基於HTTP的)
--ws  啓動WS-RPC服務(基於WebService的)
--rpcapi  value 指定需要調用的HTTP-RPC API接口,默認只有eth,net,web3
--rpcport value  HTTP-RPC服務器監聽端口(default: 8545)
--rpcport value  HTTP-RPC服務器監聽端口(default: 8545)
例子:geth --rpc --rpcapi "db,eth,net,web3,personal"

執行RPC調用的方式有很多,可以使用web3提供的接口、直接發送Json請求(缺點是拼json會很麻煩)、使用go-ethereum/ethclient包提供的函數(缺點是隻有eth接口)、也可以自己定義接口來調用。下面代碼是使用go-ethereum/ethclient包中的函數的例子。

package main

import (
    "fmt"
    "github.com/ethereum/go-ethereum/mobile"
)

func main() {
    // NewEthereumClient函數只是創建一個EthereumClient結構,並設置了HTTP連接的一些參數如的head的一些屬性,並沒有節點建立連接
    cli, err := geth.NewEthereumClient("http://127.0.0.1:8545")
    if err != nil {
        fmt.Printf("create new ethereum rpc client err:%s\n", err.Error())
    } else {
        fmt.Println("create new ethereum rpc client success")
    }
    eth_ctx := geth.NewContext()
    block, err2 := cli.GetBlockByNumber(eth_ctx, 18)
    fmt.Printf("ethereum mobile Context:%+v\n", eth_ctx)
    if err2 != nil {
        fmt.Printf("get block err:%s\n", err2.Error())
    } else {
        fmt.Printf("block:%+v\n", block)
    }
}

連的節點是本地運行的私有鏈,並且在go-ethereum源碼中加了一些日誌,執行結果:

mylog:DialContext:u:{Scheme:http Opaque: User: Host:127.0.0.1:8545 Path: RawPath: ForceQuery:false RawQuery: Fragment:};
mylog:u.Scheme:http
create new ethereum rpc client success
mylog:JSON-RPC: Client CallContext
mylog:Client.isHTTP:true
ethereum mobile Context:&{context:0xc4200ac008 cancel:<nil>}
block:Block(#18): Size: 650.00 B {
MinerHash: fd55c05ae10a5b0159b3c2d5803c6aa9469c95f5f063b9c400a2c36b49616ab3
Header(84b2cfd65e3197bdfe3f748ecebb040953af5eb73a05d8595757cf42cb40a492):
[
    ParentHash:     7892a0b31d50d67ae20d4a7ec5c24a6fe85f2f264e9f1639aa2388081305a0bd
    UncleHash:      1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
    Coinbase:       bdc61c81f67983288a6c375a884661edc77286d0
    Root:           0f30637bfc5bd6e123c6a0c38bdc743c94050626a984f9943eaf38367100b3e3
    TxSha           354d185cfa88e50f1a425e5b89500122e4445e9ec737e7a18cdd61b9350ab72b
    ReceiptSha:     a769d28981014fb6095462148a6300cd0b43fa050d75eb6f5b7595cfd13136bb
    Bloom:          00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    Difficulty:     131072
    Number:         18
    GasLimit:       131877941
    GasUsed:        21000
    Time:           1527044372
    Extra:          ׃��geth�go1.10�darwin
    MixDigest:      70c2bb422b1b834d5173d279e508ffee9dada454650fc3cf63e95deb3073cf32
    Nonce:          58b7495f112ccac2
]
Transactions:
[
    TX(57a3b17f84358098b728fc0f70f0697f175f8ba00d386c88eac0815b3afd6aad)
    Contract: false
    From:     2154bdd7070c99d1a25ff589a08b01dfd6eb65de
    To:       bdc61c81f67983288a6c375a884661edc77286d0
    Nonce:    0
    GasPrice: 0x430e23400
    GasLimit  0x15f90
    Value:    0xde0b6b3a7640000
    Data:     0x
    V:        0x41
    R:        0x45d4952c0190373c56e62ad15e54db54c0246385371b23c70bab4126b51927f8
    S:        0x618e4bb76a36482254352d7e5096c0dff4c1f495218d57c874fc3d8153915ea4
    Hex:      f86d80850430e2340083015f9094bdc61c81f67983288a6c375a884661edc77286d0880de0b6b3a76400008041a045d4952c0190373c56e62ad15e54db54c0246385371b23c70bab4126b51927f8a0618e4bb76a36482254352d7e5096c0dff4c1f495218d57c874fc3d8153915ea4
]
Uncles:
[]
}

2.2.5.2 分析:

go-ethereum/mobile包是發起RPC請求的客戶端直接使用的包。
該包中有EthereumClient結構提供了Ethereum API的接入。

// EthereumClient provides access to the Ethereum APIs.
type EthereumClient struct {
    client *ethclient.Client
}

ethclient.Client在ethclient包中,包裝了rpc.Client,rpc.Client代表與RPC服務的一個連接。

// Client defines typed wrappers for the Ethereum RPC API.
type Client struct {
    c *rpc.Client
}

RPC請求客戶端在使用時,首先傳入想要接入的節點的url作爲參數,調用mobile包中的NewEthereumClient函數。創建了EthereumClient實例,並與節點建立連接。建立的RPC連接有三種形式:HTTP、WebSocket、IPC,當傳入http://127.0.0.1:8545時,建立的是HTTP連接。

// NewEthereumClient connects a client to the given URL.
func NewEthereumClient(rawurl string) (client *EthereumClient, _ error) {
    rawClient, err := ethclient.Dial(rawurl)
    return &EthereumClient{rawClient}, err
}

設置HTTP連接的參數會調用rpc包http.go文件中的DialHTTPWithClient函數。

// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP
// using the provided HTTP Client.
func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
    req, err := http.NewRequest(http.MethodPost, endpoint, nil)
    if err != nil {
        return nil, err
    }
    // Content-Type和Accept是application/json,即發送的數據類型和接收的數據類型都是json
    req.Header.Set("Content-Type", contentType)
    req.Header.Set("Accept", contentType)

    initctx := context.Background()
    return newClient(initctx, func(context.Context) (net.Conn, error) {
        return &httpConn{client: client, req: req, closed: make(chan struct{})}, nil
    })
}

通過HTTP來做JSON-RPC調用時,需要一個geth.Context實例,通過調用mobile包中的NewContext函數,創建一個空的geth.Context實例。

// NewContext returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming requests.
func NewContext() *Context {
    return &Context{
        context: context.Background(),
    }
}

mobile包中封裝了請求區塊、區塊頭、交易等函數,這些函數調用ethclient包中的相關函數,再調用更底層rpc包中封裝的函數。
mobile包–>ethclient包–>rpc包。如mobile包中根據區塊號查找區塊的函數最後會調用rpc包中的CallContext函數。

// CallContext扮演JSON-RPC調用角色
// CallContext performs a JSON-RPC call with the given arguments. If the context is
// canceled before the call has successfully returned, CallContext returns immediately.
//
// The result must be a pointer so that package json can unmarshal into it. You
// can also pass nil, in which case the result is ignored.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
    fmt.Printf("mylog:JSON-RPC: Client CallContext\n")
    msg, err := c.newMessage(method, args...)
    if err != nil {
        return err
    }
    op := &requestOp{ids: []json.RawMessage{msg.ID}, resp: make(chan *jsonrpcMessage, 1)}

    fmt.Printf("mylog:Client.isHTTP:%+v\n",c.isHTTP)
    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); {
    case err != nil:
        return err
    case resp.Error != nil:
        return resp.Error
    case len(resp.Result) == 0:
        return ErrNoResult
    default:
        return json.Unmarshal(resp.Result, &result)
    }
}

2.2.5.3 使用POSTMAN

使用POSTMAN發送請求時,注意設置下Content-type和Accept。
body是{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":67}
這種方式雖然直接,但是自己拼json會很麻煩,所以最方便的還是調用已有的接口。

image

如果是做查詢區塊號爲18的區塊,則body是
{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x12",true],"id":1}

3. 參考

(1)以太坊源碼深入分析(3)-- 以太坊RPC通信實例和原理代碼分析(上)
https://www.jianshu.com/p/92daf6148dc5
(2)以太坊RPC
https://www.jianshu.com/p/8bd3723aa921
(3)以太坊RPC原理及實現
https://my.oschina.net/hunjixin/blog/1803161
(4)從零開始實現RPC框架 - RPC原理及實現
https://www.jianshu.com/p/dbfac2b876b1
(5)深入淺出RPC原理
https://ketao1989.github.io/2016/12/10/rpc-theory-in-action/
(6)你應該知道的RPC原理
https://www.cnblogs.com/LBSer/p/4853234.html
(7)RPC原理解析
https://www.cnblogs.com/swordfall/p/8683905.html
(8)服務之間的調用之RPC、Restful深入理解
https://blog.csdn.net/u014590757/article/details/80233901

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章