16 Funktionsweise des gRPC-Protokolls

16.1 Einführung

gRPC (gRPC Remote Procedure Calls) ist ein modernes, leistungsfähiges Framework für die Kommunikation zwischen verteilten Systemen. Es baut auf dem HTTP/2-Protokoll auf und nutzt Protocol Buffers (Protobuf) als Interface Definition Language (IDL) sowie als Serialisierungsmechanismus. In diesem Kapitel wird die detaillierte Funktionsweise von gRPC erläutert, einschließlich der zugrunde liegenden Technologien, der Architektur und der Kommunikationsmodelle.

16.2 Grundlagen von gRPC

  1. Protocol Buffers (Protobuf)

    Protobuf ist ein binäres Serialisierungsformat, das eine effiziente und plattformunabhängige Datenübertragung ermöglicht. Es dient als Interface Definition Language (IDL) für die Definition von Diensten und Nachrichtenstrukturen in gRPC.

    Beispiel einer Protobuf-Datei:

    syntax = "proto3";
    
    package com.example.bookservice;
    
    service BookService {
        rpc GetBook(GetBookRequest) returns (GetBookResponse);
        rpc ListBooks(ListBooksRequest) returns (stream ListBooksResponse);
    }
    
    message GetBookRequest {
        int32 id = 1;
    }
    
    message GetBookResponse {
        Book book = 1;
    }
    
    message ListBooksRequest {}
    
    message ListBooksResponse {
        Book book = 1;
    }
    
    message Book {
        int32 id = 1;
        string title = 2;
        string author = 3;
    }
  2. HTTP/2

    gRPC nutzt HTTP/2 als Transportprotokoll, was verschiedene Vorteile gegenüber HTTP/1.1 bietet:

16.3 Architektur von gRPC

Die gRPC-Architektur besteht aus mehreren Schlüsselkomponenten:

  1. Client

    Der Client initiiert die Kommunikation, indem er RPC-Aufrufe an den Server sendet. Er verwendet die generierten Stub-Klassen, um Methoden des definierten Dienstes aufzurufen.

  2. Server

    Der Server implementiert die definierten Dienste und Methoden. Er empfängt RPC-Aufrufe vom Client, verarbeitet sie und sendet die entsprechenden Antworten zurück.

  3. Stub

    Stubs sind clientseitige Proxy-Klassen, die die Methoden des Dienstes kapseln. Sie abstrahieren die Netzwerkkommunikation und ermöglichen es dem Entwickler, Methodenaufrufe wie lokale Methoden zu behandeln.

  4. Protobuf

    Protobuf-Dateien definieren die Struktur der Nachrichten und die Dienste. Sie werden verwendet, um sowohl den Client als auch den Server zu generieren.

16.4 Kommunikationsmodelle in gRPC

gRPC unterstützt verschiedene Kommunikationsmodelle, die unterschiedliche Anforderungen an die Datenübertragung und Interaktion zwischen Client und Server erfüllen:

  1. Unary RPC

    Ein einfaches Anfrage-Antwort-Muster, bei dem der Client eine Anfrage sendet und der Server eine einzelne Antwort zurückgibt.

  2. Server Streaming RPC

    Der Client sendet eine einzelne Anfrage und erhält eine stream von Antworten vom Server.

    Diagramm: Server Streaming RPC

    sequenceDiagram
        participant Client
        participant Server
    
        Client->>Server: ListBooks(ListBooksRequest)
        Server-->>Client: ListBooksResponse (Stream)
  3. Client Streaming RPC

    Der Client sendet eine stream von Anfragen an den Server und erhält eine einzelne Antwort zurück.

  4. Bidirektionales Streaming RPC

    Sowohl der Client als auch der Server können gleichzeitig streams von Nachrichten senden und empfangen.

16.5 Implementierung von gRPC in Spring Boot

Die Integration von gRPC in Spring Boot erfolgt durch die Verwendung von Bibliotheken wie grpc-spring-boot-starter, die eine nahtlose Einbindung und Konfiguration von gRPC-Servern und -Clients ermöglichen.

16.5.1 Schritt-für-Schritt-Anleitung zur Implementierung eines gRPC-Servers

  1. Protobuf-Datei definieren

    Erstellen Sie eine .proto-Datei, die die Dienste und Nachrichten definiert. Ein Beispiel wurde bereits in den Grundlagen gezeigt.

  2. Abhängigkeiten hinzufügen

    Fügen Sie die notwendigen Abhängigkeiten zu Ihrer pom.xml 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>
  3. Generierung der Protobuf-Klassen

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

  4. Service-Implementierung erstellen

    Implementieren Sie den definierten Dienst, indem Sie die generierte Stub-Klasse erweitern und die Methoden überschreiben.

    Beispiel: 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();
                }
            };
        }
    }

Erläuterung des Codes:**

- **@GrpcService**: Kennzeichnet die Klasse als gRPC-Service, der von Spring Boot erkannt und registriert wird.
- **BookServiceImpl**: Implementiert die Methoden des definierten `BookService`.
- **bookRepository**: Eine einfache In-Memory-Datenbank zur Speicherung der Bücher.
- **getBook**: Implementiert den Unary RPC zum Abrufen eines Buches nach ID.
- **listBooks**: Implementiert den Server Streaming RPC zum Auflisten aller Bücher.
- **addBooks**: Implementiert den Client Streaming RPC zum Hinzufügen mehrerer Bücher.
- **updateBooks**: Implementiert den Bidirektionalen Streaming RPC zum Aktualisieren von Büchern.
  1. gRPC-Client erstellen

    Um den gRPC-Server zu nutzen, muss ein Client erstellt werden, der die generierten Stub-Klassen verwendet, um RPC-Aufrufe zu tätigen.

    Beispiel: 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);
        }
    }

    Erläuterung des Codes:

Diagramm: gRPC-Kommunikationsfluss

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: GetBook(GetBookRequest)
    Server-->>Client: GetBookResponse

    Client->>Server: ListBooks(ListBooksRequest)
    Server-->>Client: ListBooksResponse (Stream)

    Client->>Server: AddBooks(AddBooksRequest) (Stream)
    Server-->>Client: AddBooksResponse

    Client->>Server: UpdateBooks(UpdateBooksRequest) (Stream)
    Server-->>Client: UpdateBooksResponse (Stream)

16.6 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

  8. Dokumentation

16.7 tl;dr

gRPC ist ein modernes RPC-Framework, das auf HTTP/2 und Protocol Buffers basiert und eine effiziente, plattformübergreifende Kommunikation zwischen verteilten Systemen ermöglicht. Es unterstützt verschiedene Kommunikationsmodelle, bietet hohe Leistung und umfassende Sprachunterstützung. Durch die Integration mit Spring Boot können Entwickler gRPC-Dienste nahtlos implementieren und von den Vorteilen moderner Infrastrukturtechnologien profitieren. Das Verständnis der Funktionsweise von gRPC ist essenziell für die Entwicklung leistungsfähiger und skalierbarer Microservices-Architekturen.