介绍
从本章节开始(四期),我们将会陆陆续续的介绍一Nestjs的核心原理和底层实现,请大家动动发财的小手关注和订阅哈🤣。
本文介绍了 有关Nestjs在微服务中常见的一些问题,包括自定义的序列化和自定义的协议;有的时候在我们的工作中必不可免的 会遇到与现有的基础设施做集成的场景,而nest官方提供的集成场景和例子相对来说比较的 标准 对于那些 非标 的场景缺少介绍,所以本文补充了这一方面;
本文主将会深入的构造 自定义 transporter 的 Server端, 注意本文有那么一点点🤏 难度。
仔细规划一下后续的功能需要做
饭🍚要一口口吃,东西要一点一点做,
我们的大概思路是这样的哈:
- 翻一下 @nestjs/micoreserve 包 发现里面允许我们自定义 TR,所以先按照它的要求配置一个
- 基于1 中的要求 我们参考 正常的 nestjs TR (比如MQTT)这种使用方式 实现 ,1中的两端,并且完成基于req/res的模式
- 完成emit 模式的实现,并且🤔️思考一下如何做到RXJS功能的集成
- 思考如何做到多路复用
文件夹和目录结构组织
在下文具体的实现中,我们将会创建三个新的目录
他们依次的作用是:
- 我们此次的CS-TR 将独立的做成一个独立的包来使用,这样的话方便你以后扩展特定条件下的CS-TR
- Http应用程序,它面向业务的开发者
- Nest的微服务部分,这里不再赘述
具体实现
仓库初始化
我们需要先把我们的仓库初始化,并且把相关的包link 上,这样后续的工作才能进行下去
- 一开始这三个工程都是由nest cli 自动初始化的很简单,不赘述
当然了因为我们希望 能够作为一个独立的包存在,所以你可以使用nest cli的 命令创建一个 lib工程,这也是可以的,但我没有这么做。 docs.nestjs.com/cli/librari…
- link操作
我们看看如何把我的独立的包给他link 出去,并且install 到其他的两个nest工程中去
我们来到 nest-faye-transporter 工程中(以下简称 faye-TR) ,
- 配置它的packjson
{
"main": "dist/index.js",
"files": [
"dist/**/*",
"*.md"
],
"scripts": {
"build": "tsc"
}
}
- 进行yarn link 操作
$ yarn link
**yarn link v1.22.22**
# 提升你本地注册包成功
success Registered "nest-faye-transporter".
# 教你如何去别的项目引入我们的包
info You can now run `yarn link "nest-faye-transporter"` in the projects where you want to use this package and it will be used instead.
✨ Done in 0.03s.
cd 到我们的其他工程中去(举例子我就在nest-http-app)(以下简称 HttpApp)
$ yarn link nest-faye-transporter
在代码里直接引入就好了,
注意:
有的时候这个link的依赖不会出现在package中,而在项目中使用却不会异常是因为node_modules的查找逻辑在做怪,本地项目找不到就会去全局找,然后发现我们的全局中有link 它就用了.....,如果你想明确的link ,可以直接在packjson中添加绝对路径就可以,也不需要link,或者你link之后,用 npm root -g 找到 全局node_modules 的目录,把他作为路径也是可以的。(为什么不用yarn gloabl link 呢?因为这个东西要配置🤣麻烦)
"nest-faye-transporter": "link:/Users/joneysli/.nvm/versions/node/v18.19.1/lib/node_modules/nest-faye-transporter"
观察一下Nest官方源码
接下来我们观察一下 一般的MQTT的 TR是如何做的
const app = await NestFactory.createMicroservice(AppModule, {
transport: Transport.MQTT,
options: {
host: 'localhost',
port: 1883,
serializer: new OutboundResponseIdentitySerializer(),
deserializer: new InboundMessageIdentityDeserializer(),
},
});
我们找到 nestjs的源代码进去看看,发现它提供了一个自定义的选项
这个就是我们要用到的,可是如何写呢?
我们可以观察一下MQTT是怎么写的,追一下createMicroservice源代码,发现他是调用 @nestjs/microservices 里的东西在创建
然后看看这个 NestMicroservice 是个啥,发现了其中关键的一些变量比如 this.server
然后我们就能够找到这些 server 各自的实现了,
于是我们就有了🤔️想法,如果我们参考这样的模式 就能够创建一个自定义的TR了,比如下面这样的
const app = await NestFactory.createMicroservice(AppModule, {
strategy: new ServerFaye({
url: 'http://localhost:8000/faye'
})
});
我们先简单的把Req/Res的模式先实现
- 进入 faye-TR 工程
构建我们的 ServerFaye , 参考Nest源码中的MQTT 就好了,基本上它怎么写 我们就怎么写
import { Server, CustomTransportStrategy } from '@nestjs/microservices';
import * as faye from 'faye';
export class ServerFaye extends Server implements CustomTransportStrategy {
// Holds our client interface to the Faye broker.
private fayeClient;
constructor(private readonly options) {
super();
// super class establishes the serializer and deserializer; sets up
// defaults unless overridden via `options`
// 我们也可以去参考 MQTT相关的写法,我这里直接放出来了
// 在另外的文件夹
this.initializeSerializer(options);
this.initializeDeserializer(options);
}
/**
* 实现CustomTransportStrategy 必须要写的方法
* listen() is required by `CustomTransportStrategy` It's called by the
* framework when the transporter is instantiated, and kicks off a lot of
* the machinery.
*/
public listen(callback: () => void) {
this.fayeClient = this.createFayeClient();
this.start(callback);
}
/**
* 初始化
*/
public createFayeClient() {
// pull out url, and strip serializer and deserializer properties
// from options so we conform to the `faye.Client()` interface
const { url, serializer, deserializer, ...options } = this.options;
return new faye.Client(url, options);
}
/**
* kick things off
*/
public start(callback) {
// register faye message handlers
this.bindHandlers();
// call any user-supplied callback from `main.ts` `app.listen()` call
callback();
}
/**
* 重点:这个是在客户端使用 `@MessageHandler()` or `@EventHandler`, 时绑定的东西通过这里来处(也就是处理 channel;)
*/
public bindHandlers() {
/**
* messageHandlers is populated by the Framework (on the `Server` superclass)
*
* It's a map of `pattern` -> `handler` key/value pairs
* `handler` is the handler function in the user's controller class, decorated
* by `@MessageHandler()` or `@EventHandler`, along with an additional boolean
* property indicating its Nest pattern type: event or message (i.e.,
* request/response)
*/
this.messageHandlers.forEach((handler, pattern) => {
// only handling `@MessagePattern()`s for now
if (!handler.isEventHandler) {
this.fayeClient.subscribe(
`${pattern}_ack`,
this.getMessageHandler(pattern, handler),
);
}
});
}
public getMessageHandler(pattern: string, handler: Function): Function {
return async message => {
const inboundPacket = this.deserializer.deserialize(message);
const response = await handler(inboundPacket.data);
const outboundRawPacket = {
err: null,
response,
isDisposed: true,
id: (message as any).id,
};
const outboundPacket = this.serializer.serialize(outboundRawPacket);
this.fayeClient.publish(`${pattern}_res`, outboundPacket);
};
}
/**
* close() is required by `CustomTransportStrategy`
*/
public close() {
this.fayeClient = null;
}
}
这个主要的server类,我们还需要把 序列化放序列化 写上,保证传输过程中的统一,为简单期间 我就洗一个反序列化的,序列化也是一样的写法
import { ConsumerDeserializer, IncomingRequest } from '@nestjs/microservices';
import { Logger } from '@nestjs/common';
export class InboundMessageIdentityDeserializer
implements ConsumerDeserializer
{
private readonly logger = new Logger('InboundMessageIdentityDeserializer');
deserialize(value: any, options?: Record<string, any>): IncomingRequest {
this.logger.verbose(
`<<-- deserializing inbound message:\n${JSON.stringify(
value,
)}\n\twith options: ${JSON.stringify(options)}`,
);
return value;
}
}
为了准确的描述 这个 sever 类我们还需要定一个 interface 把faye支持的option 也一起带过来
import { Serializer, Deserializer } from '@nestjs/microservices';
export interface FayeOptions {
/**
* faye server mount point (e.g., http://localhost:8000/faye)
*/
url?: string;
/**
* time in seconds to wait before assuming server is dead and attempting reconnect
*/
timeout?: number;
/**
* time in seconds before attempting a resend a message when network error detected
*/
retry?: number;
/**
* connections to server routed via proxy
*/
proxy?: string;
/**
* per-transport endpoint objects; e.g., endpoints: { sebsocket: 'http://ws.example.com./'}
*/
endpoints?: any;
/**
* backoff scheduler: see https://faye.jcoglan.com/browser/dispatch.html
*/
// tslint:disable-next-line: ban-types
// eslint-disable-next-line @typescript-eslint/ban-types
scheduler?: Function;
/**
* instance of a class implementing the serialize method
*/
serializer?: Serializer;
/**
* instance of a class implementing the deserialize method
*/
deserializer?: Deserializer;
}
最后的文件夹是这样的
这是Serve 哈,别忘记了我们还需要搞定 client ,一般的参考MQTT的做法,我们需要 使用prodiver方式注入这个依赖,但本次的重点是 server 所以 clinet 我们直接new 就好了 ,不注入。
接下来我们试一下看看行不行哈! (注意client 暂时不需要处理 )
- 进入 nest-microservice 工程
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
import {
ServerFaye,
OutboundResponseIdentitySerializer,
InboundMessageIdentityDeserializer,
} from 'nest-faye-transporter';
async function bootstrap() {
const logger = new Logger('Main:bootstrap');
const app = await NestFactory.createMicroservice(AppModule, {
strategy: new ServerFaye({
url: 'http://localhost:8000/faye',
serializer: new OutboundResponseIdentitySerializer(),
deserializer: new InboundMessageIdentityDeserializer(),
}),
});
app.listen();
logger.verbose('Microservice is listening...');
}
bootstrap();
// controller
import { Controller, Logger } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
interface Customer {
id: number;
name: string;
}
const customerList: Customer[] = [{ id: 1, name: 'nestjs.com' }];
@Controller()
export class AppController {
logger = new Logger('AppController');
/**
* Register a message handler for 'get-customers' requests
*/
@MessagePattern('/get-customers')
async getCustomers(data: any): Promise<any> {
const customers =
data && data.customerId
? customerList.filter(
(cust) => cust.id === parseInt(data.customerId, 10),
)
: customerList;
return { customers };
}
}
- run 看看效果
- 我们依然需要把 faye-server 启动
- 当nest-micoreserver 连接之后(subscription就生效了)
^^ client connect (# 0w5s )
++ New subscription from 0w5s on "/get-customers_ack"
- 启动 customerApp 发起一个get请求
目前来看一切正常,但是它还是缺少对 rxjs 流支持和 多路复用支持,接下来我们继续完善它. 下图表示在目前的架构下我们的req/res 流转
处理rxjs流和event
- 聊聊潜在的问题
- @EventPattern(...) 好像还没处理
- rxjs 流还不支持
- 看看如何做
- 处理流
// 既然要处理流信息,那么我们需要从消息入手,原来的处理很简单
public getMessageHandler(pattern: string, handler: Function): Function {
return async (message) => {
const inboundPacket = this.deserializer.deserialize(message);
// @ts-ignore
const response = await handler(inboundPacket.data);
const outboundRawPacket = {
err: null,
response,
isDisposed: true,
id: (message as any).id,
};
const outboundPacket = this.serializer.serialize(outboundRawPacket);
this.fayeClient.publish(`${pattern}_res`, outboundPacket);
};
}
// 现在我们要玩花活儿 ->
public getMessageHandler(pattern: string, handler: Function): Function {
return async (message: ReadPacket) => {
const inboundPacket = this.deserializer.deserialize(message, {
channel: pattern,
});
// 上下午参数主机,先不用管,要管也有它和 nest源码里的做法是一样的。
const fayeCtx = new FayeContext([pattern]);
// 直接转换成 流(Observable),这个方法是 父类 Server 自己本身就有的
const response$ = this.transformToObservable(
// @ts-ignore
await handler(inboundPacket.data, fayeCtx),
) as Observable<any>;
const publish = (response: any) => {
Object.assign(response, { id: (message as any).id });
const outgoingResponse = this.serializer.serialize(response);
return this.fayeClient.publish(`${pattern}_res`, outgoingResponse);
};
// 框架会自动处理 这个流
response$ && this.send(response$, publish);
};
}
我们解读一下源码是如何处理这个流的
// from https://github.com/nestjs/nest/blob/master/packages/microservices/server/server.ts
public send(
stream$: Observable<any>,
respond: (data: WritePacket) => void,
): Subscription {
let dataBuffer: WritePacket[] = null;
// 我们正在订阅从用户域处理程序返回的响应。如果订阅产生多个值(一个流),我们缓冲它们(`dataBuffer`)
const scheduleOnNextTick = (data: WritePacket) => {
if (!dataBuffer) {
dataBuffer = [data];
process.nextTick(() => {
// 然后使用`publish()`我们刚才构建的函数发布每个值
// (在该方法中,`publish()`函数作为参数访问`respond`)。
// 当流完成时,我们通过`isDisposed`在最终发出的响应上设置为 true 来表示这一点
dataBuffer.forEach(buffer => respond(buffer));
dataBuffer = null;
});
} else if (!data.isDisposed) {
dataBuffer = dataBuffer.concat(data);
} else {
dataBuffer[dataBuffer.length - 1].isDisposed = data.isDisposed;
}
};
return stream$
.pipe(
catchError((err: any) => {
scheduleOnNextTick({ err, response: null });
return empty;
}),
finalize(() => scheduleOnNextTick({ isDisposed: true })),
)
.subscribe((response: any) =>
scheduleOnNextTick({ err: null, response }),
);
}
这样流的部分我们就处理完了
- 关于上下文
当我们通过Proxy API 订阅主题时会发生什么? 对于 Faye,一切都很简单。我们在某个主题上注册一个Faye 订阅处理程序,当与该主题匹配的消息到达时,我们的处理程序将被调用,消息负载仅包含发布者发送的内容。
对于某些Proxy,订阅处理程序可以接收有关消息的其他上下文 - 有时在回调参数中,有时在消息有效负载中。例如,使用 NATS,消息有效负载将发布者发送的实际内容打包在顶级data
属性中,并添加两个带有上下文元数据的附加顶级属性:一个描述调用者订阅的主题,另一个包含可选reply
主题。我们将此元数据称为Context
,Nest 允许您将此信息传递给用户态处理程序(由@MessagePattern()
或修饰的方法@EventPattern()
)。
由于 Faye 根本不提供任何此类元数据,因此我们将通过在此上下文对象中传递通道来演示此行为。在这种情况下,这并不是特别有用,因为我们实际上并没有提供任何新信息,但对于 Faye,这是我们能做的最好的事情来演示对象的概念Context
。
- 处理event
接下来我们来处理 event ,主要做法还是一样的 按照 MQTT 类似的做法就好
public bindHandlers() {
/**
* messageHandlers is populated by the Framework (on the `Server` superclass)
*
* It's a map of `pattern` -> `handler` key/value pairs
* `handler` is the handler function in the user's controller class, decorated
* by `@MessageHandler()` or `@EventHandler`, along with an additional boolean
* property indicating its Nest pattern type: event or message (i.e.,
* request/response)
*/
this.messageHandlers.forEach((handler, pattern) => {
// In this version (`part3`) we add the handler for events
if (handler.isEventHandler) {
// The only thing we need to do in the Faye subscription callback for
// an event, since it doesn't return any data to the caller, is read
// and decode the request, pass the inbound payload to the user-land
// handler, and await its completion. There's no response handling,
// hence we don't need all the complexity of `getMessageHandler()`
this.fayeClient.subscribe(pattern, async (rawPacket: ReadPacket) => {
const fayeCtx = new FayeContext([pattern]); // 注意传递 上下文
const packet = this.parsePacket(rawPacket);
const message = this.deserializer.deserialize(packet, {
channel: pattern,
});
await handler(message.data, fayeCtx);
});
} else {
this.fayeClient.subscribe(
`${pattern}_ack`,
this.getMessageHandler(pattern, handler),
);
}
});
}