September 2019

Ausgabe 34, Nummer 9

[Cutting Edge]

Streamingmethoden in ASP.NET Core-gRPC-Diensten

Von Dino Esposito

Dino EspositoIm vorherigen Beitrag zu Cutting Edge habe ich das Erstellen eines neuen Diensttyps erläutert, der auf dem gRPC-Framework basiert. Dieses konnte zwar schon eine Weile lang von C#-Entwicklern genutzt werden, wurde jedoch erst mit ASP.NET Core 3.0 als nativer, von Kestrel gehosteter Dienst eingeführt. Das gRPC-Framework ist für die binäre Peer-to-Peer-Kommunikation zwischen verbundenen Endpunkten geeignet. Dabei handelt es sich meistens, aber nicht immer, um Microservices. Es unterstützt moderne technische Lösungen wie Google Protobuf für die Serialisierung von Inhalten und HTTP/2 für die Übertragung.

Visual Studio 2019 enthält eine ASP.NET Core 3.0-Projektvorlage, mit der Sie mit wenigen Klicks den Grundaufbau eines gRPC-Diensts erstellen können. Eine Einführung in gRPC und das von Visual Studio generierter Starter Kit finden Sie in meiner Juli-Kolumne unter msdn.com/magazine/mt833481. Diesen Monat gehe ich beim Thema gRPC einen Schritt weiter. Zunächst erkläre ich die zugrunde liegenden Tools etwas ausführlicher. Einige Tools sind notwendig, um den Inhalt der PROTO-Datei in C#-Klassen einzufügen, die dann als Grundlage für die Client- und Dienstimplementierungen dienen können. Zudem gehe ich kurz auf Streamingmethoden und Klassen für komplexe Meldungen ein. Zum Schluss konzentriere ich mich auf die Integration gestreamter gRPC-Methoden in die Benutzeroberfläche einer Webclientanwendung.

Erstellen des gRPC-Diensts

Die integrierte Visual Studio-Projektvorlage speichert die Schnittstellendefinitionsdatei (PROTO-Datei) für den Dienst in einem Unterordner namens „protos“, der sich im Ordner des Dienstprojekts befindet. Für diesen Artikel wähle ich jedoch einen anderen Ansatz und beginne damit, eine neue .NET Standard 2.0-Klassenbibliothek zu einer leeren Projektmappe hinzuzufügen.

Die PROTO-Klassenbibliothek enthält keine expliziten C#-Klassen. Sie enthält nur eine oder mehrere PROTO-Dateien. Sie können PROTO-Dateien nach Belieben in Ordnern und Unterordnern organisieren. In der Beispielanwendung verwende ich eine einzelne PROTO-Datei für einen Beispieldienst, die im Projektordner „protos“ gespeichert ist. Im Folgenden sehen Sie einen Auszug aus dem Dienstblock der PROTO-Beispieldatei:

service H2H {
  rpc Details (H2HRequest) returns (H2HReply) {}
}

Der H2H-Beispieldienst soll sportbezogene Informationen von einem Remotestandort abrufen. Die Details-Methode übergibt eine direkte Anforderung und empfängt das Ergebnis der letzten Spiele zwischen bestimmten Spielern oder Teams. So können die H2HRequest- und die H2HReply-Meldungen aussehen:

message H2HRequest {
  string Team1 = 1;
  string Team2 = 2;
}
message H2HReply {
  uint32 Won1 = 1;
  uint32 Won2 = 2;
  bool Success = 3;
}

Der erste Meldungstyp übergibt zu verarbeitende Informationen zu den Teams, der zweite empfängt den Verlauf der letzten Spiele und ein boolesches Flag, das angibt, ob der Vorgang erfolgreich war oder nicht. So weit, so gut. Die Meldungen sind wie im vorherigen Artikel definiert. Im gRPC-Jargon ist die Details-Methode eine unäre Methode. Das bedeutet, dass jede Anforderung eine (und nur eine) Antwort empfängt. So werden gRPC-Dienste jedoch am häufigsten programmiert. Fügen wir ein paar Streamingfunktionen hinzu. Das geht so:

rpc MultiDetails (H2HMultiRequest) returns (stream H2HMultiReply) {}

Die neue MultiDetails-Methode ist eine serverseitige Streamingmethode. Das bedeutet, dass sie für jede Anforderung von einem gRPC-Client mehrere Antworten zurückgeben kann. In diesem Beispiel könnte der Client ein Array direkter Anforderungen senden und einzelne, direkte und asynchrone Antworten erhalten, da diese am Dienstende verarbeitet werden. Hierfür muss die gRPC-Dienstmethode mit einem Streamschlüsselwort im returns-Bereich gekennzeichnet werden. Für eine Streamingmethode sind möglicherweise Ad-hoc-Meldungstypen erforderlich. Im Folgenden sehen Sie ein Beispiel:

message H2HMultiRequest {
  string Team = 1;
  repeated string OpponentTeam = 2;
}

Wie bereits erwähnt kann der Kunde einen direkten Vergleich zwischen einem bestimmten Team und mehreren anderen Teams fordern. Das wiederholte Schlüsselwort im Meldungstyp gibt lediglich an, dass das Member „OpponentTeam“ mehrmals vorkommen kann. In reinem C#-Code entspricht der H2HMultiRequest-Meldungstyp konzeptionell folgendem Pseudocode:

class H2HMultiRequest
{  string Team {get; set;}  IEnumerable<string> OpponentTeam {get; set;}}

Beachten Sie jedoch, dass der vom gRPC-Tool generierte Code sich geringfügig unterscheidet:

public RepeatedField<string> OpponentTeam {get; set;}

Tatsächlich implementieren alle Klassen, die aus dem gRPC-Meldungstyp „T“ generiert werden, die Member der Schnittstelle „Google.ProtoBuf.IMessage<T>“. Der Meldungstyp der Antwort sollte die Daten beschreiben, die in jedem Schritt der Streamingphase tatsächlich zurückgegeben werden. Jede Antwort muss sich also auf ein einzelnes, direktes Ergebnis zwischen dem primären Team und einem der im Array angegebenen gegnerischen Teams beziehen:

message H2HMultiReply {
  H2HItem Team1 = 1;
  H2HItem Team2 = 2;
}
message H2HItem {
  string Name = 1;
  uint32 Won = 2;
}

Der H2HItem-Meldungstyp gibt an, wie viele Spiele ein bestimmtes Team gegen ein anderes in der Anforderung angegebenes Team gewonnen hat.

Bevor ich näher auf die Implementierung einer Streamingmethode eingehe, sollten wir uns mit den Abhängigkeiten befassen, die für die freigegebene Klassenbibliothek erforderlich sind, mit der die PROTO-Definition eingebettet wird. Das Visual Studio-Projekt muss auf die NuGet-Pakete aus Abbildung 1 verweisen.

die NuGet-Abhängigkeiten der freigegebenen PROTO-Klassenbibliothek
Abbildung 1: die NuGet-Abhängigkeiten der freigegebenen PROTO-Klassenbibliothek

Das Projekt, das die PROTO-Quelldatei enthält, muss auf das Paket „Grpc.Tools“ und auf die Pakete „Grp.Net.Client“ (in .NET Core 3.0 Preview 6 hinzugefügt) und „Google.Protobuf“ verweisen, die für jedes gRPC-Projekt (Client, Server oder Bibliothek) erforderlich sind. Das Toolpaket ist letztendlich für die Analyse der PROTO-Datei und das Generieren der erforderlichen C#-Klassen zur Kompilierzeit verantwortlich. Ein Elementgruppenblock in der CSPROJ-Datei versorgt das Toolsystem mit weiteren Anweisungen. Dies ist der Code:

