多 SSH 客户端注册及配置方法
在实际的服务端开发中,我们经常需要同时连接多台远程服务器执行不同的运维任务。上一节我们实现了单个 SSH 客户端的注册与使用,本节将扩展 forRoot 方法,使其支持通过 name 参数注册多个具有不同连接配置的 SSH 客户端实例,并通过依赖注入的方式在 Controller 中精确获取对应的 Service。
核心设计思路
与 NestJS 连接多个数据库的模式类似,我们可以为每个 SSH 客户端注册时传入一个唯一名称(name),系统将根据该名称生成独立的 Provider Token,从而实现多客户端的区分注入。
架构关系
SshModule.forRoot({ name: 'ubuntu', ...options })
|
v
Provider Token: 'ubuntu:SSH_OPTIONS'
Provider Token: 'ubuntu' (SshService instance)
|
v
Controller: @Inject('ubuntu') private sshService: SshService
text
实现 forRoot 方法的 name 参数扩展
在 ssh.module.ts 中,forRoot 方法需要接收一个可选的 name 参数。当传入 name 时,系统会基于该名称创建专属的 Provider,避免与其他客户端的 Provider 产生冲突。
// ssh.module.ts
import { DynamicModule, Module, Provider } from '@nestjs/common';
import { SshService } from './ssh.service';
interface SshModuleOptions {
host: string;
port: number;
username: string;
password?: string;
privateKey?: string;
}
@Module({})
export class SshModule {
static forRoot(options: SshModuleOptions, name?: string): DynamicModule {
// Generate provider token based on name
const optionsProvider = {
provide: name ? `${name}:SSH_OPTIONS` : 'SSH_OPTIONS',
useValue: options,
};
const sshClientProviders: Provider[] = [];
if (name) {
// Named client: create a dedicated SshService instance
sshClientProviders.push({
provide: name,
useFactory: async () => {
const ssh = new SshService(options);
await ssh.connect();
return ssh;
},
});
}
return {
module: SshModule,
providers: [
optionsProvider,
...sshClientProviders,
// Default SshService (no name)
...(name ? [] : [SshService]),
],
exports: [
optionsProvider,
...sshClientProviders,
...(name ? [] : [SshService]),
],
};
}
}
typescript
关键设计要点:
- Provider Token 命名规则:当传入
name时,options 的 Token 变为${name}:SSH_OPTIONS,Service 的 Token 直接使用name本身。这样做可以有效避免不同模块之间的 Provider 名称冲突。 - 兼容默认模式:如果不传
name,系统沿用原先的SSH_OPTIONSToken 和SshService默认注册,保证向后兼容。 - 使用
useFactory创建命名客户端实例,在工厂函数中完成 SSH 连接初始化并返回已连接的 Service。
在 Controller 中通过名称注入
注册完命名客户端后,在 Controller 中使用 @Inject 装饰器按名称获取对应的 Service 实例:
// app.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';
import { SshModule } from './common/utils/ssh/ssh.module';
import { SshService } from './common/utils/ssh/ssh.service';
@Controller()
export class AppController {
constructor(@Inject('ubuntu') private sshService: SshService) {}
@Get('test')
async test() {
return await this.sshService.exec('docker --version');
}
}
typescript
注册多个 SSH 客户端
在 Module 中可以通过多次调用 forRoot 注册不同的客户端:
// app.module.ts
@Module({
imports: [
SshModule.forRoot(
{ host: '192.168.3.7', port: 22, username: 'root', password: '***' },
'ubuntu',
),
SshModule.forRoot(
{ host: '192.168.3.8', port: 22, username: 'root', password: '***' },
'ubuntu1',
),
],
})
export class AppModule {}
typescript
对应地在 Controller 中,不同的客户端使用不同的注入 Token:
@Controller()
export class AppController {
constructor(
@Inject('ubuntu') private sshService: SshService,
@Inject('ubuntu1') private sshService1: SshService,
) {}
@Get('test')
async test() {
return await this.sshService.exec('docker --version');
}
@Get('test1')
async test1() {
return await this.sshService1.exec('ls -la /');
}
}
typescript
模块导出注意事项
在 ssh.module.ts 中,需要将所有动态创建的 Provider 都添加到 exports 数组中,否则在其他模块中无法注入:
exports: [
optionsProvider,
...sshClientProviders,
...(name ? [] : [SshService]),
],
typescript
如果不导出 sshClientProviders,注入时会报 name is not a provider 的错误。
测试验证
使用 Bruno 或 Postman 向接口发起请求:
GET /test-- 通过ubuntu客户端执行docker --version,返回 Docker 版本信息GET /test1-- 通过ubuntu1客户端执行ls -la /,返回根目录文件列表
两个请求分别命中不同的 SSH 连接,验证了多客户端注册与注入的正确性。
后续优化方向
- 异步注册支持:下一节将实现
forRootAsync方法,支持通过useFactory异步获取配置(例如从.env文件或配置中心动态读取) - 连接池管理:对于频繁使用的连接,可以实现连接复用与自动重连机制
- 配置校验:在注册时对 SSH 连接参数进行类型检查与合法性验证
↑