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); }
这里除了注册一个未捕获异常的处理事件之外,就是这个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[0].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 == 0) continue; try { running = OnCommand(args); } catch (Exception ex) { #if DEBUG Console.WriteLine($"error: {ex.Message}"); #else Console.WriteLine("error"); #endif } } Console.ResetColor(); } } }
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 = 0; 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; }); }
我们看到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(); }
OnStop方法就简单多了
1. 析构共识服务和RPC服务
2. 析构LocalNode,调用LocalNode.Dispose方法,停止TcpListener侦听,与所有的connectedPeers断开连接,并且把所有的connectedPeers放到unconnectedPeers队列里。
3. 将unconnectedPeers写入peers.dat文件
4. 析构链结构,LevelDBBlockchain关闭底层的LevelDB存储。
三、小结
好了,到这里简单介绍了一下NEO CLI,重点讲了启动和关闭时都搞了些什么,基本上也就是NEO网络层干的事情,剩下的就是处理互相通信的消息,处理CLI上输入的指令。到这里你应该已经建立了一个多对等节点建立网络的大概过程。