やること
ローカル仮想環境(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起動するだけです。
受信するはず。
次は
認証関係やっていきたい。