taketiyo.log

Web Engineering 🛠 & Body Building 💪

【gRPC】PHPクライアントとGoサーバーで通信を行う

Programming

  / /

gRPCを用いたマイクロサービス間の通信環境を構築したのでその手順をメモしておきます。
今回はPHPクライアントとGoサーバー間でgRPC通信を行っています。
 

目次

 

達成出来ること

  • Protocol Buffer からGoのサーバーコードを生成する
  • Protocol Buffer からPHPのクライアントコードを生成し、Goサーバーと通信を行う

 

環境構築

Protocol Buffer の定義ファイルを作成する前に、まずは各言語のコンパイル環境を構築する必要があります。
 
今回はPHPコードへのコンパイル環境とGoコードへのコンパイル環境をDockerにて作成しました。
 

Go環境

# Dockerfile

FROM golang:1.11.2

# Install basics
RUN apt-get update \
 && apt-get install -y vim git zip wget

# Install grpc, protoc-gen-go
RUN go get -u google.golang.org/grpc \
 && go get -u github.com/golang/protobuf/protoc-gen-go

# Install protoc
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip \
 && unzip protoc-3.6.1-linux-x86_64.zip -d protoc \
 && cd protoc \
 && mv bin/protoc /usr/bin

ENV PATH $PATH:$GOPATH/bin

COPY ./services.sh /etc/services.sh

RUN chmod a+x /etc/services.sh

EXPOSE 50051

RUN sed -i -e '9,13s:^#::' /root/.bashrc

 
grpc, protoc-gen-go, protoc をインストールし、パスを通します。
また、Goサーバーはポート50051で待機させる予定なので、50051をEXPOSEします。
 
COPYにて指定しているservices.shの内容は下記の通りです。

#!/bin/sh
while true
do
    sleep 10
done

 
これはコンテナ起動後にコンテナを生存させ続けるためのentrypoint として用います。
 

PHP環境

# Dockerfile

FROM php:7.2-cli

# Install basics, grpc, protobuf
RUN apt-get update \
 && apt-get install -y vim git zip wget automake libtool libz-dev \
 && pecl install grpc \
 && pecl install protobuf

# Install protoc
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip \
 && unzip protoc-3.6.1-linux-x86_64.zip -d protoc \
 && cd protoc \
 && mv bin/protoc /usr/bin

# Install grpc_php_plugin
RUN git clone -b $(curl -L https://grpc.io/release) https://github.com/grpc/grpc \
 && cd grpc \
 && git submodule update --init \
 && make grpc_php_plugin

# Install composer
RUN curl -sS https://getcomposer.org/installer | php \
 && mv composer.phar /usr/bin/composer

COPY ./php.ini /usr/local/etc/php/php.ini
COPY ./services.sh /etc/services.sh

RUN chmod a+x /etc/services.sh

RUN sed -i -e '9,13s:^#::' /root/.bashrc

 
grpc, protobuf, protoc, grpc_php_plugin をインストールします。また後ほど用いるためcomposer もインストールしています。
 
COPYにて指定しているservices.shはGo環境の時のものと同様です。php.iniの内容は下記のとおりです。

extension=grpc.so
extension=protobuf.so

 
PHP環境ではini ファイル内にてextension を読み込ませる必要があります。
 

コンテナを立ち上げる

それぞれのコンテナを個々でビルド、起動、検証と進めても良いですが、今回はコンテナ間通信を簡略化するため Docker Compose を用います。
 
次のようなdocker-compose.ymlを作成します。

version: '3'
services:

  # PHPコード生成用コンテナ
  grpc_php:
    build: ./grpc-php
    volumes:
      - ../:/grpc-training
    entrypoint: /etc/services.sh

  # Golangコード生成用コンテナ
  grpc_go:
    build: ./grpc-go
    volumes:
      - ../:/go/src/grpc-training/
    entrypoint: /etc/services.sh

 
下記のようなディレクトリ構造を想定しています。

grpc-training
├── docker 
│   ├── docker-compose.yml 
│   ├── grpc-go 
│   │   ├── Dockerfile 
│   │   └── services.sh 
│   └── grpc-php 
│       ├── Dockerfile 
│       ├── php.ini 
│       └── services.sh 
└── example // 後で利用します

 
docker-compose.ymlと同階層にて下記コマンドを実行します。これでコンテナのビルド、起動、コンテナ間通信用network の生成が同時に実行されます。

docker-compose up -d

 

Protocol Buffer の定義ファイルを作成する

exampleディレクトリにexample.protoを下記内容で作成します。

syntax = "proto3";

package example;

service Example {
  rpc Echo (EchoRequest) returns (EchoResponse) {}
}

message EchoRequest {
  string message = 1;
}

message EchoResponse {
  string message = 1;
}

 
サービスのインターフェース、リクエストの内容、及びレスポンスの内容を定義しています。
この場合、ExampleサービスはEchoメソッドを持っており、その引数の型はEchoRequest、返却値の型はEchoResponseです。
EchoRequestEchoResponsestring型の変数messageを保持してる、といった具合です。
 

定義ファイルを各言語へコンパイルする

Goコードの生成

先の項目にて作成、起動したGo環境のコンテナに接続します。

docker exec -it [container name or hash] bash

 
/go/src/grpc-training/exampleに移動します。

cd /go/src/grpc-training/example

 
Goコードの出力先ディレクトリを作成します。

mkdir go

 
Goコードを生成します。

protoc example.proto --go_out=plugins=grpc:./go

 
これでgoディレクトリ内にexample.pb.goが生成されます。
 

PHPコードの生成

PHP環境のコンテナに接続します。

docker exec -it [container name or hash] bash

 
/grpc-training/exampleに移動します。

cd /grpc-training/example

 
PHPコードの出力先ディレクトリを作成します。

mkdir php

 
PHPコードを生成します。

protoc example.proto \ 
  --php_out=./php \ 
  --grpc_out=./php \ 
  --plugin=protoc-gen-grpc=/grpc/bins/opt/grpc_php_plugin

 
これでphpディレクトリ内にGPBMetadataExampleディレクトリが生成され、その中に生成されたコードが格納されます。
 

現時点でのディレクトリ構造

ここまでの作業にて、ディレクトリ構造、及びファイル配置は下記の通りになっています。

grpc-training 
├── docker 
│   ├── docker-compose.yml 
│   ├── grpc-go 
│   │   ├── Dockerfile 
│   │   └── services.sh 
│   └── grpc-php 
│       ├── Dockerfile 
│       ├── php.ini 
│       └── services.sh 
└── example 
    ├── example.proto 
    ├── go 
    │   └── example.pb.go 
    └── php 
        ├── Example 
        │   ├── EchoRequest.php 
        │   ├── EchoResponse.php 
        │   └── ExampleClient.php 
        └── GPBMetadata 
            └── Example.php

 

Goサーバー側のコードを作成する

example/goディレクトリ内にserverディレクトリを作成します。

cd example/go
mkdir server

 
作成したserverディレクトリ内にserver.goとして下記内容のファイルを作成します。

# server.go

package main

import (
    pb "grpc-training/example/go"
    "google.golang.org/grpc/reflection"
    "google.golang.org/grpc"
    "context"
    "log"
    "net"
)

const (
    PORT = ":50051"
)

type server struct{}

func (s *server) Echo(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
    return &pb.EchoResponse{Message: "Hello " + in.Message}, nil
}

func main() {

    lis, err := net.Listen("tcp", PORT)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := grpc.NewServer()

    pb.RegisterExampleServer(s, &server{})

    // Register reflection service on gRPC server.
    reflection.Register(s)

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

 
ファイルが作成できたら、Go環境のコンテナに接続し、サーバーを起動します。

docker exec -it [container name or hash] bash
cd /go/src/grpc-training/example/go/server
go run server.go

 

PHPクライアント側のコードを作成する

example/phpディレクトリ内にclientディレクトリを作成します。

cd example/php
mkdir client

 
作成したclientディレクトリ内にcomposer.jsonとして下記内容のファイルを作成します。

{
    "require": {
        "grpc/grpc": "^1.15",
        "google/protobuf": "^3.6"
    },
    "autoload": {
        "psr-4": {
            "GPBMetadata\\" : "../GPBMetadata/",
            "Example\\"     : "../Example/"
        }
    }
}

 
PHP環境のコンテナに接続し、composer installを行います。

docker exec -it [container name or hash] bash
cd /go/src/grpc-training/example/php/client
composer install

 
clientディレクトリ内にclient.phpとして下記内容のファイルを作成します。

<?php
# client.php

require_once 'vendor/autoload.php';

use Example\ExampleClient;
use Example\EchoRequest;
use Example\EchoResponse;

function request(string $requestMessage)
{
    $client = new ExampleClient('grpc_go:50051', [
        'credentials' => Grpc\ChannelCredentials::createInsecure(),
    ]);
    $request = new EchoRequest();
    $request->setMessage($requestMessage);

    list($reply, $status) = $client->Echo($request)->wait();

    /** @var EchoResponse $reply */
    return $reply->getMessage();
}

echo request('World.')."\n";

 

クライアントコードを起動する

最後に、PHP環境のコンテナ内から前項にて作成したclient.phpを起動すれば、gRPCを用いたExample サービスの通信が確認出来ます。

docker exec -it [container name or hash] bash
cd /go/src/grpc-training/example/php/client
php client.php
root@3cdadee14548:/grpc-training/example/php/client# php client.php  
Hello World.

 

おわりに

Protocol Buffer を用いたコード生成により、サーバー/言語の全く異なるサービス間でも型堅牢な通信を行うことが出来るため、非常に使い勝手が良いです。
またgRPCはHTTP/2を標準でサポートしているため、サーバー/スタブ間で高速な通信が可能です。マイクロサービス同士のオーケストレーションにとても有用ですね。