Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ブラウザから始めるgRPC 〜 gRPC-WebにPHPを添えて

ブラウザから始めるgRPC 〜 gRPC-WebにPHPを添えて

PHPerKaigi 2021 の発表資料です

サンプルコード: https://github.com/n1215/grpc-web-chat

n1215

March 27, 2021
Tweet

More Decks by n1215

Other Decks in Programming

Transcript

  1. ⾃⼰紹介 from 京都 - 中榮健⼆ (なかえけんじ) - twitter: @n_1215  -

    株式会社Nextat 取締役 - Laravel中⼼にECサイトやシステム開発 - ここ最近はLambdaを使ったりUnityを使ったりPHP以外の仕事も増加 Nextat Inc. 2
  2. 発表概要 1. gRPCとは 2. gRPCとPHP 3. gRPC-Webとは 4. サンプルアプリ ブラウザ側実装

    / C#サーバ実装 5. サンプルアプリ PHPサーバ実装 6. まとめ Nextat Inc. 3
  3. 1-1. gRPCとは Google製のRPCフレームワーク → gpc.io RPC = Remote Procedure Call

    (遠隔⼿続呼出) 'g' の意味はリリース毎に違う てっきり Google の g かと 1.0 'g' stands for 'gRPC', 1.1 'g' stands for 'good', ... ハイパフォーマンス Nextat Inc. 5
  4. 1-2. Protocol Buffers (Protobuf) gRPCが利⽤するIDL 兼 メッセージ交換⽤のバイナリフォーマット IDL = Interface

    Definition Language インタフェース記述⾔語 プログラミング⾔語に依存しない .protoファイルから⾔語実装を⾃動⽣成できる gRPCとは独⽴して使うこともできる 例)REST API + リクエストボディやレスポンスボディにProtocol Buffers Nextat Inc. 11
  5. Protocol Buffers 定義ファイル の書式 Message → リクエストやレスポンスのデータ構造を記述 Service → RPCの定義を記述

    // service.proto syntax = "proto3"; package service; service Echo { rpc Ping (Message) returns (Message) { } } message Message { string msg = 1; } Nextat Inc. 12
  6. PHP界隈のgRPC事情 PHP界隈ではgRPC関連の発表は少ない(⽇本のカンファレンスで年1くらい) php grpc-client in phpcon2018 PHPによるgRPCクライアント実装のお話 "PHPでのgRPCサーバはできない” PHPでもgRPCサーバを⽴てたいだけの⼈⽣だった(Laravel JP

    Conf 2019) "PHPerには⻭ブラシで船舶を磨く⾃由が与えられている" サーバ側もUnary RPCは可能だが、"ストリーミングはまだ試してません" RoadRunner、 spiral/php-grpc PHPでgRPCってどこまでいけるの?(PHP Conference 2019) "Unary RPCはできる" "Streaming RPCはまだはやかった" Swoole、 Hyperf Nextat Inc. 18
  7. "Works across languages and platforms" Automatically generate idiomatic client and

    server stubs for your service in a variety of languages and platforms https://grpc.io/ Nextat Inc. 20
  8. // chat.proto syntax = "proto3"; option csharp_namespace = "GrpcWebChat"; import

    "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; package GrpcWebChat; service Chat { rpc SendMessage (SendMessageRequest) returns (google.protobuf.Empty); rpc Subscribe (google.protobuf.Empty) returns (stream ChatMessage); } message SendMessageRequest { string body = 1; string name = 2; } message ChatMessage { string body = 1; string name = 2; google.protobuf.Timestamp date = 3; } Nextat Inc. 32
  9. ブラウザ側TypeScript実装(1) mode は grpc-web-text を選択 mode: grpc-web はServer Streaming⾮対応 ブラウザがStreamingのデータをバイナリで受け取ることができないため

    gRPC クライアントコード⽣成 protoc -I . -I /opt/include ./chat.proto \ --grpc-web_out=import_style=typescript,mode=grpcwebtext:/out/grpc-web-text \ --js_out=import_style=commonjs:/out/grpc-web-text \ --plugin=protoc-gen-grpc-web=/usr/local/bin/grpc_web_plugin gRPC-Web⽤のprotocプラグインを⽤いて⾃動コード⽣成 JavaScript + TypeScript型定義(d.ts)、TypeScriptのコードが⽣成される Nextat Inc. 33
  10. ブラウザ側TypeScript実装(2) ⾃動⽣成されたChatClientを利⽤するだけ // メッセージ購読の処理のイメージ import { ChatMessage } from './pb-web/chat_pb'

    import { ChatClient } from './pb-web/ChatServiceClientPb' const client = new ChatClient('https://localhost:9000') const messageStream = client .subscribe(new Empty()) as ClientReadableStream<ChatMessage> messageStream.on('data', (chatMessage: ChatMessage) => { console.log('data', chatMessage) }) messageStream.on('status', (status: Status) => { console.log('status', status) }) messageStream.on('end', () => { console.log('stream end', new Date()) }) Nextat Inc. 34
  11. サーバ側C#実装(2) ChatService.csが⾃動⽣成できるので、処理を埋めていく データストアを使わない簡易的なPub/Subに Reactive Extensions (Rx)を利⽤ ChatService.csのメッセージ投稿処理 public override Task<Empty>

    SendMessage(SendMessageRequest request, ServerCallContext context) { var chatMessage = new ChatMessage { Body = request.Body, Name = request.Name, Date = new Timestamp { Seconds = DateTimeOffset.Now.ToUnixTimeSeconds() } }; _chatMessageSubject.OnNext(chatMessage); return Task.FromResult(new Empty()); } Nextat Inc. 36
  12. ChatService.csのメッセージ購読処理 public override Task Subscribe( Empty request, IServerStreamWriter<ChatMessage> responseStream, ServerCallContext

    context ) { return _chatMessageSubject.Do(chatMessage => { responseStream.WriteAsync(chatMessage); }).ToTask(); } Nextat Inc. 37
  13. 候補1. Spiral Framework (RoadRunner)     https://spiral.dev/ RoadRunner (Golang製のPHPアプリケーションサーバ) を利⽤

    前⾯のGoサーバがgRPCのリクエストを受け、PHPのworkerに振る 開発元は Spiral Scout プレゼン資料(ロシア語): RoadRunner Unaryに加えてServer Streaming RPCに対応している(?) https://spiral.dev/docs/grpc-streaming gRPCのサービスはPHPではなくGo実装で、PHPは裏のJobとして動くのみ これをPHP実装と⾔い張るのは厳しい Nextat Inc. 42
  14. Amp HTTP Serverによるストリーミング amphp/http-server のリポジトリにStreamのコード例あり(下に抜粋) examples/stream.php StreamとProducerを使って複数回データを送っている ⾮同期処理の記述におけるyieldが特徴的 $server =

    new HttpServer($servers, new CallableRequestHandler(function (Request $request) { // We stream the response here, one line every 100 ms. return new Response(Status::OK, [ "content-type" => "text/plain; charset=utf-8", ], new IteratorStream(new Producer(function (callable $emit) { for ($i = 0; $i < 30; $i++) { yield new Delayed(100); yield $emit("Line {$i}\r\n"); } }))); }), $logger, (new Options)->withoutCompression()); Nextat Inc. 48
  15. gRPCのレスポンスの仕様 公式の gRPC over HTTP2 を⾒る ABNF(拡張バッカス・ナウア記法)で記述されている Response → (Response-Headers

    *Length-Prefixed-Message Trailers) / Trailers-Only Length-Prefixed-Message → Compressed-Flag Message-Length Message Compressed-Flag → 0 / 1 # encoded as 1 byte unsigned integer Message-Length → {length of Message} # encoded as 4 byte unsigned integer (big endian) Message → *{binary octet} Protocol Buffersのバイナリをそのまま送るだけではダメ レスポンスボディに相当するLength-Prefixed-Messageを構成する必要がある 参考: https://gkuga.hatenablog.com/entry/2019/12/14/005653 Nextat Inc. 50
  16. Length-Prefixed-Message を⽣成 pack()と⽂字列操作関数を使って簡易的に実装 $serializedMessage = $message->serializeToString(); $lengthPrefixedMessage = pack('c', 0x00)

    . pack('N', strlen($serializedMessage)) . $serializedMessage; これをAmpのHTTPサーバからStreamのデータとして返す Nextat Inc. 51
  17. C#実装を参考にChatServiceを分離 先程のLenght-Prefix-Messageの処理とAmpのStream関連クラスを使い ServerStreamWriterを作成 // ChatService の メッセージ購読の処理 public function subscribe(GPBEmpty

    $request, ServerStreamWriter $streamWriter) { $this->chatMessageSubject->subscribe( function (ChatMessage $chatMessage) use ($streamWriter) { $this->logger->debug("emit", [$chatMessage->getBody()]); $streamWriter->write($chatMessage); }, null, function () use ($streamWriter) { $streamWriter->complete(); } ); return new Success(); } Nextat Inc. 55
  18. RPCごとのルーティングを追加 Amp HTTP Server⽤のルータを追加 各RPCは POST /{ サービス名}/{ メソッド名} に対応

    $router = new Amp\Http\Server\Router(); $chatService = new ChatService( new Rx\Subject\Subject(), $logger ); $router->addRoute( 'POST', '/GrpcWebChat.Chat/SendMessage', new SendMessageRequestHandler($chatService, $requestBodyDeserializer, $responseFactory) ); $router->addRoute( 'POST', '/GrpcWebChat.Chat/Subscribe', new SubscribeRequestHandler($chatService, $responseFactory) ); Nextat Inc. 56
  19. 完成したもの https://github.com/n1215/grpc-web-chat/tree/main/server-amphp 先に作ったブラウザ側のgRPC-Webのチャットクライアントアプリで動作を確認 Unary RPC、Server Streaming RPCが可能 厳密なgRPCサーバにはなっていない Client Streaming、Bidirectional

    Streamingは未検証 grpc-timeoutなど、リクエストヘッダ周りは完全無視 サーバ側のgRPC Serviceのコード⾃動⽣成には⼿を出していない protobufのMessageはPHPクライアント⽤のものを利⽤ Nextat Inc. 58
  20. 最近の国内PHP系カンファレンスのgRPC関連発表 php grpc-client in phpcon2018 (2018/12) gRPCクライアント実装は可能だが、通常はPHPでgRPCサーバはできない PHPでもgRPCサーバを⽴てたいだけの⼈⽣だった (2019/02) サーバ側もUnary

    RPCは可能だが、Streaming RPCは未検証 PHPでgRPCってどこまでいけるの?(2019/12) Streaming RPCはまだはやかった 本発表 (2021/03) Server Streaming RPCもいけるやん! ← イマココ to be continued... Nextat Inc. 62