gRPC
gRPC est un cadre RPC moderne, open source et haute performance qui peut fonctionner dans n’importe quel environnement. Il peut connecter efficacement des services dans et à travers des centres de données avec un support modulaire pour l’équilibrage de charge, le traçage, la vérification de la santé et l’authentification.
Comme de nombreux systèmes RPC, gRPC est basé sur le concept de définir un service en termes de fonctions (méthodes) qui peuvent être appelées à distance. Pour chaque méthode, vous définissez les paramètres et les types de retour. Les services, les paramètres et les types de retour sont définis dans des fichiers .proto
en utilisant le mécanisme de buffers de protocole de Google.
Avec le transporteur gRPC, Nest utilise les fichiers .proto
pour lier dynamiquement les clients et les serveurs afin de faciliter l’implémentation des appels de procédures distantes, en sérialisant automatiquement les données structurées et en les désérialisant.
Installation
Pour commencer à construire des microservices basés sur gRPC, installez d’abord les paquets requis :
$ npm i --save @grpc/grpc-js @grpc/proto-loader
Overview
Comme d’autres implémentations de couche de transport de microservices Nest, vous sélectionnez le mécanisme du transporteur gRPC en utilisant la propriété transport
de l’objet d’options passé à la méthode createMicroservice()
. Dans l’exemple suivant, nous allons configurer un service de héros. La propriété options
fournit des métadonnées sur ce service ; ses propriétés sont décrites ci-dessous.
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, { transport: Transport.GRPC, options: { package: 'hero', protoPath: join(__dirname, 'hero/hero.proto'), },});
Dans le fichier nest-cli.json
, nous ajoutons la propriété assets
qui nous permet de distribuer des fichiers non-Typescript, et watchAssets
- pour activer le suivi de tous les actifs non-Typescript. Dans notre cas, nous voulons que les fichiers .proto
soient copiés automatiquement dans le dossier dist
.
{ "compilerOptions": { "assets": ["**/*.proto"], "watchAssets": true }}
Options
L’objet d’options du transporteur gRPC expose les propriétés décrites ci-dessous.
Propriété | Description |
---|---|
package | Nom du package Protobuf (correspond au paramètre package du fichier .proto ). Requis |
protoPath | Chemin absolu (ou relatif au répertoire racine) vers le fichier .proto . Requis |
url | URL de connexion. Chaîne au format ip address/dns name:port
(par exemple, ‘0.0.0.0:50051’ pour un serveur Docker) définissant l’adresse/port sur lequel le transporteur établit une connexion. Facultatif. Par défaut ‘localhost:5000’ . |
protoLoader | Nom du package NPM pour l’utilitaire de chargement des fichiers .proto . Facultatif. Par défaut ‘@grpc/proto-loader’ . |
loader | Options pour @grpc/proto-loader . Celles-ci fournissent un contrôle détaillé sur le comportement des fichiers .proto . Facultatif. Voir ici pour plus de détails. |
credentials | Informations d’identification du serveur. Facultatif. Lisez-en plus ici. |
Exemple de service gRPC
Définissons notre exemple de service gRPC appelé HeroesService
. Dans l’objet options
ci-dessus, la propriété protoPath
définit un chemin vers le fichier de définitions .proto
hero.proto
. Le fichier hero.proto
est structuré en utilisant protocol buffers. Voici à quoi il ressemble :
// hero/hero.protosyntax = "proto3";
package hero;
service HeroesService { rpc FindOne(HeroById) returns (Hero) {}}
message HeroById { int32 id = 1;}
message Hero { int32 id = 1; string name = 2;}
Notre HeroesService
expose une méthode FindOne()
. Cette méthode attend un argument d’entrée de type HeroById
et retourne un message Hero
(les buffers de protocole utilisent des éléments message
pour définir à la fois les types de paramètres et les types de retour).
Ensuite, nous devons implémenter le service. Pour définir un gestionnaire qui respecte cette définition, nous utilisons le décorateur @GrpcMethod()
dans un contrôleur, comme indiqué ci-dessous. Ce décorateur fournit les métadonnées nécessaires pour déclarer une méthode en tant que méthode de service gRPC.
@Controller()export class HeroesController { @GrpcMethod('HeroesService', 'FindOne') findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, any>): Hero { const items = [ { id: 1, name: 'John' }, { id: 2, name: 'Doe' }, ]; return items.find(({ id }) => id === data.id); }}
Le décorateur ci-dessus prend deux arguments. Le premier est le nom du service (par exemple, ‘HeroesService’
), correspondant à la définition du service HeroesService
dans le fichier hero.proto
. Le second (la chaîne ‘FindOne’
) correspond à la méthode rpc FindOne()
définie dans HeroesService
dans le fichier hero.proto
.
La méthode de gestion findOne()
prend trois arguments, les data
passées par l’appelant, metadata
qui stocke les métadonnées de la requête gRPC et call
pour obtenir les propriétés de l’objet GrpcCall
telles que sendMetadata
pour envoyer des métadonnées au client.
Il est également possible d’omettre le premier argument @GrpcMethod()
. Dans ce cas, Nest associe automatiquement le gestionnaire à la définition du service à partir du fichier de définitions proto en fonction du nom de la classe où le gestionnaire est défini.
Suivez l’exemple de la méthode lotsOfGreetings
pour découvrir comment implémenter des méthodes de streaming gRPC.
@GrpcStreamMethod() bidiHello(requestStream: any) { requestStream.on('data', (message) => { console.log(message); requestStream.write({ reply: 'Hello, world!' }); }); }
Veuillez noter qu’il y a une petite différence par rapport à la technique utilisée dans d’autres méthodes de transport de microservices. Au lieu de la classe ClientProxy
, nous utilisons la classe ClientGrpc
, qui fournit la méthode getService()
. La méthode générique getService()
prend comme argument un nom de service et renvoie son instance (si disponible).
Alternativement, vous pouvez utiliser le décorateur @Client()
pour instancier un objet ClientGrpc
, comme suit :
@Injectable()export class AppService implements OnModuleInit { private heroesService: HeroesService;
constructor(@Inject('HERO_PACKAGE') private client: ClientGrpc) {}
onModuleInit() { this.heroesService = this.client.getService<HeroesService>('HeroesService'); }
getHero(): Observable<string> { return this.heroesService.findOne({ id: 1 }); }}
Enfin, pour des scénarios plus complexes, nous pouvons injecter un client configuré de manière dynamique en utilisant la classe ClientProxyFactory
comme décrit ici.
Dans tous les cas, nous nous retrouvons avec une référence à notre objet proxy HeroesService
, qui expose le même ensemble de méthodes que celles définies dans le fichier .proto
. Désormais, lorsque nous accédons à cet objet proxy (c’est-à-dire heroesService
), le système gRPC sérialise automatiquement les demandes, les transmet au système distant, récupère une réponse et désérialise la réponse. Parce que gRPC nous protège de ces détails de communication réseau, heroesService
ressemble et agit comme un fournisseur local.
Notez que toutes les méthodes de service sont en camel case (pour respecter la convention naturelle de la langue). Donc, par exemple, bien que notre fichier .proto
contienne la définition de méthode FindOne()
, l’instance heroesService
fournira la méthode findOne()
.
interface HeroesService { findOne(data: { id: number }): Observable<any>;}
Un gestionnaire de message peut également renvoyer un Observable
, dans quel cas les valeurs de résultat seront émises jusqu’à ce que le flux soit complété.
@Get()call() { return this.heroesService.findOne({ id: 1 });}
Pour envoyer des métadonnées gRPC (avec la demande), vous pouvez passer un deuxième argument, comme suit :
call(): Observable<any> { const metadata = new Metadata(); metadata.add('Set-Cookie', 'yummy_cookie=choco');
return this.heroesService.findOne({ id: 1 }, metadata);}
Veuillez noter que cela nécessiterait une mise à jour de l’interface HeroesService
que nous avons définie quelques étapes plus tôt.
Exemple
Un exemple opérationnel est disponible ici.
gRPC Reflection
La spécification de réflexion de serveur gRPC est une norme qui permet aux clients gRPC de demander des détails sur l’API que le serveur expose, semblable à l’exposition d’un document OpenAPI pour une API REST. Cela peut rendre le travail avec des outils de débogage pour développeurs tels que grpc-ui ou postman beaucoup plus facile.
Pour ajouter la prise en charge de la réflexion gRPC à votre serveur, installez d’abord le package d’implémentation requis :
$ npm i --save @grpc/reflection
Ensuite, cela peut être intégré au serveur gRPC à l’aide du hook onLoadPackageDefinition
dans vos options de serveur gRPC, comme suit :
import { ReflectionService } from '@grpc/reflection';
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, { options: { onLoadPackageDefinition: (pkg, server) => { new ReflectionService(pkg).addToServer(server); }, },});
Votre serveur répondra désormais aux messages demandant des détails sur l’API en utilisant la spécification de réflexion.
gRPC Streaming
gRPC prend en charge des connexions actives à long terme, conventionnellement connues sous le nom de streams
. Les flux sont utiles pour des cas comme le chat, les observations ou les transferts de données en morceaux. Trouvez plus de détails dans la documentation officielle ici.
Nest prend en charge les gestionnaires de flux GRPC de deux manières possibles :
- Gestionnaire de sujet RxJS + Observable : peut être utile pour écrire des réponses directement à l’intérieur d’une méthode de contrôleur ou être passé à un consommateur de sujet/Observable.
- Gestionnaire de flux GRPC pur : peut être utile pour être passé à un exécuteur qui gérera le reste de la distribution pour le gestionnaire de flux duplex standard de Node.
Exemple de flux
Définissons un nouveau service gRPC d’exemple appelé HelloService
. Le fichier hello.proto
est structuré en utilisant protocol buffers. Voici à quoi il ressemble :
// hello/hello.protosyntax = "proto3";
package hello;
service HelloService { rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);}
message HelloRequest { string greeting = 1;}
message HelloResponse { string reply = 1;}
En nous basant sur ce fichier .proto
, définissons l’interface HelloService
:
interface HelloService { bidiHello(upstream: Observable<HelloRequest>): Observable<HelloResponse>; lotsOfGreetings(upstream: Observable<HelloRequest>): Observable<HelloResponse>;}
interface HelloRequest { greeting: string;}
interface HelloResponse { reply: string;}
Stratégie Subject
Le décorateur @GrpcStreamMethod()
fournit le paramètre de fonction comme un Observable
RxJS. Ainsi, nous pouvons recevoir et traiter plusieurs messages.
@GrpcStreamMethod()bidiHello(messages: Observable<any>, metadata: Metadata, call: ServerDuplexStream<any, any>): Observable<any> { const subject = new Subject();
const onNext = (message) => { console.log(message); subject.next({ reply: 'Hello, world!' }); };
const onComplete = () => subject.complete();
messages.subscribe({ next: onNext, complete: onComplete, });
return subject.asObservable();}
Pour soutenir l’interaction duplex complète avec le décorateur @GrpcStreamMethod()
, la méthode du contrôleur doit renvoyer un Observable
RxJS.
Selon la définition du service (dans le fichier .proto
), la méthode BidiHello
doit diffuser des demandes vers le service. Pour envoyer plusieurs messages asynchrones au flux à partir d’un client, nous exploitons une classe ReplaySubject
de RxJS.
const helloService = this.client.getService<HelloService>('HelloService');const helloRequest$ = new ReplaySubject<HelloRequest>();
helloRequest$.next({ greeting: 'Hello (1)!' });helloRequest$.next({ greeting: 'Hello (2)!' });helloRequest$.complete();
return helloService.bidiHello(helloRequest$);
Dans l’exemple ci-dessus, nous avons écrit deux messages dans le flux (appels next()
) et notifié le service que nous avons terminé d’envoyer les données (appel complete()
).
Gestionnaire de flux d’appel
Lorsque la valeur de retour de la méthode est définie comme stream
, le décorateur @GrpcStreamCall()
fournit le paramètre de fonction en tant que grpc.ServerDuplexStream
, qui prend en charge des méthodes standard telles que .on(‘data’, callback)
, .write(message)
ou .cancel()
. La documentation complète sur les méthodes disponibles peut être trouvée ici.
Alternativement, lorsque la valeur de retour de la méthode n’est pas un stream
, le décorateur @GrpcStreamCall()
fournit deux paramètres de fonction, respectivement grpc.ServerReadableStream
(lisez-en plus ici) et callback
.
Commençons par implémenter la méthode BidiHello
, qui doit prendre en charge une interaction complète en duplex.
@GrpcStreamCall()bidiHello(requestStream: any) { requestStream.on('data', (message) => { console.log(message); requestStream.write({ reply: 'Hello, world!' }); });}
Dans l’exemple ci-dessus, nous avons utilisé la méthode write()
pour écrire des objets dans le flux de réponse. Le callback passé à la méthode .on()
en tant que second paramètre sera appelé chaque fois que notre service reçoit un nouveau morceau de données.
gRPC Metadata
Les métadonnées sont des informations sur un appel RPC particulier sous la forme d’une liste de paires clé-valeur, où les clés sont des chaînes et les valeurs sont généralement des chaînes mais peuvent être des données binaires. Les métadonnées sont opaques pour gRPC lui-même - elles permettent au client de fournir des informations associées à l’appel au serveur et vice versa. Les métadonnées peuvent inclure des tokens d’authentification, des identifiants de requête et des tags à des fins de surveillance, ainsi que des informations telles que le nombre d’enregistrements dans un ensemble de données.
Pour lire les métadonnées dans un gestionnaire @GrpcMethod()
, utilisez le deuxième argument (métadonnées), qui est de type Metadata
(importé depuis le package grpc
).
Pour renvoyer les métadonnées depuis le gestionnaire, utilisez la méthode ServerUnaryCall#sendMetadata()
(troisième argument du gestionnaire).
@Controller()export class HeroesService { @GrpcMethod() findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, any>): Hero { const serverMetadata = new Metadata(); const items = [ { id: 1, name: 'John' }, { id: 2, name: 'Doe' }, ];
serverMetadata.add('Set-Cookie', 'yummy_cookie=choco'); call.sendMetadata(serverMetadata);
return items.find(({ id }) => id === data.id); }}
Pour lire les métadonnées dans les gestionnaires annotés avec le décorateur @GrpcStreamMethod()
, utilisez le deuxième argument (métadonnées), qui est de type Metadata
(importé depuis le package grpc
).
Pour renvoyer les métadonnées depuis le gestionnaire, utilisez la méthode ServerDuplexStream#sendMetadata()
(troisième argument du gestionnaire).
Pour lire les métadonnées à partir des gestionnaires de flux d’appels (gestionnaires annotés avec le décorateur @GrpcStreamCall()
), écoutez l’événement metadata
sur la référence requestStream
, comme suit :
requestStream.on('metadata', (metadata: Metadata) => { const meta = metadata.get('X-Meta');});