##はじめに
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経由で日付をレスポンス
##事前作業
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}
##実作業
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
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;
}
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
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を定義しているのですから、それぞれのコンパイラも一個に共通化してしまいましょう。
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が準備できました。
あとはクライアント側とサーバ側にそれぞれ固有な処理を書いていけばいい感じですね。
頑張れそうです。