• Noser.com
facebook
linkedin
twitter
youtube
  • NOSERmobile
    • Android
    • HTML 5
    • Hybrid Apps
    • iOS
    • Windows Phone
  • NOSERembedded
    • Medizintechnik
  • NOSERprojektmanagement
  • NOSERtesting
  • NOSERlifecycle
    • .NET Allgemein
    • Application Lifecylce Management
    • Apps
    • Architektur
    • ASP.NET
    • Azure
    • Cleancode
    • Cloud
    • Silverlight
    • Visual Studio / Team Foundation Server
    • Windows 8
    • Windows Presentation Foundation
  • NOSERinnovation
    • Big Data
    • Cloud
    • IoT
    • Operations Research
    • Augmented Reality
    • RFID, NFC, Bluetooth LE

gRPC Tutorial Teil 2: Streaming mit gRPC

19. Dezember 2019
Erik Stroeken
2
asp.net core, gRPC, protobuf, streaming

Dieser Beitrag demonstriert, wie man mit gRPC Streaming grosse Dateien zum Server hoch lädt oder vom Server herunterlädt. gRPC ist entwickelt durch Google, verwendet Protocol Buffers als Schnittstellenbeschreibungssprache und läuft ausschliesslich über HTTP/2. Eine detaillierte Einleitung ist im ersten Teil von diesem Tutorial beschrieben.

gRPC unterstützt ‘Server Streaming’, ‘Client Streaming’ und ‘Bidirectional Streaming’.

Bei Streaming denkt man sofort an Echtzeit-Übertragungen. Das ist aber nicht der einzige Anwendungsfall von Streaming. Man kann Streams auch verwenden um grosse Dateien zu übermitteln, diese zu zerhacken und die Stücke einzeln zu übermitteln (‘Chunking’). Ein anderer Anwendungsfall ist, den Stream offen zu lassen und zur Benachrichtigung des Empfängers zu verwenden. So kann die Duplex-Schnittstelle von WCF mit einem Serverstream nachgebildet werden. Diese Lösung wird im nächsten Teil von dieser Serie erklärt.

Mit bidirektionalem Streaming kann der Server auch Broadcasts machen. Dazu merkt sich der Server den Download-Stream von jedem Client in einer Collection und iteriert bei jedem Broadcast durch die Collection. Im Netzt gibt es ein paar Chat-Beispiele, die diese Technik mit bidirektionalem Streaming verwenden.

Grosse Dateien transferieren

Daten bis 1 kByte kann man als Parameter vom Typ Byte-Array in einem Aufruf übertragen. Grössere Dateien sollten zerhackt und mittels einem Stream übermittelt werden (‘chunking’). gRPC sorgt dafür, dass Reihenfolge und Datenkonsistenz gewährleistet bleiben.

Die optimale Chunk-Grösse liegt zwischen 1kB und 64kB.

Im nächsten Beispiel werden zwei Methoden implementiert: eine, um ein Bild von einer Person zum Server hoch zu laden, und eine, um ein Bild einer Person vom Server herunter zu laden.

Der Quellcode vom Beispiel kann hier heruntergeladen werden.

Die proto-Datei sieht so aus:

syntax = "proto3";

option csharp_namespace = "RpcStreaming.Services";

enum ImageType {
	IMAGETYPE_UNDEFINED = 0;
	IMAGETYPE_JPG = 1;
	IMAGETYPE_PNG = 2;
}

enum TransferStatus{
    TRANSFERSTATUS_UNDEFINED = 0;
    TRANSFERSTATUS_SUCCESS = 1;
    TRANSFERSTATUS_FAILURE = 2;
    TRANSFERSTATUS_INVALID = 3;
    TRANSFERSTATUS_CANCELLED = 4; 
}

service RpcStreamingService {
    rpc UploadPersonImage (stream PersonImageMessage) returns (TransferStatusMessage);
    rpc DownloadPersonImage (PersonMessage) returns (stream PersonImageMessage);
}

message TransferStatusMessage {
    string message = 1;
    TransferStatus status = 2;
}

message PersonImageMessage
{
  int32 personId = 1;
  TransferStatusMessage transferStatusMessage = 2;
  ImageType imageType = 3;
  bytes imageChunk = 4;
}

message PersonMessage
{
	int32 personId = 1;
}

Die Chunks werden im Feld ‘imageChunk’ übermittelt. Wenn bei der Servicedefinition vor der Message ‘stream’ steht, erfolgt die Übertragung als Stream.

