NEO从入门到开窗(4) - NEO CLI

时间:2023-03-09 22:48:20
NEO从入门到开窗(4) - NEO CLI

一、唠叨两句

首先,我们都知道区块链是去中心化的,其中节点都是对等节点,每个节点都几乎有完整的区块链特性,CLI就是NEO的一个命令行对等节点,当然也有GUI这个项目,图形化的NEO节点。节点之间需要通信,互通有无,我们今天主要看看这部分。

二、从入口开始

CLI是一个Console程序,那我们就从它的Main开始把。

static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
new MainService().Run(args);
}

Main

这里除了注册一个未捕获异常的处理事件之外,就是这个MainService的Run方法了。MainService继承自ConsoleServiceBase,我们看下ConsoleServiceBase的代码吧,看一眼你就明白这个Service是在搞什么东东了

using System;
using System.Reflection;
using System.Security;
using System.Text; namespace Neo.Services
{
public abstract class ConsoleServiceBase
{
protected virtual string Prompt => "service"; public abstract string ServiceName { get; } protected bool ShowPrompt { get; set; } = true; protected virtual bool OnCommand(string[] args)
{
switch (args[].ToLower())
{
case "clear":
Console.Clear();
return true;
case "exit":
return false;
case "version":
Console.WriteLine(Assembly.GetEntryAssembly().GetName().Version);
return true;
default:
Console.WriteLine("error");
return true;
}
} protected internal abstract void OnStart(string[] args); protected internal abstract void OnStop(); public void Run(string[] args)
{
OnStart(args);
RunConsole();
OnStop();
} private void RunConsole()
{
bool running = true;
#if NET461
Console.Title = ServiceName;
#endif
Console.OutputEncoding = Encoding.Unicode; Console.ForegroundColor = ConsoleColor.DarkGreen;
Version ver = Assembly.GetEntryAssembly().GetName().Version;
Console.WriteLine($"{ServiceName} Version: {ver}");
Console.WriteLine(); while (running)
{
if (ShowPrompt)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"{Prompt}> ");
} Console.ForegroundColor = ConsoleColor.Yellow;
string line = Console.ReadLine().Trim();
Console.ForegroundColor = ConsoleColor.White; string[] args = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (args.Length == )
continue;
try
{
running = OnCommand(args);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"error: {ex.Message}");
#else
Console.WriteLine("error");
#endif
}
} Console.ResetColor();
}
}
}

ConsoleServiceBase

MainService肯定重写了方法OnStart,OnStop,里面干啥了后面再讲,除此之外,主要方法RunConsole里就是等输入命令,然后根据命令进行相应操作,然后循环继续。可以创建个钱包啊,创建个地址啊,blabla的。OnCommand里实现了一些界面上的操作,那MainService类里一定就是侦听CLI可以支持的各种指令咯,具体每个指令做了什么操作顺着这个线索看代码即可了解,后面我们会选几个关键的指令溜一下代码,现在我们就只关注这个OnStart,OnStop都干啥了。

protected internal override void OnStart(string[] args)
{
Blockchain.RegisterBlockchain(new LevelDBBlockchain(Settings.Default.Paths.Chain));
if (!args.Contains("--nopeers") && File.Exists(PeerStatePath))
using (FileStream fs = new FileStream(PeerStatePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
LocalNode.LoadState(fs);
}
LocalNode = new LocalNode();
Task.Run(() =>
{
const string acc_path = "chain.acc";
const string acc_zip_path = acc_path + ".zip";
if (File.Exists(acc_path))
{
using (FileStream fs = new FileStream(acc_path, FileMode.Open, FileAccess.Read, FileShare.None))
{
ImportBlocks(fs);
}
File.Delete(acc_path);
}
else if (File.Exists(acc_zip_path))
{
using (FileStream fs = new FileStream(acc_zip_path, FileMode.Open, FileAccess.Read, FileShare.None))
using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read))
using (Stream zs = zip.GetEntry(acc_path).Open())
{
ImportBlocks(zs);
}
File.Delete(acc_zip_path);
}
LocalNode.Start(Settings.Default.P2P.Port, Settings.Default.P2P.WsPort);
bool recordNotifications = false;
for (int i = ; i < args.Length; i++)
{
switch (args[i])
{
case "/rpc":
case "--rpc":
case "-r":
if (rpc == null)
{
rpc = new RpcServerWithWallet(LocalNode);
rpc.Start(Settings.Default.RPC.Port, Settings.Default.RPC.SslCert, Settings.Default.RPC.SslCertPassword);
}
break;
case "--record-notifications":
recordNotifications = true;
break;
}
}
if (recordNotifications)
Blockchain.Notify += Blockchain_Notify;
});
}

