Skip to content

Принципы проектирования дизайна Flussonic API

Содержание:

Общая информация по дизайну API

Мы (компания "Эрливидео") предоставляем клиентам разные продукты и сервисы, такие как Media Server, Watcher, Iris, Retroview и т.д.

На этой странице Вы найдёте актуальную информацию о принципах организации HTTP API к нашим системам.

Свойства API

Мы проектируем HTTP API для доступа других программ к нашим системам. Основные принципы, которые мы стараемся соблюдать:

  • Ориентация на индустриальные стандарты и общепринятые практики с сохранением принципа разумности.
  • Использование REST + JSON при построении API.
  • Спецификация в формате OpenAPI 3.1 (бывший Swagger) для машинопригодного описания API.
  • Подход API-first — первичность API по отношению к коду. Это означает, что сначала мы проектируем API, потом генерируем по нему код с помощью разработанного нами в рамках этого подхода openapi_handler, который мы предоставляем в публичное пользование.
  • Строгое соответствие схемы API возвращаемым в payload данным. Никакие отсутствующие в схеме поля не могут быть получены от сервиса.
  • Гибкое соответствие схемы API передаваемым в payload данным. Строгая валидация известных полей. Попытка отправить известное поле в неправильном формате приведет к ошибке. Неизвестные поля в payload будут проигнорированы.
  • Удобство и простота создания как лёгких, так сложных запросов (например, простой запрос списка потоков и сложный запрос с несколькими фильтрами).
  • Обеспечение единообразия терминологии между разными системами (имя сущности в одной системе должно быть одинаковым во всех системах).
  • Обеспечение единообразия методов доступа к разным системам. На сервере может быть коллекция из одного бэкенда авторизации, а в сервисе хранения истории просмотров могут лежать триллионы записей о сессиях. Доступ к этим системам должен обеспечиваться единообразным API.
  • Предпочтение HTTP вебсокетам. Доступ к данным по вебсокетам плохо стандартизован и трудно мониторится.
  • Получение разумно ограниченного объёма данных по каждой выборке. Так клиент не будет получать огромное количество записей по умолчанию.
  • Учёт работы с динамичным типом данных.
  • Поддержка GET-запросов и задекларированных параметров в строке запроса (query string) для создания простых запросов доступа к большим коллекциям и отказ от сверхсложных правил парсинга строки запроса с переходом к JSON-языку запроса, передаваемому с помощью метода POST.
  • Идемпотентность API, т.е. одинаковый результат при многократном повторе одних и тех же запросов.
  • Обеспечение работы со сложными объектами, вложенными подобъектами и коллекциями.

Аутентификация и авторизация

Flussonic Media Server предоставляет возможность получать информацию и управлять определённой функциональностью при помощи HTTP.

Запросы на получение информации можно защитить с помощью директивы view_auth user password;, а на модификацию состояния Flussonic Media Server — с помощью директивы edit_auth user password; в конфигурационном файле /etc/flussonic/flussonic.conf.

Подробнее о настройке авторизации во Flussonic см. Авторизация.

Для доступа к HTTP API при настроенной авторизации необходимо указать логин и пароль в формате HTTP Basic Auth.

Так, например, для edit_auth user password; авторизация будет выглядеть следующим образом:

GET /streamer/api/v3/streams HTTP/1.1
Host: FLUSSONIC-IP
Authorization: Basic dXNlcjpwYXNzd29yZA==

HTTP 200 OK
Date: Sun, 19 Sep 2021 19:40:22 GMT
Content-Type: application/json
...

Можно также использовать Bearer-авторизацию, используя тот же логин и пароль:

GET /streamer/api/v3/streams HTTP/1.1
Host: FLUSSONIC-IP
Authorization: Bearer dXNlcjpwYXNzd29yZA==

HTTP 200 OK
Date: Sun, 19 Sep 2021 19:40:22 GMT
Content-Type: application/json
...

Note

Для других систем авторизация может отличаться, но чаще всего используется Bearer-авторизация с токенами.

Поддержка OpenAPI

Все системы, поддерживающие описанные в этом документе принципы, предоставляют описание сервисов в машиночитаемом формате OpenAPI 3.1. Этот формат является развитием таких форматов описания API, как Swagger и JSON Schema.

OpenAPI предусматривает получение формализованной схемы сервиса, т.е. списка доступных методов и описание входных и выходных данных.

Эту OpenAPI-схему API медиасервера можно получить с работающего сервера Flussonic по следующему URL:

curl http://FLUSSONIC-IP:8080/streamer/api/v3/schema

