Lead technique et architecte full-stack, freelance depuis 2019. Je conçois, industrialise et exploite des plateformes métier sur Azure, et je publie des moteurs logiciels open source.
Entre deux microservices, échanger du JSON sur HTTP/1.1 coûte plus qu'il n'y paraît : sérialisation verbeuse, aucun contrat que le compilateur puisse vérifier, une connexion rouverte à chaque requête. gRPC vise ce cas précis avec du Protobuf binaire sur HTTP/2, un contrat partagé et du code généré des deux côtés. En .NET, la pile Grpc.AspNetCore en fait une intégration de première classe, sans dépendance tierce à greffer.
Tout part d'un fichier .proto qui décrit les messages et le service indépendamment du langage. C'est le contrat, et ni le client ni le serveur n'en recopient les types à la main. La syntaxe proto3 tient en peu de mots : un service , des rpc , des message dont chaque champ porte un numéro.
TypeScript
1syntax = "proto3";
2
3optioncsharp_namespace = "Catalog.Pricing";
4
5servicePricing {
6// Unary: one request, one reply
7rpcGetQuote (QuoteRequest) returns (QuoteReply);
8
9// Server streaming: one request, a stream of replies
Ces numéros sont l'identité des champs sur le fil : on n'en réutilise jamais un, même après avoir supprimé le champ qui le portait. C'est ce qui rend un ancien binaire lisible par un schéma plus récent, la compatibilité ascendante par construction. Le mot-clé stream , placé sur l'entrée, la sortie ou les deux, décrit les quatre formes d'appel.
Dans le .csproj , une ligne suffit : <Protobuf Include="Protos/pricing.proto" GrpcServices="Both" /> . À la compilation, Grpc.Tools lance protoc et génère les classes de messages, la classe de base serveur Pricing.PricingBase et le client Pricing.PricingClient . Le snake_case du proto devient du PascalCase en C#, unit_price_cents se lit UnitPriceCents . Aucun DTO à écrire, aucun à tenir à jour en double.
Le serveur dérive de la classe générée et redéfinit la méthode. Pas de routing à câbler, pas de désérialisation manuelle : un message fortement typé arrive, un message typé repart.
C#
1// Server: derive from the generated base, override the method
9returnnewQuoteReply { UnitPriceCents = unit * request.Quantity };
10 }
11}
12
13// Client: injected, called like a local method, with a 2s deadline
14varreply = awaitclient.GetQuoteAsync(
15newQuoteRequest { Sku = "A-17", Quantity = 4 },
16deadline: DateTime.UtcNow.AddSeconds(2),
17cancellationToken: ct);
Le câblage serveur tient en deux lignes dans Program.cs : builder.Services.AddGrpc(); puis app.MapGrpcService<PricingService>(); . Côté appelant, Grpc.Net.Client fournit le canal, et l'enregistrement passe par l'injection de dépendances, AddGrpcClient<Pricing.PricingClient>(o => o.Address = new Uri("https://pricing:443")) . Le client injecté s'appelle comme une méthode locale ; en dessous, une seule connexion HTTP/2 multiplexe les appels concurrents plutôt que d'en rouvrir une à chaque requête.
L'appel unaire (une requête, une réponse) couvre l'essentiel du trafic et ressemble de près à un appel REST. Les trois autres formes exploitent la capacité d'HTTP/2 à garder un flux ouvert, ce que REST ne fait pas nativement.
Le server streaming pousse une suite de messages sur une seule requête : le serveur écrit dans un IServerStreamWriter<T> , le client itère avec await foreach .
C#
1// Server pushes ticks until the caller stops listening
Le client streaming inverse le sens : le client envoie un flux, le serveur le lit via un IAsyncStreamReader<T> et renvoie un seul résumé, pratique pour un import en masse. Le bidirectionnel ouvre deux flux indépendants sur le même appel, pour de la télémétrie ou une boucle de négociation, sans polling ni WebSocket à bricoler.
Un appel réseau sans borne de temps est un incident qui attend son heure. gRPC porte la notion de deadline dans le protocole : un instant absolu, pas une durée, transmis avec l'appel. Sur le client, on le passe à l'appel, deadline: DateTime.UtcNow.AddSeconds(2) .
Quand il expire, le context.CancellationToken du serveur se déclenche et le client reçoit un RpcException de statut DeadlineExceeded . Rien à voir avec un timeout purement client, qui abandonne l'attente mais laisse le serveur travailler dans le vide. Passer ce jeton aux appels en aval, base de données ou service suivant, fait remonter l'abandon dans toute la chaîne.
gRPC définit aussi un jeu fermé de codes de statut : NotFound , InvalidArgument , PermissionDenied , Unavailable , Unauthenticated . Le serveur signale un échec métier en levant throw new RpcException(new Status(StatusCode.NotFound, "unknown sku")) , et le client branche dessus avec catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) . Là où un 500 REST est un fourre-tout dont le sens se devine, le statut gRPC est typé et se teste.
Journalisation, métriques, authentification, réessais : ces préoccupations reviennent à chaque appel. Un intercepteur les factorise en enveloppant les handlers, l'équivalent d'un middleware pour le pipeline gRPC. On dérive de Interceptor et on redéfinit le maillon voulu, ici l'appel unaire côté serveur.
15log.LogWarning("{Method} failed with {Status}", context.Method, ex.StatusCode);
16throw;
17 }
18finally
19 {
20log.LogInformation("{Method} took {Elapsed}ms", context.Method, sw.ElapsedMilliseconds);
21 }
22 }
23}
On l'enregistre une fois, AddGrpc(o => o.Interceptors.Add<LoggingInterceptor>()) , et il couvre tous les services. Le même mécanisme existe côté client via .AddInterceptor<T>() , pour attacher un jeton d'authentification ou une politique de réessai sans polluer chaque appel.
gRPC n'est pas universel. Un contrat que le compilateur vérifie et un binaire compact sur HTTP/2 le destinent d'abord au trafic interne , de service à service, là où les deux extrémités du canal sont à vous. Le streaming full-duplex renforce ce positionnement.
Ses limites sont concrètes. Un navigateur ne parle pas gRPC nativement : il n'accède pas aux trailers HTTP/2 dont le protocole dépend, et il faut passer par gRPC-Web avec un proxy. Le binaire ne se lit pas à l'œil dans les logs, et le débogage réclame des outils dédiés.
Pour une API publique exposée à des tiers, REST/JSON reste souvent le meilleur choix : lisible dans un navigateur et cacheable au niveau HTTP. Le guide gRPC pour .NET détaille ce partage des rôles.
Le partage sain : REST en façade publique, gRPC à l'intérieur. Le fichier .proto devient alors la frontière formelle entre vos services, une frontière versionnée et partagée, que le compilateur vérifie aux deux extrémités.