最近有网友提到以太坊控制台的代码看不太明白,抽了点时间整理了一下。
当我们通过geth console
或者geth attach
与节点交互的时候,输入的命令是如何被处理的呢?看下面这张流程图就明白了:
- 命令行编辑器Liner等待用户输入命令
- JSRE使用一个名为scheduler的通道(chan)接收命令
- JSRE把命令发送给Javascript解释器Otto处理
- Otto中预加载了web3.js,执行对应的函数并通过provider发送RPC请求
- Web3 provider被设置为一个Bridge模块,接收请求并转发给RCP Client
- RPC Client通过全双工管道和RPC Server通信,完成RPC调用
- 将RPC调用结果输出到命令行
可以看到,流程还是很清晰的,但是涉及到很多模块。实际上,这些模块都被包含在Console的数据结构之中:
type Console struct {
client *rpc.Client
jsre *jsre.JSRE
prompt string
prompter UserPrompter
histPath string
history []string
printer io.Writer
}
下面会对这些模块一一进行介绍。
1. Liner:带历史记录的命令行编辑器
既然是控制台,那么显然需要一个命令行编辑器来输入命令并打印结果。以太坊使用的是一个开源的命令编辑器Liner,github地址:https://github.com/peterh/liner
这个命令编辑器还是挺强大的,除了基本的交互以外,还支持历史记录和自动补全。我们来看一个最简单的使用示例:
// 创建liner实例
line := liner.NewLiner()
defer line.Close()
// 设置自动补全处理函数
line.SetCompleter(func(line string) {
...
})
// 打印提示,接收用户输入
name, err := line.Prompt("What is your name? ")
if err == nil {
log.Print("Got: ", name)
// 添加历史记录
line.AppendHistory(name)
}
当然,为了可扩展性,以太坊在外面做了一层封装。默认情况下会创建一个terminalPrompter,内部其实还是直接调用liner,具体可以参见console/prompter.go。
另外,思考一个问题:当我们在控制台输入eth.getT
然后按Tab键时,会自动帮我们补全为eth.getTransaction
,这是怎么做到的?
实际上,可以通过调用Javascript的getOwnPropertyNames()函数获取对象的所有属性和方法,然后选出匹配项加入自动补全列表中。具体代码实现参见internal/jsre/completion.go以及internal/jsre/pretty.go。
2. Otto:JavaScript解释器
为了方便理解,我们先介绍一下Otto,稍后再介绍JSRE。
Otto是一个Go语言实现的JavaScript解释器,并且可以很方便地实现Javascript和Go之间的相互调用。我们来看一下具体用法,非常简单:
- 创建otto实例:
import (
"github.com/robertkrimen/otto"
)
vm := otto.New()
- 设置一个Javascript变量的值:
vm.Set("a", 88)
vm.Set("b", "hello")
- 获取一个Javascript变量值:
value, err := vm.Get("a")
{
value, _ := value.ToInteger()
}
- 执行一段Javascript代码:
vm.Run(`
console.log(b + a); // hello88
`)
- 执行一个Javascript表达式并获取返回值:
value, _ := vm.Run("b.length")
{
value, _ := value.ToInteger()
}
- 执行一个Javascript函数并获取返回值: