[四期 - 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

相关推荐
zqx_738 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
攸攸太上1 小时前
JMeter学习
java·后端·学习·jmeter·微服务
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
妍妍的宝贝2 小时前
k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载
nginx·微服务·kubernetes
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H3 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