REST и RPC: Введение в архитектуру HTTP API

REST и RPC: Введение в архитектуру HTTP API

Дмитрий Бахтенков

HTTP API

В прошлых статьях мы уже сталкивались с http api: исследовали его, а также писали свою реализацию. Настало время поговорить о подходах и правилах построения API, то есть об архитектуре. Рассмотрим подходы REST и RPC

Мем

REST

Что такое?

Самой популярной на сегодняшний день является архитектура REST - Representational State Transfer (передача состояния представления) - подход к построению HTTP API, который основан на передаче определённого представления сущностей и их состояния от сервера к клиенту. Данный подход включает в себя следующие принципы:

  • Клиент-серверная архитектура - разделение ответственности на функции клиента - потребителя данных, и сервера - поставщика этих данных. (добавить фото)
  • Stateless - этот принцип заключается в запрете хранения сервером контекста общения с клиентом. Например, сессия пользователя не должна храниться в памяти сервера, а для каждого пользователя подтягиваться из БД. Если сервер будет хранить состояние (stateful), то, например, не получится развернуть второй экземпляр сервера для распределения нагрузки (на старом сервере будет текущая сессия пользователя, а новый сервер не будет ничего знать о ней и снова создаст новую сессию)
  • Кэширование - каждый ответ сервера должен иметь пометку, можно ли его кэшировать. Например, если мы запрашиваем у сервера прогноз погоды, то для одинаковых дат нам будет выходить одинаковый результат. Его можно сохранить на клиенте, тем самым снизив нагрузку на сервер и уменьшив кол-во сетевых запросов
  • Единообразие интерфейса - это фундаментальное требование REST к построению интерфейса запросов. Состоит из 4 правил:
  1. Все ресурсы идентифицируются в запросах, например в URI: user/1 - это ресурс “Пользователь” с идентификатором “1”
  2. Манипуляция ресурсами через представление - сервер и клиент обмениваются представлениями ресурсов в различных форматах (чаще всего в формате JSON)
  3. Самодокументируемые сообщения - каждое сообщение содержит достаточно информации, чтобы клиент мог “понять”, как обрабатывать данные
  4. Гипермедиа как средство изменения состояния приложения (HATEOAS) - REST-клиенту не требуется заранее знать, как взаимодействовать с приложением или сервером, в ответе от сервера возвращаются варианты перехода к другим состояниям в виде гиперссылок
  • Слоистая архитектура - между клиентом и сервером общение не всегда происходит напрямую - запрос может обрабатываться прокси-серверами, балансировщиками нагрузки и другими системами. Суть принципа заключается в следующем: ни клиент, ни сервер не должны знать о том, как происходит цепочка вызовов
  • Код по требованию - REST предполагает возможность передачи кода (например, JavaScript) от сервера к клиенту. Это позволяет менять логику клиента с помощью сервера, что даёт больше гибкости в написании приложения

Саму архитектуру REST придумал Рой Томас Филдинг.

Модель зрелости REST API

Существует модель зрелости REST-сервисов Леонарда Ричардсона, который классифицировал REST API следующим образом:

Уровни зрелости REST-сервисов

Первый уровень - любые сервисы,которые в качестве транспорта используют HTTP и какой-либо формат представления данных, игнорируя множество эндпоинтов и http-глаголов, таких как POST, PUT и т.д.

Второй уровень - использование различных путей для ресурсов, например

  • api/users - управление пользователями
  • api/orders - управление заказами

Третий уровень - использование http-глаголов для управления сущностями, например:

  • GET api/users - получить всех пользователей
  • POST api/users - создать пользователя
  • PUT api/users/{id} - обновить пользователя с определённым идентификатором

И самый верхний, четвёртый уровень - использование HATEOAS, то есть ссылок на соседние сущности в рамках определённого ответа. Например, представим что у нас есть приложение с пользователями, товарами и заказами. Мы выполняем запрос GET api/users/12 и получаем ответ со ссылкой на список заказов, связанных с данным пользователем:

{ 
  "id": 12, 
  "fio": "Иванов иван иванович", 
  "links": 
  [ 
    { "rel": "orders", "href": "/users/12/orders" } 
  ] 
}

Запрашивая заказы - GET /users/12/orders, мы в свою очередь также получим ссылки на товары по данному заказу:

[{ 
  "id": 1, 
  "userId": 12,
  "date": "11/12/2022", 
  "links": 
  [ 
    { "rel": "user", "href": "/users/12" }, 
    { "rel": "user", "href": "/orders/1/products" } 
  ] 
}]

Программный интерфейс, соблюдающий все принципы архитектуры REST, называется RESTful API (полным по REST).

