gRPC auf der Client-Seite

Die meisten Kommunikationskomponenten zwischen Systemen, die REST verwenden, serialisieren ihre Nutzlast in JSON. Stand heute, fehlt JSON ein weit verbreiteter Standard für Schema-Validierung: JSON Schema ist nicht verbreitet. Eine standardisierte Schema-Validierung erlaubt es, die Validierung an eine Drittanbieter-Bibliothek zu delegieren und das ist es auch schon. Ohne einen solchen Standard müssen wir zur manuellen Validierung im Code zurückgreifen. Schlimmer noch, müssen wir den Validierungscode synchron mit dem Schema halten.

XML bietet Schema-Validierung „out-of-the-box“: Ein XML-Dokument kann eine Grammatik deklarieren, der es entsprechen muss. SOAP, das auf XML basiert, profitiert auch davon.

Andere Serialisierungsoptionen bieten eine Schema-Validierungsoption: Zum Beispiel Avro, Kryo und Protocol Buffers. Interessanterweise nutzt gRPC Protobuf, um RPC über verteilte Komponenten anzubieten:

gRPC ist eine moderne, quelloffene und hochleistungsfähige Remote Procedure Call (RPC)-Infrastruktur, die in jedem Umfeld lauffähig ist. Sie ermöglicht die effiziente Verbindung von Diensten in und über Rechenzentren mit erweiterter Unterstützung für Lastausgleich, Ablaufverfolgung, Integritätsprüfung und Authentifizierung. Sie ist auch für den letzten Teil der verteilten Rechenkraft zur Verbindung von Geräten, mobilen Anwendungen und Webbrowsern mit Backend-Diensten geeignet.

Warum gRPC?

Darüber hinaus ist Protocol ein binärer Serialisierungsmechanismus, der viel Bandbreite spart. Daher ist gRPC eine ausgezeichnete Option für die Kommunikation zwischen Systemen. Aber wenn alle Ihre Komponenten gRPC verwenden, wie können dann einfache Clients sie aufrufen? In diesem Beitrag werden wir einen gRPC-Dienst erstellen und zeigen, wie man ihn von cURL aus aufruft.

A Simple gRPC Service

Die gRPC-Dokumentation ist umfangreich, hier also ein Überblick:

  • gRPC ist ein Remote Procedure Call-Framework.
  • Es funktioniert in einer Vielzahl von Sprachen.
  • Es basiert auf Protocol Buffers:
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

  • Es ist Teil des CNCF-Portfolios und befindet sich derzeit in der Inkubationsphase.

Lassen Sie uns unseren gRPC-Dienst einrichten. Wir werden Java, Kotlin, Spring Boot und ein dediziertes gRPC-Spring-Boot-Integration-Projekt verwenden. Die Projektstruktur umfasst zwei Projekte: eines für das Modell und eines für den Code. Beginnen wir mit dem Modellprojekt.

I didn’t want something complicated. Reusing a simple example is enough: the request sends a string, and the response prefixes it with Hello. We design this model in a dedicated Protobuf schema file:

ProtoBuf

 

syntax = "proto3";                                        //1

package ch.frankel.blog.grpc.model;                       //2

option java_multiple_files = true;                        //3
option java_package = "ch.frankel.blog.grpc.model";       //3
option java_outer_classname = "HelloProtos";              //3

service HelloService {                                    //4
    rpc SayHello (HelloRequest) returns (HelloResponse) {
    }
}

message HelloRequest {                                    //5
    string name = 1;                                      //6
}

message HelloResponse {                                   //7
    string message = 1;                                   //6
}

  1. Protobuf-Definition Version
  2. Paket
  3. Java-spezifische Konfiguration
  4. Dienstdefinition
  5. Anforderungsdefinition
  6. Felddefinition: Zuerst kommt der Typ, dann der Name und schließlich die Reihenfolge.
  7. Antwortdefinition

Wir werden Maven verwenden, um den Java-Grundgerüstcode zu generieren:

XML

 

<project>
  <dependencies>
    <dependency>
      <groupId>io.grpc</groupId>                         <!--1-->
      <artifactId>grpc-stub</artifactId>
      <version>${grpc.version}</version>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>                         <!--1-->
      <artifactId>grpc-protobuf</artifactId>
      <version>${grpc.version}</version>
    </dependency>
    <dependency>
      <groupId>jakarta.annotation</groupId>              <!--1-->
      <artifactId>jakarta.annotation-api</artifactId>
      <version>1.3.5</version>
      <optional>true</optional>
    </dependency>
  </dependencies>
  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>                 <!--2-->
        <artifactId>os-maven-plugin</artifactId>
        <version>1.7.1</version>
      </extension>
    </extensions>
    <plugins>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>    <!--3-->
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>${protobuf-plugin.version}</version>
        <configuration>
          <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

  1. Compile-time Abhängigkeiten
  2. Informationen über das Betriebssystem abrufen; wird im nächsten Plugin verwendet
  3. Java-Code aus der proto-Datei generieren.

Nach der Kompilierung sollte die Struktur ungefähr wie folgt aussehen:

Wir können die Klassen in einem JAR verpacken und in einem Web-Anwendungsprojekt verwenden. Letzteres ist in Kotlin, aber nur, weil es meine Lieblings-JVM-Sprache ist.

Wir benötigen nur eine spezifische Spring Boot Starter-Abhängigkeit, um gRPC-Endpunkte mit Spring Boot zu integrieren:

XML

 

<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-server-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
</dependency>

Hier ist der wesentliche Teil:

Kotlin

 

@GrpcService                                                        //1
class HelloService : HelloServiceImplBase() {                       //2
  override fun sayHello(
      request: HelloRequest,                                        //2
      observer: StreamObserver<HelloResponse>                       //3
  ) {
    with(observer) {
      val reply = HelloResponse.newBuilder()                        //2
                               .setMessage("Hello ${request.name}") //4
                               .build()
      onNext(reply)                                                 //5
      onCompleted()                                                 //5
    }
  }
}

  1. Das grpc-server-spring-boot-starter erkennt die Annotation und wirkt sein Wunder.
  2. Referenzklassen, die im obigen Projekt generiert wurden
  3. Die Methodensignatur erlaubt einen StreamObserver-Parameter. Die Klasse stammt aus grpc-stub.jar
  4. Holen Sie die Anfrage und fügen Sie sie als Präfix für die Antwortnachricht hinzu.
  5. Spielen Sie die Ereignisse.

Wir können die Web-App jetzt mit ./mvnw spring-boot:run starten.

Testen des gRPC-Dienstes

Die gesamte Idee hinter dem Beitrag ist, dass der gRPC-Dienst mit regulären Tools nicht zugänglich ist. Um zu testen, benötigen wir jedoch immer noch ein spezielles Tool. Ich habe grpcurl gefunden. Installieren wir es und verwenden wir es, um verfügbare Dienste aufzulisten:

Shell

 

grpcurl --plaintext localhost:9090 list   #1-2

  1. Listen Sie alle verfügbaren gRPC-Dienste ohne TLS-Überprüfung.
  2. Um Konflikte zwischen gRPC und anderen Kanälen, z.B. REST, zu vermeiden, verwendet Spring Boot einen anderen Port.
Plain Text

 

ch.frankel.blog.grpc.model.HelloService   #1
grpc.health.v1.Health                     #2
grpc.reflection.v1alpha.ServerReflection  #2

  1. Der von uns definierte gRPC-Dienst
  2. Zwei zusätzliche Dienste, die vom benutzerdefinierten Starter bereitgestellt werden

Wir können auch tiefer in die Struktur des Dienstes eintauchen:

Shell

 

grpcurl --plaintext localhost:9090 describe ch.frankel.blog.grpc.model.HelloService
Java

 

service HelloService {
  rpc SayHello ( .ch.frankel.blog.grpc.model.HelloRequest ) returns ( .ch.frankel.blog.grpc.model.HelloResponse );
}

Schließlich können wir den Dienst mit Daten aufrufen:

Shell

 

grpcurl --plaintext -d '{"name": "John"}' localhost:9090 ch.frankel.blog.grpc.model.HelloService/SayHello
JSON

 

{
  "message": "Hello John"
}

Zugreifen auf den gRPC-Dienst mit regulären Tools

Stellen Sie sich vor, wir haben eine reguläre JavaScript-Client-seitige Anwendung, die auf den gRPC-Dienst zugreifen muss. Welche Alternativen gäbe es?

Der allgemeine Ansatz erfolgt über grpc-web:

A JavaScript implementation of gRPC for browser clients. For more information, including a quick start, see the gRPC-web documentation.

gRPC-Web-Clients verbinden sich über einen speziellen Proxy mit gRPC-Diensten; standardmäßig verwendet gRPC-Web Envoy.

In Zukunft erwarten wir, dass gRPC-Web in sprachspezifischen Web-Frameworks für Sprachen wie Python, Java und Node unterstützt wird. Details dazu finden sich in der Roadmap.

grpc-web

Die Beschreibung nennt eine einzige Einschränkung: Es funktioniert nur für JavaScript (Stand heute). Es gibt jedoch eine weitere. Es ist ziemlich eingreifend. Sie müssen die proto-Datei abrufen, Boilerplattencode generieren und Ihr Code aufrufen. Sie müssen dies für jede Client-Art tun. Schlimmer noch, wenn sich die proto-Datei ändert, müssen Sie den Client-Code in jedem von ihnen neu generieren.

