通过RPC与太坊节点交互
背景
geth提供了一整套的rpc接口,分为标准接口和扩展接口,平时我们打开geth客户端默认开放的就是标准接口,关于标准接口中所有接口的详细内容见这里,除了标准接口之外geth又添加了一系列扩展接口,如用于节点管理的admin系列接口,管理挖矿miner系列接口等,详细说明见这里,要想使用这一类rpc接口,需要在启动geth客户端时使用指定所开放的接口。
以太坊实现rpc的方法有多种,外部可以使用的有http,ipc和ws。下面分别介绍一下
HTTP-RPC
要使用这种rpc需要在启动时使用–rpc开启,默认开放在8545端口上,可以使用–rpcport修改,另外还可以用–rpcaddr指定监听地址,默认为localhost,修改外外网ip就可以在外网访问本地节点;用–rpcapi指定所开放的api。(后面的ipc和ws模式都有相关参数设置)
curl命令
由于是jsonrpc类型的接口,所以我们需要向目标地址端口发送json格式数据来,首先可以根据官方文档说明的使用curl命令体验一下,命令如下:
curl -X POST --data '{"jsonrpc":"2.0","method":"rpc_modules","params":[],"id":67}' 127.0.0.1:8545
-X POST指定使用post方式发送数据,–data指定发送的Jason数据,其中需要指定的几个字段我们稍后介绍,最后跟上地址和端口。最后返回下面数据,这里我们请求的是服务端开发的api接口:
{
"id": 67,
"jsonrpc": "2.0",
"result": {
"eth": "1.0",
"evm": "1.0",
"net": "1.0",
"personal": "1.0",
"rpc": "1.0",
"web3": "1.0"
}
}
还有一点需要注意的是,如果使用ganache测试上面的命令基本可以通过,如果用geth测试,需要加上-H “Content-Type: application/json”
代码调用
除了使用命令行的方法,我们还可以使用代码调用:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
resp,err:=http.Post("http://127.0.0.1:8545","application/x-www-form-urlencoded",strings.NewReader(`{"jsonrpc":"2.0","method":"rpc_modules","params":[],"id":67}`))
if err!=nil{
log.Fatal(err)
}
defer resp.Body.Close()
body,err:=ioutil.ReadAll(resp.Body)
if err!=nil{
log.Fatal(err)
}
fmt.Println(string(body))
}
和前面使用的curl命令效果一样,发送一个post请求,注意contentType要为”application/x-www-form-urlencoded”,但是在geth测试中要改为application/json,为了谨慎起见建议无论用什么测试都将contentType设为application/json。
既然是发送的json数据,我们实际使用时候并不想每次都直接写入json字符串,我们还可以使用json对象形式,关于json对象的数据结构,我们通过翻阅源码(详见这里)可知go-ethereum的jsonrpc数据结构如下
type jsonrpcMessage struct {
Version string `json:"jsonrpc,omitempty"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Error *jsonError `json:"error,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
无论收发都用的是jsonrpcMessage,这里也可以发现我们请求的时候需要指定一下内容:Version:版本号,字段是jsonrpc;ID:本次请求的ID,字段是id;Method:本次请求的方法,字段是method;Params:本次请求的方法参数列表,字段是params。剩余的Error和Result是响应中包含的。既然知道了json对象,我们就可以构建一个对象然后像上面一样发送请求,代码如下:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
type jsonrpcMessage struct {
Version string `json:"jsonrpc,omitempty"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Error *jsonError `json:"error,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func main() {
msg:=jsonrpcMessage{Version:"2.0",Method:"rpc_modules",Params:json.RawMessage{},ID:json.RawMessage("5")}
data,err:=json.Marshal(msg)
if err!=nil{
log.Fatal("Marshal:",err)
}
resp,err:=http.Post("http://127.0.0.1:8545","application/json",bytes.NewReader(data))
if err!=nil{
log.Fatal("Post:",err)
}
defer resp.Body.Close()
body,err:=ioutil.ReadAll(resp.Body)
if err!=nil{
log.Fatal("ReadAll:",err)
}
var result jsonrpcMessage
err=json.Unmarshal(body,&result)
if err!=nil{
log.Fatal("Unmarshal:",err)
}
fmt.Println(string(result.Result))
}
上面举得都是不带参数的例子,对于带参数的主要问题是在源码中jsonrpcMessage的Params字段是一个json.RawMessage类型,当然我们可以给他改为[]string,不过不修改也可以使用,json.RawMessage主要作用就是在序列化或反序列化时保持数据原样,而json.RawMessage本身实际上就是一个字节数组,详细信息可见json源码。所以我们在给结构体中json.RawMessage类型字段赋值时要先对数据进行一次序列化,即变为字节数组后再赋值,如下调用了web3_sha3方法
params,_:=json.Marshal([]string{"0x68656c6c6f20776f726c64"})
msg:=jsonrpcMessage{Version:"2.0",Method:"web3_sha3",Params:params,ID:json.RawMessage("5")}
IPC-RPC
不同于http-rpc,ipc形式的rpc在geth户客户端是默认开启的,默认文件路径是datadir/geth.ipc,在linux中使用的是unix socket形式
nc命令
根据官方文档提示,我们可以用nc命令实现调用,还是发送一段json数据,如下
echo '{"jsonrpc":"2.0","method":"rpc_modules","params":[],"id":1}' | nc -U mychain/chain1/geth.ipc
代码调用
go语言版本的代码调用如下
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net"
)
type jsonrpcMessage struct {
Version string `json:"jsonrpc,omitempty"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Error *jsonError `json:"error,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func main() {
msg:=jsonrpcMessage{Version:"2.0",Method:"rpc_modules",Params:json.RawMessage{},ID:json.RawMessage("5")}
data,err:=json.Marshal(msg)
if err!=nil {
log.Fatalln("Marshal: ",err)
}
conn,err:=net.Dial("unix","/home/chenw/chenyy/mychain/chain1/geth.ipc")
defer conn.Close()
if err!=nil {
log.Fatalln("dial: ",err)
}
_,err=conn.Write(data)
if err!=nil {
log.Fatalln("write: ",err)
}
temp := make([]byte,1024)
var buf bytes.Buffer
var result jsonrpcMessage
count:=0
for{
n,err:=conn.Read(temp)
if err!=nil {
log.Fatalln("read: ",err)
}
count+=n
buf.Write(temp)
if json.Valid(buf.Bytes()[:count]) {
break
}
}
json.Unmarshal(buf.Bytes()[:count],&result)
fmt.Println(string(result.Result))
}
websocket-RPC
最后再演示一下websocket的rpc调用,首先要注意的是go的标准包并不支持websocket,go-ethereum使用的是golang官方维护的一个net包–”golang.org/x/net/websocket”,其中有websocket实现,但是该包并不完善,官方推荐使用Gorilla WebSocket,所以首先要安装该包
go get github.com/gorilla/websocket
其次要测试ws,要在启动以太坊节点时加上–ws参数,完整客户端测试代码如下
package main
import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"log"
"net/url"
)
type jsonrpcMessage struct {
Version string `json:"jsonrpc,omitempty"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Error *jsonError `json:"error,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func main() {
msg:=jsonrpcMessage{Version:"2.0",Method:"rpc_modules",Params:json.RawMessage{},ID:json.RawMessage("5")}
u:=url.URL{Scheme:"ws",Host:"127.0.0.1:8546"}
c,_,err:=websocket.DefaultDialer.Dial(u.String(),nil);
if err!=nil{
log.Fatal("Dial:",err)
}
defer c.Close()
err=c.WriteJSON(msg)
if err!=nil{
log.Fatal("WriteJSON:",err)
}
var result jsonrpcMessage
err=c.ReadJSON(&result)
if err!=nil{
log.Fatal("ReadJSON:",err)
}
fmt.Println(string(result.Result))
}
跨语言原生调用
既然是jsonrpc格式请求,那么跨语言的交互也是必须的,下面用java语言演示一下httprpc调用,首先定义json数据对象
class JsonMsg{
String jsonrpc;
int id;
String method;
JsonArray params;
JsonErr error;
JsonElement result;
}
class JsonErr{
int code;
String message;
String data;
}
其中params我定义为一个JsonArray类型,result定义为JsonElement类型,处理json数据时使用Gson库:
public class GethRPC {
public static void main(String[] args) {
Gson gson = new Gson();
JsonMsg msg = new JsonMsg();
msg.jsonrpc = "2.0";
msg.method = "web3_clientVersion";
msg.id = 5;
String request = gson.toJson(msg);
System.out.println(request);
try{
URL url = new URL("http://127.0.0.1:8545");
URLConnection conn = url.openConnection();
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("charset", "utf-8");
conn.setRequestProperty("Content-length",request.length()+"");
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
BufferedOutputStream outputStream = new BufferedOutputStream(conn.getOutputStream());
outputStream.write(request.getBytes(),0,request.getBytes().length);
outputStream.close();
BufferedInputStream inputStream = new BufferedInputStream(conn.getInputStream());
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
System.out.println(result.toString("UTF-8"));
System.out.println(gson.fromJson(result.toString("UTF-8"),JsonMsg.class).result);
}catch (Exception e){
e.printStackTrace();
}
}
}
对于有参数的rpc调用如下:
public class GethRPC {
public static void main(String[] args) {
Gson gson = new Gson();
JsonMsg msg = new JsonMsg();
msg.jsonrpc = "2.0";
msg.method = "web3_sha3";
JsonArray params = new JsonArray();
params.add("0x68656c6c6f20776f726c64");
msg.params = params;
msg.id = 5;
String request = gson.toJson(msg);
System.out.println(request);
try{
URL url = new URL("http://127.0.0.1:8545");
URLConnection conn = url.openConnection();
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("charset", "utf-8");
conn.setRequestProperty("Content-length",request.length()+"");
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
BufferedOutputStream outputStream = new BufferedOutputStream(conn.getOutputStream());
outputStream.write(request.getBytes(),0,request.getBytes().length);
outputStream.close();
BufferedInputStream inputStream = new BufferedInputStream(conn.getInputStream());
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
System.out.println(result.toString("UTF-8"));
System.out.println(gson.fromJson(result.toString("UTF-8"),JsonMsg.class).result);
inputStream.close();
result.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
web3.js
像前面那样直接通过以太坊的rpc接口进行交互也行的通,但是还是比较麻烦,于是以太坊官方推出了一个库–web3.js,他是用js实现的API,用于和以太坊节点通信,底层还是rpc调用,只是帮我们封装了一系列方法,我们直接调用即可。
使用起来很简单,首先在工程目录下安装web3(当然前提是安装了node.js):
npm install web3
然后开启以太坊节点,如ganache-cli,下面提供一个示例获取节点上的所有账户和某些账户的余额:
var Web3 = require("web3");
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
web3.eth.getAccounts().then(console.log);
web3.eth.getBalance("0x383edc3e6721b037263a92bf7218cc71fe617cd9").then(console.log);
使用起来很简单,首先获取web3模块,然后创建web3对象,创建时使用HttpProvider也就是http-rpc的方式连接以太坊节点,当然也可以使用websocket的方法连接,这时首先要开启节点的websocket功能(使用–ws),然后创建web3对象时用一下方式:
var web3 = new Web3(new Web3.providers.WebsocketProvider('ws://localhost:8546'));
另外关于web3.js的api问题,由于不同版本的api变化较大,详细信息参考官方文档。
当然web3.js是是为js语言服务的,相应的还有python版、java版的类似库供我们使用。
题图来自unsplash:https://unsplash.com/photos/8pgK9350lv4