最近的公司区块链钱包,用到了以太坊的官方开发库 web3j。web3j 是Java版本的以太坊 rpc-json 接口协议封装实现,如果需要将你的 Java 或安卓应用接入以太坊,或者希望用 Java 开发一个钱包应用,那么 web3j 完全能满足你的需求。
目录:
1. rpc 简介
rpc ( Remote Procedure Call) — 远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。rpc 协议假定某些传输协议的存在,如 tcp 或 udp,为通信程序之间传输信息数据。在 OSI 网络通信模型中,rpc 跨越了传输层和应用层。rpc 使得开发包括网络分布式多程序在内的应用程序更加容易。
我从 google 上找了一张 rpc 模型图,帮助大家理解下:
2. rpc 和 http 对比
目前有很多 Java 的 rpc 框架,有基于 Json 的,有基于 XML,也有基于二进制对象的。rpc 和 平常用的 http/https 到底有什么区别,不都是写一个服务然后在客户端调用吗?
在 http 和 rpc 的选择上可能有些人是迷惑的,主要是因为,有些 rpc 框架配置复杂,如果走 http 也能完成同样的功能,那么为什么要选择 rpc,而不是更容易上手的 http 来实现?
从以下几个点来分析:
(1) 传输协议
- rpc 可以基于 tcp 协议 (可以省去了 http 报头等一系列东西,rpc 并没有规定数据传输格式,这个格式可以任意指定,不同的 rpc 协议,数据格式不一定相同),也可以基于 http 协议。
- http 只能基于 http 协议。
(2) 传输效率
- rpc 使用自定义的 tcp 协议,可以让请求报文体积更小,或者使用 http 2.0协议,也可以很好的减少报文的体积,提高传输效率。
- http 如果是基于 http 1.1的协议,请求中会包含很多无用的内容,如果是基于 http 2.0,那么简单的封装以下是可以作为一个 rpc 来使用的,这时标准 rpc 框架更多的是服务治理。
(3) 性能消耗,主要在于序列化和反序列化的耗时
- rpc 可以基于 thrift 实现高效的二进制传输。
- http 大部分是通过 json 来实现的,字节大小和序列化耗时都比 thrift 要更消耗性能。
(3) 负载均衡
- rpc 基本都自带了负载均衡策略。
- http 需要配置 Nginx,HAProxy来实现。
(3) 服务治理 (下游服务新增,重启,下线时如何不影响上游调用者)
- rpc 能做到自动通知,不影响上游。
- http 需要事先通知,修改 Nginx/HAProxy 配置。
早期的 webservice,现在热门的 dubbo,都是 rpc 的典型。rpc 服务主要是针对大型企业的,而 http 服务主要是针对小企业的,因为 rpc 效率更高,而 http 服务开发迭代会更快。
3. json-rpc 简介
json-rpc 是基于 json 的跨语言远程调用协议,比 xml-rpc、webservice 等基于文本的协议数据传输格小,相对 hessian、java-rpc 等二进制协议便于调试、实现、扩展,是很优秀的一种远程调用协议。眼下主流语言都已有 json-rpc 的实现框架,Java 语言中较好的 json-rpc 实现框架有 jsonrpc4j、jpoxy、json-rpc。
json-rpc 协议很简单,发起远程调用时向服务端数据传输格式例如以下:
{ "method": "helloCoder", "params": ["Hello JSON-RPC"], "id": 1}
参数说明:
服务端收到调用请求,处理方法调用,将方法调用结果返回给调用方,返回数据格式:
{
"result": "Hello JSON-RPC",
"error": null,
"id": 1
}
参数说明:
以上就是 json-rpc 协议规范,很简单,便于各种语言实现。
4. web3j 调用
在 gradle 中添加依赖:
// 以太坊开发库
compile 'org.web3j:core:3.3.1-android'
拿我们公司的区块链钱包为例,获取余额和转币调用了 web3j 的方法,它封装了 json-rpc 的调用过程,先看看获取余额的代码:
@Override
public BigInteger getBalance(String address) throws CTXCException {
CTXCException.checkNetwork();
EthGetBalance ethGetBalance;
try {
ethGetBalance = getWeb3j().ethGetBalance(address, PENDING).send();
if (ethGetBalance.hasError())
throw new CTXCException(true, ethGetBalance.getError().getMessage());
} catch (Exception e) {
throw new CTXCException(true, e);
}
String response = ethGetBalance.getRawResponse();
Web3jResponseProcessor processor = new Web3jResponseProcessor(response).process();
if (processor.isSuccess())
return TokenBigConverter.toBigInteger(processor.getRawResponse().result);
return BigInteger.ZERO;
}
再看看转账的调用:
@Override
public String sendTransaction(String fromAddress, String password, String toAddress, BigInteger gasPrice,
BigInteger gasLimit, BigInteger amount, String payload) throws CTXCException {
CTXCException.checkNetwork();
try {
EthGetTransactionCount ethGetTransactionCount = getWeb3j().ethGetTransactionCount(fromAddress, PENDING).sendAsync().get();
BigInteger nonce = ethGetTransactionCount.getTransactionCount();
payload = payload == null ? "" : payload;
RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, toAddress, amount, payload);
Credentials credentials = Credentials.create(WalletManagerService.instance().exportPrivateKey(fromAddress, password));
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
String hexValue = Numeric.toHexString(signedMessage);
EthSendTransaction ethSendTransaction = getWeb3j().ethSendRawTransaction(hexValue).send();
if (ethSendTransaction.hasError())
throw new CTXCException(true, ethSendTransaction.getError().getMessage());
Web3jResponseProcessor processor = new Web3jResponseProcessor(ethSendTransaction.getRawResponse()).process();
if (processor.isSuccess())
return processor.getRawResponse().result;
} catch (Exception e) {
throw new CTXCException(true, e.getMessage());
}
return "";
}
比如:getWeb3j().ethGetBalance(address, PENDING).send(),这样就能获取到某个 erc20地址的余额。而 web3j 封装了 json-rpc 的调用过程,在以太坊的开发文档上,对获取余额的 rpc 调用是这样的:
可以看到,正是上面说的 json-rpc 协议的格式。
5. web3j json-rpc 实现
以 getWeb3j().ethGetBalance(address, PENDING).send() 作为入口分析。
实现在 JsonRpc2_0Web3j 这个类:
public Request, EthGetBalance> ethGetBalance(String address, DefaultBlockParameter defaultBlockParameter) {
return new Request("eth_getBalance", Arrays.asList(address, defaultBlockParameter.getValue()), this.web3jService, EthGetBalance.class);
}
将 json-rpc 方法名"eth-getBalance"和Array.asList 将参数转换成 List 传入,返回一个 Request 对象:
public class Request {
private static AtomicLong nextId = new AtomicLong(0L);
// jsonrpc 版本
private String jsonrpc = "2.0";
// 方法名
private String method;
// 参数列表
private List params;
// jsonrpc 参数 id
private long id;
private Web3jService web3jService;
private Class responseType;
public Request() {
}
public Request(String method, List params, Web3jService web3jService, Class type) {
this.method = method;
this.params = params;
this.id = nextId.getAndIncrement();
this.web3jService = web3jService;
this.responseType = type;
}
public String getJsonrpc() {
return this.jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public String getMethod() {
return this.method;
}
public void setMethod(String method) {
this.method = method;
}
public List getParams() {
return this.params;
}
public void setParams(List params) {
this.params = params;
}
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public T send() throws IOException {
return this.web3jService.send(this, this.responseType);
}
public Future sendAsync() {
return this.web3jService.sendAsync(this, this.responseType);
}
public Observable observable() {
return (new RemoteCall(new Callable() {
public T call() throws Exception {
return Request.this.send();
}
})).observable();
}
}
这个类封装了请求参数和行为,接着看里面的 send() 方法:
public T send() throws IOException {
return this.web3jService.send(this, this.responseType);
}
调用 web3jService的 send() 方法进行 json-rpc 调用,传入 request 对象和返回类型。Web3jService 是一个接口,那这边调的是哪个实现类呢?看看 web3j 对象的构造代码:
web3j = Web3jFactory.build(new HttpService(web3jRpcURL, true));
我这边传入的是 HttpService (实现 Web3jService 接口):
protected abstract InputStream performIO(String var1) throws IOException;
public T send(Request request, Class responseType) throws IOException {
String payload = this.objectMapper.writeValueAsString(request);
InputStream result = this.performIO(payload);
return result != null ? (Response)this.objectMapper.readValue(result, responseType) : null;
}
可以看到 send() 方法:
String payload = this.objectMapper.writeValueAsString(request);
先将 request 对象转换成 json,即上面 json-rpc 协议的请求格式:
{ "method": "helloCoder", "params": ["Hello JSON-RPC"], "id": 1}
然后执行 performIO() 进行 rpc 调用,下面是 HttpService 的 performIO() 的实现:
protected InputStream performIO(String request) throws IOException {
RequestBody requestBody = RequestBody.create(JSON_MEDIA_TYPE, request);
Headers headers = this.buildHeaders();
Request httpRequest = (new okhttp3.Request.Builder()).url(this.url).headers(headers).post(requestBody).build();
Response response = this.httpClient.newCall(httpRequest).execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
return responseBody != null ? this.buildInputStream(responseBody) : null;
} else {
throw new ClientConnectionException("Invalid response received: " + response.body());
}
}
rpc 的过程用的是 okhttp3,上面说过 rpc 可以基于 http 协议,这边的 httpClient 需要传入一个 http 协议版本,这边用的是默认的:
DEFAULT_PROTOCOLS = Util.immutableList(new Protocol[]{Protocol.HTTP_2, Protocol.HTTP_1_1});
而以太坊官方的 web3j json-rpc 的文档中也写到:
可以看到它们的服务提供的 json-rpc 支持 http 进行消息传递。