REST est et “rest” era un pilier du Web, cependant des projets comme GraphQL et gRPC tentent de réduire son hégémonie avec des avantages aussi bien en termes de performances qu’en termes de cohérence des messages échangés.

Cet article fera le focus sur le framework gRPC, nous y aborderons les similitudes et divergences par rapport à ses concurrents et les cas d’usages les plus fréquents.

L’aspect technique n’y sera pas négligé avec des exemples d’implémentations client / serveur.

Présentation du Framework

gRPC est un framework RPC (Remote Procedure Call) initialement développé par Google et utilisé en interne pour la majorité de leurs APIs.
Il fut “open-sourcé” en 2015 et se base sur deux technologies sous-jacentes : 

  • Le protocole HTTP/2 qui en plus d’être mieux optimisé que son prédécesseur HTTP 1.1 (de par sa réécriture complète) permet entre autres le multiplexage des requêtes et le server push.
  • Protocol Buffers comme langage de description d’interface (IDL).

Protocol Buffers “protobuf”

Think XML, but smaller, faster, and simpler.

https://developers.google.com/protocol-buffers

Hormis quelques exceptions, on peut affirmer que les messages transmis en REST sont le plus souvent au format JSON, ce n’est pas le cas avec gRPC qui fonctionne via protobuf créé par Google en 2001.
Le fichier .proto est le point de départ de tout développement autour d’une API gRPC, il permet : 

  • De décrire l’API qui sera exposée.
  • D’assurer une rétrocompatibilité avec un système d’index au niveau des champs et des valeurs toujours optionnelles.
  • De palier à l’absence de documentation.
  • D’optimiser les échanges réseaux, grâce à un format binaire (la taille des messages étant réduit par rapport à du JSON ou XML).
  • De générer du code client et serveur dans une pléthore de langages via le compilateur protoc.
  • D’avoir un typage fort : int 16/32, string, booleen, enum, struct, date…

Comparaison avec ses concurrents

SOAP

Bien qu’ancien le protocole SOAP a quelques points communs avec gRPC.

Similitudes

  • Il s’agit d’un protocole RPC.
  • IDL par défaut (WSDL).
  • Génération de code facilitée (très pratique si le tooling le permet).
  • Fortement typé.

Divergences

  • Payload XML très lourd, peu intuitif et “human readable” (rappelez-vous de la taille des “enveloppes” SOAP…).
  • Pas de streaming.

REST

Les différences sont nombreuses vis à vis de gRPC, qu’il devient même difficile d’en trouver des similitudes.

Similitudes

  • IDL optionnel (OpenAPI).
  • Génération de code avec OpenAPI + Third Party.

Divergences

  • Payload assez lourd et “human readable”.
  • Simplicité / Versatilité.
  • Pas de streaming.

GraphQL

Certainement celui qui se rapproche le plus de gRPC dans la démarche.

Similitudes

  • Schema en tant qu’IDL (semble être aussi puissant que protobuf).
  • Streaming avec le principe des subscriptions.
  • Fortement typé.
  • Payload relativement faible.

Divergences

  • Payload “human readable”.
  • Le client choisi lui-même la donnée qu’il souhaite recevoir.
  • Gestion du cache serveur complexifié.
  • Mise en place plus difficile.

Écosystème

De nombreux projets open-source se forment autour du gRPC comme le prouve cette awesome list, comme BloomRPC qui est actuellement l’un des meilleurs “Postman-like” pour tester ses services.
Que ça soit pour la partie serveur ou client, gRPC supporte officiellement de nombreux langages. Le site officiel en fait la liste et y apporte beaucoup de documentation technique.
Le projet est membre de la Cloud Native Computing Foundation et largement soutenu par des géants de la tech’ tel que Google ou encore Microsoft.

Cas d’usage #1 : communication entre service

C’est un fait, la tendance est aux microservices ; qu’ils soient développés dans les règles de l’art ou qu’ils ressemblent plus à des migroservices, ils sont là et emmènent avec eux bon nombre de défis à relever.

Disclaimer
Dans certains cas, une communication synchrone entre services peut s’avérer nécessaire. Bien qu’anodin au premier regard, utilisé à tort et à travers on se retrouve vite en face d’un monolithe distribué. L’idée ici est de vous proposer une solution adéquate à un besoin qui doit rester de l’ordre de l’exceptionnel.

Pour ce premier exemple, nous allons créer un service gRPC qui retourne les X derniers messages d’une conversation.
Première étape : la création du chat.proto qui va nous permettre de générer une grande partie du code.

syntax = "proto3";
import "google/protobuf/timestamp.proto";
 
/* >>
import "google/api/annotations.proto";
<< */
 
package Server;
 
message GetMessagesRequest {
    int32 last_messages = 1;
}
 
