1. 程式人生 > >以太坊RPC機制與API實例

以太坊RPC機制與API實例

pro connected 再看 客戶端 side 代碼 tab 錯誤 latest

上一篇文章介紹了以太坊的基礎知識,我們了解了web3.js的調用方式是通過以太坊RPC技術,本篇文章旨在研究如何開發、編譯、運行與使用以太坊RPC接口。

關鍵字:以太坊,RPC,JSON-RPC,client,server,api,web3.js,api實例,Postman

以太坊JSON RPC API

geth命令api相關

之前介紹過這些API都可以在geth console中調用,而在實際應用中,純正完整的RPC的調用方式,

geth --rpc --rpcapi "db,eth,net,web3,personal"

這個命令可以啟動http的rpc服務,當然他們都是geth命令下的,仍舊可以拼接成一個多功能的命令串,可以了解一下上一篇介紹的geth的使用情況。下面介紹一下api相關的選項參數:

API AND CONSOLE OPTIONS:
  --rpc                  啟動HTTP-RPC服務(基於HTTP的)
  --rpcaddr value        HTTP-RPC服務器監聽地址(default: "localhost")
  --rpcport value        HTTP-RPC服務器監聽端口(default: 8545)
  --rpcapi value         指定需要調用的HTTP-RPC API接口,默認只有eth,net,web3
  --ws                   啟動WS-RPC服務(基於WebService的)
  --wsaddr value         WS-RPC服務器監聽地址(default: "localhost")
  --wsport value         WS-RPC服務器監聽端口(default: 8546)
  --wsapi value          指定需要調用的WS-RPC API接口,默認只有eth,net,web3
  --wsorigins value      指定接收websocket請求的來源
  --ipcdisable           禁掉IPC-RPC服務
  --ipcpath              指定IPC socket/pipe文件目錄(明確指定路徑)
  --rpccorsdomain value  指定一個可以接收請求來源的以逗號間隔的域名列表(瀏覽器訪問的話,要強制指定該選項)
  --jspath loadScript    JavaScript根目錄用來加載腳本 (default: ".")
  --exec value           執行JavaScript聲明
  --preload value        指定一個可以預加載到控制臺的JavaScript文件,其中包含一個以逗號分隔的列表

我們在執行以上啟動rpc命令時可以同時指定網絡,指定節點,指定端口,指定可接收域名,甚至可以同時打開一個console,這也並不產生沖突。

geth --rpc --rpcaddr <ip> --rpcport <portnumber>

我們可以指定監聽地址以及端口,如果不謝rpcaddr和rpcport的話,就是默認的http://localhost:8545。

geth --rpc --rpccorsdomain "http://localhost:3000"

如果你要使用瀏覽器來訪問的話,就要強制指定rpccorsdomain選項,否則的話由於JavaScript調用的同源限制,請求會失敗。

admin.startRPC(addr, port)

如果已進入geth console,也可以通過這條命令添加地址和端口。

Postman,HTTP請求api

Postman是一個可以用來測試各種http請求的客戶端工具,它還有其他很多用途,但這裏只用它來測試上面的HTTP-RPC服務。
技術分享圖片

看圖說話,我們指定了請求地址端口,指定了HTTP POST請求方式,設置好請求為原始Json文本,請求內容為:

{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":67}

是用來請求服務器當前web3客戶端版本的,然後點擊"Send",得到請求結果為:

{
    "jsonrpc": "2.0",
    "id": 67,
    "result": "Geth/v0.0.1-stable-930fa051/linux-amd64/go1.9.2"
}

以太坊Go源碼調用rpc

我們就以最常用的api:eth_getBalance為例,它的參數要求為:

Parameters
- DATA, 20 Bytes - address to check for balance.
- QUANTITY|TAG - integer block number, or the string "latest", "earliest" or "pending", see the default block parameter

該api要求的參數:

  • 第一個參數為需檢查余額的地址
  • 第二個參數為整數區塊號,或者是字符串“latest","earliest"以及"pending"指代某個特殊的區塊。

在go-ethereum項目中查找到使用位置ethclient/ethclient.go:

func (ec *Client) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
    var result hexutil.Big
    err := ec.c.CallContext(ctx, &result, "eth_getBalance", account, toBlockNumArg(blockNumber))
    return (*big.Int)(&result), err
}
func (ec *Client) PendingBalanceAt(ctx context.Context, account common.Address) (*big.Int, error) {
    var result hexutil.Big
    err := ec.c.CallContext(ctx, &result, "eth_getBalance", account, "pending")
    return (*big.Int)(&result), err
}

