在这一部分,咱们将使用C#开发一个最简单的.Net控制台应用,来接入以太坊节点,并打印 所链接节点旳版本信息。经过这一部分的学习,你将掌握如下技能:web
咱们将使用ganache来模拟以太坊节点。ganache虽然不是一个真正的以太坊节点软件, 但它完整实现了以太坊的JSON RPC接口,很是适合以太坊智能合约与去中心化应用开发的 学习与快速验证:json
ganache启动后将在8545端口监听http请求,所以,咱们会将JSON RPC调用请求 使用http协议发送到节点旳8545端口。不一样的节点软件可能会使用不一样的监听端口,但 大部分节点软件一般默认使用8545端口。windows
以太坊规定了节点必须实现web3_clientVersion 调用来返回节点软件的版本信息,所以咱们能够用这个命令来测试与 节点旳连接是否成功。数组
ganache-cli是以太坊节点仿真器软件ganache的命令行版本,能够方便开发者快速进行 以太坊DApp的开发与测试。在windows下你也可使用其GUI版本。启动ganache很简单,只须要在命令行执行ganache-cli便可:ganache-cli是一个完整的词,-两边是没有空格的。一切顺利的话,你会看到与下图相似的屏幕输出:app
默认状况下,ganache会随机建立10个帐户,每一个帐户中都有100ETH的余额。你能够在 命令行中指定一些参数来调整这一默认行为。例如使用-a
或--acounts
参数来指定 要建立的帐户数量为20:curl
ganache-cli -a 20
使用curl获取节点版本信息socket
以太坊规定了节点必须实现web3_clientVersion 接口来向外部应用提供节点旳版本信息。接口协议的交互流程以下:async
这是一个典型的请求/应答模型,请求包和响应包都是标准的JSON格式。其中,jsonrpc字段用来 标识协议版本,id则用来帮助创建响应包与请求包的对应关系。工具
在请求包中,使用method字段来声明接口方法,例如web3_clientVersion,使用params 字段来声明接口方法的参数数组。 在响应包中,result字段中保存了命令执行的返回结果。学习
以太坊JSON RPC并无规定传输层的实现,不过大部分节点都会实现HTTP和IPC的访问。所以 咱们可使用命令行工具curl来测试这个接口:
curl http://localhost:8545 -X POST -d '{"jsonrpc": "2.0","method": "web3_clientVersion","params": [], "id": 123}'
使用C#获取节点版本信息
就像前一节看到的,咱们只要在C#代码中按照以太坊RPC接口要求发送http请求包就能够了。 你可使用任何一个你喜欢的http库,甚至直接使用socket来调用以太坊的JSON RPC API。例如,下面的代码使用.Net内置的HttpClient类来访问以太坊节点,注意代码中的注释:
using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace GetVersionByHttpDemo { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); GetVersion().Wait(); GetAccounts().Wait(); Console.ReadLine(); } static async Task GetVersion() { HttpClient httpClient = new HttpClient(); string url = "http://localhost:7545"; string payload = "{\"jsonrpc\":\"2.0\",\"method\":\"web3_clientVersion\",\"params\":[],\"id\":7878}"; Console.WriteLine("<= " + payload); StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage rsp = await httpClient.PostAsync(url, content); string ret = await rsp.Content.ReadAsStringAsync(); Console.WriteLine("=> " + ret); } static async Task GetAccounts() { HttpClient httpClient = new HttpClient(); string url = "http://localhost:7545"; string payload = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_accounts\",\"params\":[],\"id\":5777}"; Console.WriteLine("<= " + payload); StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage rsp = await httpClient.PostAsync(url, content); string ret = await rsp.Content.ReadAsStringAsync(); Console.WriteLine("=> " + ret); } } }
序列化与反序列化
在应用逻辑里直接拼接RPC请求字符串,或者直接解析RPC响应字符串,都不是 使人舒心的事情。
更干净的办法是使用数据传输对象(Data Transfer Object)层来 隔离这个问题,在DTO层将C#的对象序列化为Json字符串,或者从Json字符串 反序列化为C#的对象,应用代码只须要操做C#对象便可。
咱们首先定义出JSON请求与响应所对应的C#类。例如:
如今咱们获取节点版本的代码能够不用直接操做字符串了:
以下图,在SerializeDemo中定义了请求与响应的model。
RpcRequestMessage
using System; using System.Collections.Generic; using System.Text; using Newtonsoft.Json; namespace SerializeDemo { class RpcRequestMessage { public RpcRequestMessage(string method, params object[] parameters) { Id = Environment.TickCount; Method = method; Parameters = parameters; } [JsonProperty("id")] public int Id; [JsonProperty("jsonrpc")] public string JsonRpc = "2.0"; [JsonProperty("method")] public string Method; [JsonProperty("params")] public object Parameters; } }
RpcResponseMessage
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Text; namespace SerializeDemo { class RpcResponseMessage { [JsonProperty("id")] public int Id { get; set; } [JsonProperty("jsonrpc")] public string JsonRpc { get; set; } [JsonProperty("result")] public object Result { get; set; } } }
RpcHttpDto
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace SerializeDemo { class RpcHttpDto { public async Task Run() { var version = await Rpc("web3_clientVersion"); Console.WriteLine("version => " + version + " type => " + version.GetType().Name); var accounts = await Rpc("eth_accounts"); Console.WriteLine("accounts => " + accounts + " type => " + accounts.GetType().Name); } public async Task<object> Rpc(string method) { HttpClient httpClient = new HttpClient(); string url = "http://localhost:7545"; RpcRequestMessage rpcReqMsg = new RpcRequestMessage(method); string payload = JsonConvert.SerializeObject(rpcReqMsg); Console.WriteLine("<= " + payload); StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage rsp = await httpClient.PostAsync(url, content); string ret = await rsp.Content.ReadAsStringAsync(); Console.WriteLine("=> " + ret); RpcResponseMessage rpcRspMsg = JsonConvert.DeserializeObject<RpcResponseMessage>(ret); return rpcRspMsg.Result; } } }
Program
using System; namespace SerializeDemo { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); Console.WriteLine("Call Ethereum RPC Api with HttpClient"); RpcHttpDto demo = new RpcHttpDto(); demo.Run().Wait(); Console.ReadLine(); } } }
使用现成的轮子
尽管可行,但我仍是建议你尽可能避免本身去封装这些rpc接口,毕竟 这个事已经作过好几回了,并且rpc接口封装仅仅是整个故事的一部分。
Nethereum是以太坊官方推荐的.Net下的rpc接口封装库,所以咱们优先 选择它。
下面是使用Nethereum获取节点版本信息的代码:
Web3 web3 = new Web3("http://localhost:7545"); string version = await web3.Client.SendRequestAsync<string>("web3_clientVersion"); Console.WriteLine("version => " + version);
Web3是Nethereum的入口类,咱们与以太坊的交互,基本上是经过 这个入口来完成的,实例化Web3须要指定要连接的节点地址,例如本地ganache节点,就 可使用http://localhost:7545这个地址。
Web3实例的Client属性是一个IClient接口的实现对象,这个接口抽象了与 节点的RPC接口交互的方法,所以与具体的通讯传输机制无关:
从上图容易看出,Nethereum目前支持经过四种不一样的通讯机制来访问以太坊: Http、WebSocket、命名管道和Unix套接字。
容易理解,当咱们提供一个节点url做为Web3实例化的参数时,Web3将自动建立 一个基于Http的IClient实现实例,即RpcClient实例。
一旦得到了Iclient的实现实例,就能够调用其SendRequestAsync<T>()方法来向节点 提交请求了,例如,下面的代码提交一个web3_clientVersion调用请求:
string version = await web3.Client.SendRequestAsync<string>("web3_clientVersion");
SendRequestAsync()是一个泛型方法,其泛型参数T用来声明返回值的类型。例如, 对于web3_clientVersion调用,其RPC响应的result字段是一个字符串,所以咱们使用 string做为泛型参数。
须要指出的是,SendRequestAsync()不须要咱们传入完整的请求报文,其返回的结果 也不是完整的响应报文,只是其中result字段的内容。
对于须要传入参数的RPC调用,例如用来计算字符串keccak哈希值的 web3_sha3调用, 能够在SendRequestAsync()方法自第3个参数开始依次写入。例如,下面的代码 计算hello,ethereum的keccak哈希:
HexUTF8String hexstr = new HexUTF8String("hello,ethereum"); Console.WriteLine("hello,ethereum => " + hexstr.HexValue); string hash = await web3.Client.SendRequestAsync<string>("web3_sha3", null, hexstr); Console.WriteLine("keccak hash => " + hash);
SendRequestAsync()方法的第2个参数表示路由名称,能够用来拦截RPC请求,知足 一些特殊的应用需求,咱们一般将其设置为null便可。因为web3_sha3调用要求传入 的参数为16进制字符串格式,例如,hello,ethereum应当表示为0x68656c6c6f2c657468657265756d, 所以咱们使用HexUtf8String类进行转换:
使用RPC接口封装类
若是你倾向于薄薄一层的封装,那么使用IClient的SendRequestAsync()接口, 已经能够知足大部分访问以太坊的需求了,并且基本上只须要参考RPC API的手册, 就能够完成工做了。不过Nethereum走的更远。
Nethereum为每个RPC接口都封装了单独的类。
例如,对于web3_clientVersion调用,其对应的实现类为Web3ClientVersion; 而对于web3_sha3调用,其对应的实现类为Web3Sha3:
有一点有助于咱们的开发:容易根据RPC调用的名字猜想出封装类的名称 —— 去掉 下划线,而后转换为单词首字母大写的Pascal风格的命名。
因为每个RPC接口的封装类都依赖于一个IClient接口的实现,所以咱们能够直接 在接口封装类实例上调用SendRequestAsync()方法,而无须再显式地使用一个IClient 实现对象来承载请求 —— 固然在建立封装类实例时须要传入IClient的实现对象。
例如,下面的代码使用类Web3ClientVersion来获取节点版本信息:
Web3ClientVersion w3cv = new Web3ClientVersion(web3.Client); string version = await w3cv.SendRequestAsync(); Console.WriteLine("version => " + version);
容易注意到封装类的SendRequestAsync()方法再也不须要使用泛型参数声明返回值的 类型,这是由于特定RPC接口的对应封装类在定义时已经肯定了调用返回值的类型。例如:
namespace Nethereum.RPC.Web3 { public class Web3ClientVersion : GenericRpcRequestResponseHandlerNoParam<string> { public Web3ClientVersion(IClient client); } }
若是RPC接口须要额外的参数,例如web3_sha3,那么在SendRequestAsync() 方法中依次传入便可。例如,下面的代码使用Web3Sha3类来计算一个字符串 的keccak哈希值:
HexUTF8String hexstr = new HexUTF8String("hello,ethereum"); Web3Sha3 w3s = new Web3Sha3(web3.Client); string hash = await w3s.SendRequestAsync(hexstr); Console.WriteLine("keccak hash => " + hash);
接口封装类比直接使用IClient提供了更多的类型检查能力,但同时也 带来了额外的负担 —— 须要同时查阅RPC API接口文档和Nethereum的接口 封装类文档,才能顺利地完成任务。
using Nethereum.Hex.HexTypes; using Nethereum.RPC.Web3; using Nethereum.Web3; using System; using System.Threading.Tasks; namespace Web3HeavyDemo { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); Console.WriteLine("Access Ethereum with Nethereum"); Task.Run(async () => { Web3 web3 = new Web3("http://localhost:7545"); Web3ClientVersion w3cv = new Web3ClientVersion(web3.Client); string version = await w3cv.SendRequestAsync(); Console.WriteLine("version => " + version); HexUTF8String hexstr = new HexUTF8String("hello,ethereum"); Web3Sha3 w3s = new Web3Sha3(web3.Client); string hash = await w3s.SendRequestAsync(hexstr); Console.WriteLine("keccak hash => " + hash); }).Wait(); Console.ReadLine(); } } }
理解Nethereum的命名规则
大多数状况下,咱们容易从以太坊的RPC接口名称,推测出Nethereum的封装类名称。可是别忘了,在C#中,还有个命名空间的问题。
Nethereum根据不一样的RPC接口系列,在不一样的命名空间定义接口实现类。 例如对于web3_*这一族的接口,其封装类的命名空间为Nethereum.RPC.Web3:
可是,对于eth_*系列的接口,并非全部的封装类都定义在Nethereum.RPC.Eth 命名空间,Nethereum又任性地作了一些额外的工做 —— 根据接口的功能划分了一些 子命名空间!例如,和交易有关的接口封装类,被纳入Nethereum.RPC.Eth.Transactions命名 空间,而和块有关的接口封装类,则被纳入Nethereum.RPC.Eth.Blocks命名空间。
显然,若是你从一个RPC调用出发,尝试推测出它在Nethereum中正确的命名空间和 封装类名称,这种设计并不友好 —— 虽然方便了Nethereume的开发者维护代码, 但会让Nethereum的使用者感到崩溃 —— 不可预测的API只会伤害开发效率。
using Nethereum.Hex.HexTypes; using Nethereum.RPC.Eth; using Nethereum.Web3; using System; using System.Threading.Tasks; namespace Web3Namerules { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); Console.WriteLine("Access Ethereum with Nethereum"); Task.Run(async () => { Web3 web3 = new Web3("http://localhost:7545"); EthAccounts ea = new EthAccounts(web3.Client); string[] accounts = await ea.SendRequestAsync(); Console.WriteLine("accounts => \n" + string.Join("\n", accounts)); EthGasPrice egp = new EthGasPrice(web3.Client); HexBigInteger price = await egp.SendRequestAsync(); Console.WriteLine("gas price => " + price.Value); }).Wait(); Console.ReadLine(); } } }
使用Web3入口类
Netherem推荐经过入口类Web3来使用接口封装类,这能够在某种程度上减轻 复杂的命名空间设计给使用者带来的困扰。
例如,咱们可使用web3.Eth.Accounts来直接访问EthAccounts类的实例, 而无须引入命名空间来实例化:
也就是说,在实例化入口类Web3的时候,Nethereum同时也建立好了全部的接口 封装类的实例,并挂接在不一样的属性(例如Eth)之下。
咱们能够先忽略Eth属性的具体类型,简单地将其视为接口封装对象的容器。 所以,当咱们须要使用EthGetBalance类的时候,经过web3.Eth.GetBalance 便可访问到其实例对象;一样,当咱们但愿使用EthSendTransaction类时, 则能够经过web3.Eth.Transactions.SendTransaction来访问其实例对象 —— 它在子容器Transactions里:
例如,下面的代码调用eth_accounts接口获取节点帐户列表,而后调用 eth_getBalance接口获取第一个帐户的余额:
class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); Console.WriteLine("Web3 Entry Demo"); Web3Entry demo = new Web3Entry(); demo.Run().Wait(); Console.ReadLine(); } }
using Nethereum.Hex.HexTypes; using Nethereum.Web3; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Web3EntryDemo { class Web3Entry { public async Task Run() { Web3 web3 = new Web3("http://localhost:7545"); string[] accounts = await web3.Eth.Accounts.SendRequestAsync(); Console.WriteLine("account#0 => " + accounts[0]); HexBigInteger balance = await web3.Eth.GetBalance.SendRequestAsync(accounts[0]); Console.WriteLine("balance => " + balance.Value); } } }
因为eth_getBalance 返回的帐户余额采用16进制字符串表示,所以咱们须要使用HexBigInteger类型 的变量来接收这个值: