やること

ローカル仮想環境(cent7.6)上で走るpodmanコンテナをgRPCサーバとし、
クライアント1(golang製)から送信したメッセージをクライアント2(golang製)とunityにpushする
golang/unity本体のセットアップ方法はここでは割愛します 他に良い記事がわんさかあります

なお、結構無理矢理実現したのでこれを実際に運用したときに問題があったりなかったりするかもしれません
もっと良い方法を知っている方はそっと教えてください

gRPCサーバ用バイナリ作成準備

この辺も良い別記事わんさかありますが、まあ念のため。
ちなみにgolang環境はGOPATHモードです。go modだとなんか別のトラブル起きそうだったので避けました。

protoc

protobufを扱うためにprotocとかいうものを突っ込みます。
乱暴な説明ですが、protoファイルからコード生成するために要ると思っとけばそれほど困らないかと。
https://github.com/protocolbuffers/protobuf/releases

自分は環境が上述の通りcent7.6でx64なので protoc-[[version]]-linux-x86_64.zip を選択。
作業時点では3.8.0でした。
ローカル環境だしrootユーザでぶんぶんやってます

sudo su -
mkdir -p /tmp/work
cd /tmp/work
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-linux-x86_64.zip
unzip protoc-3.8.0-linux-x86_64.zip
mv bin/protoc /usr/local/bin/
rm protoc-3.8.0-linux-x86_64.zip

grpcライブラリとプラグイン

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

protoファイルサンプル

双方向ストリームオンリーです。
C#用の設定があるのはunityでも利用するからでございます。

syntax = "proto3";

package communication;

// [START csharp_declaration]
option csharp_namespace = "Ore.Comm";
// [END csharp_declaration]

service Communication {
  rpc BiDirectional (stream BiRequest) returns (stream BiResponse) {}
}

message BiRequest {
  string message = 1;
}

message BiResponse {
  string message = 1;
}

こいつをPJルート下にproto/communicationってディレクトリ作った上でその中に放り込みます。
今回はファイル名はcommunication.protoです。
また、生成コードを放り込むためのディレクトリもついでに作ります。
今回はPJルート下にpbってディレクトリ掘りました。

コード生成

PJルートで

protoc -Iproto --go_out=plugins=grpc:[[PJルートフルパス]] communication/communication.proto

-Iは基準としたいディレクトリを指定します。
成功すれば、pb下にcommunication/communication.pb.goってファイルが出来てると思います。

サーバプログラム

記事用に1ファイルにガサッと詰めたりエラーハンドルが適当だったりしますがその辺は詮無きこと
Interceptorも今後間違いなく使うはずなのでとりあえず詰め込んでおいてます
[[pjname]]は各自環境に置き換えてください

ちなみにこれだとメッセージを送ってきた人にもsendしちゃうので実際使う際にはその辺もコントロールするかと思います
(送信エラーが出たりはしないので相手側がrecvしてなくても接続が確立してれば送信自体は可能?)
今はとりあえず動作/疎通確認ということで

package main

import (
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/peer"
    comm "[[pjname]]/pb/communication"
    "io"
    "log"
    "net"
    "path"
)

type CommService struct{}

func main() {
    listenPort, err := net.Listen("tcp", ":20000")
    if err != nil {
        log.Fatalln(err)
    }
    opt := []grpc.ServerOption{grpc.StreamInterceptor(streamServerInterceptor())}
    server := grpc.NewServer(opt...)
    commService := &CommService{}
    comm.RegisterCommunicationServer(server, commService)
    server.Serve(listenPort)
}

func streamServerInterceptor() grpc.StreamServerInterceptor {
    return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
        var err error
        // deferでくるむと処理完了後に走るようになります
        defer func(argu string) {
            // メソッド名が取れる
            method := path.Base(info.FullMethod)
            // ログとか取ろっか
            fmt.Printf("%v %v", argu, method)
        }("argu")

        if hErr := handler(srv, stream); err != nil {
            err = hErr
        }

        return err
    }
}

var srvs []comm.Communication_BiDirectionalServer

func (cs *CommService) BiDirectional(srv comm.Communication_BiDirectionalServer) error {
    ctx := srv.Context()
    if pr, ok := peer.FromContext(ctx); ok {
        // cred関係の情報も取れるようだから認証関係やる時も利用しそう?
        // ちなみにここではremoteIPをなんとなく出してます
        fmt.Println(pr.Addr.String())
    }
    srvs = append(srvs, srv)

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }

        req, err := srv.Recv()
        if err == io.EOF {
            // クライアントからEOFを明示的に送る手段はまだ未検証
            log.Println("recv eof, exit.")
            return nil
        }
        if err != nil {
            log.Printf("recv error %v", err)
            continue
        }
        resp := comm.BiResponse{Message: req.Message}
        for _, serv := range srvs {
            if err := serv.Send(&resp); err != nil {
                log.Printf("send error %v", err)
            }
        }
    }

    return nil
}

できたら適当な名前でビルドしておきます。linux, amd64でok

podman

is 何

redhatがrhel用に作ったコンテナエンジン
本番環境のディストリビューションがrhelもしくはrhelベースなことが多いので何かあったときにdockerより爆発することは少なかろうということで最近注目してる
k3osとかがもっと台頭してくればまた変わるかもしれない
ちなみにこの記事書いてる段階で、AWS EC2でamzn linux2上ではまだpodman使えませんでした

一応

余計なトラブルを防ぐため、ホストOS側のSELinux/firewalldは殺しておきましょう
必要な場合は疎通取れた後に復活させて問題起きたときに切り分けしやすくしましょうね

podman インスコ

centos7.6上で

yum install podman

ってやったらなんの苦労もなく入った
特にサービス登録とか起動は要らないようです

dockerfile

FROM alpine:3.10.0
RUN apk --no-cache add jq git ca-certificates && \
    mkdir /lib64 && \
    ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

※jqとgitはこの記事では使いません
※他のものはgolangバイナリを動作させるために要ります

なおビルド方法はdockerと同じく

podman build --file=/path/to/Dockerfile  --no-cache=true --rm=true --tag=ore/grpctest:latest --tag=ore/grpctest:v1.0

こんなんでいけます tagは適宜張り替えてください

起動とgolang製バイナリデプロイ

デプロイとかドヤ顔で書いてますがホスト->コンテナへバイナリ送り付けるだけです。
ロ、ローカルだし動作確認だし…
ディレクトリがtmpなのはなんの理由もありません
先にpsする理由はコンテナID特定のため。

podman run -it -p 20000:20000 ore/grpctest:latest /bin/ash
podman ps
podman cp [[バイナリファイルホスト側パス]] [[コンテナID]]:/tmp

その上dockerとコマンド同じやんけという
あとはコンテナのash上で普通にバイナリ叩けば動作するかと思います。

unity側

事前準備

https://packages.grpc.io/
の最新ビルドIDを選択し、
grpc_unity_package
Grpc.Tools

の2つをDLします。後者は仮想環境(cent)で利用するので仮想環境側に置きます。

前者は展開するとPluginsフォルダが出てくるので、unityのPluginsフォルダに適当に階層掘ってその中に中身を放り込みましょう。少なくともunity 2018.4.2f1だと特に問題なく認識してくれました。

後者は拡張子を.zipにリネームし、展開しておきます。
その際、tools/[[os arch]]ってディレクトリが出てくるはずなので、自分の環境に合わせたarch名ディレクトリ内にあるgrpc_csharp_pluginに実行権限をつけておきます。

C#用pbコード生成

protocさん再度出番です。
出力先ディレクトリは先に生成しておきます。今回は/tmp下にpbってディレクトリを掘りました。

protoc -Iproto --csharp_out=/tmp/pb --grpc_out=/tmp/pb communication/communication.proto --plugin=protoc-gen-grpc=[[さっき実行権限つけたgrpc_csharp_pluginへのフルパス]]

これで作成すると、Communication.csとCommunicationGrpc.csの2ファイルが生成されます。
csharp_outのみでやるとGrpcのほうが生成されません。
自前で作るなら必要ありませんが、まあ今回はおとなしく自動生成コードに頼りましょう。
生成したらunityのscriptsフォルダに放り込みます。

unityコード

適当なシーンに空のオブジェクト作って、下記スクリプトをアタッチします。
コード自体は「とりあえず疎通確認できりゃいい」って感じで書いてるので、このやり方が正しいとか間違っても思わないように

using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Grpc.Core;
using Ore.Comm;

public class GameMaster : MonoBehaviour
{
    static Channel mChan;
    static Communication.CommunicationClient mClient;
    static AsyncDuplexStreamingCall<BiRequest, BiResponse> mMainStream;

    void Start()
    {
        mChan = new Channel("ip.ip.ip.ip:20000", ChannelCredentials.Insecure);
        mClient= new Communication.CommunicationClient(mChan);
        mMainStream = mClient.BiDirectional();
        var _ = StreamTest();
    }

    async Task StreamTest()
    {
        var response_reader_task = Task.Run(async () =>
        {
            while (await mMainStream.ResponseStream.MoveNext())
            {
                Debug.Log(mMainStream.ResponseStream.Current.Message);
            }
        });
        await response_reader_task;
    }

    void OnApplicationQuit()
    {
        mMainStream.Dispose();
    }
}

golang製簡易クライアント

やっつけ感漂いますが、実際やっつけ
なお、2つとも永遠に終了しないので止めるときは強制終了してください
送信まで3秒待ってる理由はその間に受信側をアクティブにしてガン見しようって魂胆

package main

import (
    "context"
    "fmt"
    "io"
    "google.golang.org/grpc"
    comm "[[pjname]]/pb/communication"
    "log"
)

func main() {
    conn, err := grpc.Dial(":20000", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("can not connect with server %v", err)
    }

    client := comm.NewCommunicationClient(conn)
    mainStream, err := client.BiDirectional(context.Background())
    if err != nil {
        log.Fatalf("open Regist error %v", err)
    }
    mainCtx := mainStream.Context()

    for {
        resp, err := mainStream.Recv()
        if err == io.EOF {
            fmt.Println("server EOF")
        }
        if err != nil {
            log.Fatalf("can not receive %v", err)
        }
        fmt.Println(resp.Message)
    }

    <-mainCtx.Done()
}
package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    comm "[[pjroot]]/pb/communication"
    "log"
    "time"
)

func main() {
    conn, err := grpc.Dial(":20000", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("can not connect with server %v", err)
    }

    client := comm.NewCommunicationClient(conn)
    mainStream, err := client.BiDirectional(context.Background())
    if err != nil {
        log.Fatalf("open Regist error %v", err)
    }
    mainCtx := mainStream.Context()
    time.Sleep(3 * time.Second)
    req := comm.BiRequest{Message: "send test"}
    if err := mainStream.Send(&req); err != nil {
        log.Fatalf("can not send %v", err)
    }

    <-mainCtx.Done()
}

発動

あとはunityでシーン再生してclient1起動したらclinet2起動するだけです。
受信するはず。

次は

認証関係やっていきたい。

bannerAds