結合上面的RPC API和下面的go源碼的調用,可以看到在go語言中的調用方式:要使用客戶端指針類型變量調用到上下文Call的方法,傳入第一個參數為上下文實例,第二個參數為一個hexutil.Big類型的結果接收變量的指針,第三個參數為調用的rpc的api接口名稱,第四個和第五個為該api的參數,如上所述。

  • 跟蹤到ec.c.CallContext,CallContext方法是ec.c對象的。
// Client defines typed wrappers for the Ethereum RPC API.
type Client struct {
    c *rpc.Client
}

可以看到ethclient/ethclient.go文件中將原rpc/client.go的Client結構體進行了一層包裹,這樣就可以區分出來屬於ethclient的方法和底層rpc/client的方法。下面貼出原始的rpc.client的結構體定義:

// Client represents a connection to an RPC server.
type Client struct {
    idCounter   uint32
    connectFunc func(ctx context.Context) (net.Conn, error)
    isHTTP      bool

    // writeConn is only safe to access outside dispatch, with the
    // write lock held. The write lock is taken by sending on
    // requestOp and released by sending on sendDone.
    writeConn net.Conn

    // for dispatch
    close       chan struct{}
    didQuit     chan struct{}                  // closed when client quits
    reconnected chan net.Conn                  // where write/reconnect sends the new connection
    readErr     chan error                     // errors from read
    readResp    chan []*jsonrpcMessage         // valid messages from read
    requestOp   chan *requestOp                // for registering response IDs
    sendDone    chan error                     // signals write completion, releases write lock
    respWait    map[string]*requestOp          // active requests
    subs        map[string]*ClientSubscription // active subscriptions
}

ethclient經過包裹以後,可以使用本地Client變量調用rpc.client的指針變量c,從而調用其CallContext方法:

func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
    msg, err := c.newMessage(method, args...) // 看來CallContext還不是終點,TODO:進到newMessage方法內再看看。
    // 結果處理
    if err != nil {
        return err
    }
    // requestOp又一個結構體,封裝響應參數的,包括原始請求消息,響應信息jsonrpcMessage,jsonrpcMessage也是一個結構體,封裝了響應消息標準內容結構,包括版本,ID,方法,參數,錯誤,返回值,其中RawMessage在go源碼位置json/stream.go又是一個自定義類型,屬於go本身封裝好的,類型是字節數組[]byte,也有自己的各種功能的方法。
    op := &requestOp{ids: []json.RawMessage{msg.ID}, resp: make(chan *jsonrpcMessage, 1)}

    // 通過rpc不同的渠道發送響應消息:這些渠道在上面命令部分已經介紹過,有HTTP,WebService等。
    if c.isHTTP {
        err = c.sendHTTP(ctx, op, msg)
    } else {
        err = c.send(ctx, op, msg)
    }
    if err != nil {
        return err
    }

    // TODO:對wait方法的研究
    // 對wait方法返回結果的處理
    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)// 順利將結果數據編出
    }
}

先看wait方法,它仍舊在rpc/client.go中:

func (op *requestOp) wait(ctx context.Context) (*jsonrpcMessage, error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    case resp := <-op.resp:
        return resp, op.err
    }
}

select的使用請參考這裏。繼續正題,進入ctx.Done(),Done屬於Go源碼context/context.go:

// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation.
Done() <-chan struct{}

想知道Done()咋回事,請轉到我寫的另一篇博文Go並發模式:管道與取消,那裏仔細分析了這一部分內容。

從上面的源碼分析我感覺go語言就是一個網狀結構,從一個結構體跳進另一個結構體,它們之間誰也不屬於誰,誰調用了誰就可以使用,沒有顯式繼承extends和顯式實現implements,go就是不斷的封裝結構體,然後增加該結構體的方法,有時候你甚至都忘記了自己程序的結構體和Go源碼封裝的結構體之間的界限。這就類似於面向對象分析的類,定義一個類,定義它的成員屬性,寫它的成員方法。

