介绍
从本章节开始(四期),我们将会陆陆续续的介绍一Nestjs的核心原理和底层实现,请大家动动发财的小手关注和订阅哈🤣。
本文介绍了 有关Nestjs在微服务中常见的一些问题,包括自定义的序列化和自定义的协议;有的时候在我们的工作中必不可免的 会遇到与现有的基础设施做集成的场景,而nest官方提供的集成场景和例子相对来说比较的 标准 对于那些 非标 的场景缺少介绍,所以本文补充了这一方面;
本文主要是简单介绍一下 nodejs一个消息中间件。为后续的深入了解和构造自己的 transporter 进行做准备。
本期仓库:github.com/BM-laoli/ne...
为什么我们需要 自定义的 transporter
Nest microservices 提供了一个通信层抽象,使应用程序可以轻松地在 transporter 上进行通信。
Nest自带了各种内置的 transporter,包括 Nats,RabbitMQ,Kafka等。但是,如果您想构建自己的 transporter - 例如为Zeromq,Google Cloud Pub/sub,Amazon Kinesis或Apache ActiveMQ,该怎么办?这篇文章就是为你而生的!
简单聊聊 Nest microservices 的运作模式
假设我们现在已经使用了 Faye 构建好了 想过的 transporter
我们在之前讨论过 Nest Microservices 的一些模式,比如下面的代码
ts
@Get('customers')
getCustomers(): Observable<any> {
this.logger.log('client#send -> topic: "get-customers"');
// the following request returns a response as an Observable
return this.client.send('/get-customers', {});
}
可以看到, @nestjs/microservices 所处理功能之一是将 request/response 传递过来的消息分给 publish/subscribe 之上。
我们可以简单的定义这样的一种消息响应模式 STRPTQ,
"Subscribe To the Response, then Publish The ReQuest",中文解释就是 "响应订阅 -> 发布请求"
我们在Nest的微服务中还经常看到的装饰器
ts
@MessagePattern('/get-customers')
@EventPattern('xxx')
它实际上就是一种 发布订阅的 接收模式。我们再讨论一个概念:"topic" 主题,或者说Channel 频道 🤔️。
举个例子:假设组件 A 希望从组件 B"get-ustomer",而组件 B 可以访问DB。那么 组件 A 可以发布"/get-customer"消息,并且(假设它已经订阅了该主题)组件 B 接收该消息,查询客户数据库中的客户列表,然后发送一条响应消息回来,该响应消息被传递回 A。Topic 就是"get-ustomer" 这样的行为。在Nest中 我们一般又把它称为 Channel 频道。
我们回到下面的代码
ts
@MessagePattern('/get-customers')
在Nest内部实际上做了下面的事情:
-
"/get-customers_ ack",这是我们将在 Faye 传输器(transporter 后文统一称为TR)中使用的 channel 名称,用于 发布/订阅(以下简称P/S) 与"/get-customer"主题相关的请求消息
-
'/get-customers_res',这是我们将在 Faye TR 中使用的 channel 名称,用于P/S'/get-customer'请求产生的响应消息
我们小结一下你需要掌握的知识点:
- 理解 响应模式 STRPTQ 很重要
- 了解 P/S 和 Channel
介绍一下 Faye 这个消息中间件
为了让大家对自定义的 transporter,更能够深入了解, 我们将从零构建一个 CustomerTransporter 来与 Faye消息中间件一起工作。
Faye 是一个简单的 OSS JavaScript publish/subscribe 消息代理中间件,可以很好地在 Node.js 上运行。
Faye 使用一个非常简单的 发布/订阅协(publish/subscribe protocol)。见下图
它的官方文档在这里:faye.jcoglan.com/node.html
一些简单的代码构建
在下面的代码中我们将会构建一个最简单的案例,它以实现上面 get_customers 为目标 以展示 Faye 如何使用。
我们将会创建三个工程,他们分别是 fayeServer ,customerService, customerApp,
fayeServer Code
它是一个消息中心, 负责中心接收和传递
js
const http = require('http');
const faye = require('faye');
const mountPath = '/faye';
const port = 8000;
const server = http.createServer();
const bayeux = new faye.NodeAdapter({ mount: mountPath, timeout: 45 });
bayeux.attach(server);
server.listen(port, () => {
console.log(
`listening on http://localhost:${port}${mountPath}\n========================================`,
);
});
// 开始监听各种事件消息
bayeux.on('handshake', (clientId) =>
console.log('^^ client connect (#', clientId.substring(0, 4), ')'),
);
bayeux.on('disconnect', (clientId) =>
console.log('vv client disconnect (#', clientId.substring(0, 4), ')'),
);
bayeux.on('publish', (clientId, channel, data) => {
console.log(
`<== New message from ${clientId.substring(0, 4)} on channel ${channel}`,
`\n ** Payload: ${JSON.stringify(data)}`,
);
});
bayeux.on('subscribe', (clientId, channel) => {
console.log(
`++ New subscription from ${clientId.substring(0, 4)} on ${JSON.stringify(
channel,
)}`,
);
});
bayeux.on('unsubscribe', (clientId, channel) => {
console.log(
`-- Unsubscribe by ${clientId.substring(0, 4)} on ${JSON.stringify(
channel,
)}`,
);
});
customerService Code
它负责从 FayeService中拉消息,并且推送对应的数据出去
ts
import * as faye from 'faye';
const FAYE_URL = 'http://localhost:8000/faye';
const customerList = [{ id: 1, name: 'Acme, Inc.' }];
let lastId = customerList.length;
// Faye connection
let client;
// utils
function getPayload(value, id) {
return {
err: null,
response: value,
isDisposed: true,
id: id,
};
}
function parsePacket(content) {
try {
return JSON.parse(content);
} catch (e) {
return content;
}
}
/**
* callback handler. This is registered for '/get-customers' topic.
*
* @param packet inbound request payload
*/
function getCustomers(packet): void {
const message = parsePacket(packet);
console.log(
`\n========== <<< 'get-customers' message >>> ==========\n${JSON.stringify(
message,
)}\n=============================================\n`,
);
// filter customers list if there's a `customerId` param
const customers =
message.data && message.data.customerId
? customerList.filter(
(cust) => cust.id === parseInt(message.data.customerId, 10),
)
: customerList;
client.publish('/get-customers_res', getPayload({ customers }, message.id));
}
/**
* Callback handler. This is registered for '/add-customer' topic.
*
* @param packet inbound request payload
*/
function addCustomer(packet): void {
const message = parsePacket(packet);
console.log(
`\n========== <<< 'add-customer' message >>> ==========\n${JSON.stringify(
message,
)}\n=============================================\n`,
);
customerList.push({
id: lastId + 1,
name: message.data.name,
});
lastId++;
}
// 开始监听
async function main() {
try {
client = new faye.Client(FAYE_URL);
console.log(
'Faye customer service starts...\n===============================',
);
client.subscribe('/get-customers_ack', getCustomers);
client.subscribe('/add-customer', addCustomer);
} catch (err) {
console.log('Error connecting to Faye: ', err.stack);
}
}
main();
customerApp Code
它负责面向使用者,比如一个http过来之后它去转到其他为服务或者消息中间件
注意哈,下面的代码中使用了 args控制台参数 你在cmd运行的时候需要把参数带上
ts
import * as faye from 'faye';
import { uniqueId } from 'lodash';
const FAYE_URL = 'http://localhost:8000/faye';
// Faye connection
let client;
// utils
function usage() {
console.log('Usage: node customer-app add <customer-name> | get [id]');
console.log('\t get [id]: send get-customers request and print response');
console.log(
'\t\t if id is passed, get matching customer by id, else get all\n',
);
console.log('\t add <customer-name> : send add-customer event');
process.exit(0);
}
/**
* Build Nest-shaped payload
*
* @param pattern string - message pattern
* @param value any - payload value
* @param id number - optional (used only for requests) message Id
*/
function getPayload(pattern, value, id?) {
let payload = {
pattern: pattern,
data: value,
};
if (id) {
payload = Object.assign(payload, { id });
}
return payload;
}
/**
* Issue 'get-customers' request, return response from message broker
*
* @param customerId - id of customer to return
*/
async function getCustomers(customerId) {
// build Nest-shaped message
const payload = getPayload('/get-customers', { customerId }, uniqueId());
return new Promise<void>((resolve, reject) => {
// subscribe to the response message
const subscription = client.subscribe('/get-customers_res', (result) => {
console.log(
`==> Receiving 'get-customers' reply: \n${JSON.stringify(
result.response,
null,
2,
)}\n`,
);
});
// once response is subscribed, publish the request
subscription.then(() => {
console.log(
`<== Sending 'get-customers' request with payload:\n${JSON.stringify(
payload,
)}\n`,
);
const pub = client.publish('/get-customers_ack', payload);
pub.then(() => {
// wait .5 second to ensure subscription handler executes
// then unsubscribe and resolve
setTimeout(() => {
subscription.cancel();
resolve();
}, 500);
});
});
});
}
/**
* Issue 'add-customer' event
*
* @param name - name of customer to add
*/
async function addCustomer(name) {
const payload = getPayload('/add-customer', { name });
try {
await client.publish('/add-customer', payload);
console.log(
`<== Publishing add-customer event with payload:\n${JSON.stringify(
payload,
)}\n`,
);
} catch (error) {
console.log('Error publishing event: ', error);
}
}
async function main() {
try {
client = new faye.Client(FAYE_URL);
console.log('Faye customer app starts...\n===========================');
// Call appropriate function based on cmd line arg
if (process.argv[2] === 'add') {
if (process.argv[3]) {
await addCustomer(process.argv[3]);
} else {
usage();
}
} else if (process.argv[2] === 'get') {
await getCustomers(process.argv[3]);
} else {
usage();
}
client.disconnect();
process.exit(0);
} catch (error) {
console.log('Error connecting to Faye: ', error);
process.exit(0);
}
}
// make sure we get a command argument on OS cmd line
if (process.argv.length < 3) {
usage();
}
main();
让我们看看效果
- 启动 FayeServce
这是纯js工程,直接run
shell
$ node server.js
istening on http://localhost:8000/faye
========================================
- 启动 customerServer
这是ts工程请先编译
shell
$ node dist/service
Faye customer service starts...
===============================
这个时候你会发现 FayeServer 已经获取了 observer 的 subscription 消息
shell
$ node server.js
istening on http://localhost:8000/faye
========================================
^^ client connect (# cvd4 )
++ New subscription from cvd4 on "/add-customer"
++ New subscription from cvd4 on "/get-customers_ack"
- 启动 customerApp
这是ts工程请先编译
add
shell
$ node dist/cs-app add 1
Faye customer app starts...
===========================
<== Publishing add-customer event with payload:
{"pattern":"/add-customer","data":{"name":"1"}}
现在传了一条add 消息出去,我们能够在 fayeServr 和 customerServer看到
fayeServer
shell
^^ client connect (# umi2 )
<== New message from umi2 on channel /add-customer
** Payload: {"pattern":"/add-customer","data":{"name":"1"}}
customerServer
shell
========== <<< 'add-customer' message >>> ==========
{"pattern":"/add-customer","data":{"name":"1"}}
=============================================
如果你再传递 get 你将会看到差不多的效果,这里就不演示了这不是重点 🥸,重点全在下面的几篇文章!但是作为前置知识储备 了解这篇文章依然很重要!