О JSON-RPC

2018-02-18

Уже на втором настоящем проекте я внедряю JSON-RPC. Пока никто страшно не жалуется :)

A Logo

JSON — это, как всем известно, JavaScript Object Notation. Я не люблю JSON. Я помню ещё те времена, когда вместо JSON балом правил XML. В XML было всё: и схемы, и трансформации, и многочисленные форматы, протоколы и стандарты, на нём основанные. Хотя бы Jabber/XMPP вспомните.

JSON сейчас полностью заменил или собирается заменить XML. Хотя лучше он лишь по одному параметру: объему сериализованных данных. Ну ладно, ещё нет проблемы «тег или атрибут».

И JSON, и XML годны лишь для автоматической сериализации структурированных данных. Изначальные человекочитаемость и человекописабельность, заложенные в обоих, себя не оправдали. В XML хоть комментарии есть. А вот делать конфиги из JSON, прошу вас, не надо.

И JSON, и XML уязвимы к атакам на парсеры, ибо дозволяют неограниченный уровень вложенности. Иронично, что сам JavaScript парсит JSON документы абсолютно так же, как это делают другие языки. Ибо через eval() — ну совсем уж небезопасно.

В XML всё было текстом. И были существенные проблемы представления бинарных данных. В JSON, вслед за JavaScript, пытаются ввести какие-то типы данных. Но они такие же убогие, как в самом JavaScript. Нет целых чисел. Нет правильных дат. Тут BSON и другие бинарные форматы — вне конкуренции.

XML vs JSON

Ну ладно, есть хипстерский JSON. Значит, должен быть JSON-RPC. RPC — Remote Procedure Call. Как подсказывает Википедия, к RPC относятся такие монстры как CORBA, DCOM. И в Java есть RMI. И NFS внутрях работает через RPC. И на XML у нас имеются широко используемый SOAP и никому неизвестный XML-RPC.

Изначально это наше объектно ориентированное программирование подразумевало пересылку сообщений между объектами. Как-то быстро сложилось, что эти сообщения — это какая-то просьба сделать что-то вот с этими вот параметрами и вернуть результат. То есть вызов метода у объекта. Самые популярные реализации ООП решили, что вызов метода объекта есть то же самое, что вызов функции локального процесса. Все вот эти стеки, переходы, возвраты. Это действительно эффективно. Но также сообразили, что имеет смысл иметь один объект где-то там, и слать вызовы удалённо, по сети, сериализуя и десериализуя параметры и результаты. Это и есть RPC. Ну или микросервисы, если хотите.

Сейчас в этих наших микросервисах, по крайней мере, в вебе, модно использовать REST. Странно. REST гвоздями прибит к HTTP. А HTTP — далеко не самый эффективный транспортный протокол. REST — он про данные. Ну и что, что девяносто процентов всех обращений к серверу — это получение или запись данных. А что делать для остальных десяти процентов? Вводить фиктивные сущности и очереди-коллекции этих сущностей только для того, чтобы POST в эту коллекцию запускал какую-то процедуру?

Есть ещё системы обмена сообщениями. Но в этих самых сообщениях всё равно нужно указать получателя, и что мы от него хотим. RPC — это ведь и есть обмен сообщениями. Где «получатель» — это объект или сервис, а «что мы хотим» — это имя метода. Разница только в том, что в RPC обычно подразумевается синхронный обмен, и в ответ на вызов метода ожидается результат здесь и сейчас. А при «обычном» обмене сообщениями подразумевается асинхронный обмен, ответ либо вообще не требуется, либо придёт когда-нибудь потом, может быть, даже по другому каналу связи. Но синхронность или асинхронность — это скорее особенности организации клиента и сервера, а также работы транспортного протокола, чем принцип собственно RPC.

RPC,Messaging,REST

Вот и JSON-RPC. Это лишь «спецификация» на одной странице, как представлять запросы и ответы в виде JSON. Этой спецификации абсолютно всё равно, каким образом эти запросы и ответы пересылаются. Можно через HTTP, тогда этот JSON просто POSTится на сервер, и тогда имеет смысл замапить варианты ответов на HTTP коды. Можно через голый TCP. Можно через ZeroMQ, мы и так делали.

