gRPC сервер и клиент на Symfony - igancev.ru blog

gRPC сервер и клиент на Symfony

symfony + grpc = love

Поднимем gRPC сервер и сгенерируем к нему gRPC клиент, в рамках Symfony Framework

Этот гайд - своего рода продолжение предыдущей статьи про gRPC сервер на Spiral

Вступление

О том, что такое gRPC, и почему нам нужно для gRPC сервера на PHP использовать Roadrunner, я рассказывал тут.

Исходный код итогового демо проекта выложил на github.

❤️ Эти киты посвящаются всем проголосовавшим в прошлом посте в телеграме за то, чтобы этот мануал о Symfony + gRPC вышел в свет: 🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳🐳. Ставь хлопки в ладоши 👏 к этому посту в телеграме, если обрадовался выходу материала!

Постановка задачи и план

Наша цель - научиться использовать gRPC совместно с PHP фреймворком Symfony. Исходя из этого, мы:

  • Придумаем задачу, которую в процессе будем решать
  • Разработаем .proto контракт
  • Установим symfony
  • Настроим docker окружение для приложения
  • Установим roadrunner-bundle
  • Сгенерируем php код с заглушками gRPC сервера на основе .proto контракта
  • Протестируем сервер сторонним gRPC клиентом
  • Сгенерируем клиентский php код и протестируем его

Подготовка

В качестве учебной задачи давайте разработаем несложный сервис “Укорачиватель url адресов”. Укорачиватель на вход будет принимать длинную ссылку, а возвращать короткую.

Наглядная схема взаимодействия клиента и сервера

.proto контракт

syntax = "proto3";

package grpc.shortener;

message LongUrl {
  string url = 1;
}

message ShortUrl {
  string url = 1;
}

service UrlShortener {
  rpc shorten (LongUrl) returns (ShortUrl) {
  }
}

Установка symfony

docker run -v `pwd`:/app -u $(id -u):$(id -g) \
  composer create-project symfony/skeleton url-shortener

Настройка docker окружения для приложения

Создадим следующий Dockerfile:

# ./deployment/docker/roadrunner/Dockerfile

FROM php:8.2-cli-alpine

WORKDIR /app

COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/

RUN install-php-extensions bcmath intl opcache zip sockets grpc protobuf

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

CMD ["/app/bin/rr", "serve", "-d", "-c", "/app/.rr.yaml"]

И следующий docker-compose.yml:

# ./docker-compose.yml
version: '2'

services:
  rr:
    build:
      dockerfile: ./deployment/docker/roadrunner/Dockerfile
    ports:
      - '9001:9001' # шарим порт 9001
    user: '1000:1000' # работа от текущего юзера в системе
    volumes:
      - ./:/app # монтируем проект в /app

Пока просто соберем образ, без запуска контейнера:

docker-compose build

Установка roadrunner-bundle

Бандл baldinof/roadrunner-bundle предоставляет готовый RoadRunner Worker под Symfony, ряд конфигов и настроек. Помогает быстро стартовать на symfony в связке с Roadrunner. Установим его и согласимся с установкой рецепта (y).

docker-compose run rr composer req baldinof/roadrunner-bundle

В результате установки рецепта был зарегистрирован бандл и скопированы некоторые конфигурационные файлы.

Отредактируем yaml конфиги roadrunner .rr.yaml и .rr.dev.yaml:

  • удалим ненужный нам плагин http сервера:
    http:
      address: 0.0.0.0:8080
      ... # удаляем всю секцию
    
  • добавим вместо него секцию grpc:
    grpc:
      listen: "tcp://0.0.0.0:9001"
      proto:
        - "proto/shortener.proto"
    

Поместим ранее написанный .proto контракт в файл ./proto/shortener.proto.

Установим пакеты grpc/grpc и spiral/roadrunner-grpc:

docker-compose run rr composer req grpc/grpc spiral/roadrunner-grpc

Понадобится бинарник Roadrunner:

docker-compose run rr composer req --dev spiral/roadrunner-cli \
  && ./vendor/bin/rr get-binary -l ./bin

Кодогенерация

Нам нужно генерировать PHP код сервера и клиента gRPC. За генерацию отвечает утилита protoc, которой требуется плагин для генерации кода (protoc-gen-php-grpc).

Для генерации серверного кода воспользуемся плагином от проекта Roadrunner protoc-gen-php-grpc. Но, к сожалению, он не генерирует код классов клиента. Хоть их и несложно написать руками, нам бы хотелось максимальную автоматизацию.

Поэтому для генерации клиентского кода используем официальный плагин grpc_php_plugin, который нужно собрать из исходников руками.

Инструкции по сборке protoc и grpc_php_plugin есть в документации. Но, процесс сборки может быть муторным из-за разрешения зависимостей. Поэтому я подготовил docker образ, в котором производится сборка protoc, grpc_php_plugin, и получение protoc-gen-php-grpc. Остается лишь воспользоваться готовеньким 🙂.

Будем складывать сгенерированный код в директорию generated. Создадим ее:

mkdir ./generated

