Browser-Backend-Teilung überbrücken mit gRPC-Web
Grace Collins
Solutions Engineer · Leapcell

Einleitung
In der sich entwickelnden Landschaft der Webentwicklung hat sich die Nachfrage nach Hochleistungs-, Echtzeit- und effizienter Kommunikation zwischen Clients und Servern intensiviert. Während traditionelle RESTful-APIs lange Zeit das Rückgrat bildeten, werden ihre Einschränkungen, insbesondere in Bezug auf Serialisierungs-Overhead und mangelnde robuste Typsicherheit, für moderne, komplexe Anwendungen immer offensichtlicher. Hier kommt gRPC ins Spiel, ein leistungsfähiges, Open-Source-RPC-Framework von Google. gRPC nutzt Protocol Buffers für effiziente Serialisierung und HTTP/2 für Multiplexing und Streaming, was zu einer erheblich verbesserten Leistung und Entwicklererfahrung führt. Allerdings war eine direkte Verbindung für Webbrowser zur Kommunikation mit gRPC-Diensten historisch eine Herausforderung, bedingt durch architekturelle Unterschiede zwischen HTTP/1.1 (vorherrschend in Browsern) und HTTP/2 (erforderlich für gRPC). Diese Herausforderung führte zur Entstehung von gRPC-Web, einer entscheidenden Technologie, die browserbasierten Anwendungen die Nutzung der Leistung von gRPC ermöglicht. Dieser Artikel untersucht die Mechanismen, Vorteile und praktische Implementierung von gRPC-Web und demonstriert, wie es nahtlos die Lücke zwischen Frontend-Browsern und gRPC-Backend-Diensten schließt.
Die Kernkonzepte verstehen
Bevor wir uns den Besonderheiten von gRPC-Web widmen, ist es wichtig, die zugrunde liegenden Technologien zu verstehen, die es nutzt:
- gRPC (gRPC Remote Procedure Call): Ein modernes Open-Source-Hochleistungs-RPC-Framework, das in jeder Umgebung ausgeführt werden kann. Es ermöglicht Client- und Serveranwendungen eine transparente Kommunikation und erleichtert den Aufbau vernetzter Systeme. gRPC basiert auf der Idee, einen Dienst zu definieren, der angibt, welche Methoden mit ihren Parametern und Rückgabetypen remote aufgerufen werden können. Es verwendet Protocol Buffers als seine Interface Definition Language (IDL) und Austauschformat für Nachrichten.
- Protocol Buffers (Protobuf): Ein sprachneutraler, plattformneutraler, erweiterbarer Mechanismus zur Serialisierung strukturierter Daten. Sie definieren Ihre Datenstruktur in einer
.proto
-Datei, und Protobuf-Compiler generieren Code in verschiedenen Sprachen (Go, Java, Python, C++, C#, JavaScript usw.) für die Serialisierung und Deserialisierung, wodurch eine starke Typsicherheit gewährleistet wird. - HTTP/2: Die zweite Hauptversion des HTTP-Netzwerkprotokolls. Wesentliche Verbesserungen gegenüber HTTP/1.1 sind binäre Rahmen, Multiplexing (Senden mehrerer Anfragen/Antworten über eine einzige Verbindung), Server Push und Header-Komprimierung, die alle zu erheblichen Leistungssteigerungen beitragen. gRPC stützt sich hauptsächlich auf HTTP/2 für seine effiziente Kommunikation.
- gRPC-Web: Eine proxy-basierte Lösung, die es Webbrowsern ermöglicht, mit gRPC-Diensten zu interagieren. Browser kommunizieren hauptsächlich über HTTP/1.1 und verfügen nicht über native Unterstützung für HTTP/2-Funktionen wie Anhänge und beliebigen Datenrahmenzugriff, die gRPC kritisch nutzt. gRPC-Web funktioniert durch Hinzufügen einer kleinen Codeebene auf der Clientseite (JavaScript) und erfordert oft einen Proxy (wie Envoy oder einen benutzerdefinierten gRPC-Web-Proxy), um HTTP/1.1-Anfragen vom Browser in HTTP/2-Anfragen zu übersetzen, die vom gRPC-Backend verstanden werden.
Das Prinzip der gRPC-Web-Kommunikation
Das Kernprinzip hinter gRPC-Web ist die Übersetzung. Ein Browser, der durch HTTP/1.1 eingeschränkt ist, kann nicht direkt das HTTP/2-Protokoll sprechen, das gRPC benötigt. gRPC-Web führt eine clientseitige Bibliothek ein, die gRPC-Anfragen in ein Format umwandelt, das mit HTTP/1.1 kompatibel ist (typischerweise POST-Anfragen mit einem bestimmten Content-Type
), und diese an einen Proxy-Server sendet, der diese Anfragen dann in HTTP/2 für das gRPC-Backend übersetzt. Die Antwort vom gRPC-Backend folgt dem umgekehrten Weg: HTTP/2-Antwort an den Proxy, übersetzt zu HTTP/1.1 für den Browser und schließlich von der gRPC-Web-Clientbibliothek deserialisiert.
Dieser Prozess umfasst:
- Protocol Buffer Definition: Definition Ihrer Servicemethoden und Nachrichtenstrukturen in einer
.proto
-Datei. - Code-Generierung: Verwendung von
protoc
(dem Protocol Buffer Compiler) zusammen mit dem Pluginprotoc-gen-grpc-web
zur Generierung von clientseitigem JavaScript-Code (oder TypeScript-Definitionen) und serverseitigen gRPC-Artefakten. - Clientseitige gRPC-Web-Bibliothek: Eine JavaScript-Bibliothek, die den generierten Code verwendet, um gRPC-Anfragen in einem browserfreundlichen HTTP/1.1-Format zu erstellen.
- gRPC-Web-Proxy: Ein Vermittlerserver (wie Envoy oder ein dedizierter gRPC-Web-Proxy), der zwischen dem Browser und dem gRPC-Backend sitzt. Er übernimmt die Protokollübersetzung von HTTP/1.1 (vom Browser) zu HTTP/2 (zum gRPC-Backend) und umgekehrt.
Implementierungsschritte mit einem Beispiel
Dies veranschaulichen wir anhand eines einfachen "Greeter"-Servicediagramms.
1. Definieren Sie den Protobuf-Dienst
Erstellen Sie eine greet.proto
-Datei:
syntax = "proto3"; option go_package = "./;greet"; // Für Go-Backend package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); rpc SayHellosStream (HelloRequest) returns (stream HelloReply); // Beispiel für Server-Streaming } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
2. Generieren Sie serverseitigen Code (Go-Beispiel)
Normalerweise würden Sie protoc
verwenden, um Go-Code für Ihren gRPC-Server zu generieren:
protoc --go_out=. --go-grpc_out=. greet.proto
Dies generiert greet.pb.go
und greet_grpc.pb.go
.
3. Implementieren Sie den gRPC-Server (Go-Beispiel)
package main import ( "context" "fmt" "log" "net" "time" "google.golang.org/grpc" pb "your_module_path/greet" // Ersetzen Sie dies durch Ihren tatsächlichen Modulpfad ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, } func (s *server) SayHellosStream(in *pb.HelloRequest, stream pb.Greeter_SayHellosStreamServer) error { log.Printf("Received streaming request for: %v", in.GetName()) for i := 0; i < 5; i++ { resp := &pb.HelloReply{Message: fmt.Sprintf("Streaming Hello %s, message %d", in.GetName(), i+1)} if err := stream.Send(resp); err != nil { return err } time.Sleep(500 * time.Millisecond) } return nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
4. Generieren Sie clientseitigen gRPC-Web-Code (JavaScript-Beispiel)
Sie müssen grpc-web
und protoc-gen-grpc-web
installieren:
npm install grpc-web go install github.com/grpc/grpc-web/protoc-gen-grpc-web@latest
Generieren Sie dann JavaScript-Code für den Browser:
protoc -I=. greet.proto --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
--js_out=import_style=commonjs,binary:.
: Generiert Nachrichtenklassen.--grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
: Generiert Service-Client- und Stub-Code für gRPC-Web.grpcwebtext
ist ein gängiger Modus, der Nachrichten als Basis64-codierte Protobufs im Textformat sendet, was Proxys die Fehlersuche erleichtert. Dergrpcweb
-Modus sendet binäre Rohdaten.
Dieser Befehl generiert greet_pb.js
und GreetServiceClientPb.js
(für den gRPC-Web-Client).
5. Konfigurieren Sie einen gRPC-Web-Proxy (Envoy-Beispiel)
Envoy ist eine beliebte Wahl für einen gRPC-Web-Proxy. Hier ist eine grundlegende envoy.yaml
-Konfiguration:
admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } # Port für Browser-Traffic filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: grpc_backend # Dies ist entscheidend für gRPC-Web grpc_web: {} cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: grpc_backend connect_timeout: 0.25s type: LOGICAL_DNS # Oder STATIC, wenn die Backend-IP fest ist lb_policy: ROUND_ROBIN # Das eigentliche gRPC-Backend läuft auf Port 50051 load_assignment: cluster_name: grpc_backend endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # IP Ihres gRPC-Backends port_value: 50051 # HTTP/2 für die Kommunikation mit dem gRPC-Backend aktivieren http2_protocol_options: {}
Envoy starten: envoy -c envoy.yaml
6. Erstellen Sie den Browser-Client (JavaScript/HTML-Beispiel)
<!DOCTYPE html> <html> <head> <title>gRPC-Web Greeter</title> <script src="https://unpkg.com/google-protobuf@3.12.2/google-protobuf.js"></script> <script src="https://unpkg.com/grpc-web@1.3.1/grpc-web.js"></script> </head> <body> <h1>gRPC-Web Greeter</h1> <input type="text" id="nameInput" placeholder="Enter your name" value="World"/> <button onclick="sayHello()">Say Hello</button> <button onclick="sayHelloStream()">Say Hello Stream</button> <div id="response"></div> <script type="module"> import {GreeterClient} from './GreetServiceClientPb.js'; import {HelloRequest, HelloReply} from './greet_pb.js'; const client = new GreeterClient('http://localhost:8080', null, null); // Envoy-Proxy-URL window.sayHello = () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); client.sayHello(request, {}, (err, response) => { const responseDiv = document.getElementById('response'); if (err) { console.error(err); responseDiv.innerText = `Error: ${err.message}`; return; } responseDiv.innerText = `Greeting: ${response.getMessage()}`; }); }; window.sayHelloStream = () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); const stream = client.sayHellosStream(request, {}); const responseDiv = document.getElementById('response'); responseDiv.innerText = 'Streaming greetings...\n'; stream.on('data', (response) => { responseDiv.innerText += `\nStreaming Greeting: ${response.getMessage()}`; }); stream.on('end', () => { responseDiv.innerText += '\nStreaming finished.'; }); stream.on('error', (err) => { console.error(err); if (err.code === 2) { // UNKNOWN Fehlercode für Stream-Reset responseDiv.innerText += '\nError: Stream wurde unerwartet beendet. Details siehe Konsole.'; } else { responseDiv.innerText += `\nError: ${err.message}`; } }); }; </script> </body> </html>
Legen Sie greet_pb.js
, GreetServiceClientPb.js
und diese index.html
-Datei in ein Verzeichnis und servieren Sie sie mit einem einfachen Webserver (z.B. python3 -m http.server 8000
).
Wenn Sie nun http://localhost:8000
im Browser öffnen und auf die Schaltflächen klicken, sendet Ihr Browser HTTP/1.1-Anfragen an Envoy auf Port 8080. Envoy übersetzt diese dann in HTTP/2 und leitet sie an Ihren gRPC-Server auf Port 50051 weiter. Die Antworten fließen auf demselben Weg zurück.
Anwendungsszenarien
gRPC-Web ist in verschiedenen Szenarien besonders vorteilhaft:
- Single-Page Applications (SPAs): Für datenintensive SPAs, die mit Frameworks wie React, Angular oder Vue erstellt wurden, bietet gRPC-Web eine überlegene Leistung und Typsicherheit im Vergleich zu REST, reduziert Boilerplate-Code und verbessert die Wartbarkeit.
- Microservice-Architekturen: Wenn Ihr Backend aus gRPC-Microservices besteht, bietet gRPC-Web ein konsistentes Kommunikationsparadigma vom Frontend bis hinüber zum Service Mesh.
- Echtzeit-Dashboards und Analysen: Server-Streaming, ermöglicht durch HTTP/2 und unterstützt durch gRPC-Web, eignet sich hervorragend zum Pushen von Echtzeit-Updates an Dashboards ohne den Overhead von Pollings oder komplexen WebSocket-Implementierungen für einfache Stream-Anforderungen.
- Interne Tools: Für interne Entwicklertools oder Admin-Panels, die stark mit gRPC-Backends interagieren, kann gRPC-Web die Entwicklung erheblich beschleunigen und die Codequalität verbessern.
Fazit
gRPC-Web löst auf geniale Weise das Problem, modernen Webbrowsern die direkte Kommunikation mit Hochleistungs-gRPC-Backend-Diensten zu ermöglichen. Durch die Nutzung eines Proxys zur Überbrückung der HTTP/1.1- und HTTP/2-Teilung bringt es die Vorteile der starken Typisierung von Protocol Buffers und der Effizienz, Streaming-Fähigkeiten und Leistung von gRPC direkt in browserbasierte Anwendungen. Dies ermöglicht es Entwicklern, robustere, leistungsfähigere und wartbarere Webanwendungen zu erstellen und den Client-Server-Kommunikationsstack tatsächlich unter dem gRPC-Paradigma zu vereinheitlichen.