RPC — концепция

от автора

Свою первую апишку я написал лет 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 более свежее).


ссылка на оригинал статьи https://habr.com/ru/articles/594825/