gRPC bedeutet Google Remote Procedure Call, wurde entwickelt durch Google in 2015 und freigegeben in August 2016. gRPC läuft ausschliesslich HTTP/2 und verwendet Protocol Buffers als Schnittstellebeschreibungssprache.
Quelle: Pluragsight ‹Using gRPC in ASP.NET Core› von Shawn Wildermuth.
gRPC ist kein Ersatz für REST weil gRPC nicht geeignet ist für Webseiten:
REST ist optimal für CRUD Operationen und pure WEB Apps.
SignalR is gut für Multicasting und ‘Soft’ Echtzeit-Kommunikation.
GraphQL ist optimal für das offene Durchsuchen von grossen Datenmengen.
gRPC ist ideal zur Kommunikation zwischen Services auf den Servern.
gRPC:
Es ist sehr aufwändig gRPC in Blazor-WebAssembly Anwendungen zu nutzen (mit Blazor-Serverside geht das ohne Probleme). Browser geben im Moment keinen Zugang auf HTTP/2 Framing oder http Response Headers.
Parameter | gRPC | REST |
Serialisierung | protobuf | JSON/XML |
Protocol | HTTP/2.0 | HTTP/1.1 |
Browser Support | No | Yes |
Data Exchange | Messages | Resources and Verbs |
Request-Response Model | Supports all Types of Streaming as based on Http/2 | Only supports Request Response as based on Http/1.1 |
Payload Exchange Format | Strong Typing | Serialization JSON/XML |
Quelle: https://www.c-sharpcorner.com/article/grpc-using-c-sharp-and-net-core-day-one/
gRPC | WCF |
Service in ProtoFile | ServiceContract |
RPC Method in ProtoFile | OperationContract |
Message in ProtoFile | DataContract |
Richer error Model | FaultContract |
Unary Streaming | Request-Reply |
Bidirectional Streaming | Duplex |
Protobuf IDL | WSDL |
Quelle: https://www.c-sharpcorner.com/article/grpc-using-c-sharp-and-net-core-day-one/
Die Lösung für gRPC und Web scheint gRPC-Web zu sein welches in 2018 zum ersten Mal erschien (vom gRPC-Team). gRPC-Web besteht aus zwei Teilen: einem JavaScript-Client, der alle modernen Browser unterstützt und einem gRPC-Web-Proxy auf dem Server. Der gRPC-Web-Client ruft den Proxy auf und der Proxy leitet die gRPC-Anforderungen an den gRPC-Server weiter.
Nicht alle Funktionen von gRPC werden von gRPC-Web unterstützt. Client- und bidirektionales Streaming werden nicht und das Server-Streaming nur eingeschränkt unterstützt.
Die Verträge werden mit Protocol Buffers definiert. Eine Methode hat keinen Parameter (google.protobuf.Empty) oder einen Parameter vom Typ ‘message’ und keinen oder einen Rückgabewert (ebenfalls Typ ‘message’). Die Definition vom Service ‘AddressBookService’ in einer proto-Datei könnte so aussehen:
syntax = "proto3"; import "enums.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; option csharp_namespace = "Addressbook.Services"; service AddressBookService { rpc AddPersons (AddressBookMessage) returns (TransferStatusMessage); rpc GetAddressBook (google.protobuf.Empty) returns (AddressBookMessage); rpc UploadPersonImage (stream PersonImageMessage) returns (TransferStatusMessage); rpc DownloadPersonImage (PersonMessage) returns (stream PersonImageMessage); } message TransferStatusMessage { string message = 1; TransferStatus status = 2; } …
Es gibt vier Typen von Serveraufrufen in RPC:
Einfache Serveraufrufe mit keinem oder einem message-Parameter und keiner oder einer Rückgabe-message.
rpc GetMovie(QueryParams) returns (Movie){};
Der Client macht einen Serveraufruf mit keinem oder einem message-Parameter und der Server gibt einen offenen Stream zurück. Der Stream kann jetzt verwendet werden für:
rpc GetMovie(QueryParams) returns (stream Data){};
Wird verwendet zum Upload grosser Dateien zum Server.
rpc Upload(Stream Data) returns (Status){};
Der Client sendet einen Stream zum Server und wartet, bis der Server antwortet.
Streaming bedeutet nicht immer grosse Dateien. Das bidirektionale Streaming kann z.B. verwendet werden, um IsAlive Signale zwischen Client und Server auszutauschen, um zu kontrollieren, ob beide noch ‘gesund’ sind.
rpc CheckConnection(Stream Ping) returns (Pong){};
gRPC ist Teil vom ASP.NET Core 3.1 Framework. Im folgenden wird eine Beispielanwendung erstellt. Den Quellcode kann man hier herunterladen.
Es braucht Visual Studio 2019. Visual Studio Community, Professional und Enterprise funktionieren alle. Einfach .NET Core 3.1 herunterladen (nicht .NET Framework 4.8).
Beispiel von enums.proto
syntax = "proto3"; option csharp_namespace = "Addressbook.Services"; enum PhoneType { PHONETYPE_UNSPECIFIED = 0; PHONETYPE_MOBILE = 1; PHONETYPE_HOME = 2; PHONETYPE_WORK = 3; } enum Gender { GENDER_UNSPECIFIED = 0; GENDER_MALE = 1; GENDER_FEMALE = 2; } enum ReadingStatus{ READINGSTATUS_UNSPECIFIED = 0; READINGSTATUS_SUCCESS = 1; READINGSTATUS_FAILURE = 2; READINGSTATUS_INVALID = 3; }
Beispiel von addressbook.proto
syntax = "proto3"; import "Protos/enums.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; option csharp_namespace = "Addressbook.Services"; service AddressBookService { rpc AddPersons (AddressBookMessage) returns (StatusMessage); rpc GetAddressBook (google.protobuf.Empty) returns (AddressBookMessage); } message StatusMessage { string message = 1; ReadingStatus status = 2; } message Person { string name = 1; int32 age = 2; string email = 3; Gender gender = 4; message PhoneNumber { string number = 1; PhoneType phoneType = 2; } repeated PhoneNumber phone_numbers = 5; google.protobuf.Timestamp last_updated = 6; } message AddressBook { repeated Person people = 1; } message AddressBookMessage { AddressBook addressBook = 1; StatusMessage statusMessage = 2; }
Wenn man jetzt kompiliert werden zwei *.cs-Dateien generiert, die sich in ‘obj\Debug\netcoreapp3.1‘ befinden. Die Dateien sind im Solution Explorer nur sichtbar, wenn man ‘Show all files’ aktiviert.
namespace Addressbook.Services { public class AddressbookService : AddressBookService.AddressBookServiceBase { private readonly ILogger<AddressbookService> _logger; public AddressbookService(ILogger<AddressbookService> logger) { _logger = logger; } public override Task<StatusMessage> AddPersons(AddressBookMessage request, ServerCallContext context) { StatusMessage result = new StatusMessage { Status = ReadingStatus.Failure }; try { foreach (var p in request.AddressBook.Persons) { // Todo: create person EF and add to repo } // Todo: save all changes to repo result.Status = ReadingStatus.Success; } catch (Exception ex) { result.Message = "Message thrown during processing."; _logger.LogError($"Message thrown during processing ({ex})."); } return Task.FromResult(result); } public override Task<AddressBookMessage> GetAddressBook(Empty request, ServerCallContext context) { AddressBookMessage result = new AddressBookMessage(); result.StatusMessage = new StatusMessage() { Status = ReadingStatus.Success }; result.AddressBook = new AddressBook(); // Todo: read from repo result.AddressBook.Persons.Add(new Person() { Name = "Erik Stroeken", Gender = Gender.Male, Age = 50, Email = "[email protected]", PhoneNumbers = { new Person.Types.PhoneNumber {Number = "00 41 76 444 00 87", PhoneType = PhoneType.Mobile}, new Person.Types.PhoneNumber {Number = "00 41 44 444 83 16", PhoneType = PhoneType.Home}, new Person.Types.PhoneNumber {Number = "00 41 41 444 66 45", PhoneType = PhoneType.Work} }, LastUpdated = Timestamp.FromDateTime(DateTime.UtcNow) }); return Task.FromResult(result); } } }
AddGrpc(opt => { opt.EnableDetailedErrors = true; });
hinzufügenMapGrpcService<AddressBookService>(); in app.UseEndpoints()
hinzufügen.Startup.cs
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddGrpc(opt => { opt.EnableDetailedErrors = true; }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<AddressBookService>(); endpoints.MapControllers(); }); }
Hier ist die Log-Information wenn der Server gestartet wird:
Der Client importiert die proto-Dateien als Referenzen in das Projekt. Die import-Anweisung in der Protodatei ist relativ und bezieht sich jetzt auf das Client-Projekt, in dem die Datei enums.proto nicht vorhanden ist.
Hack
syntax = "proto3"; import "enums.proto";
<ItemGroup> <Protobuf Include="Protos\addressBook.proto" ProtoRoot="Protos\" /> <Protobuf Include="Protos\enums.proto" ProtoRoot="Protos\" /> </ItemGroup>
Füge die folgende Sektion zu der appsettings.json-Datei hinzu:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "Service": { "CustomerId": 1, "DelayInterval": 3000, "ServiceUrl" : "https://localhost:5001" } }
Hier ist der Code vom Worker:
public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private readonly IConfiguration _config; private AddressBookService.AddressBookServiceClient _client = null; public Worker(ILogger<Worker> logger, IConfiguration config) { _logger = logger; _config = config; } protected AddressBookService.AddressBookServiceClient Client { get { if (_client == null) { ChannelBase channel = GrpcChannel.ForAddress(_config["Service:ServiceUrl"]); _client = new AddressBookService.AddressBookServiceClient(channel); } return _client; } } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { AddressBook addressBook = new AddressBook(); addressBook.Persons.Add( new Person() { Name = "Jan Muster", Gender = Gender.Male, Age = 50, Email = "[email protected]", PhoneNumbers = { new Person.Types.PhoneNumber {Number = "00 41 76 444 00 87", PhoneType = PhoneType.Mobile}, new Person.Types.PhoneNumber {Number = "00 41 44 555 83 16", PhoneType = PhoneType.Home}, new Person.Types.PhoneNumber {Number = "00 41 41 666 66 45", PhoneType = PhoneType.Work} }, LastUpdated = Timestamp.FromDateTime(DateTime.UtcNow) }); AddressBookMessage msg = new AddressBookMessage { AddressBook = addressBook }; var status = Client.AddPersons(msg); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); var result = await Client.GetAddressBookAsync(new Empty()); await Task.Delay(_config.GetValue("Service:DelayInterval"), stoppingToken); } } }
Starte erst den Server und dann den Client. Die Log-Traces sehen ungefähr so aus (rechts ist Client):
Dieser Blogbeitrag gibt eine Übersicht über die Vor- und Nachteile von gRPC und eine Einführung mit einer einfachen Implementierung. In der nächsten Folge wird Server und Client Streaming erklärt mit einem einfachem Beispiel, in welchem ein Bild hoch- und runtergeladen wird.
[…] und läuft ausschliesslich HTTP/2. Eine detaillierte Einleitung ist beschrieben im ersten Teil von diesem […]