Pull to refresh

RPC — концепция

Reading time5 min
Views8.8K

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

Tags:
Hubs:
Total votes 17: ↑6 and ↓11-3
Comments16

Articles