gRPC-Webによるブラウザとバックエンドの断絶の解消
Grace Collins
Solutions Engineer · Leapcell

はじめに
進化し続けるWeb開発の状況において、クライアントとサーバー間の高性能、リアルタイム、効率的な通信に対する需要は高まっています。従来のRESTful APIは長らくバックボーンとして機能してきましたが、特にシリアライゼーションのオーバーヘッドや堅牢な型安全性がないことに関して、その限界は最新の複雑なアプリケーションにとってますます明らかになっています。そこで登場するのが、Googleが開発した強力なオープンソースのユニバーサルRPCフレームワークであるgRPCです。gRPCは、Protocol Buffersを効率的なシリアライゼーションに、HTTP/2を多重化とストリーミングに活用し、パフォーマンスと開発者エクスペリエンスを大幅に向上させます。しかし、WebブラウザがgRPCサービスと直接通信するための橋は、HTTP/1.1(ブラウザで主流)とgRPCが必要とするHTTP/2とのアーキテクチャ上の違いから、歴史的に課題となってきました。この課題こそが、ブラウザベースのアプリケーションがgRPCのパワーを直接活用できるようにする重要なテクノロジーであるgRPC-Webを生み出したのです。この記事では、gRPC-Webのメカニズム、メリット、および実践的な実装を探り、ブラウザのフロントエンドとgRPCバックエンドサービス間のギャップをどのようにシームレスに橋渡しするかを実証します。
コアコンセプトの理解
gRPC-Webの詳細に入る前に、それが活用する基盤となるテクノロジーを理解することが不可欠です。
- gRPC (gRPC Remote Procedure Call): どんな環境でも実行できる、最新のオープンソースで高性能なRPCフレームワークです。クライアントとサーバーアプリケーションが透過的に通信できるようにし、接続されたシステムの構築を容易にします。gRPCは、サービスを定義し、そのパラメータと戻り値の型でリモートから呼び出せるメソッドを指定するという考えに基づいています。Interface Definition Language (IDL) およびメッセージ交換フォーマットとしてProtocol Buffersを使用します。
- Protocol Buffers (Protobuf): 言語ニュートラルでプラットフォームニュートラルな、構造化データをシリアライズするための拡張可能なメカニズムです。
.proto
ファイルでデータ構造を定義すると、Protobufコンパイラは、シリアライズおよびデシリアライズのためにさまざまな言語(Go、Java、Python、C++、C#、JavaScriptなど)でコードを生成し、強力な型安全性を確保します。 - HTTP/2: HTTPネットワークプロトコルの2番目のメジャーバージョンです。HTTP/1.1に対する主な改善点には、バイナリフレーミング、多重化(単一接続で複数のリクエスト/レスポンスを送信)、サーバープッシュ、ヘッダー圧縮などがあり、これらすべてが大幅なパフォーマンス向上に貢献しています。gRPCは主に効率的な通信のためにHTTP/2に依存しています。
- gRPC-Web: WebブラウザがgRPCサービスと対話できるようにする、プロキシベースのソリューションです。ブラウザは主にHTTP/1.1で通信し、gRPCがクリティカルに使用するトレーラーや任意のデータフレームアクセスなどのHTTP/2機能のネイティブサポートが不足しています。gRPC-Webは、クライアント側(JavaScript)に少量のコードレイヤーを追加して機能し、多くの場合、プロキシ(EnvoyやカスタムgRPC-Webプロキシなど)を使用して、ブラウザからのHTTP/1.1リクエストをgRPCバックエンドが理解するHTTP/2リクエストに翻訳する必要があります。
gRPC-Web通信の原則
gRPC-Webの根本的な原則は翻訳です。HTTP/1.1に制約されたブラウザは、gRPCが必要とするHTTP/2プロトコルを直接話すことができません。gRPC-Webは、gRPCリクエストをHTTP/1.1と互換性のある形式(通常は特定の Content-Type
を持つPOSTリクエスト)にマーシャルし、プロキシサーバーに送信するクライアントサイドライブラリを導入します。このプロキシは、これらのリクエストをHTTP/2に翻訳してgRPCバックエンドに転送します。gRPCバックエンドからの応答は逆のパスをたどります。HTTP/2応答がプロキシに送られ、HTTP/1.1に翻訳されてブラウザに送信され、最終的にgRPC-Webクライアントライブラリによってアンマーシャルされます。
このプロセスには以下が含まれます。
- Protocol Buffer定義:
.proto
ファイルでサービスメソッドとメッセージ構造を定義します。 - コード生成:
protoc
(Protocol Bufferコンパイラ)とprotoc-gen-grpc-web
プラグインを使用して、クライアントサイドJavaScriptコード(またはTypeScript定義)とサーバーサイドgRPCアーティファクトを生成します。 - クライアントサイドgRPC-Webライブラリ: 生成されたコードを活用して、ブラウザフレンドリーなHTTP/1.1形式でgRPCリクエストを構築するJavaScriptライブラリです。
- gRPC-Webプロキシ: ブラウザとgRPCバックエンドの間に配置される仲介サーバー(Envoyや専用のgRPC-Webプロキシなど)です。ブラウザからのHTTP/1.1とgRPCバックエンドへのHTTP/2間のプロトコル変換を処理します。
例による実装手順
簡単な「Greeter」サービス例でこれを説明しましょう。
1. Protobufサービスの定義
greet.proto
ファイルを作成します。
syntax = "proto3"; option go_package = "./;greet"; // Goバックエンド用 package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); rpc SayHellosStream (HelloRequest) returns (stream HelloReply); // サーバーストリーミングの例 } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
2. サーバーサイドコードの生成 (Goの例)
通常、protoc
を使用してgRPCサーバー用のGoコードを生成します。
protoc --go_out=. --go-grpc_out=. greet.proto
これにより greet.pb.go
および greet_grpc.pb.go
が生成されます。
3. gRPCサーバーの実装 (Goの例)
package main import ( "context" "fmt" "log" "net" "time" "google.golang.org/grpc" pb "your_module_path/greet" // 実際のモジュールパスに置き換えてください ) 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. gRPC-Webクライアントサイドコードの生成 (JavaScriptの例)
grpc-web
と protoc-gen-grpc-web
をインストールする必要があります。
npm install grpc-web go install github.com/grpc/grpc-web/protoc-gen-grpc-web@latest
次に、ブラウザ用のJavaScriptコードを生成します。
protoc -I=. greet.proto --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
--js_out=import_style=commonjs,binary:.
: メッセージクラスを生成します。--grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
: gRPC-Web用のサービスクライアントとスタブコードを生成します。grpcwebtext
は、メッセージをbase64エンコードされたProtobufとしてテキスト形式で送信する一般的なモードであり、プロキシがデバッグしやすくなります。grpcweb
モードは生のバイナリを送信します。
これにより greet_pb.js
と GreetServiceClientPb.js
(gRPC-Webクライアント用)が生成されます。
5. gRPC-Webプロキシの設定 (Envoyの例)
EnvoyはgRPC-Webプロキシの一般的な選択肢です。以下は基本的なenvoy.yaml
構成です。
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 } # ブラウザトラフィック用のポート 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 # これは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 # バックエンドIPが固定されている場合はSTATIC lb_policy: ROUND_ROBIN # 実際のgRPCバックエンドはポート50051で実行されます load_assignment: cluster_name: grpc_backend endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # gRPCバックエンドのIP port_value: 50051 # gRPCバックエンド通信のためにHTTP/2を有効にする http2_protocol_options: {}
Envoyを起動します: envoy -c envoy.yaml
6. ブラウザクライアントの作成 (JavaScript/HTMLの例)
<!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プロキシ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エラーコード responseDiv.innerText += '\nError: Stream stopped unexpectedly. Check console for details.'; } else { responseDiv.innerText += `\nError: ${err.message}`; } }); }; </script> </body> </html>
greet_pb.js
、GreetServiceClientPb.js
、そしてこのindex.html
ファイルをディレクトリに配置し、シンプルなWebサーバー(例: python3 -m http.server 8000
)で提供します。
これで、ブラウザでhttp://localhost:8000
を開き、ボタンをクリックすると、ブラウザはポート8080のEnvoyにHTTP/1.1リクエストを送信します。EnvoyはこれをHTTP/2に翻訳してポート50051のgRPCサーバーに転送します。応答は同じように流れてきます。
ユースケース
gRPC-Webは、特にさまざまなシナリオで有益です。
- シングルページアプリケーション (SPA): React、Angular、Vueなどのフレームワークで構築されたデータ集約型のSPAでは、gRPC-WebはRESTよりも優れたパフォーマンスと型安全性を提供し、ボイラープレートを削減し、保守性を向上させます。
- マイクロサービスアーキテクチャ: バックエンドがgRPCマイクロサービスで構成されている場合、gRPC-Webはフロントエンドからサービスメッシュ全体まで、一貫した通信パラダイムを提供します。
- リアルタイムダッシュボードと分析: HTTP/2とgRPC-Webによってサポートされるサーバーストリーミングは、ポーリングや単純なストリーミング要件のための複雑なWebSocket実装のオーバーヘッドなしに、ダッシュボードにリアルタイムの更新をプッシュするのに最適です。
- 社内ツール: gRPCバックエンドと大量にやり取りする社内開発者ツールや管理パネルの場合、gRPC-Webは開発を大幅にスピードアップし、コード品質を向上させることができます。
結論
gRPC-Webは、最新のWebブラウザが高性能gRPCバックエンドサービスと直接通信できるようにするという問題を巧みに解決します。プロキシを使用してHTTP/1.1とHTTP/2のギャップを橋渡しすることで、Protocol Buffersの強力な型付けとgRPCの効率性、ストリーミング機能、パフォーマンスのメリットをブラウザベースのアプリケーションに直接もたらします。これにより、開発者はより堅牢でパフォーマンスが高く、保守性の高いWebアプリケーションを構築でき、クライアントサーバー通信スタックをgRPCパラダイムの下で真に統合できます。