Свою первую апишку я написал лет 7-8 назад и это был первый блин. В целом этот блин прошел кучу испытаний и модернизаций и получилось, что то вполне вменяемое. Даже сейчас я понимаю, что это ядро актуально и его можно развивать дальше и оптимизировать к текущим реалиям (пока нет подходящего проекта).

Как можно догадаться это было RPC. Наверно стоит начать с того, что я выделил ряд слоев (какие то можно опустить, какие то добавить).

  • получение запроса в текст

  • конвертация текста в ассоциативный массив

  • конвертация ассоциативного массива в класс запроса (понять имя метода, данные метода, токен и другие доп. данные (язык, версия, ...))

  • валидация наличия метода

  • права доступа (не доделал)

  • валидация (и преобразование данных в объект)

  • DI контейнер (не доделал на php)

  • логика метода (возвращает статус ответа и данные)

  • конвертируем статус объекта и данные в структуру ответа

  • преобразовываем структуру ответа в текст

Таким образом у нас получилось 10 слоев которые нам надо реализовать и которые нам кажутся очень сложными (наверно по этой причине RPC называют сложной и это очень сильно отталкивает). На самом же деле большая часть этих слоев уходит в ядро и вам не нужно их трогать. В моем случае остается зарегистрировать метод, описать схему валидации (иногда дописать метод валидации и преобразования) и написать саму логику. До схемы ответа, обычно руки доходят в последний момент...

Отдельным слоем можно добавить работу с файлами, их валидацию. У меня этот кейс встречал не так часто, по этому идем дальше.

Примеры написания методов

Простой пример метода на Go

Я не являюсь супер go разработчиком и тут было конвертировано RPC ядро с php на go за неделю. Народ на go очень специфичный и я не думаю поймать бурю аплодисментов, но пример думаю стоит вставить.

package methodGroup2

import (
    "project-my-test/example/rpcApp/methodRequestShemaItem"
    "project-my-test/src/rpc"
    "project-my-test/src/rpc/rpcInterface"
    "project-my-test/src/rpc/rpcStruct"
)

type MethodMyTest struct {
    rpc.RpcMethod
    Data struct {
        Name  *string
        Email *string
    }
    Logger rpcInterface.Logger
}

func (r *MethodMyTest) GetRequestSchema() map[string]rpcStruct.ReformSchema {
    rs := make(map[string]rpcStruct.ReformSchema)
    
    rs["Name"]  = methodRequestShemaItem.GROUP2_NAME()
    rs["Email"] = methodRequestShemaItem.GROUP2_EMAIL()
    return rs
}

func (r *MethodMyTest) Run() rpcInterface.Response {
    
    r.Logger.Info("test Info")
    r.Logger.Warning("test Warning")
    r.Logger.Error("test Error")
    r.Logger.Debug("test Debug")
    
    r.Response.SetData("test::string", "string")
    r.Response.SetData("test::int", 20)
  
    r.Response.SetData("test::Name", r.Data.Name)
    r.Response.SetData("test::Email", r.Data.Email)
  
    r.Response.GetError().SetCode("ERROR")
    return r.Response
}
Пример авторизации на php
<?php

namespace CustomRpc\Method\RpcUser;

class RpcUserAuth extends \Oploshka\Rpc\Method {

  public static function description(){
    return <<<DESCRIPTION
Авторизация пользователя (пользователь должен подтвердить почту!)
Пример объекта auth
{ "login": "test@mail.ru", "password": "12345678" }
При успешном ответе вернется session.
DESCRIPTION;
  }

  public static function validate(){
    return [
      'auth' => ['type' => 'array', 'req' => true, 'validate' => [
        'login'     => ['type' => 'string'  , 'validate' => [], 'req' => true ],
        'password'  => ['type' => 'string', 'validate' => [], 'req' => true ],
      ] ],
    ];
  }

  public function run(){

    $RpcUser = \CustomRpc\EntityQuery\RpcUserQuery::auth($this->Data['auth']['login'], $this->Data['auth']['password']);

    if(!$RpcUser){
      // TODO: add error auth count
      $this->Response->setError('ERROR_LOGIN_PASSWORD'); return;
    }

    $userSessionToken = \CustomRpc\EntityQuery\RpcUserSessionQuery::addNewUserSession($RpcUser);

    $this->Response->setData('session', $userSessionToken);
    $this->Response->setError('ERROR_NO');
  }

