Comments 255
Понапридумано и понаобсуждено и понахоливарено столько вокруг старичка, что каждый для себя сам может выбрать удобный подход для своего вполне конкретного проекта.
А ложки нет.
Стоит добавить, что JSON RPC 2.0 — это транспорто-независимый протокол. Т.е. как именно клиент передаст запрос серверу — никем не регулируется, можно хоть в файл /tmp/request.json
его записать, а ответ считать из созданного сервером файла /tmp/response.json
. Можно передавать по TCP as is (хотя нередко используют в качестве разделителя запросов/ответов \n
если неудобно использовать потоковые парсеры JSON). Удобно передавать отдельными сообщениями WebSocket. Что касается самого распространённого случая — HTTP — то поскольку есть множество способов передать кусок JSON через HTTP то был дополнительно написан стандарт JSON-RPC 2.0 Transport: HTTP, и обычно все клиенты/сервера JSON RPC 2.0 использующие в качестве транспорта HTTP пишутся в соответствии с ним.
Меня это постоянно удивляет. Если уж используется HTTP для транспорта, то почему он используется специально целенаправленно в обрезанном виде? Invalid Request с ответом 200.
Потому что как транспорт — он свою задачу выполнил корректно, поэтому и 200. Попытка для ошибок подобрать какой-то другой код обречена на неудачу в любом случае, по причине того, что в RPC возможных кодов и типов ошибок намного больше, чем в HTTP, и они разные в каждом сервисе. Плюс есть batch-ответы, в которых возвращается массив обычных ответов, часть из которых может быть ошибками. Ну и вообще смешивать уровень транспорта и приложения без веской причины плохая идея. По сумме всех этих факторов получается, что если возвращать что-то помимо 200 то это создаст больше проблем, чем решит.
Что до мониторинга, то RPC-сервисы обычно реализуют более сложные операции (по сравнению с настоящим REST), и для их мониторинга 200/400/500 всё-равно не хватит, поэтому мониторинг таких сервисов обычно реализуется через их собственные метрики уровня приложения, собираемые prometheus.
Я говорю о том, что не нужно отказываться от HTTP кодов из-за мониторинга.
В мониторинге сразу будет видно, что пошел шквал 403 или 404, к примеру. Травить монитор на json так же надо, но я все равно честно не понимаю зачем отказываться еще от одного стэка мониторов и анализаторов. При чем отказываться целенаправленно.
В одном ответе от RPC-сервера может быть несколько разных ошибок.
Например, "Access Denied" + "Method Not Found". А HTTP-код у нас только один.
А если не на пакетную операцию возвращается несколько кодов, но я бы хорошо подумал над другими вопросами.
В переложении на REST может быть не сразу очевидно. Вот классная статья по теме How to GET a Cup of Coffee. Если нужны согласованные изменения, создавайте транзакци, например используя Two-phase commit protocol .
В случае JSON RPC 2.0 — нет. Случаи не 2xx — это случаи когда транспорт свою работу не выполнил — переданный клиентом кусок JSON не был доставлен RPC-сервису.
А в случае REST — большинство не 2xx действительно не ошибки транспорта (ошибками транспорта являются обычно 502/503/504).
Мне кажется, вы наделили не свойственной ему ролью.
Я не "считаю" его транспортом, я (и не только я) вполне осознанно использую очень богатый и функциональный протокол HTTP в качестве тупого транспорта, игнорируя практически все его возможности. :)
Yet another standart, или еще один способ отказаться от простых и понятных систем мониторинга.
Меня это постоянно удивляет. Если уж используется HTTP для транспорта, то почему он используется специально целенаправленно в обрезанном виде? Invalid Request с ответом 200.
потому что HTTP для jsonrpc yet another transport
Если уж используется HTTP для транспорта, то почему он используется специально целенаправленно в обрезанном виде? Invalid Request с ответом 200
Если мы говорим о веб-браузерах, то некоторые коды ошибок они, заразы, обрабатывают самостоятельно, просто не допуская до них наше прикладное ПО.
Я бы не стал полагаться на разнообразие кодов ошибок HTTP, за небольшим исключением.
На самом деле раздражает очень сильно меня REST уже. Я работал с довольно большим количеством различных API и меня от натягивния совы на глобус уже начинает подташнивать:
— Внезапно, появляющиеся ошибки в теле ответа, в некоторых местах (причем в документации ни слова)
— Натягивание HTTP кодов на совершенно не подходящие им случаи
— GET для получения информации (который очень меня веселит когда у ребят ограничение длинны URL)
— Путаница с PATCH/POST/PUT прям в пределах проекта.
— Возврат 500, когда на самом деле 400 (как приятно это отлаживать)
— Возврат 500/400, когда на самом деле 413 (как приятно экспериментально вычислять...)
— Мое любимое: возврат 4xx/5xx без дополнительной информации но при этом частично выполнить запрос.
Я может сильно не прав, но вроде как, всякие клевые штуки должны упрощать всем жизнь а не усложнять.
Кажется, весомая часть ваших претензий не к REST как таковому, а к отсутствию стандарта. Тогда как для REST есть, например, JSON:API https://jsonapi.org/ У этой спецификации, безусловно, есть недостатки, но при использовании проблемы вроде "возврат 4xx/5xx без дополнительной информации но при этом частично выполнить запрос" уже актуальны не будут.
Ну нужно вернуть ошибку ну пусть будет ERR_CONTENT_TOO_LARGE.
А когда прилетает некая 4xx и ты сидишь и думаешь, это я накосячил в запросе, сервак отлуп по размеру дает или приложение не справляется с таким пакетом? Очень здорово, особенно учитывая всякие прикольные сервисы, вот например концовка запроса одного из: "]]]]}"
Отдельную кстати боль доставляют любители выносить параметры в заголовки, особенно доставляет когда это указано где нить в общем разделе в духе «Если вам нужны только обновленные данные используйте if-modified-since».
"JSON:RPC" не существует. Есть конкретный протокол "JSON-RPC 1.0" (устаревший и почти не использующийся) и конкретный протокол "JSON-RPC 2.0" (текущий, обычно говоря о "JSON RPC" имеют в виду именно его, но не обязательно — иногда речь о своей кустарной реализации RPC с использованием JSON для запросов/ответов).
"JSON:API" это тоже конкретный протокол. Не уверен насчёт его природы на 100% (я его смотрел поверхностно), но по-моему он намного ближе к REST/GraphQL, и полноценным RPC не является. Начинался он как REST-адаптер для фреймворка Ember.js, который потом решили попытаться отвязать от Ember.js и превратить в стандарт. На данный момент, по моему личному мнению, преимуществ перед JSON-RPC/REST/GraphQL у него нет, если только мы не пишем проект на Ember.js, а вот неприятные проблемы присутствуют (почитайте его форум).
Плюс к этому "JSON API" часто называют кустарные варианты API с передачей запросов/ответов в JSON — они могут быть по природе похожи на REST, могут быть похожи на RPC, могут быть вообще чем угодно.
На данный момент, по моему личному мнению, преимуществ перед JSON-RPC/REST/GraphQL у него нет
Преимущества JSON:API перед JSON-RPC те же самые, что и у REST перед JSON-RPC (как и недостатки)
Преимущества перед REST:
- стандартизация некоторых холиварных моментов (таких, как пагинация, HTTP-статусы ответов в большинстве случаев)
- возможность выборки только нужных полей сущности
- возможность получить только нужные связанные данные
Преимущества перед GraphQL:
- отсутствие повторяющихся данных (если мы выбираем N статей с их авторами, в GraphQL для каждой статьи продублируется автор, даже если он один, а JSON:API вернет только уникальных авторов)
Ну так REST это по сути и есть "JSON API", с рекомендациями оперировать понятиями "ресурс", "операция над ресурсом" и использовать при этом возможности протокола HTTP согласованным образом.
Суть REST не имеет вообще никакого отношения к JSON, так что не может быть "JSON API".
Но если сервер получил запрос и, исходя из бизнес-логики, решил что он ошибочный (например: объект не найден), то в таком случае я считаю правильно ответить кодом 200
Почему тогда не 400 (или 404 если ID объекта это часть урлы)?
Так, 403 может означать как ошибку авторизации, так и ошибку в адресе. А ещё 403 может означать что сервис временно недоступен и надо повторить запрос позже.
Код 401 может означать как необходимость передать токен — так и ошибку в адресе или что сервис недоступен и нужно повторить запрос позже.
Код 404 может означать некорректный id, ошибку в адресе или недоступность сервиса.
Код 405 может означать как ошибку в алгоритме — так и ошибку в адресе или недоступность сервиса.
Код 418 может означать что угодно (кроме ошибки в адресе или недоступности сервиса).
Коды 502 или 504 могут означить что угодно, включая ошибку в адресе или недоступность сервиса.
Код 503 может означать временную недоступность сервиса… или ошибку в адресе.
Про авторизацию в корне не согласен, она является частью именно бизнес-логики приложения, не очень понятно что вы имели ввиду говоря «должна быть между веб-сервером и сервером БЛ».
Альтернативы ей нет, все кто активно используют JSON RPC 2.0 про неё знают, поэтому хоть у неё и нет официального статуса де-юре, но де-факто это стандарт.
Авторизацию/токены обычно передают отдельным первым параметром (в нём так же удобно передавать дополнительные данные для трассировки запросов, отладки, в общем — контекст запроса) во все RPC-методы, т.е. на уровне HTTP не используются ни заголовки ни куки. Но, да, если на уровне приложения авторизации нет, то можно прикрыть весь сервис на уровне стоящего перед ним nginx и проверять заголовок Authorization:
, спека это не запрещает.
Про авторизацию: метод, обрабатывающий JSON-RPC запрос — это функция, которая принимает на вход то, что пришло в поле «params» и возвращает ответ — это бизнес-логика.
Позволить выполнить функцию или нет — задача системы авторизации. Она может быть реализована декоратором функции/класса или веб-прокси или ещё кучей других способов.
Защита от CSRF для JSON RPC 2.0 обычно не нужна — потому что куки не используются, авторизация реализована через передачу отдельного параметра с токеном, который стороннему сайту взять негде. В качестве вишенки API зачастую не публичный, открыт только для нашего сайта, а спека требует передать Content-Type: application/json
, который не является simple header и активирует CORS, что дополнительно расширяет возможности сервера по фильтрации левых запросов (напр. если для авторизации используется заголовок Authorization:
, но это редкость).
Нет, куки не используются потому, что проверкой аутентификации и авторизации обычно в RPC занимается слой бизнес-логики (точнее, обёртка перед ним, но это не важно — она всё-равно находится внутри RPC-сервера). А это значит, что все необходимые для проверки данные (токен) они должны получить вместе с методом и параметрами RPC. Информация с транспортного уровня (HTTP) в этот момент обычно недоступна (если совсем-совсем честно, то обычно приходится изворачиваться, чтобы дать доступ к одному значению с транспортного уровня: IP клиента, для защиты от DoS). Более того, JSON RPC 2.0 не просто "формально" независим от транспорта, он и на практике часто передаётся по TCP или WebSocket, где никаких кук нет. В общем, куки не используются не ради защиты от CSRF, а потому, что они вообще никак в JSON RPC 2.0 не лепятся.
Для JSON RPC 2.0 — он вполне себе транспортный. Потому что всё, что от него требуется — доставить кусок JSON туда и обратно. Никакие особенности HTTP при этом не нужны и не используются (в отличие от того же REST), никакие метаданные к этому куску JSON не прилагаются, etc. — стандарт рекомендует использовать конкретные методы (POST
) и заголовки (Accept/Content-Type: application/json
) исключительно ради интеоперабельности с существующими HTTP-сервисами вроде прокси, самому JSON RPC 2.0 всё это не нужно.
В примерах к apidocjs только REST однако
Возможно и не нужно дополнительных инструментов. Ендпойнт один (какой-нибудь example.com/api/), запросы всегда POST (если следовать стандарту). Нужно лишь описать параметры методов и возвращаемый ими результат. С этим справятся jsdoc, pydoc и прочие подобные.
Но вообще инструмента типа swagger действительно не хватает, автоматическая генерация/обновление доки со встроенной проверкой запросов, кода для клиентов и серверов, а так же тестовых заглушек из единого описания протокола нам бы очень пригодилась.
Не то что уровня swagger, вообще ничего нет! Видимо нет запроса от аудитории, все ушли в REST.
А чем конкретно не нравится?
Так же есть еще генерация сваггера (все в репозитории выше), пример api.myshows.me/shared/doc, пример endpoint'a api.myshows.me/v2/rpc/?smd
Удалось сделать даже документацию ошибок, которые есть в JSON-RPC. Ну и генерация самого клиента тоже есть. Вот так это все примерно выглядит (smdbox)

Во-вторых, кодов ответа в HTTP всегда меньше, чем типов ошибок бизнес-логики, которые вы бы хотели возвращать на клиент.
Кто-то всегда возвращает 200-ку, кто-то ломает голову, пытаясь сопоставить ошибки с HTTP-кодами.
В JSON-RPC весь диапазон integer — ваш.
наверное, опять холиварная тема, но в некоторых случаях предпочел оставить поле error для ошибок протокола, описанных в спецификации, а ошибки бизнес логики передавать внутри result. Некоторые случаи — это api для межсервисного взаимодействия, где сервис одновременно может поддерживать jsonrpc, grpc, очереди сообщений и не только… хочется минимизировать влияние протокола на форматы сообщейни.
Да и нужен ли Вам JSON?
Былой человекочитаемости уж не сыщешь. Эти анонимные объекты кого угодно загонят на перекур!
Открывается одна единственная, плохо различимая сквозь диоптрии, махонькая фигурная скобочка без роду и племени, без всякого названия. Начинай читать миллион полей содержимого — авось по смыслу догадаешься, чего туда светлый ум понапихал!
То ли дело XML-RPC! Все чётко, по делу, имя не понятное — глянь схему. Строгость, стройность, либы для всех языков, автовалидация, XPath, — все дела!
Да и стандартище! Не в пример доморощенным костылям.
Бросайте эти ваши ресты с джэйсонами, назад к истокам!
{«jsonrpc»: «2.0», «method»: «post.like», «params»: {«post»: «12345»}, «id»: 1}
Вот тут сразу виден недостаток стандарта (или следствие наличия «бонуса» в виде batch-операций):
метод закодирован в теле сообщения!
С точки зрения простоты протоколирования, мониторинга, возможности перекрытия реализации метода, балансировки и т.п. это неудобно.
Все таки название метода в пути, может, и не идеальное, но вполне удачное решение.
А здесь «бонус» вильнул собакой.
Нет, это следствие не batch, а того, что протокол транспорто-независимый, поэтому никаких "путей" (равно как и HTTP в принципе) в нём просто нет.
{«jsonrpc»:«2.0»,«error»:{«code»:1234,«message»:«Server not found»},«id»: 2}
И еще. Почему код — это число, а не строка? Как так?
А почему нет? Боитесь, что чисел для всех ситуаций не хватит? :)
Ну хорошо, тогда дополнительный вопрос.
Если «код» сделали числом, то почему «метод» тоже не сделали числом?
Боятся, что что чисел для идентификации методов не хватит? :)
А как связаны код и метод?
И код, и метод — это перечисляемые типы.
Представляя значение кода/метода в виде числа мы получаем последствия:
1) разрабатывая API командой (разными людьми) гораздо вероятнее получить коллизию по их значениям;
2) нужно что-то особое придумывать, что бы исправить существующие коллизии и что делать, что бы таких коллизий не было в дальнейшем;
3) представляя значение перечисляемого типа в виде числа мы автоматически попадаем на антипаттерн магическое число.
коде всё равно будут использоваться enum типы или, хотя бы, константы для каждого кода
Эти константы все равно будут иметь какие-то значения. Какая разница, оборачиваются они в красивые идентификаторы или нет в языке программирования?
И с кодами гораздо проще выделять классы ошибок.
Точно не проще. Потому что каждое число и так имеет строковое представление.
А что бы изолировать набор значений, достаточно придумать уникальный префикс. С числами такой фокус не проходит.
Ещё числовой код удобно бить на диапазоны по критичности, например, или области ответственности.
а message можно отдавать на языке пользователя
А как API узнает язык пользователя?
И какого пользователя? Оператора или программиста?
И зачем, вообще, служебные сообщения переводить?
А как API узнает язык пользователя?Из заголовка Accept-Language, например.
И зачем, вообще, служебные сообщения переводить?Если у вас мультиязычное мобильное приложение, то вполне допустимый вариант переводить их на стороне API.
Из заголовка Accept-Language, например.
Во первых, у нас же протокол «изолирован» от HTTP, какие заголовки?
Во вторых, ради чего эти усложнения, делать вам больше нечего?
Если у вас мультиязычное мобильное приложение, то вполне допустимый вариант переводить их на стороне API.
Мой идеал: API занимается обработкой данных (доступом к данным), мобильное приложение — представлением этих данных, в т.ч. локализацией. Каждый в чужую зону ответственности не лезет.
Вы определенно описали случай, далекий от моего идеала.
Банально, данные в трёх экземплярах не будешь хранить локально. Локализация вполне себе может быть "на лету".
Статику вполне допустимо локализовывать client-side и отдавать в API только string ID, а вот часто обновляемый контент удобней локализовывать server-side. Также для многих языков есть свои разные устоявшиеся практики/рекомендации на эту тему.
Нет, это следствие не batch, а того, что протокол транспорто-независимый, поэтому никаких «путей» (равно как и HTTP в принципе) в нём просто нет.Вопрос от человека незнакомого с протоколом: не повторяют ли разработчики структуру HTTP путей? То есть, в REST у нас имеется post/like, post/bookmark, post/share/vk, etc. Не будет ли в json rpc построена аналогичная структура: «method»:«post.like», «method»:«post.bookmark», «method»:«post.share.vk»?
Но самое забавное, что даже в этом примере, который должен показать типо независимость и самостоятельность json-rpc, автор по сути придумал yet another post request.
Имхо не надо придумывать миллион велосипедов в себе. Универсального все равно не будет! И не надо добиваться бюрократической точности в соблюдении стандартов без необходимости. В 95% случаев совет — юзать рестоподобную архитектуру, хорошо документируйте ее и никогда ни у кого не возникнет проблем с пониманием вашего велосипеда.
GameDeV + Realtime = http rest???? Не смешите мои тапочки. И джейсон-рпц сам по себе тут проблем не решает. Имхо тут важнее используемый транспортный протокол, чем структура данных, которую вы передаете. А передаете вы джейсон, xml или двоичные данные — вопрос уже десятый.
Вспоминая о JSON-RPC и документировании API, не забывайте о SMD.
{
"transport": "POST",
"envelope": "JSON-RPC-2.0",
"contentType": "application/json",
"SMDVersion": "2.0",
"services": {
"post.list": {
"transport": "POST",
"envelope": "JSON-RPC-2.0",
"parameters": [
{
"name": "author",
"type": "string"
},
{
"name": "token",
"type": "string",
"optional": true
}
],
"returns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"content": {
"type": "string"
},
"author": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"draft",
"published"
]
}
},
"required": [
"title",
"content",
"status",
"author"
]
}
},
"post.create": {
"transport": "POST",
"envelope": "JSON-RPC-2.0",
"returns": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"content": {
"type": "string"
},
"author": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"draft",
"published"
]
}
},
"required": [
"title",
"content",
"status",
"author"
]
},
"parameters": [
{
"name": "title",
"type": "string"
},
{
"name": "content",
"type": "string"
},
{
"name": "status",
"type": "string"
},
{
"name": "token",
"type": "string"
}
]
}
}
}
}
При желании можно и какой-нибудь UI оформить, наподобии сваггера
Ничто не мешает добавить в сервис поле description
Как здесь будет выглядеть с description?
{
// url for special service
// "serviceUrl": "http://example.com/api",
// change back when standard url will work
"serviceUrl": "http://subdomain.example.com/api"
}
Не очень понял вопрос. здесь
— это какое место?
{
"serviceUrl": "http://subdomain.example.com/api",
"description": "change back when standard url will work"
}
Вот так не получится?
{"description":"url for special service\n\"serviceUrl\": \"http:\/\/example.com\/api\"\nchange back when standard url will work"}
Но это будет приемлемо, если использовать какой-нибудь UI для SMD. В чистом виде читать это — такое себе, да...
А что касается БД — то каким образом тут SMD играет роль?
SMD — это описание, а БД — это уже реализация...
Возможно пригодится
Как-то меня смущает во всей этой холиварне слово "stateless". На большинстве сайтов, с которыми лично я сталкиваюсь, требуется аутентификация/авторизация, а это уже, как минимум, два состояния: анонимный и аутентифицированный.
Если же у вас «stateless» — то каждый запрос от клиента передаёт всю необходимую информацию не полагаясь на то, что сервер «помнит» клиента. В общем случае запросы клиента могут с помощью балансировщика направляться на разные сервера, какая тогда вообще может быть речь про «сессию» и «состояние сессии»?
В общем случае запросы клиента могут с помощью балансировщика направляться на разные сервера
Вот и вопрос, как же тогда разные сервера определяют, анонимный клиент дал запрос или он уже аутентифицирован ранее и имеет права на доступ к restricted-данным?
Ну или всё ещё проще — клиент в каждом запросе передаёт свой логин-пароль, как это сделано в HTTP Basic авторизации.
Филдинг описал концепцию построения распределённого приложения, при которой каждый запрос (REST-запрос) клиента к серверу содержит в себе исчерпывающую информацию о желаемом ответе сервера (желаемом представительном состоянии), и сервер не обязан сохранять информацию о состоянии клиента («клиентской сессии»).
Это из вики. Получается, что таблица с авторизационными токенами на сервере — это не REST. Если подходить строго, то REST возможен только при HTTP Basic аутентификации. Только тогда сам запрос содержит в себе достаточно информации, чтобы сформировать ответ. При этом на каждый запрос нужно будет проводить аутентификацию и авторизацию.
О! Вот это то самое! Спасибо :)
Всё равно проверять, есть ли у данного пользователя права на запрошенное действие.
Можно и не проверять права, а записывать их прямо в токен при выдаче. Если срок жизни токена небольшой (например, минута, цифра с потолка) — мгновенно забанить пользователя, конечно, не выйдет, но после протухания его токен превратится в тыкву, а новый ему уже никто не даст.
Но я читал и даже слушал выступления людей, вкладывающих в пейлоад информацию о правах доступа (гранты, роли и т. п.) Из чего делаю вывод, что это распространённая практика. Однако, я не призываю так делать, и я намеренно оставил за скобками обсуждение качества такого подхода.
Разве в качестве аутентификации в JWT выступает не тот факт, что подпись токена верна?
Тогда в качестве авторизации можно использовать содержимое токена без дополнительных обращений к сервису авторизации для проверки того, какие права имеет пользователь такой-то.
При этом подходе фактически задачи авторизации фактически будут переложены на сервис, занимающийся выдачей токена, что хорошо ложится в микросервисную модель.
Как я написал в первом ответе, наиболее типичным вариантом «сессией с состоянием» как правило является нечто вроде привязки состояния к TCP-соедниению между клиентом и сервером, а не то что у вас в базе данных на сервере или в клиенте хранится. Храните в базе что хотите и как хотите, передавайте информацию от клиента к серверу тоже как хотите (через куки, через тело запроса, через другие заголовки) — главное передавайте в каждом запросе если они ему нужны.
Если рассуждать как вы описали выше, то вообще любое хранение сервером данных — это нарушение REST, т.к. это хранение «состояния». Но это не так, т.к. это совершенно другое «состояние». Это состояние сервера. У клиента тоже есть какое-то своё состояние и он посредством запросов к серверу пытается «синхронизировать» своё состояние с тем, что есть на сервере. При этом ни сервер, ни клиент не должны полагаться на то, что за сколь угодно малое время между двумя запросами, состояние одного из них не изменилось. Архитектура должна проектироваться так, как будто между двумя запросами и даже одновременно с ними, происходят другие процессы меняющие «локальное» состояние участников процесса. Отсюда и происходит требование, что бы в запросе передавалась вся необходимая для его выполнения информация. И не важно в каком она виде, как называется и каким образом обрабатывается сервером. Главное что серверу будет достаточно этой информации.
Попробую привести «кухонный пример» почему stateless важен для построения масштабируемой архитектуры.
Представьте кинотеатр, кассы, билетёр на входе в зал. Вы купили билет, показали билетёру и вошли в зал. Потом решили вернуться в машину за забытыми очками. Если, когда вы возвращались, билетёр ОБЯЗАН запомнил вас в лицо и пустить без проверки билета — это не масштабируемая система. Такого билетёра нельзя подменить другим, потому что он вас не знает и не сможет впустить. Вам придётся снова проходить процедуру покупки билета, если вы его потеряли, и показывать его билетёру.
Если же билетёр ОБЯЗАН пропускать только по билету — то любой билетёр сможет пропустить вас с билетом, даже если видит вас впервые. Вот это масштабируемая схема — можно поставить хоть 10 билетёров и вы можете к любому подойти со своим билетом, а не только к тому, кто вас первым проверил.
Что значит в данном контексте "валидный токен"? Может ли у одного и того же пользователя (с одним и тем же внутренним идентификатором на сервере) быть два разных валидных токена? Что делать, если пользователь "засветил" свой токен перед злоумышленником и теперь хочет поменять его?
Но токен же должен храниться и на клиенте, и на сервере, чтобы можно было обеспечить аутентификацию/авторизацию? И по мне, так нет никакой принципиальной разницы с точки зрения stateless, как передавать токен с клиента на сервер — в заголовке Cookie
или в заголовке Authorization
.
но возникает вопрос — как тогда экспайрить такие токены?
Если токен подписан, то достаточно проверить лишь саму подпись. В этом случае при проверке на то, не отозван ли он, теряется весь смысл подписанных токенов и система приобретает состояние.
ИМХО более удачное решение в этом смысле — короткие по времени действия токены.
1. Можно всех пользователей по очереди перебирать с их ключами и к какому подойдёт того и пускать в систему.
2. Можно из payload'а извлекать уникальный айди, по нему находить пользователя, получать его ключ и проверять валидность токена. Так все и делают, кстати.
В обоих случаях проверка токена сводится к одному запросу к БД.
А в чем принципиальная разница: лезть в базу чтобы проверить что токен там есть, лезть в базу чтобы проверить что токена там нет — или лезть в базе за ключом пользователя?
А можно чуть подробнее, как сделать, чтобы это проверялось nginx'ом?
Статья на хабре
lua вам в помощь
Помимо всех плюсов навскидку вижу два минуса по сравнению с REST.
- Клиентское кеширование. Можно сколько угодно говорить, что кешировать RPC нельзя (как настаивает указанный выше JSON-RPC 2.0 Transport: HTTP), но в реальности кешировать нужно. И тогда мы либо поддерживаем два разных API — одно JSON-RPC, одно REST или что угодно с кешированием, либо начинаем реализовывать кеширование руками. Изобретаем ETag / Last-Modified, решаем что является идентификатором ресурса, делаем кучу ошибок и не можем пользоваться браузерными кешем.
Даже в вашем примере Хабра можно кешировать информацию о старых постах, профили пользователя, посты для мобильной версии (куда не включены комментарии), в общем все что редко изменяется. Это позволит сэкономить очень много ресурсов. - Микросервисная архитектура и балансировка нагрузки. На балансировщике намного дешевле проверить префикс URI, чем распарсить JSON и вытащить оттуда метод. Нехорошие клиенты могут отправлять валидные запросы с очень длинным телом, а метод в нем ставить в конце.
Да даже разделить read-only и read-write становится трудно. Я уже не говорю о том, что при необходимости некоторые параметры можно тоже включить в балансировку нагрузки и в случае REST это все тот же префикс. Да, смешиваем транспортный уровень и уровень приложения, но бывают ситуации когда это оправдано.
Упомянутый выше мониторинг это проявление той же проблемы под названием "JSON парсить дорого". В приложении это уже не так дорого по сравнению с остальной логикой, но на других уровнях — очень и очень дорого.
переусложнена, там, где не надо (из-за batch-операций возникают далеко идущие последствия), сложнее в балансировке и мониторинге относительно REST и полу-REST решений.
С одной стороны Вы правы. И многие из нас, включая меня, когда-то делали нечто подобное. Но, с годами опыта, обычно приходит понимание, что любое отступление от существующего стандарта/протокола должно требовать значительно больше плюсов, чем "мне так удобнее" или "мы получим одну классную фичу" — потому что неудобств такие отступления от стандартов, со временем, создают больше, чем изначально ожидалось. В качестве конкретного примера таких "неудобств":
- внезапно бизнес требует обеспечить совместимость со стандартным протоколом, т.к. этого требуют новые партнёры, или это нужно чтобы сделать API публичным, или ещё по куче непредсказуемых заранее причин
- нужно вручную реализовывать библиотеки для своего протокола, причём нередко со временем оказывается, что нужна не одна для языка используемого сейчас на бэкенде, а ещё одна на бэкенд для другого языка плюс ещё пара для мобильных клиентов плюс ещё одна для веба
- у стандартных протоколов есть недостатки… как и у любых других, но — недостатки стандартных протоколов уже хорошо известны, документированы, и есть рекомендации по их преодолению… а вот со своим протоколом все шишки придётся набивать самостоятельно, и узнавать о его недостатках в самый неподходящий момент — когда нужно срочно сделать фичу, а протокол начинает в этом мешать, а не помогать
- следствие предыдущего пункта — свой протокол периодически приходится изменять/расширять не продумав как следует последствия, что создаёт ещё больше проблем в будущем
Поэтому перед принятием решения пилить свой протокол крайне желательно, чтобы существующие действительно не подходили для данного проекта, и чтобы свой давал немало ценных преимуществ, чтобы компенсировать вышеупомянутые проблемы.
- изменения в протоколе минимальны, реализовать публичную точку входа на строгом JSON-RPC будет не очень сложно, и на порядок проще чем в случае с REST
- сам по себе протокол очень простой и отсутствие специальных библиотек не большая проблема
- простата протокола позволяет особо не переживать о скрытых ошибках, да и отклонения от стандарта минимальны
- следствие предыдущего пункта — свой протокол периодически не приходится изменять/расширять не продумав как следует последствия
В данном случае небольшое отклонение от стандарта дает немало ценных преимуществ, компенсирующих вышеупомянутые проблемы.
В некоторой степени вторую проблему можно решить, вынося балансировку на сторону клиента.
Можно сделать API, отдающий конфигурацию вида:
{
"post.get": "https://posts.example.com/rpc",
"post.like": "https://likes.example.com/rpc",
"comment.like": "https://likes.example.com/rpc",
"default": "https://example.com/rpc"
}
При таком подходе в любой момент времени любой метод можно вынести в отдельный микросервис.
Кешировать RPC запросы это плохая идея. Нужно кеширование — используйте REST, вместо или вместе с RPC.
Что до балансировки нагрузки на уровне HTTP, то это вообще не проблема — просто делаем для каждого публичного микросервиса свой url/endpoint, вроде https://api.example.com/rpc/microserviceA, и дальше nginx по url раскидает запросы по разным сервисам. Даже если в будущем этот микросервис будет заменён несколькими другими (что бывает достаточно редко), то мы просто на его месте поставим микросервис-прокси, который будет реализовывать старое API и преобразовывать его в новое — это в любом случае необходимо для сохранения совместимости API.
(уверен, что выглядеть будет в миллион раз хуже, чем просто xml)
Не поверите) https://en.wikipedia.org/wiki/XML-RPC
Я уверен, что уже и для типизации что-то придумали
Ну а как же! JSON Schema :)
Хотя это калька с XML Schema и не совсем типизация, но и типы и структуру валидировать может.
Впрочем, документировать такой простой API можно хоть в markdown-файле.
Извините, но это лукавство какое-то. Если у нас есть 200 сущностей, над которыми можно осуществлять 5 операций, то, условно, фронтендщику надо дать документацию на 1к возможных вариантов (эндпоинт + что в него слать и что вернётся в ответ на get/put/post + параметры). Если функционал остаётся тот же, но вы используете jsonrpc, то только переносите часть информации одействии из урла+метода в тело, но эта тысяча возможных вариантов взаимодействия никуда не денется и документировать надо ровно столько же.
По мне выгоды от JSON-RPC, в общем случае, нет.
URI это просто параметр в теле HTTP запроса, вы его из заголовка 'GET URI', перенесли в body в чем профит?)
А разделить обработку входных данные на основании пути достаточно удобно и стандартно:
/path1/val1
… обработчик 1…
/path2/val2
… обработчик 2 ..
Вам же прижется писать тот же routing только своими руками.
И не везде это применимо. Если у вас "сайт" с которым нужно взаимодействовать через браузер, очевидно будет выбор решения через url.
Забыли про HTTP-метод. В JSON-RPC вы не ограничены менее чем десятком методов со стандартной семантикой.
> Если у вас «сайт» с которым нужно взаимодействовать через браузер, очевидно будет выбор решения через url.
Только для загрузки «загрузчика» типа public/index.html, а реальные данные могут грузиться любым доступным способом.
В общем, не понимаю зачем выбирать JSON-RPC вместо GraphQL сегодня
Я немного экспериментировал с добавлением в имена RPC-методов префиксов Safe и Idemp, чтобы на клиенте было проще реализовать единообразную обработку некоторых типов ошибок. Клиент писал не я, но, по-моему, принципиальной разницы это не дало. Прикол идемпотентных REST-запросов не столько в том, что их можно повторить, сколько в том, что их повторят автоматически без нашего участия (браузер) — а в случае RPC прикол в том, что без нашего участия почти ничего не делается, больше контроль над происходящим но и больше ручной работы, в т.ч. обработки сетевых ошибок. А раз код обработки этих ошибок всё-равно надо писать, ручная автоматизация повтора части запросов в случае достаточно редких сетевых ошибок оказывается не настолько полезной, чтобы на это сильно заморачиваться.
Согласен. REST бесит тем, что данные размазаны по всему запросу. Параметры пути, query args, JSON-тело, заголовки… а в случае с RPC все в одном месте. Стандарта нет, всегда срач. Никогда не знаешь, какой метод выбрать — PUT, PATCH, etc.
Еще удобно, что тело RPC-запроса легко отдать в очередь типа Кафки и процессить воркерами.
И даже это размазано по разным RFC
Спасибо, я в курсе. Я имел в виду сложные случаи, когда апишка изменяет сразу несколько сущностей разных типов.
При чем тут батчинг? У вас может быть такая бизнес-операция, которая меняет несколько разных сущностей.
Кстати, вы напомнили про еще одну беду REST. Что якобы сущности изменяются отдельно друг от друга, а это не так.
У вас может быть такая бизнес-операция, которая меняет несколько разных сущностейВ REST такая операция дожна быть отдельным ресурсом, поскольку за один запрос вы можете производить какое-либо действие только над одним ресурсом/коллекцией ресурсов
Суть самого определения RPC это удаленный вызов процедур. Как не банально это звучит. Он не заменяет REST который — архитектурный стиль взаимодействия компонентов распределённого приложения в сети. Нужно ли говорить, что вызов REST не подразумевает вызов конкретной процедуры?
RPC вводит жесткие ограничения, которые удобны по ряду причин в бизнес-приложениях. Но он убивает взаимодействие с транспортным слоем, т.к. является транспорто-независимым. Это его сильная и слабая сторона одновременно.
RPC можно прекрасно поднять на WEBScoket канале. И очень эффективно реализовать его потенциал для online коммуникации с сервером. REST тут в принципе не может быть. Это нонсенс.
RPC очень полезная штука, но я бы сказал в совокупности с манифестами. Это позволяет держать консистентным канал связи клиента и сервера, а также, решает проблему документирования и развития интерфейса. Для бизнес-приложений с множественными интеграциями server-server по прямым каналам связи очень полезная штука.
На REST следует останавливаться тогда, когда вы делаете именно — взаимодействие компонентов распределённого приложения в сети. Т.к. REST предполагает влияние вашей бизнес-логики на транспортный слой. И это должен быть осмысленный выбор в его пользу.
для клиентского это не критично, а для серверного вполне может быть узким местом.
Например, для API Gateway это критично. Это критично для сервиса, который оперирует данными в памяти.
При обычной работе объем данных на один запрос мал и соотношение обратное 1 к 2 (парсится быстрее чем передается). А вот полная синхронизация может занимать несколько минут — суммарно на сотню запросов может уйти 1-2 минуты на передачу + 2-4 минуты на парсинг.
Выход конечно найден, но факт есть факт. Когда надо вытянуть всю информацию по нескольким сотням клиентов за раз — текстовый формат передачи не очень подходит.
HATEOAS в теории позволяет машинам читать REST API и безо всяких там документаций. В реальности REST API, поддерживающих HATEOAS, почти нет от слова совсем — их считанные единицы. А всё то, что мы называем REST API просто случайный набор эндпоинтов, которые мы именуем, как душе угодно, и описываем это всё в лучшем случае только в документации.
Если это учитывать, то становится понятно, что нет никакой разницы, где именно мы напишем ключевые слова — в урле или в json-пакете.
На мой взгляд, получить HTTP 200 с ошибкой внутри — правильно, т.к. HTTP в данном случае выступает как транспортный протокол. Но это уже дело вкуса, т.к. ложки-то не существует.
REST API должно вроде как реализовывать HATEOAS, и не может быть заменено в этой части на RPC-протоколы.
Почему?
Ведь тут дело всего лишь в ответе.
Любой способ можно использовать, он все так же может возвращать в ответе дополнительную информацию «что делать еще».
А так как один хрен доку писать и это какое-то безумие в каждый ответ вкладывать полный список возможных действий и урлов — то это никто и не делает. В итоге мы имеем что имеем — зоопарк реализаций, подходов и даже философий.
И это хорошо. Лучше делать как удобно конкретному проекту, чем корчиться в угоду некой философии.
1. Eсть ли какие-то консольные утилиты для «jsonrpc over tcp» на подобии curl?
2. Есть ли «keep-alive» режим? Понятно, что от стандарта это не зависит, но реализуют ли это авторы библиотек?
3. Как передавать бинарные данные? Конвертация в base64 ведет к распуханию и падению производительности, какие есть «бест-практис» в этом плане?
Eсть ли какие-то консольные утилиты для «jsonrpc over tcp» на подобии curl?
А чем nc (netcat) не устраивает?
- Ответили выше —
echo '{"jsonrpc":"2.0",…}' | nc host port
. Либо за 15 минут пишется своя тулза, если нужен более удобный UI. - KeepAlive для самого RPC не требуется, он stateless. Но если это нужно на уровне приложения (напр. это чат и нужно знать что юзер ещё онлайн) то делается отдельный RPC-метод "ping", который клиент должен периодически вызывать. Если это нужно на уровне транспорта, обычно в случае TCP, то на сокете включается штатный keep-alive средствами OS (слой бизнес-логики/RPC об этом не знает).
- В JSON нет поддержки бинарных данных, поэтому либо base64 (обычно для однозначности в доке API нужно ещё указать какой именно вариант base64 и нужно ли выравнивание), либо, для передачи больших объёмов данных (вроде видео), используется HTTP или REST, а на уровне RPC бегают только соответствующие url-ки.
3. Как вариант, для «over http» можно бинарь вкладывать вторым в multipart, тогда и json и бинарные данные полетят в одном запросе.
Какие коннекты, протокол-то транспорто-независимый и про коннекты ничего не знает. Вы можете открыть одно TCP-соединение, после чего отправлять по нему RPC-запросы и ответы на них хоть год. Более того, обе стороны могут одновременно быть и клиентом и сервером, и отправлять запросы друг-другу всё по тому же одному соединению. Всё, что для этого необходимо (мультиплексирование запросов в одном соединении) в протоколе уже есть — это поле "id"
.
multipart это плохой вариант, потому что на уровне RPC-сервиса доступа к транспортному уровню нет, так что добраться до второй части multipart не получится.
Какие коннекты, протокол-то транспорто-независимый и про коннекты ничего не знает.Я про это указал в вопросе.
Вы можете открыть одно TCP-соединение, после чего отправлять по нему RPC-запросы и ответы на них хоть годЕсли библиотеки не поддерживают потоковую обработку json, т.е. если пришел не весь кусок json или несколько json слипились в один пакет и т.п., тогда это нельзя сделать с текущими либами и нужно писать свой велосипед.
multipart это плохой вариант, потому что на уровне RPC-сервиса доступа к транспортному уровню нетВы же говорите, что «протокол-то транспорто-независимый»
Если библиотеки не поддерживают потоковую обработку json
Какие именно библиотеки? Библиотеки для JSON RPC 2.0 — да, не поддерживают (точнее, если их написали корректно — транспорто-независимо — то не поддерживают). Библиотеки для транспорта — ну так транспорт для JSON RPC 2.0 не стандартизирован (не считая HTTP), так что каждый сам придумывает себе транспорт и пишет под него библиотеку, если есть что писать.
В случае TCP обычно делают одно из двух: либо сериализуют JSON "в одну строку", чтобы внутри гарантированно не было переводов строк, и при передаче добавляют перевод строки после каждого запроса/ответа (а "библиотека для транспорта" читает построчно и "библиотеке для JSON RPC 2.0" отдаёт отдельные строки-запросы/ответы), либо сериализуют как попало и используют потоковый парсер JSON в "библиотеке для транспорта".
Что Вам неясно насчёт multipart я не понял.
со стороны сервра: сервис обычно спрятан за каким-нибудь nginx, который умеет HTTP 1.1, если напрямую, например, в локалке, зависит от ЯП или фреймворка, но обычно проблем с этим нет
со стороны клиента: строится так же на базе уже готового http клиента, который умеет HTTP 1.1
Как вариант, в случае использования http протокола, можно просто использовать часть url вместо ключа method в json'e запроса. К примеру `http://site.com/rpc/:method_name`. А в приложении первым делом парсить этот юрл, превращать запрос в стандартный json-rpc формат и передавать дальше в бизнес логику.
Плюс будет в том, что nginx, метрики и прочие штуки, которые стоят между клиентом и непосредственно приложением бизнес логики, смогут продолжать заниматься своими делами безо всяких переделок под rpc.
Ну а минус в том, что велосипед.
Эта спецификация предложена ещё в 2009 году.
Эта спецификация предложена ещё в 2009 году.А можно еще какой-нибудь ГОСТ 70-х готов откопать. Уверен, что если поискать, то можно что-нибудь найти. Сей стандарт можно уместить в две строки:
— Запрос делай так…
— Ответ делай так…
И сразу возникает несколько вопросов:
— А вдруг мы хотим передавать метод в URL.
— Нам не нужно два свойства для ошибки, а достаточно одного.
(еще очень много вопросов)
На эти случаи будем другие стандарты писать?
А можно еще какой-нибудь ГОСТ 70-х готов откопать.
Можно. Если стандарт хорош под ваши задачи, почему бы и не придержаться его?
И сразу возникает несколько вопросов:
— А вдруг мы хотим передавать метод в URL.
— Нам не нужно два свойства для ошибки, а достаточно одного.
(еще очень много вопросов)
На эти случаи будем другие стандарты писать?
Либо искать другие, потому что данный в этих условиях не подходит, либо писать другой, да.
Как бы вы это решили? Дополнительно внедрять MQ-сервис (rabbitmq/...) для этой процедуры или как?
(И ещё вариант если нет прямого доступа к серверам, только через балансер).
1.
Если взять аббревиатуру REST и расшифровать ее, то получим Representational State Transfer — «передача состояния представления» — тоесть есть некоторые ресурсы(чего в определении нет, но про это написано много) и протокол направлен на то, чтобы просто и эффективно передавать состояние этих ресурсов между клиентом и сервером. То есть надо всего лишь передавать состояние ресурсов и команды на их изменение. Это можно сравнить с CRUD операциями в СУБД. К слову, это помогает автоматизировать типичные операции в например админках.
2.
С другой стороны, если расшифровать RPC, то получим Remote Procedure Call, тоесть вызов некоторой «подпрограммы» на удаленном сервере, которая делает какую то сложную магию. Это можно сравнить с бизнес операциями над СУБД, состоящих из нескольких(десятков) CRUD над разными таблицами и еще много чего. И да, батчи тут очень даже к месту.
3.
Поэтому, с моей точки зрения не надо натягивать сову на глобус и искать серебрянную пулю. Я думаю что тезис «все в вебе есть ресурс и его состояния» рано или поздно приведет к проблемам, равно как и тезис «давайте делать по RPC на выборку каждой сущности по ID» в проектах больше HelloWorld.
Representational State Transfer — «передача состояния представления» — тоесть есть некоторые ресурсы(чего в определении нет, но про это написано много)определение ресурса есть в https://tools.ietf.org/html/rfc3986
История с REST API чем то похожа на другие классные штуки, как например чисто функциональное программирование или нормализация данных. Архитектура, построенная на простых принципах, но непростая в понимании и имплементации. Это порождает перманентное желание срезать углы при разработке. Желание понятное, главное правильно оценивать pros & cons.
Какая best practise для этого в JSON-RPC? Присылать по одной ошибке для каждого неверно заполненного поля, пока все они не окажутся заполненными правильно? Или изобрести свой формат поля data для подобного случая? Есть ли примеры готовой реализации?
В REST можно прислать общий код «Форма не верна» (422, например), а в теле ответа — массив/объект с информацией обо всех полях, заполненных ошибочно, после чего клиент легко и просто отображает ошибки на соответствующих полях.
например так:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": [
{
"field": "login",
"message": "Логин уже занят"
},
{
"field": "name",
"message": "Имя может состоять только из буквенных символов"
}
]
},
"id": "1"
}
код будет почти всегда равен 200Для ошибок можно отправлять 400, завист от реализации
Ну и наконец, с JSON-RPC чуть меньше удобств по просмотру лога запросов в девтулзе, и я искал расширение, но не нашёл.
Поэтому набросал черновой вариант: habr.com/ru/post/467767
Присоединяйтесь к разработке, там ещё как минимум надо фильтрацию и пакетные запросы.
REST? Возьмите тупой JSON-RPC