サーバーにruby、クライアントにPHPでgRPCを頑張る

##はじめに
google謹製のRPCフレームワークであるgRPCを使って、サーバーにruby、クライアントにPHPを使って、異なる言語間を共通のI/FでAPI通信してみたいと思います。

異なる言語間としているのはgRPCのメリットであるIDL定義によるI/Fの共通化の醍醐味を味わってみたいというより、PHPでgRPCサーバの構築ができないという現実に対する条件反射のようなものですが、結果的に醍醐味もやってくるので、味わってみたいと思います。

クライアントにPHPを使う場合、サーバー側はどうもgoで紹介しているところが多いので、ここではrubyを使ってみます（はい、go知らないの確定）。

概要はここで書くまでもないので、さっそく。
ここではAPIとして、ドメインを指定すると有効期限を取ってくる、というものを書いてみます。

INPUT:　FQDN
OUTPUT:　yyyy/mm/dd hh:ii:ss

API（gRPC）:　定義したI/Oで共通I/Fを用意
クライアント（PHP）:　サーバを通じてAPIにFQDNをリクエスト
サーバ（ruby）:　クライアントからリクエストされたFQDNからSSL証明書の有効期限を調べてAPI経由で日付をレスポンス

##事前作業
```bash
pecl install grpc
pecl install protobuf
vi (略)/php.ini
	extension=grpc.so
	extension=protobuf.so

gem install grpc
gem install grpc-tools

mkdir ~/projects/grpc_trial/
cd ~/projects/grpc_trial/

composer require grpc/grpc
composer require google/protobuf

mkdir {protos,php,ruby}
```

##実作業
```bash
vi protos/checkexpires.proto
    (後述)

grpc_tools_ruby_protoc -I ./protos --ruby_out=./ruby --grpc_out=./ruby ./protos/checkexpires.proto
ls ruby/ | grep checkexpires
	checkexpires_pb.rb
	checkexpires_services_pb.rb

protoc --proto_path=./protos --php_out=./php --grpc_out=./php ./protos/checkexpires.proto --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin
ls php/Checkexpires/
	CheckReply.php  CheckRequest.php  GetSslClient.php

vi checkexpires.rb
    (後述)

vi checkexpires.php
    (後述)

ruby ./checkexpires.rb &
php ./checkexpires.php 
php checkexpires.php example.com
	2021/12/25 23:59:59
```

```proto:checkexpires.proto
syntax = "proto3";

package checkexpires;

import "google/protobuf/timestamp.proto";

service GetSsl {
  rpc getExpire (CheckRequest) returns (CheckReply) {}
}

message CheckRequest {
  string fqdn = 1;
}

message CheckReply {
  google.protobuf.Timestamp timestamp = 1;
}
```

```ruby:checkexpires.rb
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'ruby')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

require 'time'
require 'grpc'
require 'checkexpires_services_pb'

class CheckexpiresServer < Checkexpires::GetSsl::Service
  def get_expire(check_req, _unused_call)
    end_at = `openssl s_client -connect #{check_req.fqdn}:443 </dev/null 2>/dev/null|openssl x509 -text | grep "Not After"`
    end_at = end_at.split(' : ').pop
    end_at = end_at.gsub("\n", '')
    end_at = Time.parse(end_at).to_i
    end_at = Time.at(end_at)
    Checkexpires::CheckReply.new(timestamp: end_at)
  end
end

def main
  s = GRPC::RpcServer.new
  s.add_http2_port('localhost:50051', :this_port_is_insecure)
  s.handle(CheckexpiresServer)
  s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])
end

main
```

```php:checkexpires.php
<?php
require dirname(__FILE__).'/vendor/autoload.php';
require dirname(__FILE__).'/php/Checkexpires/GetSslClient.php';
require dirname(__FILE__).'/php/Checkexpires/CheckReply.php';
require dirname(__FILE__).'/php/Checkexpires/CheckRequest.php';
require dirname(__FILE__).'/php/GPBMetadata/Checkexpires.php';
$server = 'localhost:50051'; //checkexpires.rb

if (is_null($argv[1] ?? null)) {
    echo "need to input fqdn\n";
    exit(1);
}

try {
    $client = new Checkexpires\GetSslClient($server, [
        'credentials' => Grpc\ChannelCredentials::createInsecure(),
    ]);
    $request = new Checkexpires\CheckRequest();
    $request->setFqdn($argv[1]);
    list($reply, $status) = $client->getExpire($request)->wait();

    if (($status->code ?? null) === 0) {
        $ts = $reply->getTimestamp()->getSeconds();
        echo date('Y/m/d H:i:s', $ts);
        exit(0);
    }
} catch (Exception $e) {
    echo $e->getMessage();
    exit(1);
}
```

##補足
せっかく異なる言語で共通I/Fを定義しているのですから、それぞれのコンパイラも一個に共通化してしまいましょう。

```bash
vi ~/.bashrc
	function protoc2() {
	    grpc_tools_ruby_protoc -I ./protos --ruby_out=./ruby --grpc_out=./ruby ./protos/$@ && protoc --proto_path=./protos --php_out=./php --grpc_out=./php ./protos/$@ --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin
	}

. ~/.bashrc
cd ~/projects/grpc_trial/
protoc2 checkexpires.proto
```

これで、ひとつのprotoファイルでrubyとPHPそれぞれの共通I/Fが準備できました。
あとはクライアント側とサーバ側にそれぞれ固有な処理を書いていけばいい感じですね。
頑張れそうです。