15 Einführung in gRPC

15.1 Einführung

gRPC (gRPC Remote Procedure Calls) ist ein modernes, leistungsfähiges Open-Source-Framework für Remote Procedure Calls (RPC), das von Google entwickelt wurde. Es basiert auf dem HTTP/2-Protokoll und verwendet Protocol Buffers (Protobuf) als Interface Definition Language (IDL) sowie als Serialisierungsmechanismus. gRPC ermöglicht eine effiziente, plattformübergreifende Kommunikation zwischen verteilten Systemen und eignet sich besonders für Microservices-Architekturen, bei denen Leistung und Skalierbarkeit entscheidend sind.

15.2 Geschichte und Entwicklung von gRPC

gRPC wurde ursprünglich von Google als eine Weiterentwicklung interner RPC-Systeme entwickelt, um die Kommunikation zwischen verschiedenen Diensten zu optimieren. Es wurde im Jahr 2015 als Open-Source-Projekt veröffentlicht und hat sich seitdem schnell als eine bevorzugte Methode für die Kommunikation in verteilten Systemen etabliert. Durch seine Unterstützung für verschiedene Programmiersprachen und die nahtlose Integration mit modernen Infrastrukturtechnologien hat gRPC eine breite Akzeptanz in der Entwicklergemeinschaft gefunden.

15.3 Grundlegende Konzepte von gRPC

  1. Protocol Buffers (Protobuf)

    Protobuf ist ein kompaktes, binäres Serialisierungsformat, das als Interface Definition Language (IDL) für gRPC dient. Es ermöglicht die Definition von Diensten und Nachrichtenstrukturen in einer .proto-Datei, die dann in verschiedene Programmiersprachen kompiliert werden können.

  2. HTTP/2

    gRPC nutzt HTTP/2 als Transportprotokoll, was Vorteile wie Multiplexing, Header-Komprimierung und bidirektionales Streaming bietet. Diese Eigenschaften tragen zur hohen Leistung und Effizienz von gRPC bei.

  3. Service Definition

    In gRPC werden Dienste durch Protobuf definiert, die Methoden und Nachrichtenstrukturen beschreiben. Diese Definitionen dienen als Vertrag zwischen Client und Server.

  4. Stub-Generierung

    Basierend auf den .proto-Dateien werden Client- und Server-Stubs generiert, die die Kommunikation abstrahieren und die Implementierung vereinfachen.

  5. Streaming

    gRPC unterstützt verschiedene Streaming-Modelle:

15.4 Vorteile von gRPC

15.5 Nachteile von gRPC

15.6 Anwendungsfälle von gRPC

15.7 Beispiel einer gRPC-Service-Definition

Um die Funktionsweise von gRPC zu veranschaulichen, betrachten wir die Definition eines einfachen Dienstes zur Verwaltung von Büchern.

15.7.1 book.proto

syntax = "proto3";

package com.example.bookservice;

service BookService {
    // Unary RPC zum Abrufen eines Buches nach ID
    rpc GetBook(GetBookRequest) returns (GetBookResponse);

    // Server Streaming RPC zum Abrufen aller Bücher
    rpc ListBooks(ListBooksRequest) returns (stream ListBooksResponse);

    // Client Streaming RPC zum Hinzufügen mehrerer Bücher
    rpc AddBooks(stream AddBooksRequest) returns (AddBooksResponse);

    // Bidirektionales Streaming RPC zum Aktualisieren von Büchern
    rpc UpdateBooks(stream UpdateBooksRequest) returns (stream UpdateBooksResponse);
}

message GetBookRequest {
    int32 id = 1;
}

message GetBookResponse {
    Book book = 1;
}

message ListBooksRequest {}

message ListBooksResponse {
    Book book = 1;
}

message AddBooksRequest {
    Book book = 1;
}

message AddBooksResponse {
    int32 success_count = 1;
}

message UpdateBooksRequest {
    Book book = 1;
}

message UpdateBooksResponse {
    string status = 1;
}

message Book {
    int32 id = 1;
    string title = 2;
    string author = 3;
}

15.8 Erläuterung der Service-Methoden

15.9 Implementierung des gRPC-Servers mit Spring Boot

Spring Boot bietet durch das Spring for gRPC-Projekt Unterstützung für die Integration von gRPC-Diensten. Hier zeigen wir eine einfache Implementierung basierend auf der oben definierten .proto-Datei.

15.9.1 Schritt 1: Abhängigkeiten hinzufügen

Fügen Sie in Ihrer pom.xml die notwendigen Abhängigkeiten hinzu:

