Slide 1

Slide 1 text

for PHPerKaigi 2021 ブラウザから始めるgRPC      〜 gRPC-WebにPHPを添えて 2021年3⽉27⽇ (⼟) 株式会社Nextat 中榮健⼆ Nextat Inc. 1

Slide 2

Slide 2 text

⾃⼰紹介 from 京都 - 中榮健⼆ (なかえけんじ) - twitter: @n_1215  - 株式会社Nextat 取締役 - Laravel中⼼にECサイトやシステム開発 - ここ最近はLambdaを使ったりUnityを使ったりPHP以外の仕事も増加 Nextat Inc. 2

Slide 3

Slide 3 text

発表概要 1. gRPCとは 2. gRPCとPHP 3. gRPC-Webとは 4. サンプルアプリ ブラウザ側実装 / C#サーバ実装 5. サンプルアプリ PHPサーバ実装 6. まとめ Nextat Inc. 3

Slide 4

Slide 4 text

1. gRPCとは Nextat Inc. 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

gRPCの通信プロトコルとRPCの種類 over HTTP/2を前提に策定されている データがバイナリ TCPコネクションの使い回し ネットワークリソース利⽤効率 サーバ・クライアント間のRPCは4種類 HTTP/2だから双⽅向通信が可能 Nextat Inc. 6

Slide 7

Slide 7 text

(1) Unary RPCs 1リクエスト / 1レスポンス 多くのWebアプリ開発者が慣れ親しんだもの Nextat Inc. 7

Slide 8

Slide 8 text

(2) Server streaming RPCs 1リクエストに対しサーバが複数回のレスポンスを返す 送信完了までクライアントがストリームからメッセージを読む サーバープッシュ Nextat Inc. 8

Slide 9

Slide 9 text

(3) Client streaming RPCs 複数回のリクエストを送信しサーバが1回レスポンスを返す 送信完了までサーバがストリームからメッセージを読む データのアップロードなどに利⽤可能 Nextat Inc. 9

Slide 10

Slide 10 text

(4) Bidirectional streaming RPCs リクエストとレスポンスが多対多 双⽅向ストリーミング 順序に決まりはない チャットなどに利⽤可能 Nextat Inc. 10

Slide 11

Slide 11 text

1-2. Protocol Buffers (Protobuf) gRPCが利⽤するIDL 兼 メッセージ交換⽤のバイナリフォーマット IDL = Interface Definition Language インタフェース記述⾔語 プログラミング⾔語に依存しない .protoファイルから⾔語実装を⾃動⽣成できる gRPCとは独⽴して使うこともできる 例)REST API + リクエストボディやレスポンスボディにProtocol Buffers Nextat Inc. 11

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

protoc (Protocol Compiler) .protoファイルをprotocでビルドして各⾔語の実装を⽣成する 拡張が容易 例) PHPのgRPCクライアントの⽣成 $ protoc -I . --php_out=. --grpc_out=. \ --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin ./service.proto Nextat Inc. 13

Slide 14

Slide 14 text

gRPCのメリット パフォーマンス、ネットワークリソースの効率的利⽤ 双⽅向通信 クライアント側の実装が⾔語ごとに⾃動⽣成できる サーバ側もスキーマが定まるので型のエラーが起きにくい コンパイル時のエラーないし静的解析で検知しやすい 定義ファイルの内容がそのままAPIの定義となる 実装変更の前に定義ファイルの変更が必要になるため、両者が乖離しにくい Nextat Inc. 14

Slide 15

Slide 15 text

gRPCのデメリット 開発中に通信の中⾝を確認しにくい HTTP/2は実質暗号化通信が必須 バイナリフォーマット 定義ファイルから各⾔語の実装をビルドするのが少し⼿間 Nextat Inc. 15

Slide 16

Slide 16 text

gRPCの使いどころ マイクロサービスのバックエンドでのサービス間通信 公式クライアントしかないスマホアプリ⽤のサーバ ゲームサーバ チャットやバトルシステムの通信⽅式をまとめられる ex. MagicOnion ブラウザでの利⽤を想定する⼀般向けの公開APIにそのまま⽤いるのは⾟い Nextat Inc. 16

Slide 17

Slide 17 text

2. gRPCとPHP Nextat Inc. 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

PHP界隈のgRPC事情 PHPによるgRPCクライアントは公式サポート リクエスト使い捨てのPHPの通常の動作⽅式では⻑寿命なStreamingができない ドキュメント(PHP⽤): サーバ側はNode.JSを使ってね Google Groupでのとある発⾔ : PHPでgRPCサーバ作っても特殊な構成になるし あまり役に⽴たないよね(意訳)Serers in PHP? 参考: なぜPHPはgRPCサーバーがサポートされていないのか? Nextat Inc. 19

Slide 20

Slide 20 text

"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

Slide 21

Slide 21 text

だが、PHPでのサーバ実装は駄⽬……っ! 公式の対応にPHPer涙⽬ Nextat Inc. 21

Slide 22

Slide 22 text

PHPでgRPCサーバを実現する試み 通常のPHPの構成でダメなら通常とは違う構成でやればいい PHP-FPM以外の選択肢 Swoole、RoadRunnerなど、新しいアプリケーションサーバ・HTTPサーバによ る試験的な実装が⾒つかる 資料の後半で⼀部を紹介 Nextat Inc. 22

Slide 23

Slide 23 text

PHP界隈におけるgRPCの現状 マイクロサービスなどの⽂脈で、PHPでgRPCクライアントを実装するのは問題ない が、通常のPHP+JavaScriptによるWeb開発の代替技術としての採⽤には難 (1) PHPによるgRPCサーバの実装が困難 Unary RPCは問題ないものの、Streaming RPCがネックになる 実装状況を調査した結果は2年前とあまり変わらず (2) gRPCはそのままではブラウザから利⽤しにくい 解決策 (1) → Unary RPCのみで我慢するか、Streamingに対応した実装を待つ (2) → gRPCに準じた通信をブラウザから利⽤するための gRPC-Web Nextat Inc. 23

Slide 24

Slide 24 text

3. gRPC-Webとは ようやく本編 Nextat Inc. 24

Slide 25

Slide 25 text

gRPC-Web ブラウザからgRPCに準じた通信を扱うためのJSライブラリ+プロトコル https://github.com/grpc/grpc-web gRPC公式GitHubリポジトリのdoc/PROTOCOL-WEB.mdに仕様あり gRPC over HTTP2 との主な違い HTTP/2特有の振る舞いやフレームには依存せず、HTTP/1でも利⽤できる HTTP/1やクロスブラウザサポートのため、Base64などのテキストエンコードを ⾏う場合がある RPCの⽅式はUnary RPC、Server Streaming RPCのみ Client Streaming、Bidirectional Streamingは未サポート Nextat Inc. 25

Slide 26

Slide 26 text

Content-Typeは2種類 application/grpc-web-text base64でテキストにエンコードされる 今回のサンプルアプリはこちらを利⽤ application/grpc-web デフォルトのフォーマットはProtobuf(application/grpc-web+proto) バイナリフォーマットよりJSONのほうがブラウザ上の処理が軽いという話 もあり、application/grpc-web+jsonなども想定されている Nextat Inc. 26

Slide 27

Slide 27 text

Proxy gRPCサーバとブラウザが直接通信するわけではない 間にgRPCとgRPC-Webを変換するProxyが必要 Envoy Nginx moduleもある 参考: gRPC-Web is Generally Available Nextat Inc. 27

Slide 28

Slide 28 text

gRPC-Webのロードマップ roadmap.md streaming-roadmap.md ブラウザのStreamサポートに応じてClient StreamingやBidi Streamingを追加 Envoy不要のIn-process Proxy(Python、Java、Node、C++ etc.) ブラウザがネイティブにgRPCを扱えるようになれば、gRPC-Webはオプション に WHATWG Streams API (Chromiumベースのブラウザではもう使える) fetch() upload streaming (ChromeのOrigin Trialには⼊っている) もう少し時間は掛かりそう Nextat Inc. 28

