[四期 - 1] 探索系列 - Nest 中如何自定义协议?(P1-开胃菜)

介绍

从本章节开始(四期),我们将会陆陆续续的介绍一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内部实际上做了下面的事情:

  1. "/get-customers_ ack",这是我们将在 Faye 传输器(transporter 后文统一称为TR)中使用的 channel 名称,用于 发布/订阅(以下简称P/S) 与"/get-customer"主题相关的请求消息

  2. '/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();

让我们看看效果

  1. 启动 FayeServce

这是纯js工程,直接run

shell 复制代码
$ node server.js
istening on http://localhost:8000/faye
 ========================================
  1. 启动 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"
  1. 启动 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 你将会看到差不多的效果,这里就不演示了这不是重点 🥸,重点全在下面的几篇文章!但是作为前置知识储备 了解这篇文章依然很重要!

参考

参考1

相关推荐
熊的猫44 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书