Как стать автором
Обновить

Популяризация JSON-RPC (часть 2)

Время на прочтение7 мин
Количество просмотров6.1K

Продолжим строить подобие JSON-RPC сервера, начатого в части 1 и анализировать его плюсы и минусы. В прошлой статье был описан механизм отделения бизнес логики бэкенда от транспортного протокола (HTTP) через шаблон проектирования "Front Controller", роль которого исполняет в нашем случае JsonRpcController.

API gateway

Бэкенд API до передачи запроса в код, отвечающий за бизнес логику, обычно предоставляет частичный функционал API шлюза (API gateway) - он может делать аутентификацию, авторизацию, роутинг, валидацию данных, логгирование, кэширование, обеспечивать меры безопасности и другое.

Часть указанных сервисов работает на уровне транспортного протокола (здесь HTTP), другие от него не зависят.

Оба случая можно реализовать в виде фильтров/интерсепторов. Например, аутентификация происходит при перехвате запроса внутри фреймворка (у большинства есть свои методы для создания подобных фильтров) до передачи его контроллеру, а авторизация - уже после отработки JsonRpcController. Добавить свой функционал фильтров перед передачей запроса в код бизнес логики полезно, потому что могут понадобиться и другие сервисы - например, дополнительная валидация параметров, защита от SQL инъекций, кэширование, логгирование, шифрование и прочее. Фильтры, которые могут потребоваться на уровне HTTP - управление CORS политикой, защита от CSRF, throttler, определение ботов/спайдеров.

Разделение кода на бэкенде на часть, работающую с транспортным протоколом, и чисто бизнес логику позволяет достаточно безболезненно не только поменять фреймворк, но и перейти при необходимости с HTTP на, скажем, Websockets или gRPC. При этом нужно будет переделать только то, что связано с HTTP - фильтр аутентификации, например. А код бизнес логики, включая фильтры авторизации и кэширования, останутся как есть. В случае с обычным фреймворком и реализации бэкенд API как RESTful работы будет намного больше.

Authentication & Authorization

Аутентификация (как проверка пользователя) и авторизация (как проверка разрешения конкретного пользователя на доступ к конкретному ресурсу) в случае с JSON-RPC полностью разделяются. Аутентификация обычно жестко привязана к протоколу передачи данных - для JWT, например, рефреш токен должен храниться в http-only куках, токены могут передаваться в HTTP заголовках и прочее. Авторизация же зависит от установленных правил доступа и определяется идентификатором текущего пользователя, вызываемым методом и его параметрами. Грубо говоря, по этим параметрам идет просто валидация. Аутентификация происходит до передачи запроса JsonRpcController контроллеру, авторизация - после его отработки, но до передачи выполнения программы коду бизнес логики. Аутентификация реализуется частично средствами бэкенд фреймворка, авторизация к фреймворку не привязана.

Встает вопрос, каким образом передать расшифрованную информацию об аутентифицированном пользователе дальше в бизнес логику? Варианта видится два - либо добавить параметр в тело JSON-RPC запроса внутри JsonRpcController, что сделает его формат несколько выбивающимся из спецификации, либо через контекст фреймворка. Выбор конкретной реализации особо ни на что не влияет.

File upload/download

Пожалуй, единственное, для чего кажется что требуются стандартные средства HTTP протокола - загрузка и скачивание файлов. Можно это реализовать отдельными эндпойнтами на бэкенде и специальным контроллером, можно отдельным фильтром перед JsonRpcController, но, на самом, деле любые файлы можно вполне несложно передавать как данные внутри JSON-RPC params используя средства JavaScript на фронте. Недавно стояла задача сгенерировать на бэке несколько отчетов в CSV формате, зазиповать и передать клиенту для скачивания, - так передать текстом всё на фронт, там сформировать архив и дать на сохранение оказалось проще и короче по коду.

Бинарные данные пакуем в base64. Если не нужно постоянно гонять туда-сюда десятки мегабайт файлов, то решение вполне эффективное. Не забываем настроить на вебсервере компрессию потока JSON-RPC данных.

Batch

Отправка запросов в пакете (batch) кажется довольно экзотичной задачей, но только поначалу. Представим себе страницу с товарами. Для её отображения используется метод products:list. У любого товара менеджер может поменять цену - соответственно, product:update. Можно сделать универсальный доступ к CRUD ресурсам с помощью resources:list и resources:update и в параметрах уже передаем product как имя ресурса, и цену как параметр апдейта (в данном примере это не существенно, можно так не делать, но к примеру).

При изменении цены возможны сбои - из-за сети, из-за нарушения внутренних бизнес ограничений, проблем с БД. Соответственно, после вызова update нужно удостовериться, что цена изменилась - получить подтверждение в каком-то виде. Для определенных ресурсов нужно такое подтверждение, для других - нет. Значит нужно ввести параметр в запрос, который говорит, получать ли в виде ответа новое значение из БД или нет (это ведь дополнительный запрос в БД, дополнительная ненужная нагрузка). Кроме того, при добавлении в update функционала read теряется идеология CRUD и унижается буковка S в термине SOLID.

Также теперь на фронте нам нужно добавить код, который проапдейтит конкретный продукт по пришедшему ответу. Код компонента легко может увеличиться раза в два.

Самый простой способ избежать всех этих усложнений - послать два запроса: сперва чистый update, а потом list; Но это два последовательных сетевых запроса, клиентский UX страдает.

И тут приходит JSON-RPC batch. Посылаем два запроса в одном пакете - [resources:update, resources:list]. На сервере они обрабатываются последовательно, и мы получаем на фронте актуальные данные. Ресурсы по-прежнему CRUD, всё хорошо.

Теперь представим, что на определенных ресурсах у нас есть кэширование. Бэкенд фреймворки часто прозрачно позволяют использовать Memcashed, Redis или простой файловый кэш для ускорения на порядок-другой доступа к данным по сравнению с БД. Если мы обновили данные, нам нужно обновить/очистить и кэш этого ресурса. И тут всего лишь нужно добавить третью команду: [resources:update, cache:clear, resources:list]. Возможны возражения, что таким образом бизнес логика частично переносится на клиент, но код реально становится проще, красивей и лаконичней. И не будем забывать, что с чистым REST CRUD вся логика на клиенте.

Обработка ошибок

Ответ на JSON-RPC запрос приходит с сервера либо в виде result, либо error. Вместо мучений с HTTP кодами ошибок и квази-ошибок 3хх-5хх транспортного протокола, можно определить свои коды ошибок уровня приложения и спокойно работать с ними, оставив сову и глобус в покое. Это намного удобней и намного естественней.

Кэширование

В современных SPA приложениях, использующих backend API, кэширование ресурсов может происходить:

  1. На клиенте с помощью Service Worker.

  2. На сервере, используя Redis и прочие соответствующие сервисы.

Оба эти варианта дают управляемое кэширование, когда разработчик точно знает где и какие у него данные, в отличие от неуправляемого браузерного HTTP кэширования, которое может быть, а может не быть. Полагаться на него нельзя, поэтому, в 99% случаях оно просто не нужно, а часто и приносит только проблемы. За 30 лет веб достаточно сильно развился, чтобы заменить HTTP кэширование на что-то более удобное и надежное, если сайт не простой "Hello world".

Developer eXpirience

Наверное, самый неприятный для разработчика момент работы с JSON-RPC - это отладка запросов в браузерных DevTools. Вкладка Network показывает только запросы к /rpc эндпойнту, и приходится лезть в секцию payload и смотреть детали запроса. Теряется визуальное восприятие потока выполнения программы, тратятся время и нервы.

Есть варианты решения этого через расширение браузера, которое будет показывать в списке сетевых соединений дополнительную информацию. Но можно поступить намного проще. При отправке запроса преобразуем его, добавив в конец URI название вызываемого метода JSON-RPC или что-то еще подходящее по смыслу. А на бэке просто редиректим все запросы по /rpc/(.+) на /rpc. Это можно сделать на уровне вебсервера (Apache или Nginx), либо сам фреймворк бэкенда может предоставлять такой функционал, как и в случае с CI:

$routes->post('rpc/(.+)', 'JsonRpcController::index');

Можно задавать данный URI опционально явно при вызове ( здесь {uri: 'updateSetting'} ):

Код Batch запроса примера выше
async updateSetting(keyName: string, keyValue: string | number) {
    const resp = await http.jsonRpc(
      [
        {
          method: 'utils.resources:updateByKey',
          params: {
            resource: 'settings',
            key: 'name',
            value: keyName,
            data: { value: keyValue },
          },
        },
        {
          method: 'utils.resourceCache:clear',
          params: {
            name: 'settings',
          },
        },
        {
          method: 'utils.resources:getByKey',
          params: {
            resource: 'settings',
            key: 'name',
            value: keyName,
          },
          id: 'resource',
        },
      ],
      { uri: 'updateSetting' }
    );
    return resp.find((resp: JsonRpcResponseMessage) => {
      return resp.id === 'resource';
    }).result?.data[0].value;
  }

В данном примере используется id для определения нужного ответа, потому что ответы для batch приходят тоже как массив. Для единичных запросов id особо не нужен, а для batch - вполне.

Когда uri явно не указан, используется алгоритм его построения через вызываемый метод. Для batch запросов названия методов соединяются с разделителем.

В итоге в DevTools Network получаем всю нужную информацию.

/rpc/productCategories:list
/rpc/batch[utils.products:update+utils.products:list]
/rpc/sendEmail

На бэкенде в логах вебсервера информация также будет в удобоваримом виде.

В prod env эту штуку можно отключить и использовать только /rpc эндпойнт.

Документирование

Для документирования JSON-RPC API существует спецификация OpenRPC. Но из-за того, что точка входа бэкенд API является по сути точкой входа в некий блок кода на каком-то языке и не зависит от HTTP, документировать можно чем угодно. Например, javadoc-ом для бэка на java. Бэкендер просто пишет код бизнес логики, а JSON-RPC работает именно как RPC (remote procedure call), позволяя фронту прозрачно вызывать функцию на бэке не заботясь о транспортных посредниках.

Также у OpenRPC есть небольшая экосистема в виде генераторов, расширения для VS Сode и прочий инструментарий.

Минусы JSON-RPC

Неизвестны. Может кто напишет в комментариях.

Заключение

С моей точки зрения внедрение JSON-RPC в проект является очень полезным улучшением его архитектуры, позволяя отвязать бизнес логику бэкенда от транспортного протокола, внося ясность и чистоту как в код, так и во интерфейс между фронтенд и бэкенд разработчиками, облегчая развитие и поддержку приложения. Код как asset приобретет бо́льшую ценность.

Теги:
Хабы:
+6
Комментарии5

Публикации

Изменить настройки темы

Истории

Работа

PHP программист
155 вакансий
React разработчик
59 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн