Как стать автором
Обновить
54.76
SpaceWeb
Хостинг, серверы и веб-разработка

Как мы выбирали протокол для клиентского API. Сравнение JSON-RPC 2.0 и RESTful API

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

Привет, Хабр! Меня зовут Виталий Киреев, я руководитель R&D в SpaceWeb. В статье расскажу, как мы внедрили JSON-RPC в разработку SpaceWeb и объясню, почему выбрали именно эту технологию, а не RESTful API. А ещё покажу реальные кейсы и метрики использования технологии JSON-RPC.

Как выбирали технологию: JSON-RPC 2.0 vs RESTful API

Я руковожу разработкой SpaceWeb с 2017 года. Когда я только пришел в компанию, у нас была большая задача — дать клиентам возможность управлять услугами самостоятельно через API. На тот момент в компании было больше 450 видов взаимодействия с услугами. Это довольно большой фронт работы, под который нужно тщательно выбрать технологию. Мы остановились на двух самых популярных: JSON-RPC 2.0 и RESTful API. Давайте рассмотрим их детальнее, а в процессе я объясню преимущество JSON-RPC 2.0 для наших задач.

RESTful API

В среде разработчиков эта технология самая популярная: у неё больше реализаций в серверной и клиентской части, а архитектурный стиль понятен как разработчикам, так и пользователям. К тому же она хорошо ложится на архитектуру WWW.

RESTful API состоит из уровня транспорта — протокол HTTP, уровня приложения и бизнес-логики. Причём уровень транспорта сильно связан с уровнем приложения. Технология может работать и не на этом протоколе, но общепринятое использование «из коробки» — как раз в HTTP. Также у нас есть объекты — endpoint url. Мы с ними можем взаимодействовать с помощью различных типов HTTP запросов (GET, POST, PUT, DELETE и другие). Можем использовать здесь разные возможности HTTP.

Реализация технологии RESTful API находится на уровне приложения. Мы должны сделать программный продукт, который смог бы принимать запросы, обращаться к нижнему уровню бизнес-логики и возвращать результат клиенту. Это понятная архитектура, которая отлично ложится на большинство видов взаимодействия клиентов с бизнес-логикой. Уровень Application прописан, оттестирован и имеет реализации на разных языках.

           Схематичное изображение технологии RESTful API 
           Схематичное изображение технологии RESTful API 

JSON-RPC 2.0 

В JSON-RPC 2.0 архитектура будет похожей, но на уровень Application мы добавляем дополнительный слой Middleware. Он будет предоставлять интерфейсы для получения и передачи запросов. 

Теперь о главном отличии технологии JSON-RPC 2.0. Она транспортно независимая уже «из коробки». Обмен запросами и результатами выполнения бизнес-логики происходит в формате JSON. Например, у нас есть запрос на вызов удалённой процедуры с определёнными параметрами. Процедура отработает запрос на стороне бизнес-логики, и мы вернём ответ — тоже в формате JSON. 

Первое преимущество JSON-RPC 2.0 в том, что мы можем поменять транспорт, если это нужно. Технология может работать как через HTTP, так и через транспорт брокеров сообщений — например, на протоколе AMQP. При использовании JSON-RPC мы снимаем с протокола бизнес-логику, например, по управлению ошибками. 

Схематичное изображение технологии JSON-RPC 2.0 
Схематичное изображение технологии JSON-RPC 2.0 

Различия в интеграции JSON-RPC 2.0 и RESTful API

Чтобы выбрать подходящую технологию для проекта, важно не только учесть её специфику, но и продумать архитектуру и предусмотреть различия. Разберём оба варианта интеграции на примерах. 

Интеграция RESTful API

Интеграция RESTful API будет примерно одинакова для любого языка. Для примера возьмём  Python, пакет FastAPI. Код внизу — не коммерческий продукт, а упрощённая реализация. На этом примере покажу архитектурные отличия.

from fastapi import FastAPI, Request, HTTPException, Response, status
from model.check import Success, Fail, Errors
from typing import Union

app = FastAPI() # инициация приложения

@app.delete("/user/{id}") # декоратор для роутинга
async def delete(response: Response, id:int) -> Union[Success, Fail]:
    if users.delete(id=id): # вызов бизнес-логики
         return Success(status="ok")
     else:
        errors = [Errors(message="user is not found", code="not_found")]
        response.status_code=status.HTTP_404_NOT_FOUND
        return Fail(status="fail",errors=errors)

