Grpc als Alternative zu REST

Wenn es darum geht, eine Technologie auszuwählen, um eine API zu bauen, wird heutzutage oft ohne weiter zu evaluieren REST gewählt. Es gibt auch viele gute Gründe, warum REST der De-Facto-Standard für APIs ist. Es gibt jedoch Fälle, in denen auch andere Technologien einen Blick wert sind.

Im folgenden stelle ich GRPC als Alternative vor.


Während bei REST JSON verwendet wird, um eine sprachenübergreifende Datenrepräsentation zu haben, wird bei GRPC Protobuffer benutzt. Hierbei handelt es sich um ein Binärformat, welches eine erheblich bessere Übertragungseffizienz erreicht. Ein weiterer Unterschied ist, dass es eine führende .proto Datei gibt, welche die Schnittstelle stark typisiert festlegt. Dies ermöglicht eine deutlich genauere Spezifikation einer Schnittstelle und verringert somit die Fehlerquellen. Für alle offiziell unterstützten Sprachen existiert aufbauend eine Codegenerator, der einen Server- oder Client-Stub generieren kann.

Wie sieht nun so eine .proto Datei aus? Hier ein einfaches Beispiel:

syntax = "proto3"; 
message Point {
   int32 x = 1;
   int32 y = 2;
   string label = 3;
 }  

Hierbei fällt zunächst auf, dass jedes Feld eine Zahl zugewiesen bekommt. Beim Serialisieren werden lediglich diese Zahlen übertragen, und somit der Feldname als String in der Nachricht eingespart. Hieraus ergibt sich allerdings die Notwendigkeit, bei Änderungen von alten Versionen, diese kompatibel zu gestalten.

Ebenfalls an dieser Stelle erwähnenswert: Protobuffer selbst hat erstmal keinen fest definierten Übertragungsweg, sondern definiert nur, wie ein Nachrichtenobjekt zu einem ByteArray wird. Dieses könnte beispielsweise dann auch per POST Request übertragen werden.

Um nun GRPC zu verwenden, werden noch einige Einträge mehr in der .proto Datei benötigt:

syntax = "proto3"; 
package point;
message Point {
    int32 x = 1;
    int32 y = 2;
    string label = 3;
} 

message PointResponse {
   string reply = 1;
}

service PointService{
   rpc uploadPoint(Point) returns (PointResponse);
}

Mithilfe eines kleinen Gradle Projekts ist es dann einfach möglich, den GRPC Code zu generieren:

// benötigte Plugins für protobuf, Java und Eclipse
 plugins {
   id "com.google.protobuf" version "0.8.7"
   id "java"
   id "eclipse"
 }
protobuf {
     //Protobuf anweisen, auch grpc ServiceStubs zu generieren
     plugins {
         grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.17.1" }
     }
     generateProtoTasks {
         all()*.plugins { grpc {} }
     }
 }
//Maven Central hinzufügen, um damit die Abhängigkeiten aufzulösen
 repositories {
     mavenCentral()
 }
dependencies {
     //protobuf selbst
     compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.17.1'
     //benötigte Supportklassen um GRPC stubs zu generieren
     compile group: 'io.grpc', name: 'grpc-stub', version: '1.17.1'
     //generator für die Stubs
     compile group: 'io.grpc', name: 'protoc-gen-grpc-java', version: '1.17.1'
     //implementierung für den grpc Endserver, hier auf Basis von netty
     runtime group: 'io.grpc', name: 'grpc-netty', version: '1.17.1'
 }
//In Eclipse den generierten GRPC Code als source verwenden, damit das Projekt in Eclipse bauen kann
 eclipse {
     classpath {
         file.whenMerged { cp ->
             cp.entries.add( new org.gradle.plugins.ide.eclipse.model.SourceFolder('build/generated/source/proto/main/java', null) )
             cp.entries.add( new org.gradle.plugins.ide.eclipse.model.SourceFolder('build/generated/source/proto/main/grpc', null) )
         }
     }
 }


Und so sieht das Ergebnis aus:

Abschließend, um das Generierte zu verwenden, wird noch ein Server und ein Client benötigt:

public class Test {
     public static void main(String[] args) throws InterruptedException, ExecutionException, IOException {
         testServer();
         testClient();
     }
private static void testServer() throws InterruptedException, ExecutionException, IOException {
     BindableService serviceImpl = new PointServiceImplBase() {
         public void uploadPoint(point.PointServiceOuterClass.Point request,
                 io.grpc.stub.StreamObserver responseObserver) {
             //hier ist jetzt unserer tatsächlicher Eigener Code der diesen Service implementiert
             responseObserver.onNext(PointResponse.newBuilder().setReply("hello " + request).build()); 
             //onCompleted muss umbedingt aufgerufen werden, um mitzuteilen, dass die Antwort vollständig fertig ist.
             responseObserver.onCompleted();
         }
     };
     ServerBuilder.forPort(50051).addService(serviceImpl).build().start();
     System.out.println("started server");
 }
private static void testClient() throws InterruptedException, ExecutionException {
     ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1", 50051).usePlaintext().build();
     PointServiceFutureStub service = PointServiceGrpc.newFutureStub(channel);
     System.out.println("started client");
     ListenableFuture answer = service.uploadPoint(PointServiceOuterClass.Point.newBuilder().setX(1).setY(2).build());
     System.out.println(answer.get().getReply());
 }
}

Auffällig ist hier, dass der generierte Server es einem offen lässt, sowohl synchronen als auch asynchronen Code zu schreiben, dank des verwendeten StreamObservers.

Wenn man den Code ausführt, erhält man folgende Ausgabe:

started server
started client
hello x: 1
y: 2
Short URL for this post: https://wp.me/p4nxik-3eJ
This entry was posted in SOA / Webservices and tagged , , . Bookmark the permalink.

Leave a Reply