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. Go で通信周りのコード
    を自動生成している話
    株式会社コロプラ Ryo

    View Slide

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

    View Slide

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

    View Slide

  4. 4
    HTTPの
    コード生成

    View Slide

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

    View Slide

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

    View Slide

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




    View Slide

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

    View Slide

  9. 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
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide