grpc applicazione reale

in C#, Informatica

gRPC – Un'applicazione reale

Reading Time: 5 minutes

Nel post precedente ho introdotto gRPC, il framework RPC sviluppato da Google e successivamente distribuito con licenza open source. In questo nuovo post svilupperemo una semplice applicazione client/server.

Il framework gRPC è stato sviluppato dal team di Google per la creazione di servizi che necessitano di comunicare tra loro in maniera indifferente dal linguaggio di programmazione utilizzato. A differenza delle API Web, non avviene in forma testuale (json/xml) ma in forma binaria compressa.

Evidentemente, questa caratteristica, rende gRPC molto efficiente e soprattutto il framework ideale per la comunicazione all’interno di microservizi.

Il modello utilizzato per lo scambio dei dati è RPC (Remote Procedure Call), in particolare l’evoluzione del modello originale, sfruttuando per la comunicazione il protocollo HTTP/2.

Tipologie di RPC

  • Comunicazione client/server: il client invia una richiesta al server, che risponde con una risposta. Probabilmente il tipo di comunicazione piu’ semplice.
  • Streaming Server: il client invia una richiesta al server, che risponde con una serie di risposte
  • Streaming Client: il client invia una serie di richieste al server, che risponde con una singola risposta
  • Streaming bidirezionale: client e server si scambiamo messaggi in entrambe le direzioni

Comunicazione tra servizi

La comunicazione avviene tramite la definizione di un contratto di comunicazione (tramite ProtoBuf) che verrà utilizzato da tutti i componenti per generare l’infrastruttura di comunicazione.

Il modello di comunicazione di gRPC utilizza il protocollo HTTP/2 ma non sostituisce la comunicazione tramite API e servizi REST.

Il motivo è da ricercare nella differente modalità di funzionamento: il client GRPC contatta il server ed esegue le procedure direttamente sul server stesso.

La comunicazione in ambiente REST permette al client di contattare il server, che fornisce una rappresentazione dei dati nel formato JSON (o eventualmente XML) e permette di eseguire operazioni CRUD utilizzando gli HTTP Verb.

Inoltre, le WebAPI possono essere utilizzate mediante il browser, mentre gRPC non consente questo tipo di iterazione.

gRPC e DotNet Core 3.0

Il framework dotnet Core 3.0 è di fatto la prima release del framework che consente di implemenatare gRPC, utilizzando la libreria interna grpc-dotnet.

Prima del rilascio della versione 3.0 del framework era possibile utilizzare la libreria esterna GRPC.Core, che però presentava una serie di problemi di funzionamento e soprattutto non disponeva di un template predefinito per la realizzazione di applicazioni client/server.

Realizzazione di una semplice applicazione

Nel seguito di questo post realizzeremo una semplice applicazione client/server che sfrutta gRPC per la comunicazione. In particolare realizzeremo:

  • definizione del servizio lato server
  • contratto per la comunicazione
  • implementazione del servizio
  • implementazione del client

Realizzazione servizio server

Per la creazione del componente utilizzerò il template standard di dotnet core:

 dotnet new grpc -o MyCustomService 

Il prossimo step riguarda la configurazione del contratto utilizzato per la comunicazione. In particolare, la definizione viene effettuata modificato i file .proto (Protobuf) presenti all’interno della cartella Protos:

Utilizzando il template standard, viene creato automaticamente il file di esempio greet.proto. Per il nostro servizio custom, creeremo un nuovo file ProtoBuf chiamato mycustom-service.proto e rimuoveremo greet.proto.

syntax = "proto3";

option csharp_namespace = "MyCustomService";

package CreditRating;

service CreditRatingCheck {
  rpc CheckCreditRequest (CreditRequest) returns (CreditReply);
}

message CreditRequest {
  string customerId = 1;
  int32 credit = 2;
}

message CreditReply {
  bool isAccepted = 1;
}

La definizione del servizio esposto avviene utilizzando la keyword rpc: nel nostro caso sto esponendo CreditRatingCheck: in pratica è un pò se CreditRatingCheck fosse una classe, che contiene al suo interno una serie di metodi, esposti tramite la keyword rpc. Nel caso in cui volessimo esporre più metodi, sarà sufficiente definirli all’interno di CreditRatingCheck.

La parte più interessante del file proto è quella relativa ai messaggi utilizzati per la comunicazione.

Nell’esempio è stato definito CreditRequest per la richiesta ( accetta una stringa ed un intero), e CreditReplay per la risposta (restiutisce un boolean). Come già anticipato, la comunicazione avviene in formato binario e il recupero dei valori avviene in maniera posizionale: ecco spiegato il significato dei numeri a fianco a ciascun elemento.

Ora è necessario far in modo che la nostra applicazione utilizzi il file ProtoBuf appena creato. Per fare questo è necessario modificare il file MyCustomService.csproj aggiungendo il riferimento:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="Protos\mycustom-service.proto" GrpcServices="Server" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.23.1" />
  </ItemGroup>

</Project>

L’aggiunta del file .proto nel file di configurazione consente di generare l’infrastruttura base in C#, lato server.

Implementazione del servizio

Una volta definito il contratto, e dopo averlo incluso nel file di configurazione passiamo all’implementazione del servizio.

Un servizio non è altro che una classe con i relativi metodi, posizionati all’interno della cartella Services.

Nel nostro caso, il nuovo metodo sarà:

namespace MyCustomService
{
    public class CreditRatingCheckService: CreditRatingCheck.CreditRatingCheckBase
    {
        private readonly ILogger<CreditRatingCheckService> _logger;
        private static readonly Dictionary<string, Int32> customerTrustedCredit = new Dictionary<string, Int32>() 
        {
            {"id0201", 10000},
            {"id0417", 5000},
            {"id0306", 15000}
        };
        public CreditRatingCheckService(ILogger<CreditRatingCheckService> logger)
        {
            _logger = logger;
        }

        public override Task<CreditReply> CheckCreditRequest(CreditRequest request, ServerCallContext context)
        {
            return Task.FromResult(new CreditReply
            {
                IsAccepted = IsEligibleForCredit(request.CustomerId, request.Credit)
            });
        }

        private bool IsEligibleForCredit(string customerId, Int32 credit) {
            bool isEligible = false;

            if (customerTrustedCredit.TryGetValue(customerId, out Int32 maxCredit))
            {
                isEligible = credit <= maxCredit;
            }

            return isEligible;
        }
    }
}

Il service è piuttosto semplice, da notare l’utlizzo di CreditReply e CreditRequest, definite in precedenza all’interno del file .proto, per lo scambio dei dati.

Inoltre, è necessario modificare il file Startup.cs per poter utilizzare il nuovo service:

 app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<CreditRatingCheckService>();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
                });
            });

A questo punto l’implementazione lato server è terminata. Per eseguire l’applicazione è sufficente eseguire:

dotnet run

Se tutto è andato a buon fine, il server si metterà in ascolto sulla porta 5001:

Collegandoci con il browser, dopo aver switchato da https a http, riceveremo il messaggio:

“Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909”

come definito all’interno dell’endpoint, configurato nel file Startup.cs. Il codice sorgente della componente server è scaricabile a questo link.

Realizzazione applicazione client

La seconda parte dello sviluppo è il client. Nel nostro caso realizzeremo una console application:

dotnet new console -o MyCustomClient

Avendo creato una semplice console application è necessario aggiungere i riferimenti per poter utilizzare gRPC:

dotnet add CreditRatingClient.csproj package Grpc.Net.Client
dotnet add CreditRatingClient.csproj package Google.Protobuf
dotnet add CreditRatingClient.csproj package Grpc.Tools

A questo è necessario configurare il client per comunicare con il servizio server realizzato in precedenza. Il client utilizzerà il file .proto definito all’interno del servizio server. Creiamo il file Protos all’interno della nostra Console Application:

All’interno della cartella Protos, copieremo il file mycustom-service.proto creato nell’applicazione server. A questo punto modifichiamo il file MyCustomClient.csproj includendo il riferimento al file proto.

A questo, siamo pronti per generare il codice della nostra applicazione client, con il comando:

dotnet build

Se tutto è andato a buon fine, risultato della compilazione sarà presente all’interno della cartella bin/Debug/netcoreapp3.0 .

A questo punto, siamo pronti per l’ultimo passo: la creazione del codice del client, all’interno del file Program.cs.

Il primo step è la riscrittura del metodo main in modalità asincrona, in modo da poter gestire chiamate con async e await:

class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client =  new CreditRatingCheck.CreditRatingCheckClient(channel);
            var creditRequest = new CreditRequest { CustomerId = "id0201", Credit = 7000};
            var reply = await client.CheckCreditRequestAsync(creditRequest);

            Console.WriteLine($"Credit for customer {creditRequest.CustomerId} {(reply.IsAccepted ? "approved" : "rejected")}!");
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }

Come per le chiamate RPC tradizionali, impostiamo l’indirizzo del server utilizzando la chiamata GrpcChannel.ForAddress(…) e subito dopo creiamo un oggetto Client. Non rimane che creare un oggetto CreditRequest, inviarlo al server ed attendere la risposta dalla chiamata asincrona.

Il codice dell’applicazione client è scaricabile al seguente link.

Ovviamente questa è un semplice esempio di applicazione che utilizza gRPC per il flusso di comunicazione. Uno scenario in produzione dovrebbe richiedere un processo di autenticazione prima di effettuare lo scambio dati, implementando qualche meccanismo di autenticazione come oauth2.