使用NodeJs、Express开发区块链

前言

本文作为对了解和学习区块链有一定的帮助,从技术角度,描述了区块链的相关概念,并结合源码,实现了区块链相关功能点。

从Express开始

为了便于创建API以与区块链交互,我使用包npm express-generator直接使用ExpressJS启动了项目。

npm install -g express-generator 
express ./chain
cd chain 
yarn

配置处理

任何一个应用,都会提供一些参数。对这些参数的处理,有很多种方案。但总的来说,通常需要提供一种理想环境,即默认配置,同时给你一种方法自行修改。

  1. 全局默认配置
    通常默认参数较少时,可以硬编码到代码里。但更灵活的方式,就是使用单独文件。这里就使用了文件 ./config.json 来保存全局配置,如:
{
  "port": 3001,
  "p2pPort": 6001,
  "address": "0.0.0.0",
  "fileLogLevel": "info",
  "consoleLogLevel": "log",
  "ssl": {
    "enabled": false,
    "options": {
      "port": 443,
      "address": "0.0.0.0",
      "key": "./ssl/ebookcoin.key",
      "cert": "./ssl/ebookcoin.crt"
    }
  },
  "peers": [
    "ws://localhost:6002",
    "ws://localhost:6003",
    "ws://localhost:6004"
  ]
...

使用时,只需要require就可以了。源码:

var appConfig = require("./config.json"); // app.js 

不过,为了灵活性,默认值通常允许用户修改。

  1. 使用commander组件,引入命令行选项
    commander是Node.js第三方组件(使用npm安装),常被用来开发命令行工具,用法极为简单,详细内容请看开发实践部分的分享。源码:
var program = require('commander');

program
    .version(packageJson.version)
    .option('-p, --port <port>', 'Listening port number')
    .option('-p2p, --p2pPort <p2pPort>', 'Listening p2pPort number')
    .option('-a, --address <ip>', 'Listening host name or ip')
    .option('-x, --peers [peers...]', 'Peers list')
    .option('-l, --log <level>', 'Log level')
    .parse(process.argv);

这样,就可以在命令行执行命令时,加带-c,-p等选项,例如:

node app.js -p 8888

这时,该选项就以program.port的形式被保存,于是手动修改一下:

if (program.port) {
    appConfig.port = program.port;
}

这是处理Node.js应用全局配置的一种常用且简单的方式,值得学习。

异常捕捉

  1. 使用uncaughtException捕捉进程异常
process.on('uncaughtException', function (err) {
    // handle the error safely
    logger.fatal('System error', { message: err.message, stack: err.stack });
    process.emit('cleanup');
});
  1. 使用domain模块捕获全局异常
var d = require('domain').create();
d.on('error', function (err) {
    logger.fatal('Domain master', { message: err.message, stack: err.stack });
    process.exit(0);
});
d.run(function () {
...

模块加载

整体使用async.auto进行顺序调用;在加载modules时,又使用async.parallel,使其并行运作;当发生错误时,清理工作用到了async.eachSeries。

  1. 初始网络
    我们知道,Express是Node.js重要的web开发框架,这里的网络network本质上就是以Express为基础的web应用。
network: ['config', function (scope, cb) {
	var express = require('express');

    var app = express();
    var server = require('http').createServer(app);

    cb(null, {
		express: express,
		app: app,
		server: server
	});
}]

说明:这是async.auto常用的方法,network用到的任何需要回调的方法(这里是config),都放在这个数组里,最后的回调函数(function (cb, scope) {//code}),可以巧妙的调用,如: scope.config。

这里的代码,仅仅初始化服务,没有做太多实质的事情,真正的动作在下面。
2. 构建链接

connect: ['config', 'logger', 'network', 'genesisblock', function (scope, cb) {
	var bodyParser = require('body-parser');

	scope.network.app.use(bodyParser.json());

	scope.network.server.listen(scope.config.port, scope.config.address, function (err) {
		scope.logger.log("started: " + scope.config.address + ":" + scope.config.port);

			if (!err) {
				cb(null, scope.network);
			} else {
				cb(err, scope.network);
			}
		});
 }]
  1. 加载逻辑
    看代码知道,其核心逻辑功能是:Peer、Block和Transaction。这些模块,本质上是对数据库操作的封装,peer与modules/peer模块对应,transaction与modules/transactions模块对应,block与modules/blocks模块对应。
logic: ['dbLite', 'bus', 'genesisblock', function (scope, cb) {
	var Transaction = require('./logic/transaction.js');
	var Block = require('./logic/block.js');
	var Peer = require('./logic/peer.js');

	async.auto({
		bus: function (cb) {
			cb(null, scope.bus);
		},
		dbLite: function (cb) {
			cb(null, scope.dbLite);
		},
		genesisblock: function (cb) {
			cb(null, {block: genesisBlock});
		},
		transaction: ["dbLite", "bus", 'genesisblock', function (scope, cb) {
			new Transaction(scope, cb);
		}],
		block: ["dbLite", "bus", 'genesisblock', 'transaction', function (scope, cb) {
			new Block(scope, cb);
		}],
		peer: ["dbLite", "bus", 'genesisblock', 'transaction', 'block', function (scope, cb) {
			new Peer(scope, cb);
		}]
	}, cb);
}]
  1. 加载模块
    上面所有代码的执行结果,都要被这里的各模块共享。下面的代码说明,各个模块都采用一致(不一定一样)的参数和处理方法,这样处理起来简单方便:
modules: ['network', 'connect', 'config', 'logger', 'bus', 'dbLite', function (scope, cb) {
	 var tasks = {};
	Object.keys(config.modules).forEach(function (name) {
		tasks[name] = function (cb) {
			var d = require('domain').create();

			d.on('error', function (err) {
				 scope.logger.fatal('Domain ' + name, {message: err.message, stack: err.stack});
			});

			d.run(function () {
				logger.debug('Loading module', name)
				var Klass = require(config.modules[name]);
				var obj = new Klass(scope, cb);
					modules.push(obj);
			});
		}
	});

	// 让各个模块并行运行
	async.parallel(tasks, function (err, results) {
		cb(err, results);
	});
}]

这里的模块既然都是并行处理,研究它们就不需要分先后了。

实现p2p网络

加密货币都是去中心化的应用,去中心化的基础就是P2P网络,其作用和地位不言而喻,无可替代。当然,对于一个不开源的所谓私链(私有区块链),是否必要,尚无定论。

  1. 路由扩展
    任何应用,只要提供Web访问能力或第三方访问的Api,都需要提供从地址到逻辑的请求分发功能,这就是路由。Ebookcoin是基于http协议的Express应用,Express底层基于Node.js的connect模块,因此其路由设计简单而灵活。
var Router = function () {
    var router = require('express').Router();
    router.use(function (req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });
    router.map = map;
    return router;
}
...

这段代码定义了一个Express路由器Router,并扩展了两个功能

  • 允许任何客户端调用。其实,就是设置了跨域请求,选项Access-Control-Allow-Origin设置为*,自然任何IP和端口的节点都可以访问和被访问。
  • 添加了地址映射方法。该方法的主要内容如下:
function map(root, config) {
    var router = this;
    Object.keys(config).forEach(function (params) {
        var route = params.split(" ");
        if (route.length != 2 || ["post", "get", "put"].indexOf(route[0]) == -1) {
            throw Error("wrong map config");
        }
        router[route[0]](route[1], function (req, res, next) {
            root[config[params]]({"body": route[0] == "get" ? req.query : req.body}, function (err, response) {
                if (err) {
                    res.json({"success": false, "error": err});
                } else {
                    return res.json(extend({}, {"success": true}, response));
                }
            });
        });
    });
}

该方法,接受两个对象作为参数:

  • root: 定义了所要开放Api的逻辑函数;
  • config: 定义了路由和root定义的函数的对应关系。

其运行的结果,就相当于:

router.get('/peers', function(req, res, next){
    root.getPeers(...);
})

这里关键的小技巧是,在js代码中,对象也是hash值,root.getPeers() 与 root‘getPeers’ 是一致的。不过后者可以用字符串变量代替,更加灵活,有点像ruby里的元编程。这是脚本语言的优势(简单的字符串拼接处理)。

  1. 节点路由
Router = require('../helpers/router.js');

privated.attachApi = function () {
    var router = new Router();

    router.use(function (req, res, next) {
        if (modules) return next();
        res.status(500).send({success: false, error: "Blockchain is loading"});
    });

    router.map(shared, {
        "get /": "getPeers",
        "post /addPeer": "addPeer",
    });

    router.use(function (req, res, next) {
        res.status(500).send({success: false, error: "API endpoint not found"});
    });

    library.network.app.use('/api/peers', router);
    library.network.app.use(function (err, req, res, next) {
        if (!err) return next();
        library.logger.error(req.url, err.toString());
        res.status(500).send({success: false, error: err.toString()});
    });
};
  1. 节点保存

从现实角度考虑,在一个P2P网络中,一个孤立的节点,在没有其他任何节点信息的情况下,仅仅靠网络扫描去寻找其他节点,将是一件很难完成的事情,更别提高效和安全了。

因此,在运行软件之前,初始化一些节点供联网使用,是最简单直接的解决方案。这个在配置文件config.json里,有直接体现:

"peers": [
    "ws://localhost:6002",
    "ws://localhost:6003",
    "ws://localhost:6004"
  ]
...

当然,也可以在启动的时候,通过参数–peers 1.2.3.4:70001, 2.1.2.3:7002。

写入节点,就是持久化,或者保存到数据库,或者保存到某个

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值