Преимущества REST:

  • Понятность программного интерфейса - достаточно просто трактовать HTTP-глаголы и понимать на основе URI с какой сущностью мы работаем
  • Масштабируемость - за счёт подхода Stateless можно создавать несколько копий одного API и распределять нагрузку между ними, также чётко разделены клиент и сервер

Недостатки REST:

  • Очень много запросов. Для примера рассмотрим сайт с пользователем, заказами и товарами. Чтобы определить список заказов и товаров определённого пользователя, нужно:

a. Получить список заказов по пользователю

b. Для каждого заказа получить информацию о товаре

  • Таким образом, если у пользователя 2 заказа, в каждом из которых по 2 товара, в рамках чистой архитектуры REST необходимо выполнить минимум 4 запроса. А заказов в интернет магазинах у людей обычно больше 🙂
  • Не всегда можно определить корректную иерархию сущностей. Рассмотрим пример мессенджера, где есть пользователи и групповые чаты. У каждого пользователя может быть несколько чатов, а в каждом чате - несколько пользователей. Образуется связь многие-ко-многим, которую достаточно тяжело представить в рамках REST. Запрос на добавление пользователя в чат должен выглядеть так:

POST api/chats/{chatId}/users/{userId}

Или так:

POST api/user/{userId}/chats/{chatId}?

Мы добавляем пользователя в чат, или чат к пользователю?

На самом деле оба варианта верны. У каждого разработчика “свой” REST, и необходимо просто следовать принципам, определённым в вашей организации

RPC

Общее определение RPC звучит так:

RPC - (Remote Procedure Call, удалённый вызов процедур) — класс технологий, позволяющих программам вызывать функции или процедуры в другом адресном пространстве (на удалённых узлах, либо в независимой сторонней системе на том же узле)
{ 
  "jsonrpc": "2.0", // версия протокола 
  "method": "", // название функции, которую необходимо выполнить
  "params": 
  { 
    "param1": "value", 
    "param2": "value" 
  } 
}

Например, создание пользователя: POST api/userservice/execute:

{ 
  "jsonrpc": "2.0", 
  "method": "CreateUser", 
  "params": 
  { 
    "login": "mail@example.com", 
    "password": "12345", 
    "fio": "Иванов Иван Иванович " 
  } 
}

И ответом на такой запрос может быть:

// 201 - created 
{ 
  "jsonrpc": "2.0", 
  "result": 
  { 
    "id": 6582 
  } 
} 

По сути RPC представляет собой самый нижний уровень зрелости REST-сервисов Леонарда Ричардсона. Ну, потому что rpc это не rest 🙂

Архитектура RPC была придумана гораздо раньше REST и использовалась, например, десктопных технологиях экосистемы Microsoft. Протокол SOAP также является расширением xml-rpc

Преимущества RPC

  • Проще в реализации - единый эндпоинт, вызов конкретных функций
  • Высокая производительность
  • Более понятен при описании сложных операций (не CRUD)

Недостатки RPC

  • Практически нет стандартизации
  • Сложность обнаружения и документирования
  • Сложности кэширования на уровне инфраструктуры HTTP (например, кэширование в веб-сервер nginx выполняется с помощью uri и глаголов запроса, что очень удобно в случае REST - когда у нас разные глаголы и пути, и усложняется при использовании RPC)

Что когда использовать?

REST:

  • Открытое внешнее API (например для интеграции с вашей системой)
  • Много незнакомых потребителей (например, когда мы разрабатываем соцсеть, мы не знаем кто будет потребителем и что он будет делать с системой; в случае какого-то внутреннего приложения автоматизации, мы нацелены на конкретных потребителей и сами определяем бизнес-процесс)
  • Простые CRUD-операции
  • Необходимо использование HTTP-инфраструктуры, например кэширование

RPC:

  • Общение между несколькими бэкэндами (back-to-back)
  • Заранее знаем потребителей нашего API
  • API представляет сложные бизнес-процессы (например, движение какой-то сущности по статусам с вызовом дополнительных триггеров и какими-то доп. настройками)
  • Сложные запросы на получение информации (вместо тысячи стандартных REST-запросов выполнить один-два и получить нужную информацию)
Несмотря на то, что в некоторых случаях RPC может быть более предпочтительным вариантом, чаще всего всё равно используют REST. Например, при выполнении какой-то функции, которая изменяет состояние сущности, название этой функции обычно трактуется как подресурс: POST api/requests/{id}/change-status. А в случаях, когда на странице отображается очень много информации, которая, если следовать REST, вытаскивается с помощью большого количества запросов - просто делается один эндпоинт, в рамках которого вытягивается всё что нужно для загрузки страницы.


Report Page