通过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