Bilder herunterladen

Beim Herunterladen ist der Rückgabewert ein Stream. Der Client kann dem Server eine Message mitgeben z.B. mit Filterangaben. Im Beispiel wird die ID der Person übermittelt, von welcher der Client das Bild herunterladen möchte.

Hier ist der Client Code für die Methode DownloadPersonImage, welche den Stream rekonstruiert und wenn das Bild komplett ist, das Bild als Datei auf der Festplatte speichert:

private async Task DownloadPersonImage(int personId, string fileName, CancellationToken cancellationToken)
{
    bool success = true;
    try
    {
        PersonMessage personMessage = new PersonMessage {PersonId = personId};
        using var streamingCall = Client.DownloadPersonImage(personMessage);
        await using (Stream fs = File.OpenWrite(fileName))
        {
            await foreach (PersonImageMessage personImageMsg in
                streamingCall.ResponseStream.ReadAllAsync(cancellationToken).ConfigureAwait(false))
            {
                fs.Write(personImageMsg.ImageChunk.ToByteArray());
            }
        }
    }
    // Is thrown on cancellation -> ignore...
    catch (OperationCanceledException)
    {
        success = false;
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Exception thrown");
        success = false;
    }
    if (!success)
    {
        File.Delete(fileName);
    }
}

Man kann auch die Methode MoveNext() vom ResponseStream aufrufen. Diese gibt ‘true’ zurück, wenn Informationen empfangen wurden, welche dann aus ResponseStream.Current gelesen werden können:

await using (Stream fs = File.OpenWrite(ImageFileName))
{
  while(await requestStream.MoveNext(cancellationToken))
  {
    fs.Write(requestStream.Current.ImageChunk.ToByteArray());
  }
}

Weil es nichts ausmacht, auf welchem Thread der Code nach den await Aufrufen läuft, wird immer ConfigureAwait(false) angehängt. Damit wird der Overhead zum Context speichern und wiederherstellen gespart. Nur der äusserste Aufruf soll ConfigureAwait(true) haben, um nach dem await auf dem UI Thread zu bleiben. Auch darauf kann man verzichten, wenn der Code in einer Console Anwendung läuft, weil diese keinen Synchronisationskontext hat.

Siehe dazu die Blogserie ‘C# Concurrency’, die hier beginnt.

Im Server sieht der Code wie folgt aus:

public override async Task DownloadPersonImage(PersonMessage request, 
    IServerStreamWriter<PersonImageMessage> responseStream, ServerCallContext context)
{
    // Example of exception
    if (File.Exists(ImageFileName) == false)
    {
        _logger.LogError($"File '{ImageFileName}' not found.");
        Metadata metadata = new Metadata()
            {{"Filename", ImageFileName}};
        throw new RpcException(new Status(StatusCode.NotFound, "Image file not found."), 
            metadata, "More details for the exception...");
    }
    PersonImageMessage personImageMessage = new PersonImageMessage();
    personImageMessage.TransferStatusMessage = new TransferStatusMessage();
    personImageMessage.TransferStatusMessage.Status = TransferStatus.Success;
    personImageMessage.PersonId = request.PersonId;
    personImageMessage.ImageType = ImageType.Jpg;
    byte[] image;
    try
    {
        image = File.ReadAllBytes(ImageFileName);
    }
    catch (Exception)
    {
        _logger.LogError($"Exception while reading image file '{ImageFileName}'.");
        throw new RpcException(Status.DefaultCancelled, "Exception while reading image file.");
    }
    int imageOffset = 0;
    byte[] imageChunk = new byte[ImageChunkSize];
    while (imageOffset < image.Length)
    {
        int length = Math.Min(ImageChunkSize, image.Length - imageOffset);
        Buffer.BlockCopy(image, imageOffset, imageChunk, 0, length);
        imageOffset += length;
        ByteString byteString = ByteString.CopyFrom(imageChunk);
        personImageMessage.ImageChunk = byteString;
        await responseStream.WriteAsync(personImageMessage).ConfigureAwait(false);
    }
}

Als erstes wird die Antwort ‘PersonImageMessage’ vorbereitet. Das Feld ‘ImageType’ ist nur für die Show da. Als nächstes wird das Bild von der Festplatte als Bytearray geladen und in ‘ImageChunkSize’ Stücke aufgeteilt. Der Bytearray wird mit einer Google Funktion in einen ByteString verwandelt und in den Stream geschrieben.