message MessageResponse {
    string login = 1;
    string message = 2;
    google.protobuf.Timestamp date_created = 3;
}
 
message GetMessagesResponse{
    repeated MessageResponse messages = 1;
}
 
service Chat {
  rpc GetHistory (GetMessagesRequest) returns (GetMessagesResponse) {}
}

Implémentation de la partie serveur (ASP.NET Core 3.1)

Le framework nécessite quelques dépendances à ajouter, le compilateur “protoc” va se baser sur la configuration du Server.csproj pour générer les classes à partir du fichier chat.proto créé juste au-dessus.

...
<!-- Dépendances nécessaires à la partie serveur -->
<ItemGroup>
  <PackageReference Include="Google.Protobuf" Version="3.12.3" />
  <PackageReference Include="Grpc.AspNetCore.Server" Version="2.29.0" />
  <PackageReference Include="Grpc.Tools" Version="2.29.0" PrivateAssets="All" />
</ItemGroup>
 
<!-- Chemin vers le .proto créé ci-dessus pour générer le code 'server side' -->
<ItemGroup>
  <Protobuf Include="Protos\chat.proto" GrpcServices="Server" />
</ItemGroup>
...

Si tout est ok, il nous est désormais possible d’étendre de la classe abstraite Chat.ChatBase générée via “protoc” au sein de l’un de nos use case, que nous nommerons ChatService.cs.

public class ChatService : Chat.ChatBase
{
    private readonly IChatEntryRepository iChatEntryRepository;
    private readonly IMapper iMapper;
 
    public ChatService(IChatEntryRepository iChatEntryRepository, IMapper iMapper)
    {
        this.iChatEntryRepository = iChatEntryRepository;
        this.iMapper = iMapper;
    }
 
    public override async Task<GetMessagesResponse> GetHistory(GetMessagesRequest request, ServerCallContext context)
    {
        IEnumerable<ChatEntry> entries = await iChatEntryRepository.GetLastEntries(request.LastMessages);
 
        return iMapper.Map<GetMessagesResponse>(entries);
    }
}

Les classes GetMessagesResponse et GetMessagesRequest sont générées également depuis le chat.proto, tout en respectant les règles de nommage du langage (ainsi la propriété date_created sera renommée DateCreated).
Vous pouvez accéder au reste de l’implémentation dans le Github du projet de démonstration.

Implémentation de la partie client (ASP.NET Core 3.1)

Pour se rapprocher d’une condition réelle, j’ajoute une seconde API à mon Docker Compose qui sera consommatrice de la précédente.
La configuration du .csproj est similaire à quelques détails prêts.

...
<!-- Dépendances nécessaires à la partie client -->
<ItemGroup>
	<PackageReference Include="Google.Protobuf" Version="3.12.3" />
	<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.29.0" />
	<PackageReference Include="Grpc.Net.Client" Version="2.29.0" />
	<PackageReference Include="Grpc.Tools" Version="2.29.0" PrivateAssets="All" />
	<PackageReference Include="Google.Protobuf" Version="3.12.3" />
	<PackageReference Include="Grpc.Core" Version="2.29.0" />
</ItemGroup>

<!-- Chemin vers le .proto pour activer la génération de code en mode 'client side' -->
<ItemGroup>
	<Protobuf Include="..\Server\Protos\chat.proto" GrpcServices="Client" />	
</ItemGroup>
...

Nous allons pouvoir configurer l’injection de dépendance du client gRPC auto-généré Server.Chat.ChatClient via le IServiceCollection (Server étant le namespace définie dans le chat.proto).

public void ConfigureServices(IServiceCollection services)
{
	// ...

	// Autorise l'accès à des ressources gRPC en HTTP pour les besoins de la démo
	AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

	#region gRPC
	services.AddGrpcClient<Server.Chat.ChatClient>(client => { client.Address = new Uri("http://server:81"); });
	#endregion
	
	// ...
}

Ensuite, on l’injecte et on l’utilise dans un service de cette façon :

public class ServerApiConsumer : IServerApiConsumer
{
    private readonly IMapper iMapper;
    private readonly Chat.ChatClient chatClient;
 
    public ServerApiConsumer(IMapper iMapper, Chat.ChatClient chatClient)
    {
        this.iMapper = iMapper ?? throw new ArgumentNullException(nameof(iMapper));
        this.chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient));
    }
 
    public async Task<IEnumerable<ChatEntry>> GetHistory(int lastMessages)
    {
        GetMessagesRequest request = iMapper.Map<GetMessagesRequest>(lastMessages);
 
        GetMessagesResponse response = await chatClient.GetHistoryAsync(request);
 
        return iMapper.Map<IEnumerable<ChatEntry>>(response);
    }
}

L’implémentation de la partie client et son utilisation au sein d’une API REST est disponible en intégralité sur le Github d’exemple.

