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

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

n1215
PRO
March 27, 2021

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

PHPerKaigi 2021 の発表資料です

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

n1215
PRO

March 27, 2021
Tweet

More Decks by n1215

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. 1. gRPCとは
    Nextat Inc. 4

    View Slide

  5. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. 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

    View Slide

  13. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. 2. gRPCとPHP
    Nextat Inc. 17

    View Slide

  18. 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

    View Slide

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

    View Slide

  20. "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

    View Slide

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

    View Slide

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

    View Slide

  23. 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

    View Slide

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

    View Slide

  25. 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

    View Slide

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

    View Slide

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

    View Slide

  28. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. サーバ側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

    View Slide

  36. サーバ側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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. 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

    View Slide

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

    View Slide

  50. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  61. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide