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[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();
        }
    }
}
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 = 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;
            });
        }
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上输入的指令。到这里你应该已经建立了一个多对等节点建立网络的大概过程。

posted @ 2018-03-31 18:03  Dexter Di  阅读(680)  评论(0编辑  收藏  举报