前言
在上一章节中,我们已经对一个稍具复杂的项目进行了拆分,目前工程已经被拆成功能较为单一的三个独立项目:Core、用户与物料系统。
既然每个项目的功能是单一,但是在之前的需求分析中,它们又是组成网关系统的各个重要部分,那么该如何将各个系统中有关联的服务进行联通呢?
本章将介绍如何借助 NestJS 提供的 RPC 服务来打通各个系统之间的关联。
微服务
维基上对其定义为:一种软件开发技术- 面向服务的体系结构(SOA)架构样式的一种变体,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于
HTTP的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够独立地部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建。
微服务的优势
如上所说,微服务其实是将一个庞大的系统切割成多个最小单元,每一个单元都是一个或者一组相同的功能集合。
与传统的服务开发不同的是,当一个项目拆解为微服务的时候,带来的优势有如下几点:
- 不再局限于单一技术架构的实现,根据不同模块的特殊性可以有专业的技术解决方案;
- 新的业务功能不需要承担旧的技术债,同时可以拆解服务逐步进行技术升级;
- 业务功能单一,复杂度下降,开发维护效率提高;
- 独立部署,单服务启动速度提高,必要时可以根据实际情况对某一些服务进行服务器升级、扩容。
微服务通信方式
- 同步方式:
RPC、HTTP、TCP; - 异步方式:消息队列,使用中过程中需要考虑消息的可靠传输、高性能等情况,常见的工具有
Kafka、Notify等。
HTTP 与 TCP 都是常见的通信方式,那么 RPC 又是啥?
RPC 是一种设计、实现框架,通讯协议只是其中一部分,并不限定某一类的通信协议,大部分的 RPC 协议使用的是 TCP,但也可以使用 HTTP 协议来封装,比如谷歌的 gRPC 使用的就是 HTTP2 协议。
在大概了解了微服务的一些知识之后,接下来继续我们的学习过程。
NestJS 微服务使用
NestJS 作为一款非常成熟的框架,本身就支持微服务架构的设计,同时也内置了很多 RPC 的传输器,所以在 NestJS 中使用微服务是非常方便的。
启动微服务
第一步:安装微服务依赖 @nestjs/microservices
$ yarn add @nestjs/microservices第二步:修改用户系统中 user-center/src/main.ts ,添加微服务启动配置,并启动用户系统的微服务
declare const module: any;
import { NestFactory } from '@nestjs/core';
import { UserCenterModule } from './user-center.module';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import fastify from 'fastify';
import * as cookieParser from 'cookie-parser';
import { generateDocument } from './doc'
import { FastifyLogger, catchError, AllExceptionsFilter, HttpExceptionFilter } from '@app/common';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import fastifyCookie from '@fastify/cookie';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
catchError()
async function bootstrap() {
// 初始化 fastify
const fastifyInstance = fastify({
logger: FastifyLogger,
})
// 创建 NEST 实例
const app = await NestFactory.create<NestFastifyApplication>(
UserCenterModule,
new FastifyAdapter(fastifyInstance)
);
// micro serivce
app.connectMicroservice<MicroserviceOptions>(
{
transport: Transport.TCP,
options: {
port: 4100,
host: '0.0.0.0',
},
},
{
inheritAppConfig: true, // 继承 app 配置
},
);
app.register(fastifyCookie, {
secret: 'my-secret', // for cookies signature
});
// 异常过滤器
app.useGlobalFilters(new AllExceptionsFilter(), new HttpExceptionFilter());
// 设置全局接口前缀
app.setGlobalPrefix('api');
// 格式化 cookie
app.use(cookieParser());
// 接口版本化管理
app.enableVersioning({
type: VersioningType.URI,
});
// 启动全局字段校验,保证请求接口字段校验正确。
app.useGlobalPipes(new ValidationPipe());
// 创建文档
generateDocument(app)
// 启动所有微服务
await app.startAllMicroservices();
// 启动服务
await app.listen(4000);
// 添加热更新
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();重启服务,看到控制台中有如下打印日志即代表微服务启动成功:
也可以使用 netstat -ano -p tcp|findstr 4100 检查 TCP 端口是否正常启动:
默认情况下,使用
NestJS自带的RPC将使用 TCP协议 监听消息。
第三步:在物料系统中添加 RPC 客户端连接:
.dev.yaml 文件新增新的配置项 USER_MICROSERVICES:
USER_MICROSERVICES:
host: "0.0.0.0"
port: 4100新建 materials/src/microservices/microservices.module.ts,添加如下代码,并导入 materials.module.ts 后,重启即可:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { getConfig } from '@app/common';
const { USER_MICROSERVICES } = getConfig()
@Module({
imports: [
ClientsModule.register([
{
name: 'USER-SERVER',
transport: Transport.TCP,
options: USER_MICROSERVICES,
},
]),
],
providers: [],
exports: []
})
export class MicroservicesModule { }用户系统打通
第一步:在用户系统的 user/UserController 添加如下代码:
import { MessagePattern, Payload as MicroPayload } from '@nestjs/microservices';
export class UserController {
constructor(
private readonly userService: UserService,
private readonly userRoleService: UserRoleService
) { }
@MessagePattern('userCenter.user.profile')
@Public()
micro_profile(@MicroPayload() data: Payload) {
return this.profile(data);
}
}第二步:在物料系统中移植之前的 Auth 模块,只保留以下模块:
第三步:物料系统中新增 microservices/user.service.ts:
import { Injectable, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class UserService {
constructor(
@Inject('USER-SERVER') private userServer: ClientProxy
) { }
getUser(user) {
- return this.userServer.send('userCenter.user.profile', user)
+ return firstValueFrom(this.userServer.send('userCenter.user.profile', user))
}
}注意客户端中获取
RPC服务端的接口的方法是ClientProxy中的send(),此方法请求并返回是响应数据流的Observable,这并不是正常的HTTP返回的内容,而是通过TCP协议传输的内容。所以直接获取值是获取不到的,一定要记得使用rxjs中的firstValueFrom包一层才能拿到正常的返回值。
第四步:物料系统中新增 src/auth/permission.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserService } from '../../microservices/user.service';
import { IS_PUBLIC_KEY } from '../constants';
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private reflector: Reflector, private userService: UserService) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
const loginAuth = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (loginAuth) return true;
const request = context.switchToHttp().getRequest();
const user: Payload = request.user;
const codes = await this.userService.getUser(user);
console.log('microservices===>', codes)
return codes;
}
}第五步:将新的网关验证 PermissionGuard 导入 materials.module.ts:
import { CacheModule, Module } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ConfigModule } from '@nestjs/config';
import { TransformInterceptor, getConfig } from '@app/common';
import { GroupModule } from './materials/group/group.module';
import { MaterialModule } from './materials/material/material.module';
import { ProjectModule } from './materials/project/project.module';
import { TaskModule } from './materials/task/task.module';
import * as redisStore from 'cache-manager-redis-store';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { AuthModule } from './auth/auth.module';
import { MicroservicesModule } from './microservices/microservices.module';
import { PermissionGuard } from './auth/guards/permission.guard';
@Module({
imports: [
CacheModule.register({
isGlobal: true,
store: redisStore,
host: getConfig('REDIS_CONFIG').host,
port: getConfig('REDIS_CONFIG').port,
auth_pass: getConfig('REDIS_CONFIG').auth,
db: getConfig('REDIS_CONFIG').db
}),
ConfigModule.forRoot({
ignoreEnvFile: true,
isGlobal: true,
load: [getConfig]
}),
MicroservicesModule,
GroupModule,
TaskModule,
MaterialModule,
ProjectModule,
AuthModule
],
controllers: [],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: PermissionGuard,
},
],
})
export class MaterialsModule { }然后访问物料系统的任意 API 得到如下结果则代表微服务正常启动,下图中使用的接口是http://localhost:3000/doc#/项目/ProjectController_getList
由于是两个项目,启动后是不同的端口,所以在用户系统中登录之后保存的
token是不会共享cookie在物料系统下面,所以为了方便,大家可以在用户系统登录完毕之后,手动将cookie存在物料系统下,如下图所示:
至此已经完成了用户与物料系统的微服务打通,有兴趣的话,可以再将 auth 与 microservices 模块也一起放在 libs common 模块中,这样网关 Core 系统也能直接使用通用的鉴权工具。
写在最后
本章的示例代码在 feat/microservices,后续会进行持续的迭代,有需要的同学自取。
本章主要介绍了如何在 NestJS 中使用 RPC 来打通各个微服务中的功能,前文的例子非常简单,实际上我们可以做的内容非常多的,比如在 PermissionGuard 中,我们可以通过 RPC 从用户系统中获取该登录用户的权限,然后再根据返回的权限对物料系统的接口做权限限制等。
示例中我们使用的微服务通信方式为同步模式,微服务的通信方式还有异步模式(一般也就是消息队列),但在我们的网关系统中其实是没有使用消息队列的场景,所以在网关系统中就无此实战,但是消息队列在 Devops 与物料系统的打通中会有很大的应用场景,所以各位可以持续关注 https://github.com/boty-design 这个组织,后期会以开源的方式完成整个工程的搭建。
想了解后续的工程进度的同学,记得进学习群,每一次的功能发布,我都会在群里及时通知。
如果你有什么疑问,欢迎在评论区提出或者加群沟通。 👏
