本章主要分为三个小节:node
现在,Node.js 已经成为国际上许多科技公司的首选方案。特别对于在服务器端须要费阻塞特性的场景,Node.js 俨然成了最好的选择。git
本章咱们主要讲Seneca 和 PM2 做为构建、运行微服务的框架。虽然选择了Seneca和PM2,但并不意味着其余框架很差。github
业界还存在一些其余被选方案,例如 restify或Express、Egg.js 可用于构建应用,forever或者nodemon可用于运行应用。而Seneca和PM2我以为是构建微服务最佳的组合,主要缘由以下:web
Node.js 中最兴奋的理念之一就是简单。只要熟悉 JavaScript,你就能够在几天内学会Node.js。用Node.js编写的代码要比使用其余语言编写的代码更加简短:数据库
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
复制代码
上述代码建立了一个服务端程序,并监听 3000 端口。运行代码后可在浏览器中输入:http://127.0.0.1:3000,既可预览到Hello World
。npm
Node.js 采用的是异步处理机制。这表示在处理较慢的事件时,好比读取文件,Node.js 不会阻塞线程,而是继续处理其余事件,Noede.js 的控制流在读取文件完毕时,会执行相应的方法来处理返回信息。json
以上一个小节代码为例,http.createServer
方法接受一个回调函数,这个回调函数将在接收一个HTTP请求时被执行。可是在等待HTTP请求同时,线程仍然能够处理其余事件。segmentfault
每当谈论微服务,咱们总会说起模块化,而模块化归结于如下设计原则:api
你应该将代码以模块的形式进行组织。一个模块应该是代码的聚合,他负责简单地处理某件事情,而且能够处理的很好,例如操做字符串。可是请注意,你的模块包含越多的函数(类、工具),它将越缺少内聚性,这是应该极力避免的。浏览器
在Node.js中,每一个JavaScript文件默认是一个模块。固然,也可使用文件夹的形式组织模块,可是咱们如今只关注的使用文件的形式:
function contains(a, b) {
return a.indexOf(b) > -1;
}
function stringToOrdinal(str) {
let result = '';
for (let i = 0, len = str.length; i < len; i++) {
result += charToNuber(str[i]);
}
return result;
}
function charToNuber(char) {
return char.charCodeAt(0) - 96;
}
module.exports = {
contains,
stringToOrdinal
}
复制代码
以上代码是一个有效的Node.js模块。这三个模块有三个函数,其中两个做为共有函数暴露外部模块使用。
若是想使用这个模块只须要require()
函数,以下所示:
const stringManipulation = request('./string-manipulation');
console.log(stringManipulation.stringToOrdinal('aabb'));
复制代码
输出结果是1122
。
结合 SOLID原则,回顾一下咱们的模块。
module.exports
变量将共有函数的接口暴露给调用者,这样具体实现的修改并不会影响到使用者的代码编写。Seneca 是一个能让您快速构建基于消息的微服务系统的工具集,你不须要知道各类服务自己被部署在何处,不须要知道具体有多少服务存在,也不须要知道他们具体作什么,任何你业务逻辑以外的服务(如数据库、缓存或者第三方集成等)都被隐藏在微服务以后。
这种解耦使您的系统易于连续构建与更新,Seneca 能作到这些,缘由在于它的三大核心功能:
在 Seneca 中,消息就是一个能够有任何你喜欢的内部结构的 JSON 对象,它们能够经过 HTTP/HTTPS、TCP、消息队列、发布/订阅服务或者任何能传输数据的方式进行传输,而对于做为消息生产者的你来说,你只须要将消息发送出去便可,彻底不须要关心哪些服务来接收它们。
而后,你又想告诉这个世界,你想要接收一些消息,这也很简单,你只需在 Seneca 中做一点匹配模式配置便可,匹配模式也很简单,只是一个键值对的列表,这些键值对被用于匹配 JSON 消息的极组属性。
在本文接下来的内容中,咱们将一同基于 Seneca 构建一些微服务。
让咱们从一点特别简单的代码开始,咱们将建立两个微服务,一个会进行数学计算,另外一个去调用它:
const seneca = require('seneca')();
seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: ( msg.left + msg.right )})
});
seneca.act({
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});
复制代码
目前,这一切都发生在同一个过程当中,没有网络流量。进程内函数调用也是一种消息传输!
该seneca.add
方法将新的操做模式添加到Seneca实例。它有两个参数:
动做功能有两个参数:
响应函数是带有标准error, result签名的回调函数。
让咱们再把这一切放在一块儿:
seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
复制代码
在示例代码中,操做计算经过消息对象的left
和 right
属性提供的两个数字的总和。并不是全部消息都会生成结果,但因为这是最多见的状况,所以Seneca容许您经过回调函数提供结果。
总之,操做模式role:math,cmd:sum
对此消息起做用:
{role: 'math', cmd: 'sum', left: 1, right: 2}
复制代码
产生这个结果:
{answer: 3}
复制代码
这些属性role
并无什么特别之处cmd
。它们刚好是您用于模式匹配的那些。
该seneca.act
方法提交消息以进行操做。它有两个参数:
响应回调是您使用标准error, result
签名提供的功能。若是存在问题(例如,消息不匹配任何模式),则第一个参数是 Error对象。若是一切按计划进行,则第二个参数是结果对象。在示例代码中,这些参数只是打印到控制台:
seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, function (err, result) {
if (err) return console.error(err)
console.log(result)
})
复制代码
sum.js文件中的示例代码向您展现了如何在同一个Node.js进程中定义和调用操做模式。您很快就会看到如何在多个进程中拆分此代码。
模式 - 与网络地址或主题相对 - 使扩展和加强系统变得更加容易。他们经过逐步添加新的微服务来实现这一点。
让咱们的系统增长两个数相乘的能力。
咱们但愿看起来像这样的消息:
{role: 'math', cmd: 'product', left: 3, right: 4}
复制代码
产生这样的结果:
{answer: 12}
复制代码
您可使用role: math, cmd: sum
操做模式做为模板来定义新 role: math, cmd: product
操做:
seneca.add({role: 'math', cmd: 'product'}, function (msg, respond) {
var product = msg.left * msg.right
respond(null, { answer: product })
})
复制代码
你能够用彻底相同的方式调用它:
seneca.act({role: 'math', cmd: 'product', left: 3, right: 4}, console.log)
复制代码
在这里,您可使用console.log快捷方式打印错误(若是有)和结果。运行此代码会产生:
{answer: 12}
复制代码
把这一切放在一块儿,你获得:
var seneca = require('seneca')()
seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
seneca.add({role: 'math', cmd: 'product'}, function (msg, respond) {
var product = msg.left * msg.right
respond(null, { answer: product })
})
seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, console.log)
.act({role: 'math', cmd: 'product', left: 3, right: 4}, console.log)
复制代码
在上面的代码示例中,seneca.act调用连接在一块儿。Seneca提供连接API做为方便。连接的调用按顺序执行,但不是按顺序执行,所以它们的结果能够按任何顺序返回。
模式使您能够轻松扩展功能。您只需添加更多模式,而不是添加if语句和复杂逻辑。
让咱们经过添增强制整数运算的能力来扩展加法动做。为此,您须要向消息对象添加一个新属性integer:true
。而后,为具备此属性的邮件提供新操做:
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right)
respond(null, {answer: sum})
})
复制代码
如今,这条消息
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}
复制代码
产生这个结果:
{answer: 3} // == 1 + 2, as decimals removed
复制代码
若是将两种模式添加到同一系统会发生什么?Seneca如何选择使用哪个?更具体的模式老是赢。换句话说,具备最多匹配属性的模式具备优先权。
这里有一些代码来讲明这一点:
var seneca = require('seneca')()
seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
// 下面两条消息都匹配 role: math, cmd: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right)
respond(null, { answer: sum })
})
//下面这条消息一样匹配 role: math, cmd: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)
// 可是,也匹配 role:math,cmd:sum,integer:true
// 可是由于更多属性被匹配到,因此,它的优先级更高
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)
复制代码
它产生的输出是:
2016 ... INFO hello ...
null { answer: 4 }
null { answer: 4 }
null { answer: 4 }
null { answer: 3 }
复制代码
前两个.act调用都匹配role: math, cmd: sum
动做模式。接下来,代码定义仅整数动做模式role: math, cmd: sum, integer: true
。在那以后,第三次调用.act与role: math, cmd: sum
行动一致,但第四次调用 role: math, cmd: sum, integer: true
。此代码还演示了您能够连接.add和.act调用。此代码在sum-integer.js文件中可用。
经过匹配更具体的消息类型,轻松扩展操做行为的能力是处理新的和不断变化的需求的简单方法。这既适用于您的项目正在开发中,也适用于实时项目且须要适应的项目。它还具备您不须要修改现有代码的优势。添加新代码来处理特殊状况会更安全。在生产系统中,您甚至不须要从新部署。您现有的服务能够保持原样运行。您须要作的就是启动新服务。
动做模式能够调用其余动做模式来完成它们的工做。让咱们修改咱们的示例代码以使用此方法:
var seneca = require('seneca')()
seneca.add('role: math, cmd: sum', function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
seneca.add('role: math, cmd: sum, integer: true', function (msg, respond) {
// 复用 role:math, cmd:sum
this.act({
role: 'math',
cmd: 'sum',
left: Math.floor(msg.left),
right: Math.floor(msg.right)
}, respond)
})
// 匹配 role:math,cmd:sum
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5',console.log)
// 匹配 role:math,cmd:sum,integer:true
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5, integer: true', console.log)
复制代码
在此版本的代码中,role: math, cmd: sum, integer: true
操做模式的定义使用先前定义的role: math, cmd: sum
操做模式。可是,它首先修改消息以将left和right属性转换为整数。
在action函数内部,context变量this是对当前Seneca实例的引用。这是在行动中引用Seneca的正确方法,由于您得到了当前动做调用的完整上下文。这使您的日志更具信息性等。
此代码使用缩写形式的JSON来指定模式和消息。例如,对象文字形式
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5}
复制代码
变为:
'role: math, cmd: sum, left: 1.5, right: 2.5'
复制代码
这种格式jsonic,做为字符串文字提供,是一种方便的格式,可使代码中的模式和消息更简洁。
sum-reuse.js文件中提供了上述示例的代码。
你定义的 Action 模式都是惟一了,它们只能触发一个函数,模式的解析规则以下:
这里有些例子:
a:1, b:2
优先于 ,a:1
由于它有更多的属性。
a:1, b:2
优先于 a:1, c:3
如b以前谈到c的字母顺序。
a:1, b:2, d:4
优先于 a:1, c:3, d:4
如b以前谈到c的字母顺序。
a:1, b:2, c:3
优先于 ,a:1, b:2
由于它有更多的属性。
a:1, b:2, c:3
优先于 ,a:1, c:3
由于它有更多的属性。
不少时间,提供一种可让你不须要全盘修改现有 Action 函数的代码便可增长它功能的方法是颇有必要的,好比,你可能想为某一个消息增长更多自定义的属性验证方法,捕获消息统计信息,添加额外的数据库结果中,或者控制消息流速等。
我下面的示例代码中,加法操做指望 left 和 right 属性是有限数,此外,为了调试目的,将原始输入参数附加到输出的结果中也是颇有用的,您可使用如下代码添加验证检查和调试信息:
const seneca = require('seneca')()
seneca
.add(
'role:math,cmd:sum',
function(msg, respond) {
var sum = msg.left + msg.right
respond(null, {
answer: sum
})
})
// 重写 role:math,cmd:sum with ,添加额外的功能
.add(
'role:math,cmd:sum',
function(msg, respond) {
// bail out early if there's a problem
if (!Number.isFinite(msg.left) ||
!Number.isFinite(msg.right)) {
return respond(new Error("left 与 right 值必须为数字。"))
}
// 调用上一个操做函数 role:math,cmd:sum
this.prior({
role: 'math',
cmd: 'sum',
left: msg.left,
right: msg.right,
}, function(err, result) {
if (err) return respond(err)
result.info = msg.left + '+' + msg.right
respond(null, result)
})
})
// 增长了的 role:math,cmd:sum
.act('role:math,cmd:sum,left:1.5,right:2.5',
console.log // 打印 { answer: 4, info: '1.5+2.5' }
)
复制代码
seneca 实例提供了一个名为 prior 的方法,让能够在当前的 action 方法中,调用被其重写的旧操做函数。
prior 函数接受两个参数:
在上面的示例代码中,已经演示了如何修改入参与出参,修改这些参数与值是可选的,好比,能够再添加新的重写,以增长日志记录功能。
在上面的示例中,也一样演示了如何更好的进行错误处理,咱们在真正进行操做以前,就验证的数据的正确性,若传入的参数自己就有错误,那么咱们直接就返回错误信息,而不须要等待真正计算的时候由系统去报错了。
错误消息应该只被用于描述错误的输入或者内部失败信息等,好比,若是你执行了一些数据库的查询,返回没有任何数据,这并非一个错误,而仅仅只是数据库的事实的反馈,可是若是链接数据库失败,那就是一个错误了。
上面的代码能够在 sum-valid.js 文件中找到。
一个 seneca 实例,其实就只是多个 Action Patterm 的集合而已,你可使用命名空间的方式来组织操做模式,例如在前面的示例中,咱们都使用了 role: math,为了帮助日志记录和调试, Seneca 还支持一个简约的插件支持。
一样,Seneca插件只是一组操做模式的集合,它能够有一个名称,用于注释日志记录条目,还能够给插件一组选项来控制它们的行为,插件还提供了以正确的顺序执行初始化函数的机制,例如,您但愿在尝试从数据库读取数据以前创建数据库链接。
简单来讲,Seneca插件就只是一个具备单个参数选项的函数,你将这个插件定义函数传递给 seneca.use 方法,下面这个是最小的Seneca插件(其实它什么也没作!):
function minimal_plugin(options) {
console.log(options)
}
require('seneca')()
.use(minimal_plugin, {foo: 'bar'})
复制代码
seneca.use 方法接受两个参数:
上面的示例代码执行后,打印出来的日志看上去是这样的:
{"kind":"notice","notice":"hello seneca 3qk0ij5t2bta/1483584697034/62768/3.2.2/-","level":"info","when":1483584697057}
(node:62768) DeprecationWarning: 'root' is deprecated, use 'global'
{ foo: 'bar' }
复制代码
Seneca 还提供了详细日志记录功能,能够提供为开发或者生产提供更多的日志信息,一般的,日志级别被设置为 INFO,它并不会打印太多日志信息,若是想看到全部的日志信息,试试如下面这样的方式启动你的服务:
node minimal-plugin.js --seneca.log.all
复制代码
会不会被吓一跳?固然,你还能够过滤日志信息:
node minimal-plugin.js --seneca.log.all | grep plugin:define
复制代码
经过日志咱们能够看到, seneca 加载了不少内置的插件,好比 basic、transport、web 以及 mem-store,这些插件为咱们提供了建立微服务的基础功能,一样,你应该也能够看到 minimal_plugin 插件。
如今,让咱们为这个插件添加一些操做模式:
function math(options) {
this.add('role:math,cmd:sum', function (msg, respond) {
respond(null, { answer: msg.left + msg.right })
})
this.add('role:math,cmd:product', function (msg, respond) {
respond(null, { answer: msg.left * msg.right })
})
}
require('seneca')()
.use(math)
.act('role:math,cmd:sum,left:1,right:2', console.log)
复制代码
运行 math-plugin.js 文件,获得下面这样的信息:
null { answer: 3 }
复制代码
看打印出来的一条日志:
{
"actid": "7ubgm65mcnfl/uatuklury90r",
"msg": {
"role": "math",
"cmd": "sum",
"left": 1,
"right": 2,
"meta$": {
"id": "7ubgm65mcnfl/uatuklury90r",
"tx": "uatuklury90r",
"pattern": "cmd:sum,role:math",
"action": "(bjx5u38uwyse)",
"plugin_name": "math",
"plugin_tag": "-",
"prior": {
"chain": [],
"entry": true,
"depth": 0
},
"start": 1483587274794,
"sync": true
},
"plugin$": {
"name": "math",
"tag": "-"
},
"tx$": "uatuklury90r"
},
"entry": true,
"prior": [],
"meta": {
"plugin_name": "math",
"plugin_tag": "-",
"plugin_fullname": "math",
"raw": {
"role": "math",
"cmd": "sum"
},
"sub": false,
"client": false,
"args": {
"role": "math",
"cmd": "sum"
},
"rules": {},
"id": "(bjx5u38uwyse)",
"pattern": "cmd:sum,role:math",
"msgcanon": {
"cmd": "sum",
"role": "math"
},
"priorpath": ""
},
"client": false,
"listen": false,
"transport": {},
"kind": "act",
"case": "OUT",
"duration": 35,
"result": {
"answer": 3
},
"level": "debug",
"plugin_name": "math",
"plugin_tag": "-",
"pattern": "cmd:sum,role:math",
"when": 1483587274829
}
复制代码
全部的该插件的日志都被自动的添加了 plugin 属性。
在 Seneca 的世界中,咱们经过插件组织各类操做模式集合,这让日志与调试变得更简单,而后你还能够将多个插件合并成为各类微服务,在接下来的章节中,咱们将建立一个 math 服务。
插件经过须要进行一些初始化的工做,好比链接数据库等,可是,你并不须要在插件的定义函数中去执行这些初始化,定义函数被设计为同步执行的,由于它的全部操做都是在定义一个插件,事实上,你不该该在定义函数中调用 seneca.act 方法,只调用 seneca.add 方法。
要初始化插件,你须要定义一个特殊的匹配模式 init: ,对于每个插件,将按顺序调用此操做模式,init 函数必须调用其 callback 函数,而且不能有错误发生,若是插件初始化失败,则 Seneca 会当即退出 Node 进程。因此的插件初始化工做都必须在任何操做执行以前完成。
为了演示初始化,让咱们向 math 插件添加简单的自定义日志记录,当插件启动时,它打开一个日志文件,并将全部操做的日志写入文件,文件须要成功打开而且可写,若是这失败,微服务启动就应该失败。
const fs = require('fs')
function math(options) {
// 日志记录函数,经过 init 函数建立
var log
// 将全部模式放在一块儿会上咱们查找更方便
this.add('role:math,cmd:sum', sum)
this.add('role:math,cmd:product', product)
// 这就是那个特殊的初始化操做
this.add('init:math', init)
function init(msg, respond) {
// 将日志记录至一个特写的文件中
fs.open(options.logfile, 'a', function (err, fd) {
// 若是不能读取或者写入该文件,则返回错误,这会致使 Seneca 启动失败
if (err) return respond(err)
log = makeLog(fd)
respond()
})
}
function sum(msg, respond) {
var out = { answer: msg.left + msg.right }
log('sum '+msg.left+'+'+msg.right+'='+out.answer+'\n')
respond(null, out)
}
function product(msg, respond) {
var out = { answer: msg.left * msg.right }
log('product '+msg.left+'*'+msg.right+'='+out.answer+'\n')
respond(null, out)
}
function makeLog(fd) {
return function (entry) {
fs.write(fd, new Date().toISOString()+' '+entry, null, 'utf8', function (err) {
if (err) return console.log(err)
// 确保日志条目已刷新
fs.fsync(fd, function (err) {
if (err) return console.log(err)
})
})
}
}
}
require('seneca')()
.use(math, {logfile:'./math.log'})
.act('role:math,cmd:sum,left:1,right:2', console.log)
复制代码
在上面这个插件的代码中,匹配模式被组织在插件的顶部,以便它们更容易被看到,函数在这些模式下面一点被定义,您还能够看到如何使用选项提供自定义日志文件的位置(不言而喻,这不是生产日志!)。
初始化函数 init 执行一些异步文件系统工做,所以必须在执行任何操做以前完成。 若是失败,整个服务将没法初始化。要查看失败时的操做,能够尝试将日志文件位置更改成无效的,例如 /math.log。
以上代码能够在 math-plugin-init.js 文件中找到。
如今让咱们把 math 插件变成一个真正的微服务。首先,你须要组织你的插件。 math 插件的业务逻辑 ---- 即它提供的功能,与它以何种方式与外部世界通讯是分开的,你可能会暴露一个Web服务,也有可能在消息总线上监听。
将业务逻辑(即插件定义)放在其本身的文件中是有意义的。 Node.js 模块便可完美的实现,建立一个名为 math.js 的文件,内容以下:
module.exports = function math(options) {
this.add('role:math,cmd:sum', function sum(msg, respond) {
respond(null, { answer: msg.left + msg.right })
})
this.add('role:math,cmd:product', function product(msg, respond) {
respond(null, { answer: msg.left * msg.right })
})
this.wrap('role:math', function (msg, respond) {
msg.left = Number(msg.left).valueOf()
msg.right = Number(msg.right).valueOf()
this.prior(msg, respond)
})
}
复制代码
而后,咱们能够在须要引用它的文件中像下面这样添加到咱们的微服务系统中:
// 下面这两种方式都是等价的(还记得咱们前面讲过的 `seneca.use` 方法的两个参数吗?)
require('seneca')()
.use(require('./math.js'))
.act('role:math,cmd:sum,left:1,right:2', console.log)
require('seneca')()
.use('math') // 在当前目录下找到 `./math.js`
.act('role:math,cmd:sum,left:1,right:2', console.log)
复制代码
seneca.wrap 方法能够匹配一组模式,同使用相同的动做扩展函数覆盖至全部被匹配的模式,这与为每个组模式手动调用 seneca.add 去扩展能够获得同样的效果,它须要两个参数:
pin 是一个能够匹配到多个模式的模式,它能够匹配到多个模式,好比 role:math 这个 pin 能够匹配到 role:math, cmd:sum 与 role:math, cmd:product。
在上面的示例中,咱们在最后面的 wrap 函数中,确保了,任何传递给 role:math 的消息体中 left 与 right 值都是数字,即便咱们传递了字符串,也能够被自动的转换为数字。
有时,查看 Seneca 实例中有哪些操做是被重写了是颇有用的,你能够在启动应用时,加上 --seneca.print.tree 参数便可,咱们先建立一个 math-tree.js 文件,填入如下内容:
require('seneca')()
.use('math')
复制代码
而后再执行它:
❯ node math-tree.js --seneca.print.tree
{"kind":"notice","notice":"hello seneca abs0eg4hu04h/1483589278500/65316/3.2.2/-","level":"info","when":1483589278522}
(node:65316) DeprecationWarning: 'root' is deprecated, use 'global'
Seneca action patterns for instance: abs0eg4hu04h/1483589278500/65316/3.2.2/-
├─┬ cmd:sum
│ └─┬ role:math
│ └── # math, (15fqzd54pnsp),
│ # math, (qqrze3ub5vhl), sum
└─┬ cmd:product
└─┬ role:math
└── # math, (qnh86mgin4r6),
# math, (4nrxi5f6sp69), product
复制代码
从上面你能够看到不少的键/值对,而且以树状结构展现了重写,全部的 Action 函数展现的格式都是 #plugin, (action-id), function-name。
可是,到如今为止,全部的操做都还存在于同一个进程中,接下来,让咱们先建立一个名为 math-service.js 的文件,填入如下内容:
require('seneca')()
.use('math')
.listen()
复制代码
而后启动该脚本,便可启动咱们的微服务,它会启动一个进程,并经过 10101 端口监听HTTP请求,它不是一个 Web 服务器,在此时, HTTP 仅仅做为消息的传输机制。
你如今能够访问 http://localhost:10101/act?ro... 便可看到结果,或者使用 curl 命令:
curl -d '{"role":"math","cmd":"sum","left":1,"right":2}' http://localhost:10101/act
复制代码
两种方式均可以看到结果:
{"answer":3}
复制代码
接下来,你须要一个微服务客户端 math-client.js:
require('seneca')()
.client()
.act('role:math,cmd:sum,left:1,right:2',console.log)
复制代码
打开一个新的终端,执行该脚本:
null { answer: 3 } { id: '7uuptvpf8iff/9wfb26kbqx55',
accept: '043di4pxswq7/1483589685164/65429/3.2.2/-',
track: undefined,
time:
{ client_sent: '0',
listen_recv: '0',
listen_sent: '0',
client_recv: 1483589898390 } }
复制代码
在 Seneca 中,咱们经过 seneca.listen 方法建立微服务,而后经过 seneca.client 去与微服务进行通讯。在上面的示例中,咱们使用的都是 Seneca 的默认配置,好比 HTTP 协议监听 10101 端口,但 seneca.listen 与 seneca.client 方法均可以接受下面这些参数,以达到定抽的功能:
注意:在 Windows 系统中,若是未指定 host, 默认会链接 0.0.0.0,这是没有任何用处的,你能够设置 host 为 localhost。
只要 client 与 listen 的端口号与主机一致,它们就能够进行通讯:
Seneca 为你提供的 无依赖传输 特性,让你在进行业务逻辑开发时,不须要知道消息如何传输或哪些服务会获得它们,而是在服务设置代码或配置中指定,好比 math.js 插件中的代码永远不须要改变,咱们就能够任意的改变传输方式。
虽然 HTTP 协议很方便,可是并非全部时间都合适,另外一个经常使用的协议是 TCP,咱们能够很容易的使用 TCP 协议来进行数据的传输,尝试下面这两个文件:
require('seneca')()
.use('math')
.listen({type: 'tcp'})
复制代码
require('seneca')()
.client({type: 'tcp'})
.act('role:math,cmd:sum,left:1,right:2',console.log)
复制代码
默认状况下, client/listen 并未指定哪些消息将发送至哪里,只是本地定义了模式的话,会发送至本地的模式中,不然会所有发送至服务器中,咱们能够经过一些配置来定义哪些消息将发送到哪些服务中,你可使用一个 pin 参数来作这件事情。
让咱们来建立一个应用,它将经过 TCP 发送全部 role:math 消息至服务,而把其它的全部消息都在发送至本地:
require('seneca')()
.use('math')
// 监听 role:math 消息
// 重要:必须匹配客户端
.listen({ type: 'tcp', pin: 'role:math' })
复制代码
require('seneca')()
// 本地模式
.add('say:hello', function (msg, respond){ respond(null, {text: "Hi!"}) })
// 发送 role:math 模式至服务
// 注意:必须匹配服务端
.client({ type: 'tcp', pin: 'role:math' })
// 远程操做
.act('role:math,cmd:sum,left:1,right:2',console.log)
// 本地操做
.act('say:hello',console.log)
复制代码
你能够经过各类过滤器来自定义日志的打印,以跟踪消息的流动,使用 --seneca... 参数,支持如下配置:
若是你运行上面的进程,使用了 --seneca.log.all,则会打印出全部日志,若是你只想看 math 插件打印的日志,能够像下面这样启动服务:
node math-pin-service.js --seneca.log=plugin:math
复制代码
Seneca不是一个Web框架。 可是,您仍然须要将其链接到您的Web服务API,你永远要记住的是,不要将你的内部行为模式暴露在外面,这不是一个好的安全的实践,相反的,你应该定义一组API模式,好比用属性 role:api,而后你能够将它们链接到你的内部微服务。
下面是咱们定义 api.js 插件。
module.exports = function api(options) {
var validOps = { sum:'sum', product:'product' }
this.add('role:api,path:calculate', function (msg, respond) {
var operation = msg.args.params.operation
var left = msg.args.query.left
var right = msg.args.query.right
this.act('role:math', {
cmd: validOps[operation],
left: left,
right: right,
}, respond)
})
this.add('init:api', function (msg, respond) {
this.act('role:web',{routes:{
prefix: '/api',
pin: 'role:api,path:*',
map: {
calculate: { GET:true, suffix:'/{operation}' }
}
}}, respond)
})
}
复制代码
而后,咱们使用 hapi 做为Web框架,建了 hapi-app.js 应用:
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() => {
const server = new Hapi.Server();
server.connection({
port: 3000
});
server.route({
path: '/routes',
method: 'get',
handler: (request, reply) => {
const routes = server.table()[0].table.map(route => {
return {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp,
}
})
reply(routes)
}
});
return server;
})()
};
const seneca = Seneca()
.use(SenecaWeb, config)
.use('math')
.use('api')
.ready(() => {
const server = seneca.export('web/context')();
server.start(() => {
server.log('server started on: ' + server.info.uri);
});
});
复制代码
启动 hapi-app.js 以后,访问 http://localhost:3000/routes ,你即可以看到下面这样的信息:
[
{
"path": "/routes",
"method": "GET",
"cors": false
},
{
"path": "/api/calculate/{operation}",
"method": "GET",
"cors": false
}
]
复制代码
这表示,咱们已经成功的将模式匹配更新至 hapi 应用的路由中。访问 http://localhost:3000/api/cal... ,将获得结果:
{"answer":3}
复制代码
在上面的示例中,咱们直接将 math 插件也加载到了 seneca 实例中,其实咱们能够更加合理的进行这种操做,如 hapi-app-client.js 文件所示:
...
const seneca = Seneca()
.use(SenecaWeb, config)
.use('api')
.client({type: 'tcp', pin: 'role:math'})
.ready(() => {
const server = seneca.export('web/context')();
server.start(() => {
server.log('server started on: ' + server.info.uri);
});
});
复制代码
咱们不注册 math 插件,而是使用 client 方法,将 role:math 发送给 math-pin-service.js 的服务,而且使用的是 tcp 链接,没错,你的微服务就是这样成型了。
注意:永远不要使用外部输入建立操做的消息体,永远显示地在内部建立,这能够有效避免注入攻击。
在上面的的初始化函数中,调用了一个 role:web 的模式操做,而且定义了一个 routes 属性,这将定义一个URL地址与操做模式的匹配规则,它有下面这些参数:
你的URL地址将开始于 /api/。
rol:api, path:*
这个 pin 表示,映射任何有 role="api" 键值对,同时 path 属性被定义了的模式,在本例中,只有 role:api,path:calculate
符合该模式。
map 属性是一个对象,它有一个 calculate 属性,对应的URL地址开始于:/api/calculate。
按着, calculate 的值是一个对象,它表示了 HTTP 的 GET 方法是被容许的,而且URL应该有参数化的后缀(后缀就类于 hapi 的 route 规则中同样)。
因此,你的完整地址是 /api/calculate/{operation}
。
而后,其它的消息属性都将从 URL query 对象或者 JSON body 中得到,在本示例中,由于使用的是 GET 方法,因此没有 body。
SenecaWeb 将会经过 msg.args 来描述一次请求,它包括:
如今,启动前面咱们建立的微服务:
node math-pin-service.js --seneca.log=plugin:math
而后再启动咱们的应用:
node hapi-app.js --seneca.log=plugin:web,plugin:api
访问下面的地址:
http://localhost:3000/api/cal... 获得 {"answer":6}
http://localhost:3000/api/cal... 获得 {"answer":5}
启动
pm2 start app.js
复制代码
如pm2 start rpc_server.js -w -i max -n s1 --ignore-watch="rpc_client.js" -e ./server_error.log -o ./server_info.log
在cluster-mode,也就是-i max下,日志文件会自动在后面追加-${index}保证不重复
pm2 stop app_name|app_id
复制代码
process.on('SIGINT', () => {
logger.warn('SIGINT')
connection && connection.close()
process.exit(0)
})
复制代码
当进程结束前,程序会拦截SIGINT信号从而在进程即将被杀掉前去断开数据库链接等等占用内存的操做后再执行process.exit()从而优雅的退出进程。(如在1.6s后进程还未结束则继续发送SIGKILL信号强制进程结束)
ecosystem.config.js
const appCfg = {
args: '',
max_memory_restart: '150M',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
},
// source map
source_map_support: true,
// 不合并日志输出,用于集群服务
merge_logs: false,
// 经常使用于启动应用时异常,超时时间限制
listen_timeout: 5000,
// 进程SIGINT命令时间限制,即进程必须在监听到SIGINT信号后必须在如下设置时间结束进程
kill_timeout: 2000,
// 当启动异常后不尝试重启,运维人员尝试找缘由后重试
autorestart: false,
// 不容许以相同脚本启动进程
force: false,
// 在Keymetrics dashboard中执行pull/upgrade操做后执行的命令队列
post_update: ['npm install'],
// 监听文件变化
watch: false,
// 忽略监听文件变化
ignore_watch: ['node_modules']
}
function GeneratePM2AppConfig({ name = '', script = '', error_file = '', out_file = '', exec_mode = 'fork', instances = 1, args = "" }) {
if (name) {
return Object.assign({
name,
script: script || `${name}.js`,
error_file: error_file || `${name}-err.log`,
out_file: out_file|| `${name}-out.log`,
instances,
exec_mode: instances > 1 ? 'cluster' : 'fork',
args
}, appCfg)
} else {
return null
}
}
module.exports = {
apps: [
GeneratePM2AppConfig({
name: 'client',
script: './rpc_client.js'
}),
GeneratePM2AppConfig({
name: 'server',
script: './rpc_server.js',
instances: 1
})
]
}
复制代码
pm2 start ecosystem.config.js
复制代码
避坑指南:processFile文件命名建议为*.config.js格式。不然后果自负。
在本章中,你掌握了Seneca 和 PM2 的基础知识,你能够搭建一个面向微服务的系统。