Пошук уроків, статей та іншого контенту
Налаштуєте TCP-транспорт для обміну повідомленнями між NestJS-сервісами.
NestJS підтримує мікросервісну архітектуру. Окремі NestJS-застосунки можуть обмінюватися повідомленнями через різні транспорти, зокрема через TCP.
TCP-транспорт підходить для обміну повідомленнями між сервісами, які працюють:
на одному комп’ютері;
у різних контейнерах;
на різних серверах у локальній мережі або хмарній інфраструктурі.
На відміну від HTTP, TCP-транспорт у NestJS використовує шаблони повідомлень:
клієнт надсилає повідомлення з певним шаблоном;
сервер знаходить обробник із таким самим шаблоном;
сервер повертає результат клієнту.
Для роботи з мікросервісами встановіть пакет @nestjs/microservices у кожному застосунку, який бере участь в обміні повідомленнями:
npm install @nestjs/microservicesУ прикладі будуть два окремі NestJS-застосунки:
tcp-server — приймає TCP-повідомлення;
tcp-client — надсилає повідомлення серверу через HTTP-маршрут.
Створити їх можна за допомогою Nest CLI:
nest new tcp-server
nest new tcp-clientTCP-сервер створюється за допомогою методу NestFactory.createMicroservice().
Файл tcp-server/src/main.ts:
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: '127.0.0.1',
port: 8877,
},
},
);
await app.listen();
}
bootstrap();Тут:
Transport.TCP вказує, що застосунок використовує TCP-транспорт;
host визначає адресу, на якій сервер прийматиме з’єднання;
port визначає TCP-порт сервера.
Для локального прикладу використовується адреса 127.0.0.1. Вона означає, що підключатися до сервера можна лише з цього самого комп’ютера.
Щоб реагувати на повідомлення, потрібно створити метод із декоратором @MessagePattern().
Файл tcp-server/src/app.controller.ts:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'sum' })
calculateSum(numbers: number[]): number {
return numbers.reduce((total, number) => total + number, 0);
}
}Метод calculateSum() обробляє повідомлення з таким шаблоном:
{ cmd: 'sum' }Дані повідомлення передаються другим аргументом клієнтського методу send(). У цьому прикладі очікується масив чисел.
Файл tcp-server/src/app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}Тепер сервер очікує повідомлення з шаблоном { cmd: 'sum' } на порту 8877.
Клієнт використовує ClientProxy для надсилання повідомлень мікросервісу.
Спочатку зареєструємо TCP-клієнт у модулі.
Файл tcp-client/src/app.module.ts:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'CALCULATION_SERVICE',
transport: Transport.TCP,
options: {
host: '127.0.0.1',
port: 8877,
},
},
]),
],
controllers: [AppController],
})
export class AppModule {}Властивість name — це внутрішнє ім’я клієнта. За ним клієнт буде отриманий через dependency injection.
Значення host і port мають збігатися з налаштуваннями TCP-сервера.
Файл tcp-client/src/app.controller.ts:
import { Controller, Get } from '@nestjs/common';
import { ClientProxy, Inject } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
@Controller()
export class AppController {
constructor(
@Inject('CALCULATION_SERVICE')
private readonly calculationClient: ClientProxy,
) {}
@Get('sum')
async getSum(): Promise<number> {
const result = await firstValueFrom(
this.calculationClient.send<number>(
{ cmd: 'sum' },
[10, 20, 30],
),
);
return result;
}
}Метод send() приймає два аргументи:
шаблон повідомлення;
дані, які потрібно передати серверу.
У прикладі клієнт надсилає:
{ cmd: 'sum' }і масив:
[10, 20, 30]Метод send() повертає Observable. Функція firstValueFrom() перетворює його на Promise, який можна використати з await.
HTTP-маршрут GET /sum потрібен лише для того, щоб було зручно перевірити роботу TCP-клієнта. Сам обмін між tcp-client і tcp-server відбувається через TCP.
Файл tcp-client/src/main.ts залишаємо стандартним:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();У першому терміналі запустіть TCP-сервер:
cd tcp-server
npm run start:devУ другому терміналі запустіть HTTP-клієнт:
cd tcp-client
npm run start:devПісля цього виконайте запит до клієнта:
curl http://localhost:3000/sumКлієнт передасть TCP-серверу масив [10, 20, 30]. Сервер обчислить суму та поверне результат:
60Повний ланцюжок роботи виглядає так:
HTTP-запит надходить до tcp-client.
Метод getSum() викликає ClientProxy.send().
TCP-клієнт підключається до 127.0.0.1:8877.
TCP-сервер знаходить обробник із шаблоном { cmd: 'sum' }.
Сервер обчислює суму.
Результат повертається до tcp-client.
tcp-client повертає результат HTTP-клієнту.
send() та emit()Для обміну повідомленнями NestJS має два основні методи:
send() — запит із відповіддю;
emit() — повідомлення без очікування відповіді.
У цьому уроці використовується send(), оскільки клієнту потрібен результат обчислення.
send() застосовують, коли:
сервер має повернути результат;
клієнт повинен дочекатися відповіді;
операція схожа на виклик методу.
emit() застосовують для подій, коли клієнту не потрібно отримувати результат. Наприклад, сервіс може повідомити інший сервіс про створення замовлення.
Обробник події створюють із декоратором @EventPattern():
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@EventPattern('order.created')
handleOrderCreated(order: { id: number }): void {
console.log(`Створено замовлення ${order.id}`);
}
}Для запиту з відповіддю використовується @MessagePattern(), а для події без очікування відповіді — @EventPattern().
Шаблон у клієнта та сервері має збігатися.
Правильний варіант:
// Клієнт
client.send({ cmd: 'sum' }, [1, 2, 3]);
// Сервер
@MessagePattern({ cmd: 'sum' })
handleSum(numbers: number[]) {
// ...
}Якщо клієнт надішле інший шаблон, наприклад { cmd: 'total' }, сервер не знайде відповідний обробник.
Шаблоном може бути не лише об’єкт, а й рядок:
// Клієнт
client.send('get_status', null);
// Сервер
@MessagePattern('get_status')
getStatus() {
return { status: 'ok' };
}Для команд об’єкт із властивістю cmd часто є зручним і зрозумілим варіантом.
Якщо TCP-сервер недоступний, клієнт не зможе отримати відповідь. Запит до ClientProxy у такому випадку завершиться помилкою.
Помилку можна обробити за допомогою try...catch:
import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
import { ClientProxy, Inject } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
@Controller()
export class AppController {
constructor(
@Inject('CALCULATION_SERVICE')
private readonly calculationClient: ClientProxy,
) {}
@Get('sum')
async getSum(): Promise<number> {
try {
return await firstValueFrom(
this.calculationClient.send<number>(
{ cmd: 'sum' },
[10, 20, 30],
),
);
} catch {
throw new ServiceUnavailableException(
'Сервіс обчислень недоступний',
);
}
}
}Так HTTP-клієнт отримає зрозумілу помилку, якщо TCP-сервіс вимкнений або вказано неправильний порт.
Порт у клієнті має відповідати порту сервера:
// Сервер
port: 8877
// Клієнт
port: 8877Якщо значення відрізняються, клієнт не зможе встановити з’єднання.
Спочатку запустіть tcp-server, а потім tcp-client. HTTP-клієнт може працювати, навіть якщо TCP-сервер недоступний, але запит до GET /sum завершиться помилкою.
Ці шаблони є різними:
{ cmd: 'sum' }
{ cmd: 'add' }Обробник сервера має використовувати точно такий самий шаблон, як і клієнт.
Якщо обидва застосунки запущені локально, використовуйте однакову адресу, наприклад 127.0.0.1.
Якщо застосунки працюють у різних контейнерах або на різних серверах, 127.0.0.1 може бути неправильною адресою. У такому випадку клієнт має підключатися до адреси, доступної з його середовища виконання.
firstValueFrom()send() повертає Observable, а не готове значення:
const result = this.client.send({ cmd: 'sum' }, [1, 2, 3]);Щоб отримати значення через await, використовуйте:
const result = await firstValueFrom(
this.client.send({ cmd: 'sum' }, [1, 2, 3]),
);TCP-транспорт у NestJS призначений для обміну повідомленнями між мікросервісами.
TCP-сервер створюють через NestFactory.createMicroservice().
Для TCP використовується Transport.TCP.
Обробники запитів позначають декоратором @MessagePattern().
TCP-клієнта реєструють через ClientsModule.register().
Клієнт отримують через @Inject() і використовують ClientProxy.
send() надсилає повідомлення та очікує відповідь.
Шаблон повідомлення клієнта має збігатися з шаблоном обробника сервера.
Адреса та порт клієнта мають відповідати налаштуванням TCP-сервера.