Серверный и клиентский код будем генерировать одной и той же командой, будет лишь отличаться значение аргумента --plugin.

Серверный код

Для генерации серверных заглушек используем плагин /usr/bin/protoc-gen-php-grpc:

docker run -u $(id -u):$(id -g) -v `pwd`:/app \
  ghcr.io/igancev/protoc-php-plugins:latest protoc \
  --plugin=protoc-gen-grpc=/usr/bin/protoc-gen-php-grpc \
  --php_out=./generated \
  --grpc_out=./generated \
  ./proto/shortener.proto

Как результат, в директории generated мы увидим следующий набор файлов:

GPBMetadata
  - Proto
    - Shortener.php
Grpc
  - Shortener
    - LongUrl.php
    - ShortUrl.php
    - UrlShortenerInterface.php

Где:

  • LongUrl и ShortUrl - классы, отражающие DTO нашего proto контракта.
  • UrlShortenerInterface - интерфейс сервиса, который нам нужно будет реализовать, поместив туда логику приложения.

Реализуем UrlShortenerInterface:

<?php
// ./src/UrlShortenerService.php

declare(strict_types=1);

namespace App;

use Grpc\Shortener\LongUrl;
use Grpc\Shortener\ShortUrl;
use Grpc\Shortener\UrlShortenerInterface;
use Spiral\RoadRunner\GRPC;

final class UrlShortenerService implements UrlShortenerInterface
{
    public function shorten(GRPC\ContextInterface $ctx, LongUrl $in): ShortUrl
    {
        $longUrl = $in->getUrl();
        $shortedUrl = $this->buildShortUrl($longUrl);

        return (new ShortUrl())->setUrl($shortedUrl);
    }

    private function buildShortUrl(string $longUrl): string
    {
        // Вместо настоящей реализации мы имитируем бурную деятельность
        $randomStr = bin2hex(\random_bytes(5));

        return 'https://examp.le/' . $randomStr;
    }
}

Клиентский код

Для генерации клиента используем плагин /usr/bin/grpc_php_plugin:

docker run -u $(id -u):$(id -g) -v `pwd`:/app \
  ghcr.io/igancev/protoc-php-plugins:latest protoc \
  --plugin=protoc-gen-grpc=/usr/bin/grpc_php_plugin \
  --php_out=./generated \
  --grpc_out=./generated \
  ./proto/shortener.proto

На выходе появится лишь один новый класс UrlShortenerClient, это и есть реализация gRPC клиента:

Grpc
  - Shortener
    - UrlShortenerClient.php

composer.json autoload

Для автозагрузки сгенерированных файлов, добавим следующие строки в секцию autoload.psr-4 файла composer.json:

...
"autoload": {
  "psr-4": {
    ...
    "GPBMetadata\\": "generated/GPBMetadata",
    "Grpc\\": "generated/Grpc"
  }
},
...

Применим изменения:

docker-compose run rr composer du

Запуск

docker-compose up

Все хорошо, если в выводе нет ошибок, и есть примерно такой текст grpc server was started 🚀.

Тестируем сервер сторонним клиентом

Можно тестировать любым удобным gRPC клиентом, а я в этот раз воспользуюсь grpcurl:

grpcurl -plaintext -d '{"url":"https://example.com/long-url"}' \
    -import-path ./proto -proto shortener.proto \
    localhost:9001 grpc.shortener.UrlShortener/shorten

Успех! 🥳 Мы получили следующий результат:

{
  "url": "https://examp.le/bc427772f9"
}

Тестируем взаимодействие php клиента с сервером

Создадим консольную команду, из которой будем вызывать клиентский код. Результат будем выводить в консоль:

<?php
// ./src/ShortenUrlCliCommand.php

declare(strict_types=1);

namespace App;

use Grpc\ChannelCredentials;
use Grpc\Shortener\LongUrl;
use Grpc\Shortener\ShortUrl;
use Grpc\Shortener\UrlShortenerClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand('shortener:shorten', 'Shorten url by shortener grpc service')]
final class ShortenUrlCliCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $urlShortenerClient = new UrlShortenerClient('127.0.0.1:9001', [
            'credentials' => ChannelCredentials::createInsecure(),
        ]);

        $longUrl = (new LongUrl())->setUrl('https://example.com/long-url');

        /** @var ShortUrl $shortUrl */
        [$shortUrl] =  $urlShortenerClient->shorten($longUrl)->wait();

        $output->writeln('Short url: ' . $shortUrl->getUrl());

        return Command::SUCCESS;
    }
}

Запустим команду:

docker-compose exec rr bin/console shortener:shorten

Результат:

Short url: https://examp.le/29197d611f

👍 👏 💪

Заключение

Сегодня мы подружили Symfony и gRPC, реализовав простую, демонстрационную задачку. И мы молодцы.

Исходный код итогового демо проекта выложил на github.

Комментарии

Если есть желание обсудить эту тему, то сделать это можно под постом данной статьи в телеграм канале, на который также можно подписаться, чтобы не пропустить новые материалы. Всем добра! 🤚