На 5 строке инициализируем уровень самого приложения. Дальше — описание и формирование метода работы и передачи состояния. Здесь мы должны описать определенный роутинг url. После этого описываем тип запроса: это может быть GET, POST, PUT, DELETE и др. В примере мы берём DELETE. 

Затем делаем декоратор, входящий в пакет FastAPI — он будет обслуживать роутинг и тип запроса. По этому декоратору будет отрабатывать сама функция API. В ней мы уже описываем формат запроса, формат ответа, вызов непосредственно бизнес-логики. А с помощью users.delete мы должны вызвать нужную бизнес-логику в API в явном виде. 

В конце примера — вариации ошибок, где мы можем использовать код 404. Чтобы всё работало корректно, мы должны продумать все роутеры, форматы запросов и ответов, типы запросов и ответов. Ещё стоит предусмотреть управление кодами ответов — например, часто вместо кода 200 используется код 201.

Интеграция JSON-RPC 2.0

Для начала интеграции инициализируем приложение через def application. И затем оборачиваем в декоратор JSON-RPC. 

from users import Users
import inspect
from werkzeug.wrappers import Request, Response
from jsonrpc import JSONRPCResponseManager, dispatcher

@Request.application
def application(request):
     users = Users() # класс к которому открываем доступ
     method_list = inspect.getmembers(users, predicate=inspect.isfunction)
      for method in method_list:
            # диспетчер методов {<method_name>: callable}
            dispatcher[method[0]] = method[1]
response = JSONRPCResponseManager.handle(
       request.data, dispatcher)
return Response(response.json, mimetype='application/json')

Чтобы реализовать интеграцию с бизнес-логикой, мы должны в приложении инстанцировать объект, с которым будем работать. В нашем примере это 8 строка: мы инстанцируем объект класса Users, с помощью него получаем доступ к бизнес-логике. 

Дальше происходит магия RPC — второе важное преимущество технологии. Через inspect мы получаем все методы класса и передаём их в диспетчер удалённого вывода. Так диспетчер получает методы класса бизнес-логики. Пример в нашем случае тривиальный, но мы можем передавать не все методы, например, не передавать приватные или сделать отдельный класс для реализации нужных методов. 

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

Резюме: разница в интеграции RESTful API и JSON-RPC 2.0

RESTful API:

  • Нужен роутинг для каждого метода.

  • Продумываем формат параметров и возвращаемых значений для каждого вызова API.

  • При необходимости управляем кодами ответов HTTP.

Вывод: тратим время на разработку не только бизнес-логики, но и на разработку методов для API.

JSON-RPC 2.0:

  • Подключаем класс напрямую.

  • Управление параметрами, методами и возвращаемыми значениями находится на уровне класса.

Вывод: тратим время только на разработку бизнес-логики.

Итоги в нашем проекте: что дал нам выбор технологии JSON-RPC 2.0

Вернёмся к вводным по нашей задаче — в SpaceWeb есть более 450 способов взаимодействия с услугами, и нам нужно реализовать API для этого. Если мы выбираем технологию, которая требует оборачивание каждого вызова в декораторы, придётся тратить на это много времени. Это не так сложно, но мы будем тратить дополнительный ресурс, который очень важен в коммерческой разработке, особенно на запуске нового продукта.

Сравнив технологии, мы пришли к выводу: технология JSON-RPC 2.0 в нашем проекте позволяет сократить время на разработку API более чем в 2 раза по сравнению с FastAPI. И более чем в 4 раза по сравнению с Django. Для нас эти цифры были весомыми на этапе, когда мы оценивали объём работ. А когда освоили технологию JSON-RPC 2.0, мы внедрили её и в других наших проектах. 

Ниже расскажу о технологии JSON-RPC 2.0 детальнее. А ещё о других профитах, которые она нам дала. 

Описание технологии JSON-RPC 2.0 

Формат запроса 

Структура JSON запроса:

  • Версия протокола. С её помощью можно синхронизироваться с клиентом, сервером и проверять совместимости.

  • Методы. Удалённая процедура или метод, который вы собираетесь вызывать на стороне приложения

  • Параметры. Объект с именованными параметрами или массив с позиционными параметрами. Иногда их может не быть в запросе.

  • ID запроса. Это одна из важнейших вещей, которая позволяет идентифицировать запрос «из коробки». Об этом расскажу подробнее ниже.

Пример запроса:

{"jsonrpc": "2.0", "method": "delete", "params": {"user_id": 23}, "id": 3}
  • "jsonrpc" — версия протокола

  • "method" — удалённая процедура

  • "params" — параметры (не обязательно)

  • "id" — уникальный ID запроса (не обязательно, если отсутствует, то будет сгенерирован на стороне сервера)

Поддерживается запрос с позиционными параметрами:

{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}

Если посмотреть на пример запроса внимательно, можно увидеть, что процедура имеет имя метода. Возникает вопрос: параметр для метода только один, а классов в бизнес логике может быть несколько. Что делать? Решить проблему можно двумя способами:

Способ 1. Привязываем endpoint к определённому объекту. Например, у нас есть url, по которому доступен объект. Мы берём методы, которые отправляем в RPC, и делаем так, чтобы они вызывались у нашего объекта. Это решение мы использовали в нашем проекте. 

Способ 2. Делаем единый endpoint и методы условно через точку: “class.method”. Такая реализация может потребовать донастройки на стороне JSON-RPC сервера. 

Формат ответа при успехе

Пример:

{"jsonrpc": "2.0", "result": true, "id":3}
  • "jsonrpc" — версия протокола

  • "result" — результат

  • "id" — уникальный ID запроса

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

Но ещё в JSON-RPC могут быть запросы, которые не подразумевают ответа. Они называются Notification. Это тоже рабочая технология «из коробки», которая позволяет нам вызывать RPC и не заставлять клиента ждать ответ. Чаще всего запрос используется для уведомлений — отсюда и название.

Формат ответа при ошибке 

Пример:

{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
  • "jsonrpc" — версия протокола

  • "error" — объект ошибки

  • "id" — уникальный ID запроса

Что делать, если что-то пошло не так? Для этого в JSON-RPC 2.0 есть формат ответа об ошибке — объект error, который состоит из кода и сообщения. Здесь важно передать ID запроса — клиент должен чётко понимать, на какой запрос пришла ошибка. 

Возможные коды ошибок:

code

message

meaning

-32700

Parse error

Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.

-32600

Invalid Request

The JSON sent is not a valid Request object

-32601

Method not found

The method does not exist / is not available

-32602

Invalid params

Invalid method parameter(s).

-32603

Internal error

Internal JSON-ROC error.

-32000 to -32099

Server error

Reserved for implementation-defined server-errors.

Преимущество независимости технологии JSON-RPC 2.0 от протокола 

RESTful API включает в себя полный цикл запроса HTTP: клиент отправляет запрос, сервер работает с бизнес-логикой и возвращает ему ответ. Это несёт большие накладные расходы.

JSON-RPC не привязан к протоколу, здесь мы можем внедрить асинхронную работу. Для этого нужно сменить транспорт на протокол брокера сообщений. Так, если у нас есть соединение через брокера сообщений, мы можем ставить задачи на выполнение для бизнес-логики и отдельно получать ответы. Клиенту не нужно будет ждать ответ, как в случае с RESTful API.

Схема слева — RESTful API, схема справа — JSON-RPC 2.0
Схема слева — RESTful API, схема справа — JSON-RPC 2.0

Мы сравнили скорость запроса к API на уровне протокола взаимодействия. Результаты:

  • запрос по HTTP — 0,075 секунды.

  • запрос по AMQP — 0,031 секунды.

Опираясь на эти данные, мы выбрали протокол AMQP вместо HTTP. Итог: выбор протокола AMQP вместо HTTP для транспорта позволил в 2 раза увеличить скорость взаимодействия микросервисов с API.

Batch в JSON-RPC 2.0

Ещё одна особенность JSON-RPC, которая может дать существенный профит во времени — выполнение нескольких запросов одновременно, так называемые пачки или batch. 

Работает это так: мы упаковываем в массив несколько JSON запросов, чтобы получить по ним ответы, собрать их и вернуть клиенту. По некоторым ответам мы можем не ждать окончания запросов, например Notification, тогда этот ответ не будет включаться в общий ответ. 

Пример вызова нескольких удалённых процедур:

[ 
        {"jsonrpc": "2.0", "method": "delete", "params": {"user_id": 23}, "id": 1},
        {"jsonrpc": "2.0", "method": "notify", "params": {"message_id": 10}, "id": 2},
   ]

Ответы будут по каждому ID запроса:

[ 
        {"jsonrpc": "2.0", "result": true, "id": "1"},
        {"jsonrpc": "2.0", "result": true, "id": "2"},
   ]

В RESTful API эта функция не поддерживается «из коробки» — нам пришлось бы отправлять запрос, ждать ответ и только потом отправлять следующий запрос. Чтобы получить возможность параллельной отправки, придётся допиливать функционал на клиенте.

Профит — возможность выполнять несколько запросов к API параллельно за 1 вызов.

Плюсы чистой архитектуры в JSON-RPC 2.0

В технологии JSON-RPC 2.0 можно найти и такие преимущества, которые отлично ложатся на плюсы чистой архитектуры. Вот они:

  • Есть разделения на слои транспорта, JSON-RPC сервера, приложения и бизнес-логики. При этом JSON-RPC сервер может использоваться микросервисами по одному протоколу или веб-клиентом по другому протоколу. Важно, что это будет работать «из коробки», нам ничего не придется допиливать. 

  • Мы можем покрыть тестами каждый уровень.

  • Можно отдельно настроить мониторинги каждого уровня. 

Плюсы идемпотентности и версионности

Это преимущество возвращает нас к ID запроса. Особенность в том, что ID уникален для каждого запроса: он генерируется на стороне клиента или на стороне сервера. Это позволяет получить все плюсы идемпотентности: 

  • Мы гарантированно получим один и тот же ответ на одинаковые запросы. Это позволит избежать двойной работы бизнес-логики — например, не будет такой ситуации, когда у клиента спишутся деньги дважды из-за проблем с несколькими попытками отправки запроса. 

  • По идентификатору запросов мы легко сможем находить ошибки. А клиент может определять, на какой запрос пришла ошибка. Это даёт возможность легко информировать клиента о запросах, выполненных с ошибкой.

  • В запросе передаётся версия. Это позволяет синхронизироваться с разными клиентами. Если вы разрабатывали когда-то мобильные приложения с интеграцией с API, то наверняка встречались с проблемой, когда разные клиенты могут быть с разными версиями приложений, из-за чего API работает некорректно. Эта проблема будет решаться только версионностью: когда сравниваем версии клиента и сервера, а потом выполняем бизнес-логику, которая корректна для версии клиента.

  • Удобно использовать при множестве разных клиентов: веб, iOS, Android, микросервисы.

Спецификация OpenRPC для JSON-RPC 2.0

Спецификация OpenRPC сильно отличается от YAML OpenAPI. Пример:

{
   "methods":  [
        {
          "name":  "rpc.discover",
          "description": "Returns an OpenRPC schema as a description of this service",
           "params": [],
           "result": {
              "name": "OpenRPC Schema",
              "schema": {
                   "$ref": "https://raw.githubusecontent.com/open-rpc/meta-schema/master/schema.json"
            }
         }
       }
   ]
}

Автоматическое формирование документации из OpenRPC

В OpenRPC есть несколько автоматических генераторов, но они нам не подошли, потому что мы используем технологию на нескольких endpoint. Так мы сделали свой собственный генератор документов, собрали его на ReactJS. 

Наш генератор документов формирует красивую визуализацию документов с описанием методов, endpoint, параметров, возвращаемых значений. Подробно не буду рассказывать о реализации, но вот ссылка на GitHub проекта.

Автоматическое создание клиента из OpenRPC

В свободном доступе уже достаточно генераторов кода клиента. Из хороших — генераторы TypeScript-клиентов, которые позволяют использовать клиента как на уровне фронтенда, так и на уровне бэкенда. 

Но мы также сами сделали генераторы кода клиентов для наших основных языков — Python и PHP. Вот ссылка на GitHub проекта.

Общие итоги: профит от использования JSON-RPC 2.0

Главные итоги работы с JSON-RPC 2.0:

  • Скорость взаимодействия микросерверов с API увеличилась в 2 раза за счёт асинхронной работы и смены транспорта.

  • Создали генератор клиентов для Python и PHP, который используем сами и даём партнерам.

  • Запустили генератор документации на ReactJS.

  • Увеличили число клиентов, использующих наше API в 3 раза.

Также у нас значительно сократилось время на разработку API:

  • Сократили разворачивание API для новых серверов в 4 раза.

  • Добавление новых функций в API ускорилось в 2 раза. После интеграции мы можем не дорабатывать Middleware, работаем только с бизнес-логикой.

  • Сократили количество unit тестов в 1,5 раза. 

В конце хочется отметить, что обе технологии — JSON-RPC 2.0 и RESTful — позволили бы решить наши задачи. Нет смысла говорить о том, что какая-то из них лучше или хуже. Однако конкретно в нашем кейсе, где нужно было сделать 450 вариантов взаимодействий, да ещё и покрыть тестами, разница во времени на разработку ощутимая. Именно поэтому мы отдали предпочтение JSON-RPC 2.0.

А какую технологию для API используете вы? Делитесь в комментариях!

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

Публикации

Информация

Сайт
sweb.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия