以太坊RPC機制與API實例


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

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

rpc簡介

RPC(remote process call),名曰遠程過程調用。意思就是兩台物理位置不同的服務器,其中一台服務器的應用想調用另一台服務器上某個應用的函數或者方法,由於不在同一個內存空間不能直接調用,因此需要通過網絡來表達語義以及傳入的參數。RPC是跨操作系統,跨編程語言的網絡通信方式。

RMI(remote method invocation),遠程方法調用,只能被Java調用,可返回Java對象和基本類型,可以說是Java版的RPC。它需要一個RMI Registry來注冊服務地址。

JMS(java message service),java消息服務,網絡傳輸的是Java對象,而RMI還是遠程調用方法,網絡傳輸的是請求參數和返回值。

SOAP(simple object access protocol),簡單對象訪問協議,是一種交換信息的服務,通過WSDL(Web Services Description Language)方法描述文件來指導我們去調用遠程服務,獲得自己想要的對象,傳輸格式為xml。

RPC規定在網絡傳輸中參數和返回值均被序列化為二進制數據,這個過程被稱為序列化(Serialize)或編組(marshal)。通過尋址和傳輸將序列化的二進制發送給另一台服務器。另一台服務器收到二進制數據以后會反序列化,恢復為內存中的表達方式,然后找到對應方法調用將返回值仍舊以二進制形式返回給第一台服務器,然后再反序列化讀取返回值。

netty

Netty框架不局限於RPC,更多的是作為一種網絡協議的實現框架。通過上面的rpc介紹,我們指定rpc需要:

  • 通訊方式,TCP/IP socket, HTTP
  • 高效的序列化框架,protobuf, thrift
  • 尋址方式,registry,endpoint URI, UDDI,另外服務注冊可以選擇redis,zookeeper等技術。
  • 會話和狀態保持 session

netty提供了一種事件驅動的,責任鏈式的,流水線式的網絡協議實現方式。Netty對NIO有很好的實現,解決了眾多的RPC的后顧之憂並且開發效率也有了很大程度的提高,很多RPC框架是基於Netty來構建的。

希望未來可以有更多的使用到netty的具體工作場景。

rpc框架

rpc框架簡直不要太多。

  • dubbo 阿里出品,基於Netty,java實現的分布式開源RPC框架,但於12年年底已停止維護升級
  • motan 新浪出品,微博使用的rpc框架。
  • rpcx
  • gRPC Google出品,強大新潮,基於HTTP2,以及protobuf構建並且支持多語言。
  • thrift
  • protobuf 序列化框架

以太坊rpc客戶端機制研究

geth命令中rpc相關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,也可以通過這條命令添加地址和端口。

rpc客戶端:Postman模擬HTTP請求api

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

看圖說話,我們指定了請求地址端口,指定了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"
}

rpc客戶端的研究:Go源碼調用rpc

這種rpc客戶端可以有兩種,一種是通過j調用web3.js來實現,另一種是在geth consol中通過manageAPI來實現,但是它們的內部運行機制是一樣的,包括上面的Postman模擬瀏覽器發起HTTP請求也是一樣,下面我們通過一個完整的客戶端調用例子來研究整個以太坊源碼中對於客戶端這塊是如何處理的。這里我們就以最常用的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層。

以太坊rpc服務端機制研究

以上介紹了各種客戶端的調用方式,包括通過web3提供的接口,從頁面js調用,或者從ethclient調用,或者直接通過頁面發起Json請求(Postman),無論什么形式,最終都是通過JSON-RPC框架傳輸到以太坊服務端。那么下面我們開發一個自己的api,首先從服務端接口開發開始:

服務端源碼都在ethapi包下,首先它有一個Backend結構體,這個結構體是一個接口,提供了一套基本API服務的描述。然后就通過函數GetAPIs返回一個數組,其中包括各種不同命名空間,版本,服務地址,以及公開私密權限的API對象。下面進到其中的一個API類PublicBlockChainAPI,它包含一個backend屬性,代表了它具備后台所有API的能力。這個類有很多方法,其中就包括上面我們講到的關於eth_getBalance接口的服務端實現方法。

// GetBalance returns the amount of wei for the given address in the state of the
// given block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta
// block numbers are also allowed.
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()
}

可以看到GetBalance方法是PublicBlockChainAPI的方法,並且方法名首字母大寫,說明是外部可訪問的。它通過Backend的方法s.b.StateAndHeaderByNumber獲取余額,上面提到了Backend只是描述了API的接口,那么具體實現還得繼續探索,

StateAndHeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*state.StateDB, *types.Header, error)