Кроме того, доступны публичные справочники API, перечисленные на странице с обзором API. По любой из приведенных там ссылок также можно получить схему, добавив в конце URL .json, например так:

https://flussonic.com/doc/api/reference.json

Ниже приведён пример того, как можно в несколько строчек кода с помощью React Query и openapi-client-axios получить доступ к списку потоков:

import React from 'react';
import { useQuery } from 'react-query'
import OpenAPIClientAxios from 'openapi-client-axios';


const api = new OpenAPIClientAxios({
  definition: 'http://localhost:8080/streamer/api/v3/schema',
  axiosConfigDefaults: {
    headers: {
      'Authorization': 'Basic dXNlcjpwYXNzd29yZA==',
    },
  },  
});


function Streams() {
  const { isLoading, error, data } = useQuery({
    queryKey: "streams", 
    queryFn : () => {
      return api.init()
        .then(client => client.streams_list({}))
        .then(res => res.data);
    },
    keepPreviousData: true,
    refetchInterval: refetchInterval,
  });

  if(isLoading) return <div>Loading</div>;
  return <div>
    {data.streams.map((stream) => <div key={stream.name}>{stream.name}</div>}
  </div>;
}

Основная идея этого кода в том, что библиотека прочитает схему API, получит из неё список конечных точек, или эндпойнтов, и сгенерирует из них набор функций. В данном примере streams_list взят именно из схемы и сгенерирован программно.

Приватное и публичное API

Важно отметить, что OpenAPI не даёт возможности разделять приватное и публичное API.

Существует следующее правило, касающееся приватного и публичного API: если у поля нет описания (description) или есть параметр x-private: true, то это часть приватного API.

Мы используем этот подход, чтобы вводить экспериментальные поля, которые могут поменяться без предупреждения и обратной совместимости.

Когда мы уверены, что экспериментальное поле работает как ожидается, мы можем добавить для него описание или убрать параметр x-private: true, и, таким образом, поле станет частью публичного API.

Устаревшие поля

Некоторые поля во Flussonic API отмечены как deprecated: true. Это означает, что мы решили через некоторое время удалить это поле и использовать вместо него новое поле.

Обычно для устаревших полей мы задаем параметр x-delete-at, в котором указываем версию Flussonic, в которой планируется удаление. Например, x-delete-at: 23.02 означает, что мы планируем удалить это поле в версии Flussonic 23.02. Если вы используете это поле и не хотите, чтобы мы его удаляли, вы можете связаться с нами в течение оставшегося периода времени и обсудить возможность оставить это поле.

Коллекции

Практически все объекты организованы в коллекции (аналог SQL-таблиц). Сетевой доступ к системе на чтение данных во многом определяется способом чтения объектов из коллекции.

Для быстро работающих пользовательских интерфейсов и предсказуемо работающих программ необходимо:

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

Эти два требования немного конфликтуют друг с другом. Требование на ограничение набора данных подразумевает получение лишь части объектов из коллекции, например, 50 потоков из 2000, что есть на сервере.

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

Note

Мы не предлагаем единого подхода к фиксированию снапшотов коллекции и допускаем подобный риск потери ряда записей при постраничной выборке из динамически меняющейся коллекции.

Для предсказуемого доступа к подмножеству коллекции мы описываем и реализуем язык, определяющий следующие действия над коллекцией:

  • фильтрация (аналог SQL WHERE),
  • сортировка (аналог SQL ORDER BY),
  • ограничение количества элементов (аналог SQL LIMIT),
  • дополнительная фильтрация по курсору (аналог SQL OFFSET),
  • ограничение набора полей (аналог SQL SELECT)

HTTP-методы доступа к коллекциям

Стандартный способ передачи запроса на чтение с клиента на сервер осуществляется с помощью языка строки запроса (query string) и метода GET. Традиционно считается, что доступ, не предполагающий каких-либо модификаций, осуществляется с помощью GET-запроса (в т.ч. для кеширования).

Note

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

При использовании простого и даже примитивного языка строки запроса (query string) необходимо или использовать стандартный подход вида key=value, или использовать нестандартные разделители. В последнем случае такая строка запроса будет парситься нестандартно, что может вызвать ряд затруднений:

  1. Слишком сложные запросы при вложенных условиях. Написание такого рода условий в строке запроса (query string) будет выглядеть крайне громоздко и сложно, что противоречит одному из наших главных принципов — удобство и простота создания запросов.
  2. Неверная обработка запросов при использовании дополнительных разделителей в подмножествах. В таком случае необходимо помнить какие символы можно использовать без последствий, а какие — нет. Например, запятая (",") или дефис ("-") не используются в именах полей и не преобразуются в строке запроса. Таким образом, их можно использовать, в то время как амперсанд ("&") уже нельзя, т.к. это специальный символ для строки запроса и, если его не экранировать, то могут возникнуть проблемы при обработке запроса.

Так, например, некоторые компании используют в запросе сортировки символы - и + для указания направления сортировки. Символ + является спец. символом и при декодировании строки запроса будет расцениваться как пробел. Значит, необходимо либо экранировать его в %2B, либо воспользоваться нестандартным парсером, чтобы избежать возможных проблем с выполнением запроса. Использование нестандартных парсеров затрудняет подготовку запроса стандартными библиотеками.

Note

Существует ещё одна особенность, связанная с использованием символов Юникода (Unicode). У некоторых компаний аналог SQL запроса WHERE age > 20 кодируется в строке запроса (query string) как age>20. Использование Unicode-символа вместо стандартного ASCII-символа > позволяет избежать экранирования для размещения в HTML, но набрать такой запрос с клавиатуры весьма сложно. Мы, в свою очередь, отказались от использования символов Юникода, так что набрать из консоли их не удастся.

Описанные выше проблемы призваны проилюстрировать сложность упаковки разнообразных условий и аналогов языка SQL в очень ограниченный язык HTTP query string (язык строки запроса).

Более сложный вариант — передача языка запроса в JSON-формате в теле POST-запроса. Такой подход выглядит неизбежным решением при использовании сложносоставных вложенных условий.

Формат ответа

При доступе к коллекции возвращается JSON-объект со следующими полями:

{
  "ITEMS": [...],
  "next": ...,
  "prev": ...,
  "estimated_count": ...,
  "timing": ...
}

Вместо поля ITEMS подставляется имя той коллекции, которую запрашивали. Для потоков это будет streams, для сессий — sessions.

  • next и prev — это значения курсоров (следующий и предыдущий соответственно).
  • estimated_count — примерное количество элементов в коллекции. Точность этого значения не уточняется.
  • timing — служебный объект с различными временами доступа. Не описывается, может измениться или удалиться.

Фильтрация коллекций

Чтобы отфильтровать запрос, добавьте параметры в URL строку запроса.

Фильтрация по значению

Запрос на фильтрацию по значению (например, provider) передается следующим образом:

curl http://FLUSSONIC-IP:8080/streamer/api/v3/streams?provider=Sky

Поддерживается запрос на вхождение в список:

provider=Sky,Canal,CNN (значения перечисляются через запятую)

Можно также указать условие на поле вложенного объекта:

stats.alive=true

Фильтрация по условию

Для фильтрации можно также использовать условия непрямого сравнения. Для кодирования таких условий мы используем суффиксы типа _lt или _gt. Так условие WHERE stats.delay < 5000 будет записано следующим образом: stats.delay_lt=5000.

Мы определили фиксированный набор суффиксов, обеспечивая тем самым два непересекающихся множества с имеющимся множеством имён полей во всех наших системах. Это позволяет нам предоставлять достаточный набор возможностей для создания запросов доступа.

Note

Так, например, в одном из вариантов API было принято решение передавать запросы не по прямому сравнению с помощью .: delay.lt=5000. Однако в этом случае авторы API лишились возможности давать доступ к полям вложенных объектов.

Ниже приведён список поддерживаемых суффиксов:

Запись в строке запроса SQL Комментарий
age=50 age = 50
age_lt=50 age < 50
age_lte=50 age <= 50
age_gt=50 age > 50
age_gte=50 age >= 50
age_is=null age IS NULL Только для сравнения с NULL.
age_is_not=null age IS NOT NULL Только для сравнения с NULL.
age_like=pattern age LIKE '%pattern%' Только для строковых параметров

Несколько указанных в query string фильтров применяются к коллекции последовательно, подобно запросам с использованием AND в SQL:

curl  http://FLUSSONIC-IP:8080/streamer/api/v3/streams?stats.bitrate_gt=4000&stats.clients_count_lt=10

Note

Аналога OR для query string мы не предлагаем в силу сложной записи скобок.

Note

Поля фильтрации указаны без префикса. Например, можно было бы указывать их через filter.age=50. Это может быть удобнее с точки зрения программирования, валидации и т.п., но неудобно для человека, который будет пользоваться таким API.

Сортировка коллекций

Сортировка коллекций нужна для отображения в веб-интерфейсе Flussonic UI и получения предсказуемой постраничной выборки коллекции.

Параметры сортировки передаются через запятую к ключу:

sort=stats.ts_delay,-stats.bitrate

По умолчанию используется сортировка по возрастанию. Для смены направления укажите - перед названием поля.

Отсутствие префикса перед полями фильтрации означает, что мы не допускаем в нашем API поля sort в объектах.

Важно отметить, что в нашем API везде добавляется имплицитная сортировка. Если клиент закажет сортировку по полю provider (т.е. по полю, которое заведомо неуникально), то получится несколько групп объектов, которые отсортированы непонятным образом. Для этого мы дописываем в хвост запроса на сортировку поля типа name и position, применяя тем самым имплицитную сортировку.

Если они явно указаны в списке полей сортировки, то повторная сортировка по ним уже не производится.

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

Ограничение количества элементов (курсоры)

В этом документе неоднократно упоминалось, что HTTP API должен предоставлять доступ к коллекциям огромного размера, отдавая их по частям.

Получить первые 100 элементов из коллекции очень просто: достаточно добавить поле limit=100 в строку запроса (query string). Вопрос в том, как получить следующиее 100 элементов.

Note

Классический ответ из баз данных SQL — передать поле offset. Однако мы от него отказались, поскольку он вычислительно дорогой (из-за алгоритма "маляра") и не имеет практической ценности. Мы говорили о том, что ряд коллекций может изменяться между соседними запросами, а значит, offset будет гарантированно промахиваться и отдавать все записи не последовательно, а неизвестно как, без шанса вычислить, что было потеряно.

Note

При постраничном доступе не требуется получать записи со смещением 100. Необходимо получить следующую пачку записей. Такой подход с курсорами совершенно непривычен для mysql, по причине их практического отсутствия в этой БД. Однако в более старых БД курсоры всё ещё используются.

В ответе на запрос первых 100 элементов мы отдаем поле next (а в следующих выборках и prev). Значение этого поля необходимо передать в качестве параметра cursor= в строке запроса и, таким образом, получить следующую выборку:

$ curl -sS "http://FLUSSONIC-IP:8080/streamer/api/v3/streams?select=name&limit=1&name_like=a&sort=name"  |jq 
{
  "estimated_count": 5,
  "next": "JTI0cG9zaXRpb25fZ3Q9MiZuYW1lX2d0PWEx",
  "prev": null,
  "streams": [
    {
      "effective": {
        "name": "a1",
        "position": 2,
        "section": "stream",
        "static": true
      },
      "name": "a1"
    }
  ],
  "timing": {
    "filter": 0,
    "limit": 0,
    "load": 3,
    "select": 0,
    "sort": 0
  }
}

$ curl -sS "http://FLUSSONIC-IP:8080/streamer/api/v3/streams?select=name&limit=1&name_like=a&sort=name&cursor=JTI0cG9zaXRpb25fZ3Q9MiZuYW1lX2d0PWEx"  |jq 
{
  "estimated_count": 5,
  "next": "JTI0cG9zaXRpb25fZ3Q9MyZuYW1lX2d0PWEy",
  "prev": "JTI0cG9zaXRpb25fbHQ9MyZuYW1lX2x0PWEyJiUyNHJldmVyc2VkPXRydWU=",
  "streams": [
    {
      "effective": {
        "name": "a2",
        "position": 3,
        "section": "stream",
        "static": true
      },
      "name": "a2"
    }
  ],
  "timing": {
    "filter": 0,
    "limit": 0,
    "load": 1,
    "select": 0,
    "sort": 0
  }
}

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

Ограничение набора возвращаемых полей

Мы предусмотрели возможность частичного отображения полей. Суммарное количество полей в информации о потоке в медиасервере превышает 100 и, если необходимо получить только имена потоков, то запрашивать всю информацию довольно опрометчиво и нецелесообразно.

Для того, чтобы вернуть только часть полей, достаточно указать опцию select, например: select=name,title. Также можно запросить поля вложенных объектов: select=name,stats.media_info.

Создание и обновление (upsert)

Warning

Мы приводим Flussonic Media Server API к стандарту JSON Merge Patch. Это позволит привести метод частичного обновления списков к единому формату среди всех продуктов экосистемы Flussonic. Пока мы рекомендуем начать передавать в API-запросах полные вложенные списки.

Традиционно концепция REST диктует разделение методов для создания и обновления уже созданных объектов.

Мы практически отказались от такого разделения и предпочитаем более устойчивый к сетевым сбоям подход с одновременным созданием или обновлением объектов. Если методы на создание и обновление разделены, то в выполняемом коде необходимо писать условие с повторами и осуществить проверку в цикле. Мы же сделали так, что сначала создаётся объект на сервере и, если пришел ответ о том, что такой объект уже создан, то попытаться его обновить (или же нет), в зависимости от ситуации.

Фактически этот подход соответствует правилам обработки формата документов JSON merge patch:

  • Если передаваемый JSON merge patch содержит данные JSON, которые отсутствуют в целевом документе JSON, то эти данные добавляются.
  • Если целевой документ уже содержит передаваемые данные, их значения обновляются.
  • Значение null данных в JSON merge patch означает, что существующие данные в целевом документе удаляются.
  • Если данные в передаваемом JSON merge patch представляют собой что-либо, кроме объекта, то весь целевой документ перезаписывается передаваемым JSON merge patch.
  • Частичное изменение данных, не являющихся объектом, невозможно. Например, не получится заменить только некоторые значения в массиве.

Note

Если на сервере и на клиенте не реализовать Idempotency token, т.е. уникальный идентификатор (ID) действия, то повторные запросы на создание могут приводить к созданию нескольких новых объектов. Клиент может и не знать об этом, потому что сетевые запросы могут прерываться и без получения ответа. В случае с теми данными, которыми мы оперируем, как правило, получается создать ID объекта на стороне клиента, а значит, не нужно запрашивать генерацию ID на сервере (как это бывает в случае с буквальным следованием REST).

Необходимо сделать PUT-запрос:

curl -X PUT -H "Content-Type: application/json" -d '{"title":"ORT"}' "http://FLUSSONIC-IP:8080/streamer/api/v3/streams/ort"

Важный момент: для API очень удобно, чтобы ID объекта занимал один сегмент. Это означает, что, если, например, имя потока составлено из нескольких сегментов: sports/football, то в доступе через API необходимо экранировать символ / с использованием %2F следующим образом:

curl -X PUT -H "Content-Type: application/json" -d '{"key":"value"}' "http://FLUSSONIC-IP:8080/streamer/api/v3/streams/sports%2Ffootball"

Такой подход мы называем UPSERT, потому что это аналог SQL UPSERT.

Idempotency token для POST

Idempotency token гарантирует, что одна и та же операция не будет выполнена дважды. Это особенно важно для облачных платформ, чтобы средства не списались со счета несколько раз за одно и то же действие.

Рассмотрим работу Idempotency token на примере:

  1. Клиенты используют для создания и обновления потоков запросы POST. В заголовке каждого запроса от клиента передается поле Idempotency-Key — это уникальное значение, которое создается на стороне клиента и которое сервер использует для распознавания последующих попыток выполнения одного и того же запроса.
  2. Когда клиент создает в облаке новый поток, имя этого потока генерируется Flussonic (а не на стороне клиента) — это гарантирует уникальность имен потоков.
  3. Если связь прерывается, клиенту приходится перезапустить запрос с тем же самым значением Idempotency-Key. Flussonic поймет, что это тот же самый запрос, и вернет сгенерированное для него имя. А если Idempotency-Key отсутствует, Flussonic посчитает повторную попытку отдельным запросом и сгенерирует новое имя потока.

Чтение объекта

Прочитать объект можно с помощью простого HTTP-метода GET:

curl -sS "http://FLUSSONIC-IP:8080/streamer/api/v3/streams/ort"  |jq 

Ответ: JSON с информацией об объекте.

{
  "effective": {
    "name": "ort",
    "position": 7,
    "section": "stream",
    "static": true
  },
  "name": "ort",
  "named_by": "config",
  "position": 7,
  "static": true,
  "stats": {
    "alive": false,
    "bytes_in": 0,
    "bytes_out": 0,
    "client_count": 0,
    "dvr_enabled": false,
    "dvr_only": false,
    "dvr_replication_running": false,
    "id": "61496e6e-a22f-46af-bce5-5479f9067ead",
    "input_error_rate": 0,
    "last_access_at": 1632202350648,
    "lifetime": 0,
    "opened_at": 1632202350648,
    "out_bandwidth": 0,
    "publish_enabled": false,
    "remote": false,
    "retry_count": 19,
    "running": true,
    "running_transcoder": false,
    "start_running_at": 1632202350648,
    "transcoder_overloaded": false
  }
}

Удаление объектов

Удалить объект можно HTTP-методом DELETE:

curl -sS -X DELETE "http://FLUSSONIC-IP:8080/streamer/api/v3/streams/ort"

Ответ: HTTP 204 без тела.