<dependencies>
    <!-- gRPC und Protobuf Abhängigkeiten -->
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty-shaded</artifactId>
        <version>1.42.1</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>1.42.1</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>1.42.1</version>
    </dependency>
    <!-- Spring Boot Starter für gRPC -->
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-spring-boot-starter</artifactId>
        <version>2.12.0.RELEASE</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- Protobuf Maven Plugin -->
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.19.1:exe:${os.detected.classifier}</protocArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

15.9.2 Schritt 2: Generierung der Protobuf-Klassen

Nach dem Hinzufügen der Protobuf-Maven-Plugin-Konfiguration wird der Code aus der book.proto-Datei automatisch generiert, wenn das Projekt gebaut wird.

15.9.3 Schritt 3: Implementierung des Service

Erstellen Sie eine Service-Implementierung, die die generierten gRPC-Stub-Klassen erweitert.

15.9.3.1 BookServiceImpl.java

package com.example.bookservice;

import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@GrpcService
public class BookServiceImpl extends BookServiceGrpc.BookServiceImplBase {

    private final ConcurrentHashMap<Integer, Book> bookRepository = new ConcurrentHashMap<>();
    private final AtomicInteger idCounter = new AtomicInteger(1);

    @Override
    public void getBook(GetBookRequest request, StreamObserver<GetBookResponse> responseObserver) {
        Book book = bookRepository.get(request.getId());
        if (book != null) {
            GetBookResponse response = GetBookResponse.newBuilder().setBook(book).build();
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } else {
            responseObserver.onError(new Throwable("Buch nicht gefunden"));
        }
    }

    @Override
    public void listBooks(ListBooksRequest request, StreamObserver<ListBooksResponse> responseObserver) {
        for (Book book : bookRepository.values()) {
            ListBooksResponse response = ListBooksResponse.newBuilder().setBook(book).build();
            responseObserver.onNext(response);
        }
        responseObserver.onCompleted();
    }

    @Override
    public StreamObserver<AddBooksRequest> addBooks(final StreamObserver<AddBooksResponse> responseObserver) {
        return new StreamObserver<AddBooksRequest>() {
            int successCount = 0;

            @Override
            public void onNext(AddBooksRequest addBooksRequest) {
                Book book = addBooksRequest.getBook();
                int id = idCounter.getAndIncrement();
                Book newBook = Book.newBuilder()
                        .setId(id)
                        .setTitle(book.getTitle())
                        .setAuthor(book.getAuthor())
                        .build();
                bookRepository.put(id, newBook);
                successCount++;
            }

            @Override
            public void onError(Throwable t) {
                // Fehlerbehandlung
            }

            @Override
            public void onCompleted() {
                AddBooksResponse response = AddBooksResponse.newBuilder()
                        .setSuccessCount(successCount)
                        .build();
                responseObserver.onNext(response);
                responseObserver.onCompleted();
            }
        };
    }

    @Override
    public StreamObserver<UpdateBooksRequest> updateBooks(final StreamObserver<UpdateBooksResponse> responseObserver) {
        return new StreamObserver<UpdateBooksRequest>() {
            @Override
            public void onNext(UpdateBooksRequest updateBooksRequest) {
                Book book = updateBooksRequest.getBook();
                if (bookRepository.containsKey(book.getId())) {
                    bookRepository.put(book.getId(), book);
                    UpdateBooksResponse response = UpdateBooksResponse.newBuilder()
                            .setStatus("Erfolgreich aktualisiert")
                            .build();
                    responseObserver.onNext(response);
                } else {
                    UpdateBooksResponse response = UpdateBooksResponse.newBuilder()
                            .setStatus("Buch nicht gefunden")
                            .build();
                    responseObserver.onNext(response);
                }
            }

            @Override
            public void onError(Throwable t) {
                // Fehlerbehandlung
            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    }
}

15.9.4 Erläuterung des Codes

15.10 Implementierung des gRPC-Clients mit Spring Boot

Für den Zugriff auf den gRPC-Dienst erstellen wir einen einfachen Client. Dies kann innerhalb derselben Anwendung oder in einer separaten Anwendung erfolgen.

15.10.1 BookClient.java

package com.example.bookclient;

import com.example.bookservice.*;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class BookClient {

    private final BookServiceGrpc.BookServiceBlockingStub blockingStub;
    private final BookServiceGrpc.BookServiceStub asyncStub;

    public BookClient(String host, int port) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // Für Entwicklungszwecke; in der Produktion SSL/TLS verwenden
                .build();
        blockingStub = BookServiceGrpc.newBlockingStub(channel);
        asyncStub = BookServiceGrpc.newStub(channel);
    }

    public void getBook(int id) {
        GetBookRequest request = GetBookRequest.newBuilder().setId(id).build();
        try {
            GetBookResponse response = blockingStub.getBook(request);
            System.out.println("Buch: " + response.getBook().getTitle() + " von " + response.getBook().getAuthor());
        } catch (Exception e) {
            System.err.println("Fehler beim Abrufen des Buches: " + e.getMessage());
        }
    }

    public void listBooks() {
        ListBooksRequest request = ListBooksRequest.newBuilder().build();
        try {
            blockingStub.listBooks(request).forEachRemaining(response -> {
                System.out.println("Buch: " + response.getBook().getTitle() + " von " + response.getBook().getAuthor());
            });
        } catch (Exception e) {
            System.err.println("Fehler beim Auflisten der Bücher: " + e.getMessage());
        }
    }

    public void addBooks() throws InterruptedException {
        final CountDownLatch finishLatch = new CountDownLatch(1);

        StreamObserver<AddBooksResponse> responseObserver = new StreamObserver<AddBooksResponse>() {
            @Override
            public void onNext(AddBooksResponse value) {
                System.out.println("Erfolgreich hinzugefügt: " + value.getSuccessCount() + " Bücher");
            }

            @Override
            public void onError(Throwable t) {
                System.err.println("Fehler beim Hinzufügen der Bücher: " + t.getMessage());
                finishLatch.countDown();
            }

            @Override
            public void onCompleted() {
                finishLatch.countDown();
            }
        };

        StreamObserver<AddBooksRequest> requestObserver = asyncStub.addBooks(responseObserver);
        try {
            for (int i = 1; i <= 3; i++) {
                Book book = Book.newBuilder()
                        .setTitle("Neues Buch " + i)
                        .setAuthor("Autor " + i)
                        .build();
                AddBooksRequest request = AddBooksRequest.newBuilder().setBook(book).build();
                requestObserver.onNext(request);
                Thread.sleep(100); // Simuliert Verzögerung
            }
        } catch (RuntimeException e) {
            requestObserver.onError(e);
            throw e;
        }
        requestObserver.onCompleted();

        if (!finishLatch.await(1, TimeUnit.MINUTES)) {
            System.err.println("addBooks kann nicht abgeschlossen werden");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BookClient client = new BookClient("localhost", 9090);
        client.addBooks();
        client.listBooks();
        client.getBook(1);
    }
}

15.10.2 Erläuterung des Codes

15.11 Best Practices für den Einsatz von gRPC

  1. Verwendung von Protobuf-Schemata

  2. Optimierung der Leistung

  3. Sicherheitsimplementierung

  4. Fehlerbehandlung

  5. Monitoring und Logging

  6. Versionierung von Diensten

  7. Skalierung und Load Balancing

15.12 Integration von gRPC mit Spring Boot

Spring Boot erleichtert die Integration von gRPC-Diensten durch Bibliotheken wie grpc-spring-boot-starter. Diese Bibliotheken bieten automatisierte Konfigurationen und vereinfachen die Erstellung und Verwaltung von gRPC-Servern und -Clients innerhalb von Spring-Anwendungen.

15.12.1 Schritt 1: Konfiguration des gRPC-Servers

In der application.properties können Sie gRPC-spezifische Einstellungen vornehmen, wie z.B. den Port:

grpc.server.port=9090

15.12.2 Schritt 2: Implementierung der gRPC-Services

Wie im obigen Beispiel gezeigt, implementieren Sie Ihre Dienste durch Erweiterung der generierten Stub-Klassen und Kennzeichnung der Implementierung mit @GrpcService.

15.12.3 Schritt 3: Starten und Testen des Servers

Starten Sie die Spring Boot-Anwendung, die den gRPC-Server enthält. Nutzen Sie den gRPC-Client, um Anfragen an den Server zu senden und die Funktionalität zu testen.

15.13 tl;dr

gRPC ist ein leistungsstarkes RPC-Framework, das auf HTTP/2 und Protocol Buffers basiert und effiziente, plattformübergreifende Kommunikation zwischen verteilten Systemen ermöglicht. Es bietet hohe Leistung, vielseitige Streaming-Modelle und umfassende Sprachunterstützung, eignet sich jedoch am besten für Anwendungen, die komplexe, leistungsintensive Kommunikationsanforderungen haben. Mit Spring Boot können Entwickler gRPC-Dienste nahtlos integrieren und von den Vorteilen moderner Infrastrukturtechnologien profitieren.