通過查看Backend中對於該方法的定義,得到該方法的返回值為StateDB的指針對象。而這個對象正是下面真正獲取余額所需要的對象。

b := state.GetBalance(address)

然后我們進入到該對象中,找到GetBalance方法。

// Retrieve the balance from the given address or 0 if object not found
func (self *StateDB) GetBalance(addr common.Address) *big.Int {
	stateObject := self.getStateObject(addr)
	if stateObject != nil {
		return stateObject.Balance()
	}
	return common.Big0
}

首先通過StateDB的getStateObject方法獲得一個stateObject結構體的對象,其中該對象的一個屬性是Account類型的data成員變量,進到Account中可以發現它的結構為:

type Account struct {
	Nonce    uint64
	Balance  *big.Int
	Root     common.Hash // merkle root of the storage trie
	CodeHash []byte
}

一切了然,這里存了Balance信息,那么外部stateObject通過一個方法Balance()

func (self *stateObject) Balance() *big.Int {
	return self.data.Balance
}

這個方法可以取出上面Accout中的Balance信息。stateObject是數據存儲的一致性對象。

leveldb

以太坊geth是采用leveldb作為數據庫,它是:

  • 過程數據庫,沒有sql,只支持api調用
  • Go自帶實現
  • 鍵值對存儲方式
  • 多層磁盤存儲,后台有調理
  • 記錄可追蹤可證明

go-ethereum引入了 github\.com/syndtr/goleveldb/leveldb/db.go ,查看此源文件,可以看到它定義了一個DB結構體,包含很多字段以及方法,此外該文件還包含一些函數。其中就有func openDB(s *session) (*DB, error)函數,該函數可以開啟一個數據庫會話並返回一個數據庫實例。

上面的stateObject結構體的字段,它按照錢包address為key存儲了賬戶對象,通過key可以取出賬戶對象,進而取出賬戶余額信息。

以太坊的存儲結構

以太坊並不存在中心服務器,而是基於p2p協議連接起來的平等節點,在眾多節點中存儲了全部數據。當用戶發起一筆交易,會通過廣播通知每一個節點,礦工對此進行驗證打包並進一步廣播,在區塊鏈確認以后,此操作即認為是不可更改的。

在每個節點上,數據是以區塊鏈來存儲的。區塊鏈由一個個塊串起來而組成。以太坊被描述為一個交易驅動的狀態機。它在某個狀態下接收一些輸入,會確定的轉移到另一個狀態。

狀態機,包含一組狀態集(states)、一個起始狀態(start state)、一組輸入符號集(alphabet)、一個映射輸入符號和當前狀態到下一狀態的轉換函數(transition function)的計算模型。

在以太坊的一個狀態下,每個賬戶都有確定的余額和狀態信息,當他接收到新的交易以后就會進入一個新的狀態,從創世塊開始,不斷的收到新的交易,由此能進入到一系列新的狀態。

以太坊每隔一段時間就會把一批交易信息打包存儲到一個塊里,這個塊中包含的信息有:

  1. ParentHash:父塊的哈希值
  2. Number:塊編號
  3. Timestamp:塊產生的時間戳
  4. GasUsed:交易消耗的Gas
  5. GasLimit:Gas限制
  6. Difficulty:POW的難度值
  7. Beneficiary:塊打包手續費的受益人,也稱礦工
  8. Nonce: (可選參數) 整型數字,可以通過使用相同nonce值來復寫你的pending狀態的交易(注意與ethash挖礦隨機數做區分)。

這些字段我們在上面的web3接口中都可以獲取得到。

nonce,整數類型,允許使用相同隨機數覆蓋自己發送的處於pending狀態的交易。

為了防止交易的重播攻擊,每筆交易必須有一個nonce隨機數,針對每一個賬戶nonce都是從0開始,當nonce為0的交易處理完之后,才會處理nonce為1的交易,並依次加1的交易才會被處理。以下是nonce使用的幾條規則:

  • 當nonce太小,交易會被直接拒絕。
  • 當nonce太大,交易會一直處於隊列之中,這也就是導致我們上面描述的問題的原因;
  • 當發送一個比較大的nonce值,然后補齊開始nonce到那個值之間的nonce,那么交易依舊可以被執行。
  • 當交易處於queue中時停止geth客戶端,那么交易queue中的交易會被清除掉。

以太坊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中測試,可以看到
image
乘數已經改為3(輸出4是為了測試,實際上已在局部變量rateFlag保存了乘數3)
然后我們再發送請求余額測試,
image
可以看到返回值為一串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。希望本文對您有所幫助。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM