C#以太坊基础入门

简介: C#以太坊基础入门在这一部分,我们将使用C#开发一个最简单的.Net控制台应用,来接入以太坊节点,并打印 所连接节点旳版本信息。通过这一部分的学习,你将掌握以下技能: 如何使用节点仿真器如何在命令行访问以太坊节点如何在C#代码中访问以太坊节点我们将使用ganache来模拟以太坊节点。

C#以太坊基础入门
在这一部分,我们将使用C#开发一个最简单的.Net控制台应用,来接入以太坊节点,并打印 所连接节点旳版本信息。通过这一部分的学习,你将掌握以下技能:

如何使用节点仿真器
如何在命令行访问以太坊节点
如何在C#代码中访问以太坊节点
我们将使用ganache来模拟以太坊节点。ganache虽然不是一个真正的以太坊节点软件, 但它完整实现了以太坊的JSON RPC接口,非常适合以太坊智能合约与去中心化应用开发的 学习与快速验证:

ganache启动后将在8545端口监听http请求,因此,我们会将JSON RPC调用请求 使用http协议发送到节点旳8545端口。不同的节点软件可能会使用不同的监听端口,但 大部分节点软件通常默认使用8545端口。

以太坊规定了节点必须实现web3_clientVersion 调用来返回节点软件的版本信息,因此我们可以用这个命令来测试与 节点旳链接是否成功。

ganache-cli是以太坊节点仿真器软件ganache的命令行版本,可以方便开发者快速进行 以太坊DApp的开发与测试。在windows下你也可以使用其GUI版本。启动ganache很简单,只需要在命令行执行ganache-cli即可:ganache-cli是一个完整的词,-两边是没有空格的。一切顺利的话,你会看到与下图类似的屏幕输出:

默认情况下,ganache会随机创建10个账户,每个账户中都有100ETH的余额。你可以在 命令行中指定一些参数来调整这一默认行为。例如使用-a或--acounts参数来指定 要创建的账户数量为20:

ganache-cli -a 20

使用curl获取节点版本信息

以太坊规定了节点必须实现web3_clientVersion 接口来向外部应用提供节点旳版本信息。接口协议的交互流程如下:

这是一个典型的请求/应答模型,请求包和响应包都是标准的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()方法来向节点 提交请求了,例如,下面的代码提交一个web3_clientVersion调用请求:

string version = await web3.Client.SendRequestAsync("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类型 的变量来接收这个值:

---------------我是有底线的--------------------
出处:http://www.cnblogs.com/5ishare/

相关文章
|
1月前
|
存储 编译器 数据处理
C#基础入门之数据类型
C#基础入门之数据类型
|
2月前
|
开发框架 .NET 程序员
C#/.NET该如何自学入门?
C#/.NET该如何自学入门?
|
3月前
|
定位技术 C# 图形学
Unity和C#游戏编程入门:创建迷宫小球游戏示例
Unity和C#游戏编程入门:创建迷宫小球游戏示例
71 2
|
3月前
|
C#
C# 布尔值和条件语句:入门指南和实用示例
在编程中,通常需要一个只能有两个值之一的数据类型,比如: 是 / 否 开 / 关 真 / 假 为此,C# 有一个 bool 数据类型,可以取 true 或 false 的值。
69 3
|
3月前
|
存储 C#
C#入门开发(Hello World,运算符)
C#入门开发(Hello World,运算符)
27 0
|
3月前
|
Java 程序员 C#
C# 介绍、应用领域、入门、语法、输出和注释详解
C#(发音为“C-Sharp”)是一种由 Microsoft 创建的面向对象的编程语言,运行在 .NET Framework 上。源于 C 家族,与流行的语言如 C++ 和 Java 相近。首个版本发布于 2002 年,而最新版本,C# 12,于 2023 年 11 月发布
66 0
|
9月前
|
Web App开发 存储 C#
C# 10分钟入门基于WebOffice实现在线编辑文档,实时保存到服务器(所有office,兼容WPS)
C# 10分钟入门基于WebOffice实现在线编辑文档,实时保存到服务器(所有office,兼容WPS)
|
4月前
|
数据采集 JSON JavaScript
C# 解析“JSON“格式数据和网络实战案例 入门
C# 解析“JSON“格式数据和网络实战案例 入门
|
4月前
|
存储 算法 搜索推荐
C# 入门算法“冒泡排序“ 升序 降序 最大值 最小值 平均值
C# 入门算法“冒泡排序“ 升序 降序 最大值 最小值 平均值
|
5月前
|
算法 C# 微服务
【C#教程】零基础从入门到精通
【C#教程】零基础从入门到精通
61 0