Es fällt auf, dass im ResponseStream kein CompleteAsync () vorhanden ist, um anzugeben, dass der Download abgeschlossen ist. Der RequestStream hat ein CompleteAsync().

Bilder hochladen

Beim Hochladen sind die Rollen umgekehrt.

Hier der Client-Code:

private async Task UploadPersonImage(int personId, string fileName, CancellationToken cancellationToken)
{
    var stream = Client.UploadPersonImage();
    PersonImageMessage personImageMessage = new PersonImageMessage();
    personImageMessage.PersonId = personId;
    personImageMessage.ImageType = ImageType.Jpg;
    byte[] image = File.ReadAllBytes(fileName);
    int imageOffset = 0;
    byte[] imageChunk = new byte[imageChunkSize];
    while (imageOffset < image.Length && !cancellationToken.IsCancellationRequested)
    {
        int length = Math.Min(imageChunkSize, image.Length - imageOffset);
        Buffer.BlockCopy(image, imageOffset, imageChunk, 0, length);
        imageOffset += length;
        ByteString byteString = ByteString.CopyFrom(imageChunk);
        personImageMessage.ImageChunk = byteString;
        await stream.RequestStream.WriteAsync(personImageMessage).ConfigureAwait(false);
    }
    await stream.RequestStream.CompleteAsync().ConfigureAwait(false);
    if (!cancellationToken.IsCancellationRequested)
    {
        var uploadPersonImageResult = await stream.ResponseAsync.ConfigureAwait(false);
        // Process answer...
    }
}

Nach dem Hochladen von allen Chunks wird explizit CompletAsync() aufgerufen, um dem Server mitzuteilen, dass das Streaming beendet ist. Mit ResponseAsync kann die Server-Antwort abgeholt werden.

Server-Code:

public override async Task<TransferStatusMessage> UploadPersonImage(
    IAsyncStreamReader<PersonImageMessage> requestStream, ServerCallContext context)
{
    TransferStatusMessage transferStatusMessage = new TransferStatusMessage();
    transferStatusMessage.Status = TransferStatus.Success;
    try
    {
        await Task.Run(
            async () =>
            {
                CancellationToken cancellationToken = context.CancellationToken;
                await using (Stream fs = File.OpenWrite(ImageFileName))
                {
                    await foreach (PersonImageMessage personImageMessage in
                        requestStream.ReadAllAsync(cancellationToken).ConfigureAwait(false))
                    {
                        fs.Write(personImageMessage.ImageChunk.ToByteArray());
                    }
                }
            }).ConfigureAwait(false);
    }
    // Is thrown on cancellation -> ignore...
    catch (OperationCanceledException)
    {
        transferStatusMessage.Status = TransferStatus.Cancelled;
    }
    catch (RpcException rpcEx)
    {
        if (rpcEx.StatusCode == StatusCode.Cancelled)
        {
            transferStatusMessage.Status = TransferStatus.Cancelled;
        }
        else
        {
            _logger.LogError($"Exception while processing image file '{ImageFileName}'. Exception: '{requestStream}'");
            transferStatusMessage.Status = TransferStatus.Failure;
        }
    }
    // Delete incomplete file
    if (transferStatusMessage.Status != TransferStatus.Success)
    {
        File.Delete(ImageFileName);
    }
    return transferStatusMessage;
}

Das Sammeln aller versendeten Chunks wird parallel auf einem separaten Task gemacht. Der aufrufende Thread wartet  ‘await’, bis alle Daten gesammelt und verarbeitet sind. Dann wird die Rückgabemessage zusammengestellt und versendet.

Was wenn der client den Upload abbrechen möchte? Im Beispiel wird die ‘while’ Schleife beendet und ‘CompleteAsync()’ gesendet. Der Server sieht so aber nicht den Unterschied zwischen ‘Fertig’ oder ‘Abbruch’. Als Work-around könnte man das Feld TransferMessageStatus in PersonImageMessage verwenden um den Server mitzuteilen, dass entweder den Transfer abgebrochen wurde oder das Bild komplett übermittelt wurde. Besser wäre aber den eingebauten Mechanismus zum Abbrechen zu verwenden. Die Frage liegt im Moment bei StackOverflow…

Fazit