Vous pouvez également voir ce cas d’usage en action dans un contexte microservice, dans le projet de démonstration de Microsoft : eShopOnContainers.

Cas d’usage #2 : streaming

gRPC facilite l’implémentation de use case nécessitant du streaming unidirectionnel (serveur vers client ou client vers serveur) mais également ceux nécessitant du streaming bidirectionnel (un chat par exemple).

L’implémentation d’un cas d’usage de ce genre est assez lourd, nous allons donc “zoomer” sur les points importants non abordés dans le cas d’usage précédent.

Nous rajoutons au chat.proto précédent un nouveau service Participate qui prend en entrée un stream de PostMessageRequest et qui retourne un stream de MessageResponse.

service Chat {
  rpc GetHistory (GetMessagesRequest) returns (GetMessagesResponse) {}
  rpc Participate (stream PostMessageRequest) returns (stream MessageResponse) {} 
}

Implémentation de la partie serveur (ASP.NET Core 3.1)

Le framework permet d’identifier de manière unique une connexion via context.GetHttpContext().Connection.Id, ce qui dans notre cas nous permet d’y associer un participant.
Voici donc l’implémentation de l’entrée / sortie et participation à une “chat room” en mode stream :

public override async Task Participate(IAsyncStreamReader<PostMessageRequest> requestStream, IServerStreamWriter<MessageResponse> responseStream, ServerCallContext context)
{
    if (!await requestStream.MoveNext())
    {
        return;
    }
 
    string connectionId = context.GetHttpContext().Connection.Id;
    Participant participant = iMapper.Map<Participant>((requestStream.Current, connectionId, responseStream));
 
    await Connect(participant);
 
    try
    {
        while (await requestStream.MoveNext())
        {
            ChatEntry chatEntry = iMapper.Map<ChatEntry>(requestStream.Current);
 
            if (!string.IsNullOrWhiteSpace(chatEntry.Message))
            {
                await SendMessage(chatEntry);
            }
        }
    }
    catch
    {
        await Disconnect(participant);
    }
}

Implémentation de la partie client (Android, Java)

L’implémentation du client gRPC est disponible sur le Github, cependant il n’est pas tout à fait conforme à l’état de l’art.
Néanmoins, il permet d’avoir une idée du travail à accomplir pour réaliser ce genre de besoin.

La library gRPC sur Android est tout à fait acceptable, même si elle reste moins accessible que sa jumelle .NET.
Heureusement, la documentation du site officiel est bien fourni.

Conclusion

Il est important de noter que gRPC est un framework “production-ready” par conséquent, si votre projet coïncide en terme d’infrastructure et de besoin, il peut-être judicieux de s’y intéresser.
Récemment chez INEAT nous l’avons utilisé sur une refonte microservice d’un projet qui s’y prête parfaitement ; dans certains cas il s’est avéré nécessaire de créer une communication “synchrone” entre deux services et nous l’avons privilégié à son homologue REST pour des raisons de performance, mais surtout pour une assurance au niveau des échanges que procure Protobuf.

Les gains en performance sont élevés, @JamesNK le créateur du package Nuget le plus téléchargé au monde “Newtonsoft.Json”, qui travaille désormais sur l’implémentation gRPC au sein du framework .NET a réalisé un benchmark comparant les performances REST vs gRPC et gRPC vs WebSocket sous .NET 5 et le constat est sans appel.

  • JSON : 48 000 RPS (Requests per second)
  • gRPC : 59 000 RPS
  • SignalR bidi streaming : 80 000 RPS
  • gRPC bidi streaming : 115 000 RPS

Malheureusement, gRPC amène son lot de complexité notamment au niveau de l’infrastructure choisie, il faut en effet un serveur web capable d’exposer des services gRPC et qu’il soit par conséquent HTTP/2 “compliant” : NGINX est une bonne piste.
Il n’est pas encore possible de contacter directement des services gRPC depuis un browser web, il faut donc passer par des solutions détournées telle que “gRPC-Web”. @JamesNK encore lui, décrit dans cet article son utilisation dans une API ASP.NET Core 3.1.

En résumé

Avantages

  • Performance : HTTP/2 + protobuf.
  • Génération de code.
  • Spécifications stricte.
  • Streaming possible.
  • Adapté aux microservices.
  • Environnement dit « Polyglotte ».
  • Faible bande passante.

Inconvénients

  • Support des navigateurs Web très limité, mais gRPC-Web est un bon début.
  • Payload non human readable, mais protobuf permet la conversation vers et depuis un JSON.
  • Ce n’est pas une API dite « accessible », il faut en effet une bonne période d’adaptation.
  • Il faut lui préférer des frameworks plus haut niveau pour du chat etc. Comme SignalR.

Pour aller plus loin