<ItemGroup>
  <Protobuf Include="Protos\h2h.proto"
            GrpcServices="Server, Client"
            Generator="MSBuild:Compile" />
  <Content Include="@(Protobuf)" />
  <None Remove="@(Protobuf)" />
</ItemGroup>

Die wichtigsten Bestandteile des ItemGroup-Blocks sind der Protobuf-Knoten und das GrpcServices-Attribut. Das Server-Token im zugeordneten Zeichenfolgenwert gibt an, dass das Tool die Dienstklasse für die Prototypschnittstelle generieren muss. Das Client-Token gibt an, dass die Erstellung der Basisclientklasse erwartet wird, um den Dienst aufzurufen. Dadurch enthält die resultierende DLL-Datei C#-Klassen für die Meldungstypen, die Basisdienstklasse und die Clientklasse. Das Dienstprojekt und das Clientprojekt (Konsole, Web oder Desktop) müssen dann nur auf die DLL-Prototypdatei verweisen, um mit dem gRPC-Dienst zu interagieren.

Implementieren des Diensts

Der gRPC-Dienst ist ein ASP.NET Core-Projekt mit speziellen Konfigurationen in der Startklasse. Er verweist nicht nur auf die ASP.NET Core-Serverplattform und die Prototypenassembly, sondern auch auf das gRPC-Framework von ASP.NET Core und das Paket „Google.Protobuf“. Die Startklasse fügt den gRPC-Runtimedienst in der Configure-Methode hinzu und fügt die gRPC-Endpunkte in der ConfigureServices-Methode an. Dieser Vorgang wird in Abbildung 2 veranschaulicht.

Abbildung 2: Konfigurieren des gRPC-Diensts

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{  // Some other code here   ...
  app.UseRouting();
  app.UseEndpoints(endpoints =>
  {
    endpoints.MapGrpcService<H2HService>();
  });
}

Die Dienstklasse erbt von der Basisdienstklasse, die das Tool erstellt hat, je nach Inhalt der PROTO-Datei. Hier sehen Sie ein Beispiel:

public class H2HService : Sample.H2H.H2HBase
{
  // Unary method Details
  public override Task<H2HReply> Details(
              H2HRequest request, ServerCallContext context)
  {
    ...
  }
  ...
}

Unäre Methoden wie die Details-Methode haben eine einfachere Signatur als Streamingmethoden. Sie geben ein Task<TReply>-Objekt zurück und akzeptieren ein TRequest-Objekt und eine Instanz von ServerCallContext, um auf den Inhalt der eingehenden Anforderung zu reagieren. Eine serverseitige Streamingmethode enthält einen zusätzlichen Antwortstreamparameter, der vom Implementierungscode verwendet wird, um Pakete zurück zu streamen. In Abbildung 3 sehen Sie die Implementierung der MultiRequest-Streamingmethode.

Abbildung 3: eine serverseitige Streamingmethode des gRPC-Diensts

public override async Task MultiDetails(      H2HMultiRequest request,
      IServerStreamWriter<H2HMultiReply> responseStream,
      ServerCallContext context)
{  // Loops through the batch of operations embedded   // in the current request
  foreach (var opponent in request.OpponentTeam)
  {
    // Grab H2H data to return
    var h2h = GetHeadToHead_Internal(request.Team, opponent);
    // Copy raw data into an official reply structure    // Raw data is captured in some way: an external REST service
    // or some local/remote database    var item1 = new H2HItem {
     Name = h2h.Id1, Won = (uint) h2h.Record1};
    var item2 = new H2HItem {
     Name = h2h.Id2, Won = (uint) h2h.Record2};
    var reply = new H2HMultiReply { Team1 = item1, Team2 = item2 };
    // Write back via the output response stream
    await responseStream.WriteAsync(reply);
  }
  return;
}

Wie Sie sehen können, akzeptiert die Streamingmethode im Vergleich zu klassischen unären Methoden einen zusätzlichen Parameter vom Typ IServerStreamWriter<TReply>. Diesen Ausgabestream verwendet die Methode, um die fertigen Ergebnisse zurück zu streamen. Im Code in Abbildung 3 führt die Methode eine Schleife für jeden angeforderten Vorgang aus (in diesem Fall ein Array von Teams, um die vergangenen Spiele abzurufen). Die Ergebnisse werden dann an eine lokale Datenbank, eine Remotedatenbank oder einen Webdienst zurück gestreamt, wenn die Abfrage ein Ergebnis zurückgibt. Wenn dieser Vorgang abgeschlossen ist, gibt die Methode ein Ergebnis zurück, und die zugrunde liegende Runtimeumgebung schließt den Stream.

Schreiben eines Clients für eine Streamingmethode

Im Beispielcode handelt es sich bei der Clientanwendung um eine einfache ASP.NET Core 3.0-Anwendung. Sie enthält Verweise auf die Pakete „Google.Protobuf“ und „Grpc.Net.Client“ und auf die freigegebene Prototypbibliothek. Auf der Benutzeroberfläche ist eine Schaltfläche vorhanden, der JavaScript-Code angefügt ist, um einen Beitrag an eine Controllermethode zu übermitteln. Sie können auch ein klassisches HTML-Formular verwenden, durch die Verwendung von Ajax wird jedoch der Empfang von Benachrichtigungen für Antworten und das Aktualisieren der Benutzeroberfläche vereinfacht. Den Code sehen Sie in Abbildung 4.

Abbildung 4: Aufrufen des gRPC-Diensts

[HttpPost]
public async Task<IActionResult> Multi()
{
  // Call the RPC service
  var serviceUrl = "http://localhost:50051";
    AppContext.SetSwitch(
                "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
                true);
    var httpClient = new HttpClient() {BaseAddress = new Uri(serviceUrl) };
  var client = GrpcClient.Create<H2H.H2HClient>(httpClient);
  var request = new H2HMultiRequest() { Team = "AF-324" };
  request.OpponentTeam.AddRange(new[] { "AW-367", "AD-683", "AF-510" });
  var model = new H2HMultiViewModel();
  using (var response = client.MultiDetails(request))
  {
    while (await response.ResponseStream.MoveNext())
    {
      var reply = response.ResponseStream.Current;      // Do something here ...
    }  }
  return View(model);
}

Bedenken Sie, dass der Port des gRPC-Diensts vom Visual Studio-Projekt abhängt, während die Clientaufrufklasse in der Prototypbibliothek definiert wird. Wenn Sie die Anforderung auf eine serverseitige Streamingmethode ausrichten möchten, müssen Sie nur den Eingabemeldungstyp auffüllen. Wie bereits erwähnt handelt es sich bei der OpponentTeam-Sammlung um einen enumerierten .NET-Typ, der mit AddRange oder wiederholten Aufrufen von Add aufgefüllt werden kann. Der tatsächliche Typ zählt nicht zu den .NET Core-Sammlungstypen, aber er ist ein Sammlungstyp, obwohl er im Paket „Google.Protobuf“ implementiert wird.

Da eine serverseitige Methode Pakete bis zum Ende des Streams zurück streamt, gibt ein Aufruf der Methode ein Streamobjekt zurück. Anschließend enumeriert der Clientcode die Pakete, die auf das Ende der Antwort warten. Jede Iteration der while-Schleife in Abbildung 4 erfasst ein einzelnes Antwortpaket des gRPC-Diensts. Die nächsten Schritte hängen von der Clientanwendung ab. Insgesamt gibt es drei mögliche Szenarios.

Im ersten besitzt die Clientanwendung eine eigene Benutzeroberfläche, aber kann auf die gesamte Antwort warten, bevor dem Benutzer eine Aktualisierung angezeigt wird. In diesem Fall laden Sie die vom aktuellen Antwortobjekt übertragenen Daten in das Ansichtsmodell, das von der Controllermethode zurückgegeben wird. Im zweiten ist keine Benutzeroberfläche vorhanden (z. B. wenn der Client ein funktionierender Microservice ist). In diesem Fall werden die empfangenen Daten verarbeitet, sobald Sie verfügbar sind. Im dritten besitzt die Clientanwendung eine eigene reaktionsfähige Benutzeroberfläche und kann den Benutzern die Daten anzeigen, sobald diese vom Server eingehen. In diesem Fall können Sie der Clientanwendung einen SignalR Core-Endpunkt anfügen und die Benutzeroberfläche in Echtzeit benachrichtigen (Abbildung 5).

die Beispielanwendung in Aktion
Abbildung 5: die Beispielanwendung in Aktion

Aus dem folgenden Codeausschnitt wird ersichtlich, wie der Clientcode geändert werden muss, wenn ein SignalR-Hub mit einem gRPC-Aufruf verwendet wird:

var reply = response.ResponseStream.Current;
await _h2hHubContext.Clients
                    .Client(connId)
                    .SendAsync("responseReceived",
        reply.Player1.Name,
        reply.Player1.Won,
        reply.Player2.Name,
        reply.Player2.Won);

Die vollständigen Details der Lösung finden Sie im Quellcode. In Bezug auf SignalR gibt es einige wichtige Punkte zu beachten. Erstens wird SignalR nur von der Clientanwendung verwendet, die eine Verbindung mit dem gRPC-Dienst herstellt. Der Hub wird in den Controller der Clientanwendung eingefügt, nicht in den gRPC-Dienst. Zweitens und letztens muss beim Streaming beachtet werden, dass SignalR Core eine eigene Streaming-API besitzt.

Weitere gRPC-Streams

In diesem Artikel lag mein Schwerpunkt auf serverseitigen gRPC-Streamingmethoden, es gibt jedoch noch weitere. Das gRPC-Framework unterstützt auch clientseitige Streamingmethoden (mehrere Anforderungen, eine Antwort) und bidirektionale Streamingmethoden (mehrere Anforderungen, mehrere Antworten). Beim clientseitigen Streaming besteht der einzige Unterschied darin, dass IAsyncStreamReader als Eingabestream in der Dienstmethode verwendet wird. Ein Beispiel finden Sie in folgendem Code:

public override async Task<H2HReply> Multi(
         IAsyncStreamReader<H2HRequest> requestStream,
         ServerCallContext context){  while (await requestStream.MoveNext())
  {
    var requestPacket = requestStream.Current;   
      // Some other code here
      ...  } }

Eine bidirektionale Methode gibt „void“ zurück und akzeptiert keine Parameter, da sie Eingabe- und Ausgabedaten über Eingabe- und Ausgabestreams liest und schreibt.

Zusammengefasst ist gRPC also ein vollständiges Framework für die Herstellung einer Verbindung zwischen zwei Endpunkten (Client und Server) über ein binäres, flexibles Open-Source-Protokoll. Die gRPC-Unterstützung durch ASP.NET Core 3.0 ist hervorragend ausgeprägt und wird im Lauf der Zeit weiter verbessert. Jetzt ist also ein guter Zeitpunkt, um in gRPC einzusteigen und mit dem Framework zu experimentieren – das gilt insbesondere für die Kommunikation zwischen Microservices.


Dino Esposito hat in seiner 25-jährigen Karriere über 20 Bücher und mehr als 1.000 Artikel verfasst. Als Autor von „The Sabbatical Break“, einer theatralisch angehauchten Show, schreibt Esposito Software für eine grünere Welt als digitaler Stratege bei BaxEnergy. Folgen Sie ihm auf Twitter: @despos.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: John Luo, James Newton-King
James Newton-King ist Mitglied des ASP.NET Core-Entwicklerteams und arbeitet an gRPC für .NET Core.

John Luo ist Mitglied des ASP.NET Core-Entwicklerteams und arbeitet an gRPC für .NET Core.


Diesen Artikel im MSDN Magazine-Forum diskutieren