gRPC Tutorial Teil 1: gRPC in ASP.NET Core
1 Einleitung
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:
- gRPC funktioniert nur mit HTTP/2, nicht alle Browser supporten HTTP/2
- Das Format ist binär und schwierig zu Parsen durch JavaScript
- gRPC verlangt Verträge und WEB Apps sind nicht optimiert dafür
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:
- Ist optimiert für Geschwindigkeit und Grösse.
- Läuft immer über eine gesicherte Verbindung (HTTPS TLS).
- Unterstützt uni- und bidirektionales Streaming.
- Ist ideal für low Resource Clients (IoT), Microservices.
- Ist gut in Queueing und Messaging.
- Unterstützt kein Multicasting (=> SignalR).
- Nicht einfach lesbar durch Menschen.
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.
1.1 gRPC vs.REST
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/
1.2 gRPC vs. WCF
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/
1.3 Und was ist mit gRPC-Web?
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.
2 Vertragsdefinition in gRPC
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; } …
2.1 Die vier Wege von gRPC
Es gibt vier Typen von Serveraufrufen in RPC:
- Unary RPC
- Server Streaming RPC
- Client Streaming RPC
- Bidirectional Streaming RPC
2.1.1 Unary RPC
Einfache Serveraufrufe mit keinem oder einem message-Parameter und keiner oder einer Rückgabe-message.
rpc GetMovie(QueryParams) returns (Movie){};
2.1.2 Server Streaming RPC
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:
- Echtzeit-Transfer (einzelne kleine Objekte ‘abfeuern’).
- Server-zu-Client Benachrichtigungen (Stream offenhalten)
- Chunking, das bedeutet eine grosse Datei zerhacken und in Stücken zu versenden. gRPC garantiert Integrität und Reihenfolge der Daten.
rpc GetMovie(QueryParams) returns (stream Data){};
2.1.3 Client Streaming RPC
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.
2.1.4 Bidirectional Streaming RPC
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){};
3 gRPC und ASP.NET Core
gRPC ist Teil vom ASP.NET Core 3.1 Framework. Im folgenden wird eine Beispielanwendung erstellt. Den Quellcode kann man hier herunterladen.
3.1 PC Einrichten
Es braucht Visual Studio 2019. Visual Studio Community, Professional und Enterprise funktionieren alle. Einfach .NET Core 3.1 herunterladen (nicht .NET Framework 4.8).
3.2 Server Projekt erstellen
- Erstelle ein neues Projekt «ASP.NET Core Web Application»
- Nenne es ‘Addressbook’
- Wähle API aus mit ‘Configure for HTTPS’ aber ohne ‘Authentication’
3.3 Integrieren von Protocol Buffers (Protobuf)
3.3.1 Verträge erstellen
- Erstelle ein Unterverzeichnis \Protos
- New item -> Protocol Buffer File -> enums.proto
- Füge die Option “csharp_namespace to “Addressbook.Services” hinzu (dadurch wird der Tag ‘Package’ in C# nicht generiert)
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; }
- New item -> Protocol Buffer File -> addressbook.proto
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; }
- Hinzufügen NuGet Paket Grpc.AspNetCore (v2.25.0)
- Jetzt kann man die Build Action von beiden proto-Dateien auf ‘Protobuf compiler’ setzen.
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.
3.3.2 Service implementieren
- Unterverzeichnis ‘Services’ erstellen
- Neue Klasse ‘AddressbookService’ erstellen, welche von AddressbookService.AddressbookServiceBase ableitet. Diese Klasse ist generiert aus den proto-Dateien.
- Implementiere jetzt die generierten Methoden durch ‘override’ (siehe unten).
- Interessant ist, dass parameterlose Methoden den Parameter ‘Empty’ haben.
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 = "erik@stroeken.ch", 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); } } }
3.3.3 Den Service verdrahten
- In Startup.cs unter ConfigureService()
AddGrpc(opt => { opt.EnableDetailedErrors = true; });
hinzufügen - In der Methode Configure()
MapGrpcService<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(); }); }
- Man muss im Moment noch den Kestrel Server verwenden, weil IIS Express kein HTTP/2 unterstützt.
Hier ist die Log-Information wenn der Server gestartet wird:
3.4 Erstellen des gRPC Clients
- Ein weiteres Projekt vom Type «.NET Core Worker Service» hinzufügen.
- Dem Client Projekt eine Service Referenz hinzufügen:
- Wähle jetzt die addressbook.proto-Datei aus und setze den Type auf Client.
- Selektiere die enums.proto-Datei aber setzte den Typ zu «Messages only».
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
- Entferne “Protos/” from import Tag
syntax = "proto3"; import "enums.proto";
- Öffne die Projektdatei vom Server und füge die Attribute ProtoRoot mit «Protos\» hinzu:
<ItemGroup> <Protobuf Include="Protos\addressBook.proto" ProtoRoot="Protos\" /> <Protobuf Include="Protos\enums.proto" ProtoRoot="Protos\" /> </ItemGroup>
3.5 Implementiere den Client
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 = "jan.muster@firma.ch", 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):
4. Fazit
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.