Что из себя представляет протокол JSON-RPC и чем он мне нравится
Дисклеймер для тех кто любит ярые холивары на тему выбора лучшего способа обмена данными с сервисами.
На всяких форумах постоянно любят устраивать холивары на тему REST vs JSON-RPC vs gRPC и т.д. Сразу хочу сказать, что в статье я намеренно стараюсь избегать их полного сравнения, поскольку это бессмысленно. У каждого из них есть свои сильные стороны и в зависимости от задачи, одна технология может быть предпочтительнее другой. По этой же причине я рекомендую расширять свой кругозор и знакомиться с несколькими технологиями, а не пихать везде одно и то же решение в стиле Золотого Молотка.
С протоколом JSON-RPC я впервые ознакомился лет 8 или 10 назад, когда к нашей CMS прикручивал внешний API и тогда я искал какое-либо простое, но стандартизированное решение и при этом не такое многословное как XML-RPC. И по мере поисков оптимальных решений, я познакомился с протоколом JSON-RPC который тогда искренне поразил меня своей простотой.
Что такое JSON-RPC
JSON-RPC - это транспортонезависимый протокол вызова удаленных процедур (RPC - Remote Procedure Call), в котором обмен четко сообщениями происходит в формате JSON, структура которого регламентирована спецификацией. Вся спецификация JSON RPC 2.0 размещена на одной странице и сама по себе небольшая и наглядная.
Обратите внимание на то, что JSON-RPC транспортонезависимый протокол (привет модели OSI) и соответственно вы можете обмениваться сообщениями хоть через HTTP, хоть через голубиную почту.
Давайте теперь рассмотрим формат сообщений, благо он вполне регламентирован стандартом.
Запрос клиента к серверу состоит из объекта с следующими полями:
- "jsonrpc" — благодаря этому полю определяется версия протокола, по которому происходит взаимодействие между клиентом и сервером. В это поле указывается значение "2.0".
- "method" — метод который вы хотите вызвать на сервере (у нас же RPC как-никак).
- "params" — параметры передаваемые методу. Если нет параметров, то это поле можно не указывать.
- "id" — идентификатор вашего запроса для получения ответа. Если вам не нужно получать ответ, то это поле можно не указывать.
А вот структура ответа сервера, который передается обратно клиенту (если было указано поле "id"):
- "jsonrpc" — все то-же самое, что и в случае запросов от клиента к серверу. По умолчанию должно быть значение "2.0"
- "result" — тут передается результат работы запрашиваемого метода с переданными параметрами. Это поле не обязательное, но надо учитывать, что если метод отработал с ошибками, то это поле не передается!
- "error" — это поле указывается только при ошибке выполнения метода и не должно указываться в случае его успешного выполнения. Это поле представляет собой поле с четко регламентированной структурой:
- "code" — числовой код ошибки (см. спецификацию).
- "message" — краткий текст ошибки
- "data" — тут указывается всякая дополнительная информация об ошибке. Это поле не является обязательным.
- "id" — идентификатор изначального запроса, на который дается этот ответ. Т.е. с помощью этого поля клиентр сможет определить, на какой из его запросов пришел соответствующий ответ. Это поле должно быть указано всегда, при отправке ответа.
Приведем пару примеров:
// Запрос от клиента к серверу
{"jsonrpc": "2.0", "method": "addBook", "params": {author: "М.Ю. Лермонтов", name: "Герой нашего времени"}, "id": "1"}
// Ответ от сервера к клиенту
{"jsonrpc": "2.0", "result": {bookId: 17864, success: true}, "id": "1"}
// Просто отправляем уведомления серверу в одностороннем порядке (т.е. не указываем id и соответственно не получаем ответа)
{"jsonrpc": "2.0", "method": "writeToLog", "params": {user: "Иванов", text: "Пользователь вошел в систему", "datetime": "2025-01-30 00:08:33"}}
{"jsonrpc": "2.0", "method": "writeToLog", "params": {user: "Иванов", text: "Пользователь что-то там сделал в системе", "datetime": "2025-01-30 00:10:45"}}
{"jsonrpc": "2.0", "method": "writeToLog", "params": {user: "Иванов", text: "Система что-то сделала с пользователем", "datetime": "2025-01-30 00:12:50"}}
{"jsonrpc": "2.0", "method": "writeToLog", "params": {user: "Иванов", text: "Пользователь вышел из системы", "datetime": "2025-01-30 00:15:20"}}
// Для примера отправим кривой запрос к серверу (у которого нет указанного метода)
{"jsonrpc": "2.0", "method": "runUnknownMethod", "id": "44"}
// В ответ ожидается ошибка вида:
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "44"}
В общем если вы захотите посмотреть больше наглядных примеров, то вам опять же достаточно просто взглянуть на страницу спецификации, где уже представлены наглядные примеры.
Возможность отправки пакетных запросов (batch-запросы)
Отдельной возможностью JSON-RPC является отправка пакетных запросов. Т.е. вы отправляете не по одному запросу, а целыми пачками.
В таком случае, клиент на сервер отправляет не запрос-объект, а уже массив вышеприведенных объектов. Соответственно и ответ от сервера также должен представлять собой массив объектов-ответов, как на примере ниже:
// Для примера отправим на сервер несколько разнотиповых запросов:
[
{"jsonrpc": "2.0", "method": "addUser", "params": {name: "Иванов"}, "id": "1"},
{"jsonrpc": "2.0", "method": "addUser", "params": {name: "Петров"}, "id": "2"},
{"jsonrpc": "2.0", "method": "addUser", "params": {name: "Сидоров"}, "id": "3"},
{"jsonrpc": "2.0", "method": "removeUser", "params": {userId: "7785-876"}, "id": "4"},
{"jsonrpc": "2.0", "method": "writeToLog", "params": {user: "Иванов", text: "Пример безответной записи в лог"},
{"jsonrpc": "2.0", "method": "runUnknownMethod", "id": "5"}
]
// В ответ клиенту соответственно также придет массив:
[
{"jsonrpc": "2.0", "result": {userId: "1234-567", success: true}, "id": "1"},
{"jsonrpc": "2.0", "result": {userId: "2345-678", success: true}, "id": "2"},
{"jsonrpc": "2.0", "result": {userId: "3456-789", success: true}, "id": "3"},
{"jsonrpc": "2.0", "result": {success: true}, "id": "4"},
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"}
]
Нередко подобные пакетные запросы используются в целях оптимизации. Например представим, что при каждом вашем запросе, создается соединение с базой данных, выполняется операция и соединение закрывается и при этом открытие и закрытие соединения являются долгой операцией, то в таком случае вам имеет смысл один раз открыть соединение, пачкой обработать все ваши запросы с базой данных и закрыть соединение.
Что мне нравится в JSON-RPC
Если уж и перечислять все то, за что мне нравится JSON-RPC, то наверное получится подобный список:
- простота и минималистичность спецификации — вполне легко написать свою реализацию клиента и сервера JSON-RPC.
- транспортонезависимость — не всё же вокруг одного HTTP-крутится.
- асинхронность обмена сообщениями — например если вы отправите два любых отдельных запроса, ответы на них могут прийти в любом порядке и с разной задержкой ибо какие-то запросы могут быстро обрабатываться, а на какие-то нужно будет очень долго ждать ответа
- возможность пакетной обработки — как я уже упоминал, это в каких-то случаях может помочь с оптимизацией обработки запросов.
- для сообщений используется простой и широкораспространенный текстовых формат JSON.
- все правила в спецификации сформулированы четко, исключая какое-либо двойное толкование.
- опять же в силу своей простоты принцип его работы изучается очень быстро.
Естественно у этого протокола есть и определенные неудобства, как например обмен бинарными данными, но это в основном исходит из проблем самого формата JSON. Т.к. протокол транспортонезависимый, то либо придется кодировать бинарные данные в base64 и передавать в параметрах (что увеличит передаваемый объем сообщения и скорость его обработки, но зато универсально), либо воспользоваться помощью стороннего протокола, передавая бинарные данные через него. Нередко так и делают, когда реализуют JSON-RPC поверх HTTP, где и JSON-сообщение и бинарные данные передаются в теле HTTP-запроса в виде multipart/form-data.
Заключение
Как и многие другие протоколы обмена данными с сервисами, JSON-RPC также имеет свои сильные и слабые стороны. Просто на мой взгляд, одной из задач разработчика является адекватный подбор наиболее подходящей технологии (архитектуры, протоколов и пр.) под конкретную задачу и при этом еще желательно не перемудрить и не переусложнить.
Лично я считаю, что JSON-RPC вполне достоин того, чтобы обратить на него внимание. Его изучение не займет много времени и сил, зато возможно в каких-то задачах он вполне сможет оказаться удачным решением.