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

Goで通信周りのコードを自動生成している話/ColoplTech-09-01

COLOPL Inc.
December 01, 2022

 Goで通信周りのコードを自動生成している話/ColoplTech-09-01

※資料内の参照リンクを選択し閲覧する場合は、ダウンロードをお願いいたします

\積極的に技術発信を行なっております/
▽ Twitter/COLOPL_Tech
https://twitter.com/colopl_tech

▽ connpassページ
http://colopl.connpass.com

▽ COLOPL Tech Blog
http://blog.colopl.dev

COLOPL Inc.

December 01, 2022
Tweet

More Decks by COLOPL Inc.

Other Decks in Technology

Transcript

  1. 氏名  : 部署  : 2 自己紹介 • 2020年4月新卒入社 • prizm(内製リアルタイムエンジン)

    チームに所属 Ryo 技術基盤本部第2バックエンドエンジニア部
  2. 3 • コロプラではいくつかのケースで Go を利用している • Go を使う中で HTTP や内製プロトコルでの通信を実装する必要がある

    • そうした通信部分の実装は量も多く似たような作業が多いので面倒 ⇒一部自動化しているのでその話をします 今回話すこと APIサーバー リアルタイムサーバー ユーザークライアント 負荷試験 bot 今回の対象 HTTP 内製プロトコル ※状態の同期などで  この経路が必要な場合がある
  3. • Request / Response のボディは io.Reader になるので バイト列のハンドリングが必要 5 Go

    で HTTP を叩く cl := &http.Client{} // リクエスト文字列を io.Reader に変換する必要がある req, err := http.NewRequest( "POST", "http://example.com/hoge", bytes.NewBuffer([]byte("{\"name\":\"alice\"")), ) if err != nil { /* エラーハンドリング */ } resp, err := cl.Do(req) if err != nil { /* エラーハンドリング */ } defer resp.Body.Close() // resp.Body.Read() などしてレスポンスを読み込む
  4. • Request / Response の内容は JSON など一定の形式が定義されて いるのでstruct との相互変換をしたい ⇒struct

    の定義が必要! 6 Go で JSON な HTTP API を叩く // レスポンスと対応する構造体を用意する type Response struct { ID int `json:"id"` Name string `json:"name"` } resp, err := cl.Get("http://example.com/user/1") if err != nil { /* エラーハンドリング */ } defer resp.Body.Close() // encoding/json を使ってレスポンスの中身を構造体に変換 res := &Response{} d := json.NewDecoder(resp.Body) err = d.Decode(res) if err != nil { /* エラーハンドリング */ }
  5. • API がたくさんあると struct を用意するのが面倒 ◦ 100〜の API に対して req

    / res を調べて struct を書くのはとても大変 ◦ 変更に追従する必要もあり更に大変 7 Req / Res に対応する struct を定義する……? type Response struct { ID int `json:"id"` Name string `json:"name"` ... ... ... ... } type Response struct { ID int `json:"id"` Name string `json:"name"` ... ... ... ... } type Response struct { ID int `json:"id"` Name string `json:"name"` ... ... ... ... } type Response struct { ID int `json:"id"` Name string `json:"name"` ... ... ... ... } type Response struct { ID int `json:"id"` Name string `json:"name"` ... ... ... ... } type FooResponse struct { ID int `json:"id"` Name string `json:"name"` ... ... ... ... } ・ ・ ・ ・
  6. • コロプラでは API 周りのコード(PHP / C#)を YAML を用いた定義で自動 生成する仕組みを内製していたので Go

    クライアントの自動生成に流用した 8 採用した解決策 • リクエストのスキーマ • レスポンスのスキーマ • エンドポイント • データ型 • Enum • …… • リクエストの構造体 • レスポンスの構造体 • エンドポイントに 対応する関数 • データ型を表す構造体 • Enum を表す型と値 • …… YAML Go
  7. 1. YAML を読み込んで構造体にマッピングする 9 定義ファイルから自動生成する流れ type requestSpec struct { Params

    map[string]*paramSpec `yaml:"params"` } // r: io.Reader dec := yaml.NewDecoder(r) var req *requestSpec if err := dec.Decode(&req); err != nil { return err }
  8. 2. 読み込んだファイルの内容を Go 標準のテンプレートに流し込んで Go のソースコードを生成 10 定義ファイルから自動生成する流れ apiRequestTemplate =

    ` type {{ $apiName }}Request struct { {{- range $paramName, $param := $action.Request.Params }} {{ $paramName | toCamel}} {{ $param | paramToGoType }} ` + "`" + `json:"{{ $paramName | toLowerCamel }}"` + "`" + ` {{- end }} ` tmpl, err := newTemplate(apiRequestTemplate) if err != nil { /* エラーハンドリング */ } // req は YAML から読み込んだ構造体 if err := tmpl.Execute(w, req); err != nil { /* エラーハンドリング */ }
  9. • HTTP や io.Reader などを意識せずロジックの実装に集中できるようになった ◦ req / res を用意→API

    を叩く→res を利用した処理を記述 といった流れが 追いやすくなった 11 利用するときのイメージ // api.Client は自動生成したコードを使うためのラッパー client := &api.Client{} req := &FooAPIRequest{ ID: id, Name: name, } res := &FooAPIResponse{} if err := apigen.FooAPI(ctx, client, req, res); err != nil { // エラーハンドリング } // res を利用した処理……
  10. • コロプラでは prizm という内製のリアルタイム通信フレームワークを 用いてリアルタイムサーバー・クライアントを実装 ◦ 非同期にメッセージを送り合う(req / res が1:1対応していない)

    ◦ ペイロードはバイト列をそのまま扱う ◦ etc… 14 内製のリアルタイムサーバーについて 丸投げ!? 自由に実装でき るから後はよろ しく!
  11. • .proto ファイルをもとに Go / C# のコードを自動生成する仕組みがある ◦ メッセージ構造の定義 ◦

    メッセージ送信やメッセージ受信時のハンドラの実装に必要なスタブコード ◦ TCP / UDP の使い分けや内部 ID などのオプション 15 prizm の自動生成 GameService.cs gameservice.go syntax = "proto3"; service GameService { rpc SendMessage() ... } message TestMessage { string body = 1; }
  12. • https://github.com/jhump/protoreflect を使って読み込み ◦ Windows 環境などでも導入が楽になるように protoc は使わず Go でフルに実装

    16 .proto ファイルの読み込み syntax = "proto3"; service GameService { rpc SendMessage() ... } message TestMessage { string body = 1; } fd := protoreflect.FileDescriptor services := fd.Services() rpcs := services.Get(0).Methods() messages := fd.Messages()
  13. • コード生成での分岐が多くテンプレートを使いづらいので1行1行出力 17 定義ファイルから prizm のコードを自動生成する // ここらへんでメッセージを受ける interface の定義をしている

    // interface に定義されるメッセージハンドラの定義部分 for i := 0; i < methods.Len(); i++ { m := methods.Get(i) methodName := gengo.GetMethodName(m, false) requestTypeName := /* 生成処理 */ responseTypeName := /* 生成処理 */ // オプションなどによる分岐が非常に多いのでテンプレートベースのアプローチが困難 if prizmgenproto.IsNoneResponseRPC(m) { g.e.EmitLine("On%s(ctx context.Context, request *%s, player prizm.Player)", methodName, requestTypeName) } else { g.e.EmitLine("On%s(ctx context.Context, request *%s, player prizm.Player) (*%s, error)", methodName, requestTypeName, responseTypeName) } }
  14. • .proto の rpc はリクエストとレスポンスを定義する必要があるため 非同期に(一方的に)送りつけるような通信の定義ができない ◦ クライアントからサーバーへ送信しレスポンスが不要なメッセージ ◦ サーバーからクライアントへ送信しレスポンスが不要なメッセージ

    • それぞれを以下の特別な message を定義してコードを自動生成する際に レスポンスが不要なメッセージとして扱うようにしている ◦ rpc ClientMessage(RequestMessage) returns (colopl.prizm.NoneResponse) ◦ rpc ServerMessage(colopl.prizm.ListenMessage) returns (ServerMessage) 18 非同期なメッセージの取り扱い
  15. 19 今日話したこと YAML HTTP API definition PHP API server impl.

    Unity (C#) API client impl. Go API client impl. 話してない 話してない Go 標準テンプレート を用いて生成 ProtoBuf prizm message definition Unity (C#) prizm client impl. Go prizm server / client impl. 話してない .proto の定義を もとに1行1行 ソースコードを 生成
  16. • Go で HTTP / 独自プロトコルで通信するためのコードを自動生成している ◦ Go(サーバー連携 / 自動テスト

    / 負荷試験) / C# (Unity) • 自動生成するための定義ファイルは YAML や Protocol Buffer など 世の中にある形式を使えば Go で扱いやすい • 定義を読み込んだあとはだいぶ筋肉 ◦ テンプレートを使えるなら使う ◦ テンプレートを使いづらいなら1行1行生成するしかない 20 まとめ