前言
本文作为对了解和学习区块链有一定的帮助,从技术角度,描述了区块链的相关概念,并结合源码,实现了区块链相关功能点。
从Express开始
为了便于创建API以与区块链交互,我使用包npm express-generator直接使用ExpressJS启动了项目。
npm install -g express-generator
express ./chain
cd chain
yarn
配置处理
任何一个应用,都会提供一些参数。对这些参数的处理,有很多种方案。但总的来说,通常需要提供一种理想环境,即默认配置,同时给你一种方法自行修改。
- 全局默认配置
通常默认参数较少时,可以硬编码到代码里。但更灵活的方式,就是使用单独文件。这里就使用了文件 ./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
不过,为了灵活性,默认值通常允许用户修改。
- 使用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应用全局配置的一种常用且简单的方式,值得学习。
异常捕捉
- 使用uncaughtException捕捉进程异常
process.on('uncaughtException', function (err) {
// handle the error safely
logger.fatal('System error', { message: err.message, stack: err.stack });
process.emit('cleanup');
});
- 使用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。
- 初始网络
我们知道,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);
}
});
}]
- 加载逻辑
看代码知道,其核心逻辑功能是: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);
}]
- 加载模块
上面所有代码的执行结果,都要被这里的各模块共享。下面的代码说明,各个模块都采用一致(不一定一样)的参数和处理方法,这样处理起来简单方便:
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网络,其作用和地位不言而喻,无可替代。当然,对于一个不开源的所谓私链(私有区块链),是否必要,尚无定论。
- 路由扩展
任何应用,只要提供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里的元编程。这是脚本语言的优势(简单的字符串拼接处理)。
- 节点路由
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()});
});
};
- 节点保存
从现实角度考虑,在一个P2P网络中,一个孤立的节点,在没有其他任何节点信息的情况下,仅仅靠网络扫描去寻找其他节点,将是一件很难完成的事情,更别提高效和安全了。
因此,在运行软件之前,初始化一些节点供联网使用,是最简单直接的解决方案。这个在配置文件config.json里,有直接体现:
"peers": [
"ws://localhost:6002",
"ws://localhost:6003",
"ws://localhost:6004"
]
...
当然,也可以在启动的时候,通过参数–peers 1.2.3.4:70001, 2.1.2.3:7002。
写入节点,就是持久化,或者保存到数据库,或者保存到某个