MainService.OnStart

我们看到OnStart方法里做了如下的事情:

1. 注册LevelDBBlockChain作为本地的链的存储

2. 正常启动需要与其他节点通讯的情况,会先找一个叫做peers.dat的文件。这个文件里存储的是其他对等节点的地址,读取的到地址存储在LocalNode类的静态属性UnconnectedPeers里,拿到这些节点地址可以建立与其的通信。

3. 新建了一个LocalNode实例,这个实例比较关键了,作为本地节点的通讯模块,与之对应的是RemoteNode,每个已连接上的对等节点都会有一个对应的RemoteNode实例,保存在LocalNode实例的connectedPeers列表里。

4. 查看是否有chain.acc或者chain.acc.zip文件,这个文件是链的存储文件,如果不想同步节点,可以使用这个文件作为离线文件启动,免去同步数据的时间。

5. 调用LocalNode.Start方法,该方法里做了啥?

a. 启动两个线程connectThread和poolThread,这两个线程都干啥呢?

I. connectThread: 如果前面讲述的unconnectedPeers列表里还有未连接的节点,就创建对应的任务去连接。如果没有未连接的节点了,就从connectedPeers列表里拿出来已连接的RemoteNode节点,发送一个getaddr消息。如果两个列表都是空,就拿出系统默认的feed节点去连接。

在连接节点的时候,会创建一个RemoteNode实例,添加到connectedPeers列表里,并调用RemoteNode的StartProtocal,这个方法里表达出的是两个对等节点建立连接的过程:

给对方发送Version消息

接收到Version消息,就发送VerAck消息

接收VerAck消息,根据Version消息中的信息判断是不是对方持有更新的数据,如果是就发送getheaders和getblocks消息获取最新的数据

II. poolThread: 首先先说下两个数据结构temp_pool和mem_pool,都是存储交易的,temp_pool存储的是最近接收到的交易,mem_pool是在内存中存储所有未验证交易。当节点接收到了交易消息,会把消息放在temp_pool里,然后在这个poolThread的一个loop中把mem_pool和temp_pool合并,对每笔交易进行验证,验证通过的交易都会发送一个inv消息,把消息发送到已连接上的RemoteNode中去。

b. 开启TcpListener侦听其他对等节点的消息,开启WebSocket侦听WebSocket消息。

6. 如果需要开启rpc,则开启RPC服务。

protected internal override void OnStop()
{
if (consensus != null) consensus.Dispose();
if (rpc != null) rpc.Dispose();
LocalNode.Dispose();
using (FileStream fs = new FileStream(PeerStatePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
LocalNode.SaveState(fs);
}
Blockchain.Default.Dispose();
}

MainService.OnStop

OnStop方法就简单多了

1. 析构共识服务和RPC服务

2. 析构LocalNode,调用LocalNode.Dispose方法,停止TcpListener侦听,与所有的connectedPeers断开连接,并且把所有的connectedPeers放到unconnectedPeers队列里。

3. 将unconnectedPeers写入peers.dat文件

4. 析构链结构,LevelDBBlockchain关闭底层的LevelDB存储。

三、小结

好了,到这里简单介绍了一下NEO CLI,重点讲了启动和关闭时都搞了些什么,基本上也就是NEO网络层干的事情,剩下的就是处理互相通信的消息,处理CLI上输入的指令。到这里你应该已经建立了一个多对等节点建立网络的大概过程。