Dieser Blog zeigt, wie Streams zum Up- und Download grosser Dateien verwendet werden, in dem sie zerhackt und in einzelnen Paketen versendet werden. Im nächsten Blog wird die Duplex Funktionalität von WCF mit einem Server-Stream nachgebildet, in dem der Server asynchron den Client benachrichtigen kann. Das Beispiel ist gedacht für einfache und robuste Peer-to-Peer Anwendungen in der Praxis. Wenn eine industrielle Anlage eingeschaltet ist, kann es gut sein, dass der Client bereit ist, bevor der Server hochfahren konnte. Also greift der Client beim Verbinden ins Leere. Auch wird das Problem Verbindungsunterbrechung gelöst. Der offene Serverstream bricht dann ab und muss bei einer neuen Verbindung wieder neu geöffnet werden.

← Vorige Post
Nächster Post →

gRPC Tutorial Teil 1: gRPC in ASP.NET Core

19. Dezember 2019
Erik Stroeken
1
asp.net core, gRPC, protobuf, protocol buffers, Tutorial
Noser Engineering - Erik Stroeken: gRPC Blog Serie

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.

Noser Engineering gRPC in ASP.NET Core

Geschichte der verteilen APIs

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:

  1. Unary RPC
  2. Server Streaming RPC
  3. Client Streaming RPC
  4. 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’
Noser Engineering AG - gRPC Tutorial - new ASP.NET Core web application

Erstelle neue ASP.NET Core web Applikation

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.
Noser Engineering AG - gRPC Tutorial - Aktion 'Protobuf compiler' einstellen

Aktion ‘Protobuf compiler’ einstellen

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.
Noser Engineering AG - gRPC Tutorial - Kestrel einstellen

Kestrel einstellen

Hier ist die Log-Information wenn der Server gestartet wird:

Noser Engineering AG - gRPC Tutorial - Output vom gRPC Server

Output vom gRPC Server

3.4  Erstellen des gRPC Clients

  • Ein weiteres Projekt vom Type «.NET Core Worker Service» hinzufügen.
Noser Engineering AG - gRPC Tutorial - Add new Worker Project

Add new Worker Service Project

  • Dem Client Projekt eine Service Referenz hinzufügen:
Noser Engineering AG - gRPC Tutorial - Service Referenz hinzufügen

Service Referenz hinzufügen

  • Wähle jetzt die addressbook.proto-Datei aus und setze den Type auf Client.
Noser Engineering AG - gRPC Tutorial - Nur client Code generieren

Nur client Code generieren

  • Selektiere die enums.proto-Datei aber setzte den Typ zu «Messages only».
Noser Engineering AG - gRPC Tutorial - Nur messages generieren

Nur messages generieren

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):

Noser Engineering AG - gRPC Tutorial - Output vom Server und Client

Output vom Server und 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.

Nächster Post →

‹ Previous1234567Next ›Last »

Tag Cloud

.NET android Angular AngularJs Arduino ASP.Net automated testing Azure Big Data C# C++ Cloud continuous integration Elm Embedded Führung gRPC Internet of Things IoT Java Javascript M2M OWASP Projektmanagement protobuf Python Raspberry Pi Reactive Programming REST Scrum Security Softwarequalität SPA Testen testing Testmanagement Teststrategie UX Visual Studio WebAPI windows WPF Xamarin Xamarin.Android Xamarin.Forms

Archive

Current Posts

  • Akzente setzen mit der Android Splash Screen API unter .NET MAUI
  • Do You have Your Personal Space?
  • Automated provisioning with ARM Templates
  • Asynchrone Beobachtungen und Versprechungen in Angular
  • Simplify Your Automated Tests With Fluent Syntax

Last Comments

  • Hans Reinsch bei Der Safety-Plan: Die wichtigsten Antworten mit Checkliste
  • George H. Barbehenn bei Modeling Optocouplers with Spice
  • Noser Blog Touch-Actions in Xamarin.Forms - Noser Blog bei Mach mehr aus Animationen in Xamarin.Forms mit SkiaSharp
  • Noser Blog Focus on the Secure Storage service of Trusted Firmware (TFM) - Noser Blog bei First run of the Trusted Firmware (TFM) application
  • Noser Blog First run of the Trusted Firmware (TFM) application - Noser Blog bei Focus on the Secure Storage service of Trusted Firmware (TFM)

Popular Posts

Xamarin.Android Code Obfuscation

6 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 1 - Aufbruch

5 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 2 - Kein Picknick

4 Comments

Contact us

  1. Name *
    * Please enter your name
  2. Email *
    * Please enter a valid email address
  3. Message *
    * Please enter message
© 2013 NOSER ENGINEERING AG. All rights reserved. Datenschutz | Cookie-Richtlinie