Skip to content

仲灏小栈

仲灏约 1 分钟

前言

在上一章节中,我们已经对一个稍具复杂的项目进行了拆分,目前工程已经被拆成功能较为单一的三个独立项目:Core、用户与物料系统。

既然每个项目的功能是单一,但是在之前的需求分析中,它们又是组成网关系统的各个重要部分,那么该如何将各个系统中有关联的服务进行联通呢?

本章将介绍如何借助 NestJS 提供的 RPC 服务来打通各个系统之间的关联。

微服务

维基上对其定义为:一种软件开发技术- 面向服务的体系结构(SOA)架构样式的一种变体,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于 HTTPRESTful API)。每个服务都围绕着具体业务进行构建,并且能够独立地部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建。

微服务的优势

如上所说,微服务其实是将一个庞大的系统切割成多个最小单元,每一个单元都是一个或者一组相同的功能集合。

与传统的服务开发不同的是,当一个项目拆解为微服务的时候,带来的优势有如下几点:

  1. 不再局限于单一技术架构的实现,根据不同模块的特殊性可以有专业的技术解决方案;
  2. 新的业务功能不需要承担旧的技术债,同时可以拆解服务逐步进行技术升级;
  3. 业务功能单一,复杂度下降,开发维护效率提高;
  4. 独立部署,单服务启动速度提高,必要时可以根据实际情况对某一些服务进行服务器升级、扩容

微服务通信方式

  1. 同步方式:RPCHTTPTCP
  2. 异步方式:消息队列,使用中过程中需要考虑消息的可靠传输、高性能等情况,常见的工具有KafkaNotify 等。

HTTPTCP 都是常见的通信方式,那么 RPC 又是啥?

RPC 是一种设计、实现框架,通讯协议只是其中一部分,并不限定某一类的通信协议,大部分的 RPC 协议使用的是 TCP,但也可以使用 HTTP 协议来封装,比如谷歌的 gRPC 使用的就是 HTTP2 协议。

在大概了解了微服务的一些知识之后,接下来继续我们的学习过程。

NestJS 微服务使用

NestJS 作为一款非常成熟的框架,本身就支持微服务架构的设计,同时也内置了很多 RPC 的传输器,所以在 NestJS 中使用微服务是非常方便的。

启动微服务

第一步:安装微服务依赖 @nestjs/microservices

shell
$ yarn add @nestjs/microservices

第二步:修改用户系统中 user-center/src/main.ts ,添加微服务启动配置,并启动用户系统的微服务

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();

重启服务,看到控制台中有如下打印日志即代表微服务启动成功:

image.png|700x373

也可以使用 netstat -ano -p tcp|findstr 4100 检查 TCP 端口是否正常启动:

image.png

默认情况下,使用 NestJS 自带的 RPC 将使用 TCP协议 监听消息。

第三步:在物料系统中添加 RPC 客户端连接:

.dev.yaml 文件新增新的配置项 USER_MICROSERVICES

yml
USER_MICROSERVICES:
  host: "0.0.0.0"
  port: 4100

新建 materials/src/microservices/microservices.module.ts,添加如下代码,并导入 materials.module.ts 后,重启即可:

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 { }

image.png

用户系统打通

第一步:在用户系统的 user/UserController 添加如下代码:

ts
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 模块,只保留以下模块:

image.png

第三步:物料系统中新增 microservices/user.service.ts

diff
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

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

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

image.png

由于是两个项目,启动后是不同的端口,所以在用户系统中登录之后保存的 token 是不会共享 cookie 在物料系统下面,所以为了方便,大家可以在用户系统登录完毕之后,手动将 cookie 存在物料系统下,如下图所示:

image.png

至此已经完成了用户与物料系统的微服务打通,有兴趣的话,可以再将 authmicroservices 模块也一起放在 libs common 模块中,这样网关 Core 系统也能直接使用通用的鉴权工具。

写在最后

本章的示例代码在 feat/microservices,后续会进行持续的迭代,有需要的同学自取。

本章主要介绍了如何在 NestJS 中使用 RPC 来打通各个微服务中的功能,前文的例子非常简单,实际上我们可以做的内容非常多的,比如在 PermissionGuard 中,我们可以通过 RPC 从用户系统中获取该登录用户的权限,然后再根据返回的权限对物料系统的接口做权限限制等。

示例中我们使用的微服务通信方式为同步模式,微服务的通信方式还有异步模式(一般也就是消息队列),但在我们的网关系统中其实是没有使用消息队列的场景,所以在网关系统中就无此实战,但是消息队列在 Devops 与物料系统的打通中会有很大的应用场景,所以各位可以持续关注 https://github.com/boty-design 这个组织,后期会以开源的方式完成整个工程的搭建。

想了解后续的工程进度的同学,记得进学习群,每一次的功能发布,我都会在群里及时通知。

如果你有什么疑问,欢迎在评论区提出或者加群沟通。 👏

上次更新: