寫在篇頭
本篇要介紹的內(nèi)容大概是回答以下幾個問題。
- geth是怎樣或者使用何種技術(shù)在終端中實現(xiàn)了一個javascript的運行環(huán)境的匀谣。
- 在終端中輸入的一個命令是如何調(diào)到以太坊的底層函數(shù)杆麸,從而拿到想要的結(jié)果的瓶您。
- 如何增加自己的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í)行流程如下圖褂删,大致流程為:
- 終端部分獲取用戶的請求(這部分交互模式與非交互模式略有不同)飞醉。
- 獲取到用戶請求后,調(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端的處理流程如下圖:
- 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