web3與rpc的關系

這裏再多啰嗦一句,重申一下web3和rpc的關系:

To make your app work on Ethereum, you can use the web3 object provided by the web3.js library. Under the hood it communicates to a local node through RPC calls. web3.js works with any Ethereum node, which exposes an RPC layer.

翻譯過來就是為了讓你的api工作在以太坊,你可以使用由web3.js庫提供的web3對象。底層通過RPC調用本地節點進行通信。web3.js可以與以太坊任何一個節點通信,這一層就是暴露出來的RPC層。

開發自己的api

設定一個小需求:就是將余額數值乘以指定乘數,這個乘數是由另一個接口的參數來指定的。

在ethapi中加入

var rateFlag uint64 = 1
// Start forking command.
// Rate is the fork coin‘s exchange rate.
func (s *PublicBlockChainAPI) Forking(ctx context.Context, rate uint64) (uint64) {
    // attempt: store the rate info in context.
    // context.WithValue(ctx, "rate", rate)
    rateFlag = rate
    rate = rate + 1
    return rate
}

然後在ethclient中加入

// Forking tool‘s client for the Ethereum RPC API
func (ec *Client) ForkingAt(ctx context.Context, account common.Address, rate uint64)(uint64, error){
    var result hexutil.Uint64
    err := ec.c.CallContext(ctx, &result, "eth_forking", account, rate)
    return uint64(result), err
}

保存,make geth編譯,然後在節點目錄下啟動

geth --testnet --rpc console --datadir node0

然後進入到Postman中測試,可以看到
技術分享圖片
乘數已經改為3(輸出4是為了測試,實際上已在局部變量rateFlag保存了乘數3)
然後我們再發送請求余額測試,
技術分享圖片
可以看到返回值為一串16進制數,通過轉換結果為:417093750000000000000,我們原始余額為:139031250000000000000,正好三倍。

rpc客戶端

我們上面已經在rpc服務端對api進行了增加,而客戶端調用采用的是Postman發送Post請求。而rpc客戶端在以太坊實際上有兩種:一個是剛才我們實驗的,在網頁中調用JSON-RPC;另一種則是geth console的形式,而關於這種形式,我還沒真正搞清楚它部署的流程,只是看到了在源代碼根目錄下build/_workspace會在每一次make geth被copy進去所有的源碼作為編譯後環境,而我修改了源碼文件,_workspace下文件,均未生效,可能還存在一層運行環境,我並沒有修改到。但這無所謂了,因為實際應用中,我們很少去該console的內容,直接修改web3.js引入到網頁即可。下面介紹一下配合上面自己的api,如何修改web3.js文件:

上面講過了web3.js的結構,是一個node.js的module結構,因此我們先決定將這個api放到eth對象下,檢查eth對應的id為38,找到對象體,在methods中增加對應api調用操作,

var forking = new Method({
    name: ‘forking‘,
    call: ‘eth_forking‘,
    params: 1,
    inputFormatter: [null],
    outputFormatter: formatters.outputBigNumberFormatter
});

然後在對象體返回值部分將我們新構建的method添加進去,

return [
    forking,
    ...

改好以後,我們將該文件引用到頁面中去,即可通過web3.eth.forking(3)進行調用了。

總結

本文介紹了rpc的概念,rpc的流行框架,以太坊使用的rpc框架為JSON-RPC。接著描述了如何啟動JSON-RPC服務端,然後使用Postman來請求JSON-RPC服務端api。通過這一流程,我們仔細分析並跟蹤了源碼中的實現,抽絲剝繭,從最外層的JSON-RPC的調用規範到源碼中外層封裝的引用,到內部具體實現,期間對各種自定義結構體進行了跟蹤研究,直到Go源碼庫中的結構體,研究了服務端從接收客戶端請求到發送響應的過程。最後我們仔細研究了web3.js文件的結構並且做了一個小實驗,從服務端到客戶端模仿者增加了一個自定義的api。希望本文對您有所幫助。

更多文章請轉到醒者呆的博客園。

以太坊RPC機制與API實例