以太坊RPC機制與API實例
上一篇文章介紹了以太坊的基礎知識,我們了解了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實例