Slide 1

Slide 1 text

Go で通信周りのコード を自動生成している話 株式会社コロプラ Ryo

Slide 2

Slide 2 text

氏名  : 部署  : 2 自己紹介 ● 2020年4月新卒入社 ● prizm(内製リアルタイムエンジン) チームに所属 Ryo 技術基盤本部第2バックエンドエンジニア部

Slide 3

Slide 3 text

3 ● コロプラではいくつかのケースで Go を利用している ● Go を使う中で HTTP や内製プロトコルでの通信を実装する必要がある ● そうした通信部分の実装は量も多く似たような作業が多いので面倒 ⇒一部自動化しているのでその話をします 今回話すこと APIサーバー リアルタイムサーバー ユーザークライアント 負荷試験 bot 今回の対象 HTTP 内製プロトコル ※状態の同期などで  この経路が必要な場合がある

Slide 4

Slide 4 text

4 HTTPの コード生成

Slide 5

Slide 5 text

● 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() などしてレスポンスを読み込む

Slide 6

Slide 6 text

● 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 { /* エラーハンドリング */ }

Slide 7

Slide 7 text

● 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"` ... ... ... ... } ・ ・ ・ ・

Slide 8

Slide 8 text

● コロプラでは API 周りのコード(PHP / C#)を YAML を用いた定義で自動 生成する仕組みを内製していたので Go クライアントの自動生成に流用した 8 採用した解決策 ● リクエストのスキーマ ● レスポンスのスキーマ ● エンドポイント ● データ型 ● Enum ● …… ● リクエストの構造体 ● レスポンスの構造体 ● エンドポイントに 対応する関数 ● データ型を表す構造体 ● Enum を表す型と値 ● …… YAML Go

Slide 9

Slide 9 text

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 }

Slide 10

Slide 10 text

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 { /* エラーハンドリング */ }

Slide 11

Slide 11 text

● 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 を利用した処理……

Slide 12

Slide 12 text

12 リアルタイム プロトコルの コード生成

Slide 13

Slide 13 text

● コロプラでは prizm という内製のリアルタイム通信フレームワークを 用いてリアルタイムサーバー・クライアントを実装 ○ 非同期にメッセージを送り合う(req / res が1:1対応していない) ○ ペイロードはバイト列をそのまま扱う ○ etc… 13 内製のリアルタイムサーバーについて 自由に実装でき るよ!

Slide 14

Slide 14 text

● コロプラでは prizm という内製のリアルタイム通信フレームワークを 用いてリアルタイムサーバー・クライアントを実装 ○ 非同期にメッセージを送り合う(req / res が1:1対応していない) ○ ペイロードはバイト列をそのまま扱う ○ etc… 14 内製のリアルタイムサーバーについて 丸投げ!? 自由に実装でき るから後はよろ しく!

Slide 15

Slide 15 text

● .proto ファイルをもとに Go / C# のコードを自動生成する仕組みがある ○ メッセージ構造の定義 ○ メッセージ送信やメッセージ受信時のハンドラの実装に必要なスタブコード ○ TCP / UDP の使い分けや内部 ID などのオプション 15 prizm の自動生成 GameService.cs gameservice.go syntax = "proto3"; service GameService { rpc SendMessage() ... } message TestMessage { string body = 1; }

Slide 16

Slide 16 text

● 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()

Slide 17

Slide 17 text

● コード生成での分岐が多くテンプレートを使いづらいので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) } }

Slide 18

Slide 18 text

● .proto の rpc はリクエストとレスポンスを定義する必要があるため 非同期に(一方的に)送りつけるような通信の定義ができない ○ クライアントからサーバーへ送信しレスポンスが不要なメッセージ ○ サーバーからクライアントへ送信しレスポンスが不要なメッセージ ● それぞれを以下の特別な message を定義してコードを自動生成する際に レスポンスが不要なメッセージとして扱うようにしている ○ rpc ClientMessage(RequestMessage) returns (colopl.prizm.NoneResponse) ○ rpc ServerMessage(colopl.prizm.ListenMessage) returns (ServerMessage) 18 非同期なメッセージの取り扱い

Slide 19

Slide 19 text

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行 ソースコードを 生成

Slide 20

Slide 20 text

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