Slide 29

Slide 29 text

4. サンプルアプリ ブラウザ側実装 + C#サーバ実装 Nextat Inc. 29

Slide 30

Slide 30 text

お題: 簡易チャット チャットルームにメッセージを投稿できる 他の⼈が投稿したメッセージをリアルタイムに確認できる チャットルームは⼀つだけ 過去ログの閲覧やチャットメッセージの永続化は特に考えない Nextat Inc. 30

Slide 31

Slide 31 text

RPCの設計とProtobuf定義(スキーマ駆動は良いぞ!) 双⽅向ストリーミングは(今の)gRPC-Webでは使えないが、なくても問題ない メッセージ投稿はUnary RPC メッセージ購読にServer Streaming RPC 定義ファイルは次ページ Nextat Inc. 31

Slide 32

Slide 32 text

// 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

Slide 33

Slide 33 text

ブラウザ側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

Slide 34

Slide 34 text

ブラウザ側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 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

Slide 35

Slide 35 text

サーバ側C#実装(1) ASP.NET Core を利⽤ gRPCとgRPC-Webに対応したパッケージあり gRPC-Web実装はIn-process Proxy⽅式 サーバ内にgRPC-Webへの変換を組み込む形式なので、Envoy不要 C#のHTTPサーバ(Kestrel)⾃体がgRPC-Web形式でやりとり ドキュメントを⽤意しているMicrosoftはgRPC-Webガチ勢なのか? そういえば クライアント側:TypeScript + サーバ側:C# ですね アンダース・ヘルスバーグ先⽣に感謝の祈りを捧げながら実装します Nextat Inc. 35

Slide 36

Slide 36 text

サーバ側C#実装(2) ChatService.csが⾃動⽣成できるので、処理を埋めていく データストアを使わない簡易的なPub/Subに Reactive Extensions (Rx)を利⽤ ChatService.csのメッセージ投稿処理 public override Task 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

Slide 37

Slide 37 text

ChatService.csのメッセージ購読処理 public override Task Subscribe( Empty request, IServerStreamWriter responseStream, ServerCallContext context ) { return _chatMessageSubject.Do(chatMessage => { responseStream.WriteAsync(chatMessage); }).ToTask(); } Nextat Inc. 37

Slide 38

Slide 38 text

動いた! 最低ノルマ達成。最悪PHP実装は動かなくても許されるはず! Nextat Inc. 38

Slide 39

Slide 39 text

content-type: application/grpc-web-text Nextat Inc. 39

Slide 40

Slide 40 text

5. サンプルアプリ PHPサーバ実装 Nextat Inc. 40

Slide 41

Slide 41 text

"PHPでgRPCサーバ" に再挑戦 In-process Proxy⽅式にする場合もStreamingは必須 どうせならEnvoyでProxyすることにして、gRPCサーバに再挑戦 Nextat Inc. 41

Slide 42

Slide 42 text

候補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

Slide 43

Slide 43 text

今回は⻭ブラシ(PHP)で船舶を磨くことが ⽬的なのであって 船舶を効率良く磨く⽅法は求めていない spiral/php-grpc も Streaming周りの進展はなさそう だったため、⾒送り Nextat Inc. 43

Slide 44

Slide 44 text

候補2. Swoole https://www.swoole.co.uk/ コルーチンベースの⾮同期並⾏実⾏ライブラリ(PHP拡張/C⾔語) PHPのコードでHTTPサーバを実装することが可能。HTTP/2にも対応 Unary RPCのサンプル実装が存在 上記を元に2回レスポンスのデータを送ればOKかと思いきや⽅法が不明 \Swoole\Http\Response::end() は名前の通り2回以上は呼べない チャンク⽤の\Swoole\Http\Response::write() はHTTP/2⾮対応 \Swoole\Http\Server::send()でバイナリデータを送ればいけるのか? Nextat Inc. 44

Slide 45

Slide 45 text

困った…… どちらかで⾏けるだろうと思っていた またServer Streaming RPCの前に涙を飲むしかないのか? Nextat Inc. 45

Slide 46

Slide 46 text

そんなところに救世主が! Nextat Inc. 46

Slide 47

Slide 47 text

候補3. Amp https://amphp.org/ イベント駆動な並⾏処理のフレームワーク Event Loop、Promise、Coroutine、Stream PHPのコードでHTTPサーバを実装可能。HTTP/2対応 HTTP/2のHeader圧縮(HPACK)などもPHPで書かれている → PHPで制御できる部分がかなり多そう Nextat Inc. 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

それだよAmp!!! キミにきめた!!! 時間を空けてテキストデータを複数のDATAフレームで送ることが可能だと確認 あとはレスポンスをgRPCの仕様に合わせれば…… Nextat Inc. 49

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Length-Prefixed-Message を⽣成 pack()と⽂字列操作関数を使って簡易的に実装 $serializedMessage = $message->serializeToString(); $lengthPrefixedMessage = pack('c', 0x00) . pack('N', strlen($serializedMessage)) . $serializedMessage; これをAmpのHTTPサーバからStreamのデータとして返す Nextat Inc. 51

Slide 52

Slide 52 text

Wiresharkによる通信内容の確認 HTTP/2の通信はそのままでは確認が困難なので、Wiresharkの出番 有名なネットワークプロトコル解析⽤のソフト TLS鍵情報のログを参照すれば、ブラウザからのHTTP/2通信内容を⾒られる 参考: HTTP/2 over TLS の通信をダンプする⽅法 gRPCにも対応している protoファイルの検索パスを指定すればより詳細な情報も出せる模様 参考: https://grpc.io/blog/wireshark/ ⾮常にお世話になりました Nextat Inc. 52

Slide 53

Slide 53 text

⼀旦 GET / に対しStreamでレスポンスを返し、ブラウザとWiresharkで確認 Nextat Inc. 53

Slide 54

Slide 54 text

(GRPC)(PROTOBUF) Wiresharkのお墨付き! これで勝ったも同然! Nextat Inc. 54

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Envoyの設定 EnvoyのProxy設定で少しハマった ローカル環境だったので⾃⼰署名証明書の設定に⼿間取る Envoyのタイムアウト系の設定ですぐにメッセージの購読が途切れたりしていた Nextat Inc. 57

Slide 58

Slide 58 text

完成したもの 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

Slide 59

Slide 59 text

PHPサーバ側のログ Nextat Inc. 59

Slide 60

Slide 60 text

x-powered-by: PHP8.0.0 Nextat Inc. 60

Slide 61

Slide 61 text

6. まとめ gRPCとProtocol Buffersを使ったスキーマ駆動はいいぞ gRPC-Web⽤のプロキシを⽤意すればブラウザからでも使える In-process ProxyかEnvoyが⼿軽 gRPC-Web⽤に⾃動コード⽣成するのは簡単 将来的にはgRPCがネイティブにブラウザから使えるようになる展望 PHPのgRPCサーバは公式サポートがない が、Server Streaming RPCに対応したPHP製サーバは作れる PHP製gRPCサーバとgRPC-Webの組み合わせも夢じゃない C#のgRPC-Webサーバ実装はめっちゃ楽でした Nextat Inc. 61

Slide 62

Slide 62 text

最近の国内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

Slide 63

Slide 63 text

次のどなたかの発表を楽しみにしています!! Nextat Inc. 63

Slide 64

Slide 64 text

PR: ITエンジニアを募集しています 株式会社Nextat nextat.co.jp 受託開発 業務システム、ECサイト、スマホアプリ... 本社は京都ですが、東京・名古屋などリモートワーク実績あり 設計の話に付き合ってくれる⽅⼤歓迎! Nextat Inc. 64

Slide 65

Slide 65 text

ご清聴ありがとうございました Nextat Inc. 65