  public static function return(){
    return [
      'session'     => ['type' => 'string'  , 'validate' => [], 'req' => true ],
    ];
  }

}
Пример обновления данных пользователя
<?php

namespace CustomRpc\Method\RpcUser;

class RpcUserInfoUpdate extends \Oploshka\Rpc\Method {

  public static function description(){
    return <<<DESCRIPTION
Обновление своих данных
gender = 'NULL' 'MALE' 'FEMALE'
dateOfBirth = date format YYYY-MM-DD

region - отправляй regionId
image - отправляется посредством multipart в отдельном поле (от запроса), в бинарном виде.
DESCRIPTION;
  }

  public static function validate(){
    return [
      'session'       => ['type' => 'rpcUserSession'  , 'validate' => [], 'req' => true ],
      'nickname'      => ['type' => 'string'    , 'validate' => [], 'req' => false ],
      'region'        => ['type' => 'region'    , 'validate' => [], 'req' => false ],
      'gender'        => ['type' => 'string'    , 'validate' => [], 'req' => false ],
      'dateOfBirth'   => ['type' => 'string'    , 'validate' => [], 'req' => false ],
      'aboutUs'       => ['type' => 'string'    , 'validate' => [], 'req' => false ],
    ];
  }

  public function run(){
    $RpcUser = $this->Data['session'];

    $updateField = [];


    if( $this->Data['nickname'] ){
      // проверить зарегистрированность nickname
      if ( \CustomRpc\EntityQuery\RpcUserQuery::checkRegisteredNickname( $this->Data['nickname'] ) ) {
        $this->Response->setError('ERROR_NICKNAME_REGISTERED'); return;
      }
      $updateField['nickname'] = $this->Data['nickname'];
    }

    $this->Data['region']       && $updateField['region_id']      = $this->Data['region']->id;
    $this->Data['gender']       && $updateField['gender']         = $this->Data['gender'];
    $this->Data['dateOfBirth']  && $updateField['date_of_birth']  = $this->Data['dateOfBirth'];
    $this->Data['aboutUs']      && $updateField['about_us']       = $this->Data['aboutUs'];

    $file = \CustomRpc\Entity\RpcUserHelper::loadAndSaveImage('image');
    $file && $updateField['image_id'] = $file->id();

    \CustomRpc\EntityQuery\RpcUserQuery::update($RpcUser, $updateField);
    $this->Response->setError('ERROR_NO');
  }

  public static function return(){
    return [];
  }

}

Весь процесс написания RPC метода сводиться к тому, что нам нужно написать класс (структуру в случае с go), описать схему валидации (можно описать структуру данных в отдельном классе или добавить свойства в текущий класс) и саму логику.

Были идеи, что RPC ядро должно запустить чистую логику, которая ничего не знает про api, но обычно, в этом не было большого смысла (это увеличивало количество кода, а переиспользования как такового не было)

По итогу мы получаем:

  • гибкость. Можно получать и отдавать запрос в разных форматах (на логику, это не влияет)

  • хорошую отказоустойчивость. Ошибки обрабатываются ядром RPC и вероятность падения сведена к миниму. В любом случае мы можем добавить дополнительную проверку/обработку, не меняя код методов.

  • простоту тестирования. Не нужно дергать сетевой слой. Есть возможность написать общие тесты для всех методов. Сам метод можно протестировать, передав сырые данные или же валидный объект.

  • документация RPC методов. В целом можно написать адаптер для swager'a или генерировать простенький html (я выбрал последнее).

Заключение

Я показывал пример RPC разным людям, кому то такой подход нравиться, кому то нет, кто то пытался сделать подобное. В любом случае есть куда расти и есть идеи для реализации. Примеры не тянут на оскар и демонстрируют как можно писать. RPC ядро на go можно посмотреть тут, на php тут (develop более свежее).