Es gibt jedoch eine Alternative, wenn Sie eine API-Gateway verwenden. Ich werde beschreiben, wie man es mit Apache APISIX macht, aber vielleicht können andere Gateways dasselbe tun. grpc-transcode ist ein Plugin, das die Transkodierung von REST-Anrufen zu gRPC und zurück ermöglicht.

Der erste Schritt besteht darin, die proto-Datei in Apache APISIX zu registrieren:

Shell

 

curl http://localhost:9180/apisix/admin/protos/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d "{ \"content\": \"$(sed 's/"/\\"/g' ../model/src/main/proto/model.proto)\" }"

Der zweite Schritt besteht darin, eine Route mit dem oben genannten Plugin zu erstellen:

Shell

 

curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/helloservice/sayhello",                           #1
  "plugins": {
    "grpc-transcode": {
      "proto_id": "1",                                       #2
      "service": "ch.frankel.blog.grpc.model.HelloService",  #3
      "method": "SayHello"                                   #4
    }
  },
  "upstream": {
    "scheme": "grpc",
    "nodes": {
      "server:9090": 1
    }
  }
}'

  1. Definieren Sie eine granulare Route.
  2. Verweisen Sie auf die in der vorherigen Befehlszeile definierten Proto-Datei.
  3. gRPC-Dienst
  4. gRPC-Methode

Ab diesem Zeitpunkt kann jeder Client eine HTTP-Anfrage an das definierten Endpunkt stellen. Apache APISIX transcodiert den Aufruf in gRPC, leitet ihn an den definierten Dienst weiter, erhält die Antwort und transcodiert sie erneut.

Shell

 

curl localhost:9080/helloservice/sayhello?name=John
JSON

 

{"message":"Hello John"}

Im Vergleich zu grpc-web ermöglicht der Ansatz mit dem API Gateway die gemeinsame Nutzung der proto-Datei mit einem einzigen Komponenten: dem Gateway selbst.

Vorteile der Transkodierung

Ab diesem Zeitpunkt können wir die Fähigkeiten des API Gateways nutzen. Stellen Sie sich vor, wir möchten einen Standardwert, z.B. World, verwenden, wenn kein name übergeben wird. Entwickler würden es gerne im Code setzen, aber jede Änderung des Werts würde einen vollständigen Build und eine Bereitstellung erfordern. Änderungen können nahezu sofort erfolgen, wenn wir den Standardwert in der Routenverarbeitungskette des Gateways platzieren. Ändern wir unsere Route entsprechend:

Shell

 

curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/helloservice/sayhello",
  "plugins": {
    "grpc-transcode": {
      ...
    },
    "serverless-pre-function": {                    #1
      "phase": "rewrite",                           #2
      "functions" : [
        "return function(conf, ctx)                 #3
          local core = require(\"apisix.core\")
          if not ngx.var.arg_name then
            local uri_args = core.request.get_uri_args(ctx)
            uri_args.name = \"World\"
            ngx.req.set_uri_args(uri_args)
          end
        end"
      ]
    }
  },
  "upstream": {
      ...
  }
}'

  1. Allzweck-Plugin, wenn keines passt
  2. Anfrage umschreiben.
  3. Magischer Lua-Code, der den Trick anstellt

Jetzt können wir die Anfrage mit einem leeren Argument ausführen und das erwartete Ergebnis erhalten:

Shell

 

curl localhost:9080/helloservice/sayhello?name
JSON

 

{"message":"Hello World"}

Schlussfolgerung

In diesem Beitrag haben wir gRPC kurz beschrieben und erklärt, wie es der Kommunikation zwischen Diensten zugute kommt. Wir haben einen einfachen gRPC-Dienst mit Spring Boot und grpc-server-spring-boot-starter entwickelt. Es gibt jedoch einen Preis: normale Clients können den Dienst nicht zugreifen. Wir mussten auf grpcurl zurückgreifen, um ihn zu testen. Das Gleiche gilt für auf JavaScript basierende Clients – oder den Browser.

Um diese Einschränkung zu umgehen, können wir ein API Gateway nutzen. Ich habe gezeigt, wie man Apache APISIX mit dem grpc-transcode Plugin konfiguriert, um das gewünschte Ergebnis zu erzielen.

Der vollständige Quellcode für diesen Beitrag kann auf GitHub gefunden werden.

Weiterführende Schritte

Source:
https://dzone.com/articles/grpc-on-the-client-side