Но ведь через HTTP, TCP и ZeroMQ можно слать любые JSON сообщения. Зачем использовать JSON-RPC? Потому что вам в любом случае придётся выработать какое-то соглашение. Что кодировать в JSON? Что угодно? А как кодировать ответ? Всё равно ведь придёте к чему-нибудь типа { "success": true }. Вот JSON-RPC и есть такое простенькое соглашение, которое уже есть. И ничуть не хуже любых других соглашений, которые вы можете выдумать.

JSON-RPC не определяет адресацию. Адресация, то есть поиск того самого объекта, на котором будем делать вызовы, это забота транспортного протокола. В HTTP это будет URL эндпоинта, куда POSTтить. В TCP это будет просто хост и порт, куда слать. В ZeroMQ это будет identity получателя или вообще та схема адресации, которую вы выдумаете.

JSON-RPC определяет синтаксис запроса.

{
  "jsonrpc": "2.0",
  "method": "subtract",
  "params": { "subtrahend": 23, "minuend": 42 },
  "id": 3
}

Есть обозначение версии протокола ;) Есть имя метода. Есть параметры. Тут может быть либо JSON объект в случае именованных параметров, либо JSON массив в случае позиционных параметров. Но не может быть одиночного значения. А ещё params может быть совсем опущен, если это метод без аргументов.

А ещё у нас есть id. Строка или число. По id клиент сможет связать ответ с запросом. Именно наличие id позволяет JSON-RPC без проблем работать асинхронно. Это уже забота транспорта и клиента, когда и как получить ответ. А по id уже можно понять, ответ на что это был. (Совершенно немыслимая забота в мире HTTP и других синхронных протоколов).

Можно послать сообщение и без id. В JSON-RPC это называется «Notification». В этом случае клиента не беспокоит судьба его запроса и ответ ему не интересен. А сервер не должен на нотификации отвечать. Чем не «обычная» отправка сообщения?

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

{
  "jsonrpc": "2.0",
  "result": 19,
  "id": 3
}

Результатом может быть любой валидный JSON, включая строки, числа, объекты, массивы, и null. (Кстати, при отображении в Java-объекты фиг ты различишь, где там null, а где отсутствие данного поля).

Если что-то пошло не так, результата не будет, будет error.

{
  "jsonrpc": "2.0",
  "error": { "code": -32601, "message": "Method not found" },
  "id": "1"
}

В error имеется код. Есть несколько предопределённых кодов для типичных ситуаций (вроде отсутствия метода), но для специфичных ошибок вам придётся придумать свои коды. Имеется сообщение, типа человеку. И можно добавить data с чем угодно, jsonrpc4j туда пихает класс и сообщение из исключения.

Ну вот и вся спецификация. Есть ещё батчи, но там всё очевидно. Просто? Понятно? Имхо, да.

JSON-RPC Format

Мы в спринговых микросервисах используем jsonrpc4j. Кажется, это единственная серьёзная реализация для Java. Там есть свои клиенты и серверы, и какая-то интеграция со Spring. В результате получается весьма прозрачно, как оно и положено в RPC. Есть интерфейс, с объявленными методами, с заданными именами параметров. На серверной стороне есть реализация этого интерфейса, которая и представляет собой этот наш объект, который будем удалённо вызывать. На клиентской стороне создаётся Proxy, который подключается к серверу, и реализует этот самый интерфейс для локальных вызовов. В качестве параметров и результатов можно использовать всё что угодно, пока оно сериализуется Jacksonом. Получается красиво, но синхронно.

Очень хочется этот jsonrpc4j слегка допилить. Убрать задавание path для HTTP сервера в интерфейсе, всё же это личное дело сервера, по какому адресу располагать эндпоинт, а значит, path должен быть задан у реализации, а не у интерфейса. Убрать странные и дурацкие ограничения на этот path. Например, что мешает на один path навешать кучу объектов, пока они не пересекаются по методам? Добавить поддержку автоматического распознавания имён параметров, ибо это должно работать в Java 8 и Kotlin. Но пока некогда... И так неплохо работает.

UPD

А как же Protocol Buffers? Ведь JSON всё же имеет лишний вес...

Protobuf вполне торт, особенно в контексте вот этого выступления: https://2017.codefest.ru/lecture/1167. Но я про него пока ничего не могу сказать, не щупал.

Моя мысль тут про то, что если уж JSON, то прежде чем выдумывать свои соглашения, посмотрите на JSON-RPC.