Комментарии (15)


  1. Dmitry3A
    10.12.2021 21:39
    +1

    Недавно была статья про gRPC всё написанное там применимо к вашей системе.

    — Интересно есть плагин чтобы из браузера просматривать запросы/ответы в удобоваримом виде?

    — Плюс для простых случаев, для REST можно через какой-нибудь API прокси делать интересные вещи без изменений бэкенда, а с протобуфером конечно сильно сложнее по сравнению с json.

    — Для манипулирования данными гораздо интереснее конечно что-то типа odata/graphql.

    — Ну и самый большой недостаток, людей знающих как это всё заставить взлететь сильно меньше, так что если это не твой личный проект, то десять раз подумаешь прежде чем вкорячивать подобное.
    У вас ещё добавляется отсутсвие автогенерации клиента.

    Плюс в статье отсутсвуют данные про сериализацию — какие стандарты поддерживаются? Какой-нибудь JSON-RPC можно запилить?

    Обработка ошибок и возврат деталей о ошибках как реализован?

    Плюс на сколько я понял у вас получается каждый API метод это отдельный класс?

    По идее надо использовать языки с reflection, там можно небольшую обёртку навесить сверху и универсально обрабатывать вызовы, а тут у вас совсем велосипедно выходит, хоть и возраст очень солидный.


    1. oploshka Автор
      11.12.2021 11:09

      Я понял что статья не зашла (по крайней мере сообществу), все равно спасибо. Я не хотел писать трехтомный пост и углубляться в каждый уголок.
      Да автогенерация отсутствует, но по факту, вам нужно писать только 1 отдельный класс для каждого метода. Обработка ошибок - если не усложнять, то блок try catch в котором находиться вызов всех слоев с отдельным классом ошибки.
      Вот с сериализацией все гораздо интереснее, можно прикрутить json-rpc, а можно общаться xml (мало ли), можно доставать данные из get параметров и тд... По факту за это отвечают первые 3 слоя (откуда достать, как достать и какая структура) и они кладут вам все это в класс запроса метода.


  1. FSA
    10.12.2021 21:40
    +2

    Что такое Rpc в понимании автора, при чём тут API? Что это вообще было? У меня был свой велосипед для аутентификации пользователей 7-8 лет назад, может даже раньше. Сейчас он использует Redis (потому что у меня не миллион пользователей и так проще). Но я не пишу про это статьи на Хабре, потому что те, кто найдёт и будет пользоваться - я буду рад. Найдёт и укажет на ошибки, тем более буду рад. Ну а я просто пользуюсь.


    1. oploshka Автор
      11.12.2021 11:34

      Пример Rpc метода с авторизацией приведен для того чтоб показать как это можно оформить (не авторизацию, а сам метод). А основная суть, что мы его за 20-30 строк кода, оформляем в полноценный метод который можно вызвать клиентом (при чем с валидацией данных), например так:

      { "method": "auth", "auth": {"login": "test@mail.ru", "password": "12345678"} }

      Если так не устраивает пример, можете использовать JSON-RPC 2.0 Specification.


      1. alfss
        11.12.2021 13:56

        Зачем велосипеды , есть уже готовые решения https://github.com/vmkteam/zenrpc, авторы русские .

        https://github.com/vmkteam/rpcgen генератор Клиента


  1. Racheengel
    10.12.2021 22:35
    +1

    С го не работал, но вот для с++ есть вполне приличная библиотека rpclib. Она кроссплатформенная, не требует IDL и компилятора в отличие от grpc, да и заводится клиент и сервер с полпинка...

    И с rpc в итоге намного проще, чем ручками запросы парсить и шуровать :)


    1. oploshka Автор
      13.12.2021 12:08

      Правильная ключевая фраза "В ИТОГЕ". Rpc проще), но пока писал статью, я понял почему все считают rpc сложнее чем rest...


  1. yAnTar_yAnTar
    10.12.2021 23:48
    -1

    Вот и встретились 2 львенка, сорри за оффтоп, не удержался.


  1. hello_my_name_is_dany
    11.12.2021 00:07

    Мне казалось, что RPC в первую очередь протокол. Где его описание? Как передаются данные, поверх TCP, HTTP? В каком формате: JSON, Binary, XML или может что-то своё? Запускать просто нужный метод - это не совсем большая проблема...


    1. oploshka Автор
      11.12.2021 11:48

      Увы, но это не протокол, как и REST. Просто набор каких то размытых правил. Дальше есть надстройки как протокол SOAP, спецификация JSON-RPC 2.0 и не будем упускать gRPC. Я так понял народу больше интересно первые три слоя, которые размывают границу, как передается, в каком формате и в какой структуре.


      1. hello_my_name_is_dany
        11.12.2021 17:40
        -1

        Потому что от этого многое зависит. В хайлоаде частенько используют TCP или HTTP2 с бинарной сериализацией. Где перформанс не самое важное, можно и по HTTP 1.1 гонять JSON/XML и в этом случае сделать свою реализацию RPC ничего не стоит, это точно такой же контракт, как и в REST, который собственно и был создан заменой для RPC


        1. oploshka Автор
          13.12.2021 11:56

          Как по мне Rpc идет своим путем и этот путь менее популярен (пока что). В целом я видел проекты где есть и Rest и Rpc и они живут в одном проекте. А вот углубляться в сеть, когда это вопрос 50 строк кода и в формат данных (пусть еще 50 строк), это не целесообразно (для человека который пишет простой метод). Это вопрос оптимизации. Для браузера, мобильных приложений пока HTTP 1.1 + JSON, реже сокеты. Для общения между сервисами gRpc.

          А теперь мы поднимаем образно 4 урла "/rpc" "/rest" "/soket" "/grpc" и один и тот же код работает везде (правда под rest и soket придется по мучаться дольше). Джуны пишут простые методы (getNewsList, getNewsById). Мидлы дописывают дополнительные валидационные штуки и другие более сложные вещи. Сеньеры решают что и как гонять, в каком формате, как настроить права доступа + другие попутные вещи.

          А вывод, транспорт влияет на время выполнения, но никак не должен влиять на логику и нужно иметь возможность его поменять.


  1. bBars
    12.12.2021 00:02
    +1

    Я тоже давно делаю похожим образом. Отличие в том, что вместо явного описания метода я использую reflexion. То есть, просто написал функцию — и всё. Только их нужно все строго группировать — чтобы была возможность их все найти. За доставку запросов-ответов отвечают узлы вышестоящей подсистемы, которые делаю по мере надобности: для хттп одна, для socketio другая; для cli имеется, причем с автокомплитом под bash_completion. Недавно фронтовик попросил сделать генерацию схемы типов для typescript — запросто. Генерация js-клиента есть, документации и коллекции для постмана. В общем удобно. Но сообщество такие поделки не одобряет, это да )

    И хотя я солидарен по сути вопроса, статья выглядит неполной какой-то. Идея изложена, но пюсов/минусов не видно, применения тоже. Типа, читайте сорцы


    1. oploshka Автор
      13.12.2021 11:11

      Промахнулся, комент ниже.


  1. oploshka Автор
    13.12.2021 11:10
    +1

    Я хотел написать чтиво выходного дня (чтоб народ мог подумать и не читать много). По большому счету это описание слоев (которые можно сделать на любом языке для rpc), пример как выглядит метод и немного о том, что не обязательно писать 500+ строк кода, ради одного метода. Про плюсы и минусы для Rpc - их уже озвучили тысячу раз.

    Можно было бы углубиться, что схема для валидации это круто, удобно, многофункционально. Не обязательно получать из данных простые типы (например можно сразу получить объект пользователя по полю user_id), но это не изменит точку зрения людей. Пока не напишут свой велосипед, не успокоятся (увы такая реальность и это нормально). Не исключаю и поклонников готовых библиотек.

    Говорить что json-rpc 2.0 морально устарел и его не хватает для нормального общения... это не для хабра...

    Если про Go, то я столкнулся с проблемой, что первые 3 слоя влияют на слой валидации (если хочется оптимизации), но это детали языка. В целом соглашусь с тем, что генерация кода - это довольно интересная вещь, но это не популярно у простого народа на php и в ряде других языков (если не брать во внимание фреймворки), хотя в далекие времена писал сборщики js файлов и какие то мини генераторы.

    статья выглядит неполной какой-то. Идея изложена, но пюсов/минусов не видно, применения тоже. Типа, читайте сорцы

    Я хотел дописать какие то главы, но сейчас понимаю, что на это нет смысла тратить время, так же как и писать трехтомный пост. Про плюсы - гибкость (можно заменить слой в ядре, добавить или убрать), минусы - вам захочется или придется писать свой велосипед (для примера можно посмотреть сорцы). А лить воду, кидать кучу года из исходников, говорить "я сделал хранилище запросов" и другие банальные вещи - это не цель этой статьи.

    В любом случае спасибо и я рад, что я не один такой)