У нас было пятьдесят операционных систем, десяток языков программирования и бесконечное множество библиотек и фреймворков всех сортов и расцветок, а также кофе, немного времени и щепотка здравого смысла. Не то чтобы это был необходимый запас для сетевой разработки, но раз уж начал коллекционировать дичь, то сложно остановиться..
Вызовы туда и обратно
Что вы знаете об удаленных вызовах процедур?
Только честно и без поиска в Гугле?
Если вы из молодого поколения разработчиков, то врядли слышали даже такой термин — в современной разработке их заменили применяемые по поводу и без REST с JSON.
Многие из читателей постарше вспомнят монструозный SOAP. Вспомнят и немедленно вздрогнут — от воспоминаний о многочисленных сбоях, генерации кучи мусорного кода из WSDL и внезапных различий в реализации (привет Microsoft), вылезающих в самый неподходящий момент при сдаче проекта.
Еще более возрастные представители профессии вспомнят сетевой COM+ и CORBA — еще более жуткие и с еще большими сложностями при использовании.
Но есть кое-что объединяющее все поколения разработчиков, а именно рожденная практикой мысль:
вызывать из одной программы метод другой по сети это сложно
Поэтому пытаться делать такое своими руками, без каких-либо готовых решений точно не стоит. Ну что же, пришло время снова разрушать мифы, внимание на экран:
Всего ~500 строк Java-кода на серверную и клиентскую части и вы спокойно можете вызывать методы на любой ОС и в любом окружении.
Как обычно без каких-либо внешних библиотек и фреймворков.
Ну почему мне никто не рассказывал о таком во времена бессонных ночей, убитых на проклятый SОАР? Где спрашивается были тогда все «мегаэксперты по всем вопросам», гуру и профессионалы от разработки?
Они разумеется были, но существовавший в 90е и 2000е тренд на заведомое переусложнение корпоративного ПО просто не давал им шансов.
XML-RPC
Начну с цитаты:
XML-RPC (от англ. eXtensible Markup Language Remote Procedure Call — XML-вызов удалённых процедур) — стандарт/протокол вызова удалённых процедур, использующий XML для кодирования своих сообщений и HTTP в качестве транспортного механизма[1]
Является прародителем SOAP, отличается исключительной простотой в применении.
XML-RPC, как и любой другой интерфейс Remote Procedure Call (RPC), определяет набор стандартных типов данных и команд, которые программист может использовать для доступа к функциональности другой программы, находящейся на другом компьютере в сети.
На самом деле за этими сухими строками скрывается очередная эпичная история:
Протокол XML‑RPC был изначально разработан Дэйвом Винером из компании «UserLand Software» в сотрудничестве с Майкрософт, в 1998 году. Однако корпорация Майкрософт вскоре сочла этот протокол слишком упрощённым, и начала расширять его функциональность.
Ничего не напоминает?
Например известную историю с расширениями для HTML от Microsoft. Или с расширениями для C++ от Microsoft, или с расширениями CSS от Microsoft — думаю вы поняли насколько эта замечательная компания любит все расширять.
После нескольких циклов по расширению функциональности, появилась система, ныне известная как SOAP.
Да, теперь вы тоже знаете как оно появилось на свет и чьи уши торчат из WSDL и кодогенерации.
Ну и закономерный финал:
Позднее Майкрософт начала широко рекламировать и внедрять SOAP, а изначальный XML-RPC был отвергнут.
Несмотря на все прилагаемые усилия (например до сих пор не существует официальной реализации XML‑RPC для любого из продуктов Microsoft, только созданные сторонними разработчиками), проект XML‑RPC похоронить с концами так и не удалось — протокол жив и активно используется до сих пор, в том числе и самим автором.
Вот так выглядит вызов:
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
При такой простоте реализации XML‑RPC есть практически для всех известных языков поддерживающих работу с сетью (что я и покажу ниже), а автора статьи XML‑RPC не раз выручал в проектах, где использовать «большой» SOAP было проблематично а реализовывать свой протокол или обмениваться файлами — слишком уж криво или долго.
Словом XML‑RPC это очень важный и нужный инструмент для любого профессионального разработчика, на любом языке и в любом окружении, поскольку дает предсказуемый вариант взаимодействия между системами, даже очень далекими и максимально несовместимыми.
Проект
Я решил реализовать свою версию клиента и сервера XML-RPC, причем минимально возможных размеров и разумеется без всяких зависимостей.
Исходный код, вместе с примерами использования выложен на Github.
Данный проект — еще одна иллюстрация известной истины, гласящей что «в основе всех сложных вещей лежат очень простые принципы».
Для разработки использовались новые возможности Java 17, но ввиду размеров и экстремальной простоты проекта — все легко портируется хоть на Java 1.4 и точно будет работать в любом окружении.
Даже генерация XML реализована полностью вручную, а стандартный SAX-парсер используется только для разбора входящих запросов.
Также данную реализацию можно использовать как образец для повторения на любом произвольном языке и в любом окружении, поддерживающим работу по сети — настолько все просто.
Библиотека
Собственно вся библиотека реализующая как клиентскую так и серверную стороны XML-RPC состоит из трех файлов:
Конечно же внутренняя структура несколько сложнее и присутствуют вложенные классы, но общая логика такая:
XmlRPC — содержит общую для клиента и сервера логику разбора запросов и формирования ответов XML-RPC;
XmlRpcClient — содержит логику клиентской стороны, в первую очередь это подключение и формирование запроса к серверу;
XmlRpcServer — содержит серверную логику, главное из которой это непосредственно вызов методов.
Пройдусь по ключевым частям.
Первое что бросается в глаза это вот такое перечисление, содержащее список всех типов данных XML-RPC:
enum DATA_TYPES { String,Integer,Boolean,Double,
Date,Base64,Struct,Array,Nil }
И заранее заданный формат дат:
private static final SimpleDateFormat
XMLRPC_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
Не поверите, но вообщем-то это все описание типов XML-RPC, настолько тут все просто.
Вот так выглядит формирование ответа сервера:
..
void writeObject(Object what, XmlWriter writer) {
writer.startEl("value");
if (what == null)
writer.emptyEl("nil");
else if (what instanceof String)
writer.write(what.toString(), true);
else if (what instanceof Integer)
writer.writeEl("int", what.toString());
else if (what instanceof Boolean b)
writer.writeEl("boolean", (b ? "1" : "0"));
else if (what instanceof Double || what instanceof Float)
writer.writeEl("double", what.toString());
else if (what instanceof Date d)
writer.writeEl("dateTime.iso8601", XMLRPC_DATE_FORMAT.format(d));
else if (what instanceof byte[] b)
writer.writeEl("base64", Base64.getEncoder().encodeToString(b));
else if (what instanceof List<?> v) {
writer.startEl("array").startEl("data");
for (Object o : v)
writeObject(o, writer);
writer.endEl("data").endEl("array");
} else if (what instanceof Map<?, ?> h) {
writer.startEl("struct");
for (Map.Entry<?, ?> e : h.entrySet()) {
if (!(e.getKey() instanceof String nk))
continue;
final Object nv = e.getValue();
writer.startEl("member").startEl("name")
.write(nk, false).endEl("name");
writeObject(nv, writer); writer.endEl("member");
}
writer.endEl("struct");
} else
throw new RuntimeException("unknown type: %s"
.formatted(what.getClass()));
writer.endEl("value");
}
..
Да, в одном этом методе содержится вся логика формирования ответа.
Мы просто последовательно проверяем тип отдаваемых данных и вручную формируем XML-теги.
И все.
Обработка входящих запросов чуть объемнее из-за использования потокового SAX-парсера, поэтому приведу лишь ключевые части:
..
@Override
public void startElement(String uri, String localName,
String qName, Attributes attributes) {
if (LOG.isLoggable(Level.FINE))
LOG.fine("startElement: %s".formatted(qName));
switch (qName) {
case "fault" -> this.fault = true;
case "value" -> {
final Value v = new Value();
this.values.push(v);
this.cvalue = v;
this.cdata.setLength(0);
this.readCdata = true;
}
case "methodName", "name", "string" -> {
this.cdata.setLength(0);
this.readCdata = true;
}
case "i4", "int" -> {
this.cvalue.setType(DATA_TYPES.Integer);
this.cdata.setLength(0);
this.readCdata = true;
}
case "boolean" -> {
this.cvalue.setType(DATA_TYPES.Boolean);
this.cdata.setLength(0);
this.readCdata = true;
}
case "double" -> {
this.cvalue.setType(DATA_TYPES.Double);
this.cdata.setLength(0);
this.readCdata = true;
}
case "dateTime.iso8601" -> {
this.cvalue.setType(DATA_TYPES.Date);
this.cdata.setLength(0); this.readCdata = true;
}
case "base64" -> {
this.cvalue.setType(DATA_TYPES.Base64);
this.cdata.setLength(0); this.readCdata = true;
}
case "struct" -> this.cvalue.setType(DATA_TYPES.Struct);
case "array" -> this.cvalue.setType(DATA_TYPES.Array);
case "nil" -> this.cvalue.setType(DATA_TYPES.Nil);
}
}
..
Тут происходит сопоставление названия тега к поддерживаемому типу из перечисления, например при разборе вот такого блока будет установлен тип DATA_TYPES.Integer:
<value><i4>41</i4></value>
Само же значение будет преобразовано согласно типу чуть ниже:
...
public void characterData(String cdata) {
switch (this.type) {
case Integer -> this.value = Integer.valueOf(cdata.trim());
case Boolean -> this.value = "1".equals(cdata.trim());
case Double -> this.value = Double.valueOf(cdata.trim());
case Date -> {
try {
this.value = XMLRPC_DATE_FORMAT.parse(cdata.trim());
} catch (ParseException p) {
throw new RuntimeException(p.getMessage());
}
}
case Base64 -> this.value = Base64.getDecoder()
.decode(cdata.getBytes());
case String -> this.value = cdata;
case Struct -> nextMemberName = cdata;
default -> throw new IllegalStateException(
"Unexpected value: %s".formatted(this.type));
}
}
...
Указанный выше метод characterData вызывается при завершении обработки элемента:
..
@Override
public void endElement(String uri, String localName, String qName) {
if (LOG.isLoggable(Level.FINE))
LOG.fine("endElement: %s".formatted(qName));
if (this.cvalue != null && this.readCdata) {
this.cvalue.characterData(this.cdata.toString());
this.cdata.setLength(0); this.readCdata = false;
}
}
..
И.. все.
Это вся обработка XML-RPC.
Достойны упоминания еще работа с пулом исполнителей (Worker) как на стороне клиента так и сервера, а также логика вызова метода с помощью «Reflection API».
Пул исполнителей
Как клиент так и сервер содержат вот такую переменную класса:
private final Deque<ServerWorker> pool = new ArrayDeque<>();
это пул, в котором хранятся готовые к использованию копии «исполнителей» — специальных классов, отвечающих за обработку запроса.
Сделано это было для управления нагрузкой — чтобы не произошел отказ из‑за слишком большого количества выполняемых запросов. Все же вызов метода это не передача файла, любой вызов может привести к непредсказуемым результатам.
Кешировать ответы вызовов методов очевидно нельзя, если конечно хотите универсальности
Как на клиенте, так и на сервере логика обработки запроса выглядит одинаково:
..
public byte[] execute(InputStream is, String user, String password) {
final ServerWorker serverWorker = getWorker();
// execute call
try { return serverWorker.execute(is, user, password); } finally {
this.pool.push(serverWorker); // push worker back to pool
}
}
..
Сначала из пула достается первый доступный исполнитель, с его помощью осуществляется обработка и в конце он снова возвращается в пул.
Отличие клиентской стороны в дополнительной проверке на ошибку в ответе сервера:
..
if (!clientWorker.fault)
this.pool.push(clientWorker);
..
Если такая ошибка есть — исполнитель обратно в пул не возвращается.
Вызовы и Reflection API
Второй интересной особенностью реализации является сам вызов метода через Reflection API:
..
public Object execute(String methodName, List<Object> params)
throws Exception {
final List<Class<?>> argClasses = new ArrayList<>();
final List<Object> argValues = new ArrayList<>();
if (params != null && !params.isEmpty()) {
// here we check provided params and try
// to unwrap basic types
for (final Object v : params) { argValues.add(v);
if (LOG.isLoggable(Level.FINE))
LOG.fine("param class: %s value=%s"
.formatted(v.getClass().getName(), v));
argClasses.add(v.getClass().isPrimitive()
? MethodType.methodType(v.getClass())
.unwrap().returnType() : v.getClass());
}
}
final Method method; // method to call
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Calling method: %s".formatted(methodName));
for (int c = 0; c < argClasses.size(); c++)
LOG.fine("Parameter %d: %s = %s"
.formatted(c, argClasses.get(c), argValues.get(c)));
}
// get method via 'Reflection API'
method = this.targetClass.getMethod(methodName,
argClasses.toArray(new Class[0]));
try {
// and try to invoke
return method.invoke(this.invokeTarget,
argValues.toArray(new Object[0]));
} catch (InvocationTargetException it_e) {
throw new RuntimeException(it_e.getTargetException());
}
}
}
..
Тут стоит обратить внимание на такой код:
..
argClasses.add(v.getClass().isPrimitive()
? MethodType.methodType(v.getClass())
.unwrap().returnType() : v.getClass());
..
Он разворачивает примитивные типы аргументов в их «оберточные» версии:
int -> Integer, boolean -> Boolean и так далее.
Столь простая логика требует чтобы все методы обработчиков, вызываемые через Reflection API имели в качестве параметров только классы-обертки но не примитивы.
Если проще, то вот так будет работать:
..
public Map<String, Object> sumAndDifference(Integer x, Integer y) {
final Map<String, Object> result = new HashMap<>();
result.put("sum", x + y);
result.put("difference", x - y);
return result;
}
..
а вот так уже нет:
..
public Map<String, Object> sumAndDifference(int x, int y) {
final Map<String, Object> result = new HashMap<>();
result.put("sum", x + y);
result.put("difference", x - y);
return result;
}
..
Столь серьезное упрощение требуется для того чтобы сразу получать нужный метод по одной его сигнатуре:
method = this.targetClass.getMethod(methodName,
argClasses.toArray(new Class[0]));
Без поиска и перебора вариантов, вроде такого:
..
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
String mname = m.getName();
if (!mname.startsWith("test")
|| (m.getGenericReturnType() != boolean.class)) {
continue;
}
..
Теперь перейдем к примерам использования.
Пример сервера XML-RPC
Начну с самого важного — с реализации standalone-сервера XML-RPC на базе этой замечательной библиотеки.
Исходный код (аж целый один класс SampleServer) находится в подпроекте tiny-xmlrpc-library-sample-server:
Вся логика за исключением обработчиков расположена внутри метода main:
public static void main(String[] args) throws IOException {
//check for 'appDebug' parameter
boolean debugMessages = Boolean
.parseBoolean(System.getProperty("appDebug", "false"));
// adjust logging levels to show more messages,
// if appDebug was set
if (debugMessages) {
LOG.setUseParentHandlers(false);
final Handler systemOut = new ConsoleHandler();
systemOut.setLevel(Level.FINE);
LOG.addHandler(systemOut); LOG.setLevel(Level.FINE);
LOG.fine("debug messages enabled");
}
// create HTTP-server
final HttpServer server = HttpServer
.create(new InetSocketAddress(8000), 50);
// initialize default handler
final DefaultServerHttpHandler dsh = new DefaultServerHttpHandler();
// add some demo handlers
dsh.addHandler("example", new DemoXmlRpcHandler());
// one with authentication enabled
dsh.addHandler("auth", new SampleAuthenticatedXmlRpcHandler());
// setup default XML-RPC handler
dsh.addHandler("$default", new DefaultXmlRpcHandler());
server.createContext("/", dsh);
server.setExecutor(null); // creates a default executor
LOG.info("Started XML-RPC server on http://%s:%d"
.formatted(server.getAddress().getHostName(),
server.getAddress().getPort()));
server.start(); //finally that the server
}
Как видите, реализация использует класс com.sun.net.httpserver, встроенный в JDK/JRE с незапамятных времен и реализующий очень простой HTTP-сервер.
Основной обработчик, связывающий сервер с логикой обработки XML-RPC выглядит вот так:
..
public static class DefaultServerHttpHandler implements HttpHandler {
// an instance of XmlRpcServer
private final XmlRpcServer xrs = new XmlRpcServer();
/**
* Binds provided handler to XML-RPC server instance
* @param handlerName
* a handler's unique name
* @param h
* handler instance
*/
public void addHandler(String handlerName, Object h) {
this.xrs.addHandler(handlerName, h);
}
/**
* Handles input HTTP request
* @param t the exchange containing the request from the
* client and used to send the response
* @throws IOException
* on I/O errors
*/
public void handle(HttpExchange t) throws IOException {
// ignore all non POST requests
if (!"POST".equals(t.getRequestMethod())) {
t.sendResponseHeaders(400, 0);
t.close();
return;
}
if (LOG.isLoggable(Level.FINE))
LOG.fine("got http request: %s"
.formatted(t.getRequestURI()));
// process request
try (OutputStream so = t.getResponseBody()) {
String[] creds = null; // check for Basic Auth
if (t.getRequestHeaders().containsKey("Authorization"))
creds = this.xrs.extractCredentials(
t.getRequestHeaders().get("Authorization").get(0));
// execute call and get result
// (there would be XML encoded in byte array)
final byte[] result = creds!=null?
this.xrs.execute(t.getRequestBody(),creds[0],creds[1]) :
this.xrs.execute(t.getRequestBody());
// set response 'content-type' header
t.getResponseHeaders().add("Content-type", "text/xml");
// send headers
t.sendResponseHeaders(200, result.length);
// send body
so.write(result); so.flush();
} catch (Exception e) {
LOG.warning(e.getMessage());
}
}
}
..
Вся логика заключается в пробросе POST-запросов для их последующей обработки в классе XmlRpcServer:
final byte[] result = this.xrs.execute(t.getRequestBody());
и последующей выдаче готового результата вызова клиенту:
t.getResponseHeaders().add("Content-type", "text/xml");
// send headers
t.sendResponseHeaders(200, result.length);
// send body
so.write(result); so.flush();
Также в качестве примера реализованы несколько тестовых методов для вызова снаружи через XML-RPC, например для проверки авторизации:
..
static class SampleAuthenticatedXmlRpcHandler
implements XmlRPC.AuthenticatedXmlRpcHandler {
public Object execute(String method,
List<Object> v,
String user, String password) throws Exception {
i
f ("admin".equals(user) && "admin1".equals(password))
return "Hello %s".formatted(user);
throw new XmlRPC.XmlRpcException(5, "Access denied");
}
}
..
Вот так выглядит вызов со стороны клиента:
XmlRpcClient clientAuth = new XmlRpcClient(
new URL("http://localhost:8000"));
//set auth credentials
clientAuth.setBasicAuthentication("admin","admin1");
System.out.println(clientAuth.execute("auth.execute", List.of(1,2)));
Помимо обработчика авторизации, был добавлен еще один тестовый — в виде POJO:
public static class DemoXmlRpcHandler {
/**
* Sample method, to call from XML-RPC
* @param x
* some integer
* @param y
* some another integer
* @return
* a map with 2 properties: sum - would contain sum of two provided integers
* difference - would be x - y result
*/
public Map<String, Object> sumAndDifference(Integer x, Integer y) {
final Map<String, Object> result = new HashMap<>();
result.put("sum", x + y);
result.put("difference", x - y);
return result;
}
}
Тут нет ни интерфейсов ни аннотаций — весь контроль над созданием данного класса полностью на стороне разработчика, а значит подобным обработчиком легко может выступать например управляемый контейнером бин Spring.
Вызов со стороны клиента выглядит так:
XmlRpcClient client2 = new XmlRpcClient(new URL("http://localhost:8000"));
System.out.println(client2.execute("example.sumAndDifference",
List.of(15,55)));
Результат вызова:
{difference=-40, sum=70}
XML-RPC сервер на базе Jakarta Servlet
Следующим примером будет реализация сервера на базе обычного сервлета:
Она еще меньше и проще:
public class SampleServlet extends HttpServlet {
// an XML-RPC server instance
protected XmlRpcServer xmlrpc = new XmlRpcServer();
@Override
public void init(ServletConfig config) {
//register our sample handler
this.xmlrpc.addHandler("hello", new DemoXmlRpcHandler());
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException {
// execute XML-RPC call and get response as byte array
final byte[] result = this.xmlrpc.execute(req.getInputStream());
// set response content type and length
res.setContentType("text/xml");
res.setContentLength(result.length);
// respond to client
try (ServletOutputStream so = res.getOutputStream()) {
so.write(result); so.flush();
}
}
}
Логика работы полностью совпадает со standalone‑версией, за тем исключением что не проверяется даже тип входящего запроса — сервлет сам переопределяет метод doPost, а значит реагирует только на POST‑запросы.
Тестовый обработчик вернет все входные параметры одной строкой:
..
static class DemoXmlRpcHandler implements XmlRPC.XmlRpcHandler {
public Object execute(String methodname, List<Object> params) {
final StringBuilder out = new StringBuilder("Request was:\n");
for (Object p :params)
out.append("param: ").append(p).append('\n');
return out.toString();
}
}
..
Вот так выглядит клиентская сторона с вызовом:
XmlRpcClient client3 = new XmlRpcClient(
new URL("http://localhost:8080/api/xmlrpc"));
System.out.println(client3.execute("hello", List.of(15,55,33,77)));
Вызовы из других языков
Разумеется рассказ о гибкости и универсальности XML-RPC был бы неполным без конкретных примеров работы из других языков. Поэтому вторая часть статьи полностью посвящена именно таким примерам с использованием самых разных языков и окружений.
Помните про ~500 строк исходного кода и разработку полностью с нуля — как много проблем может решить даже столь простой проект.
Все примеры ниже являются клиентскими, т. е. реализуют вызов тестового сервера по протоколу XML‑RPC, весь код — рабочий на момент написания статьи, тестировался из разных окружений и с различным набором данных.
Python 3
Для вызова использовался стандартный пакет xmlrpc, поставляемый вместе с Python:
import xmlrpc.client
with xmlrpc.client.ServerProxy("http://localhost:8000") as proxy:
print("call result: %s" % str(proxy.example.sumAndDifference(22,9)))
Perl 5
Вызов осуществляется с помощью модуля XML::RPC, устанавливаемого из CPAN:
use XML::RPC;
my $xmlrpc = XML::RPC->new('http://localhost:8000');
my $result = $xmlrpc->call( 'example.sumAndDifference',
{ state1 => 12, state2 => 28 } );
print $result;
Tcl
Использовался пакет xmlrpc для Tcl, который присутствует в репозиториях всех популярных дистрибутивов Linux и *BSD:
package require xmlrpc
if {[catch {set res [xmlrpc::call "http://127.0.0.1:8000"
"" "example.sumAndDifference" { {int 221} {int 22} }]} e]} {
puts "xmlrpc call failed: $e"
} else {
puts "res: $res."
}
Также этот пакет присутствует в сборках ActiveTcl для Windows.
Common Lisp
Использовалась библиотека cxml-rpc, вот так выглядит вызов:
(xrpc:call "http://localhost:8000/" "example.sumAndDifference"
'(:integer 41 :integer 22))
Для сравнения — пример вызова внешнего тестового XML‑RPC сервиса:
(xrpc:call "http://betty.userland.com/RPC2" "examples.getStateName"
'(:integer 41))
C++
Использовалась кроссплатформенная библиотека ulxmlrpcpp, код достаточно объемный, но это же C++:
#include <ulxmlrpcpp/ulxmlrpcpp.h>
#include <ulxmlrpcpp/ulxr_tcpip_connection.h>
#include <ulxmlrpcpp/ulxr_http_protocol.h>
#include <ulxmlrpcpp/ulxr_requester.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <vector>
#include <sys/time.h>
#include <time.h>
int main(int argc, char **argv)
{
const std::string ipv4 = "127.0.0.1";
const unsigned port = 8000;
ulxr::IP myIP;
myIP.ipv4 = ipv4;
ulxr::TcpIpConnection conn (ipv4, port,
ulxr::TcpIpConnection::DefConnectionTimeout);
ulxr::HttpProtocol prot(&conn);
ulxr::Requester client(&prot);
ulxr::MethodCall testcall ("example.sumAndDifference");
testcall.addParam(ulxr::Integer(123));
testcall.addParam(ulxr::Integer(21));
ulxr::MethodResponse resp = client.call(testcall,"/");
std::cout << "call result: \n" << resp.getXml(0);
}
Сборка:
g++ -I/opt/src/ulxmlrpcpp test.cpp -o test-xml-rpc
/opt/src/ulxmlrpcpp/lib/libulxmlrpcpp.a -lexpat -lssl -lcrypto -lpthread
Чистый C
Использовалась самая популярная библиотека xmlrpc-c, которая присутствует в большинстве дистрибьютивов Linux и других ОС, даже редких:
#include <stdlib.h>
#include <stdio.h>
#include <xmlrpc-c/base.h>
#include <xmlrpc-c/client.h>
static void
die_if_fault_occurred(xmlrpc_env *const envP,
const char *const fun)
{
if (envP->fault_occurred)
{
fprintf(stderr, "%s failed. %s (%d)\n",
fun, envP->fault_string, envP->fault_code);
exit(-1);
}
}
int main(int argc, char **argv)
{
xmlrpc_env env;
xmlrpc_value *resultP;
const char *const method_name = "example.sumAndDifference";
const char *const server_url = "http://localhost:8000";
xmlrpc_env_init(&env);
xmlrpc_client_init2(&env, XMLRPC_CLIENT_NO_FLAGS,
"Test XML-RPC", "1.0", NULL, 0);
die_if_fault_occurred(&env, "xmlrpc_client_init2()");
resultP = xmlrpc_client_call(&env, server_url, method_name,
"(ii)",
(xmlrpc_int32) 65,
(xmlrpc_int32) 17);
die_if_fault_occurred(&env, "xmlrpc_client_call()");
xmlrpc_int32 sum, difference;
xmlrpc_decompose_value(&env, resultP, "{s:i,s:i,*}",
"sum", &sum,
"difference", &difference);
printf("Result is sum: %d ,difference: %d\n", sum,difference);
xmlrpc_DECREF(resultP);
xmlrpc_env_clean(&env);
xmlrpc_client_cleanup();
return 0;
}
Вот так выглядит Makefile для сборки:
CC = clang
CFLAGS = -Wall -Ofast
LDFLAGS =
XMLRPC_C_CONFIG = xmlrpc-c-config
SOURCE_CLIENT = test_client.c
EXECUTABLE_CLIENT = test_client
OBJECTS_CLIENT = $(SOURCE_CLIENT:.c=.o)
LIBS_CLIENT = $(shell $(XMLRPC_C_CONFIG) client --libs)
INCLUDES_CLIENT = $(shell $(XMLRPC_C_CONFIG) client --cflags)
.PHONY: all client clean
.SUFFIXES: .c .o
default: all
.c.o:
$(CC) $(CFLAGS) -c $< -o $@
$(EXECUTABLE_CLIENT): $(OBJECTS_CLIENT)
$(CC) $(LDFLAGS) $(LIBS_CLIENT) $(OBJECTS_CLIENT) -o $@
client: $(EXECUTABLE_CLIENT)
all: client
clean:
rm -f $(OBJECTS_CLIENT)
rm -f $(EXECUTABLE_CLIENT)
В работе:
Обратите внимание на трассировку вызовов с отображением XML запроса и ответа — одна из фич библиотеки xmlrpc‑c, включаемая из переменной окружения.
Ruby
Использовалась стандартная библиотека, которая есть в дистрибьютиве Ruby:
require 'xmlrpc/client'
require 'pp'
server = XMLRPC::Client.new2("http://localhost:8000")
result = server.call("example.sumAndDifference", 5, 3)
pp result
Rust
Вызов XML-RPC реализован с помощью «crate» xmlrpc, хотя мне сложно судить насколько это стандартный способ:
extern crate xmlrpc;
use xmlrpc::{Request, Value};
fn main() {
let req = Request::new("example.sumAndDifference").arg(22).arg(8);
let res = req.call_url("http://127.0.0.1:8000");
println!("Result: {:?}", res);
}
Файл для сборки Cargo.toml:
[package]
name = "xmlrpc-test"
version = "1.0.0"
edition = "2024"
[dependencies]
xmlrpc = "0.15.1"
Golang
Использовалась библиотека go-xmlrpc, стандартной у гошечки к сожалению нет:
package main
import (
"fmt"
"alexejk.io/go-xmlrpc"
)
func main() {
client, _ := xmlrpc.NewClient("http://localhost:8000")
req := &struct {
Param1 int
Param2 int
}{
Param1: 12,
Param2: 45,
}
resp := &struct {
Body struct {
Sum int
Difference int
}
}{}
_ = client.Call("example.sumAndDifference", req, resp)
fmt.Printf("Results, sum: %d ,difference: %d \n",
resp.Body.Sum, resp.Body.Difference)
}
Haskell
Использовался пакет haxr, реализация достаточно сложная, как вообщем-то и сам Haskell:
module Main where
import Network.XmlRpc.Client
import Network.XmlRpc.THDeriveXmlRpcType
import Network.XmlRpc.Internals
server = "http://localhost:8000"
data Resp = Resp { summary :: Int, difference :: Int } deriving Show
instance XmlRpcType Resp where
fromValue v = do
t <- fromValue v
n <- getField "sum" t
a <- getField "difference" t
return Resp { summary = n, difference = a }
add :: String -> Int -> Int -> IO Resp
add url = remote url "example.sumAndDifference"
main = do
let x = 4
y = 7
z <- add server x y
putStrLn (show x ++ " + " ++ show y ++ " = " ++ show z)
Обратите внимание что поле в структуре называется «summary», а не «sum», это было сделано чтобы не переопределять или скрывать системный sum.
Cкриншот с демонстрацией в работе:
Node.js
Разумеется все работает легко, просто и красиво, использовалась библиотека davexmlrpc:
const xmlrpc = require ("davexmlrpc");
xmlrpc.client ("http://localhost:8000",
"example.sumAndDifference", [53,14], "xml", function (err, data) {
console.log (err ? err.message : JSON.stringify (data));
});
Готовый package.json для сборки:
{
"name": "xmlrc-nodejs",
"version": "1.0.0",
"scripts": {
"app": "node client.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"davexmlrpc": "^0.4.26"
}
}
В работе:
C# и .NET
Как это ни странно, но для .NET все найденные реализации XML‑RPC оказались немного заброшенными, хотя и рабочими — видимо длинная рука Microsoft до сих пор пытается загубить этот протокол.
Реализация была сделана с помощью библиотеки Kveer.XmlRPC, которая имеет в репозитории Nuget больше всего установок:
using CookComputing.XmlRpc;
public class Program
{
[XmlRpcUrl("http://localhost:8000")]
public interface ISampleService: IXmlRpcProxy
{
[XmlRpcMethod("example.sumAndDifference")]
XmlRpcStruct SumAndDifference(int num1, int num2);
}
public static void Main(string[] args)
{
ISampleService proxy = XmlRpcProxyGen.Create<ISampleService>();
var res = proxy.SumAndDifference(41,26);
Console.WriteLine($"response, sum: {res["sum"]}, " +
$"difference: {res["difference"]}");
}
}
В работе:
Free Pascal и Lazarus
С трудом, но все же получилось оживить и заставить работать библиотеку DXmlRpc, с реализацией XML-RPC как для Delphi/Kylix так и для Lazarus.
Исходный код:
unit Hello;
interface
uses
LCLIntf, LCLType, LMessages, Messages, SysUtils,
Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, XmlRpcTypes, XmlRpcClient;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
procedure TForm1.Button1Click(Sender: TObject);
var
RpcCaller: TRpcCaller;
RpcFunction: IRpcFunction;
RpcResult: IRpcResult;
begin
RpcCaller := TRpcCaller.Create;
try
RpcCaller.EndPoint := '/';
RpcCaller.HostName := 'localhost';
RpcCaller.HostPort := 8000;
RpcFunction := TRpcFunction.Create;
RpcFunction.ObjectMethod := 'example.sumAndDifference';
RpcFunction.AddItem(21);
RpcFunction.AddItem(5);
RpcResult := RpcCaller.Execute(RpcFunction);
if RpcResult.IsError then
ShowMessageFmt('Error: (%d) %s', [RpcResult.ErrorCode,
RpcResult.ErrorMsg])
else
ShowMessage('Success: ' + RpcResult.AsString);
finally
RpcCaller.Free;
end;
end;
end.
Вот так выглядит в работе:
Итого
Этой статьей я хотел еще раз продемонстрировать:
в основе всех сложных вещей лежат очень простые идеи
Далеко не всегда имеет смысл заморачиваться с SOAP или современными реализациями REST+JSON, несмотря на общие тенденции и «модность».
Очень простой протокол с фиксированными типами данных и минимальной реализацией способен решить очень много практических проблем, без погружения в особенности сериализации сложных типов данных, работы с датами или дробными числами.
Устаревшее окружение, экзотические ОС, встраиваемые системы — вариантов применения масса.
Пользуйтесь ;)
Это немного отцезурированная и доработанная версия статьи, оригинал которой доступен в нашем блоге.
0x08 Software
Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.
Оживляем давно умершее, чиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.
Комментарии (29)
ptr128
23.08.2024 13:00Странно видеть описание протокола четвертьвековой давности. Например, gRPC у него выигрывает по всем статьям.
alex0x08 Автор
23.08.2024 13:00Ну покажите как ваш gRPC работает на десяти разных ОС, включая сильно устаревшие, из разных диких языков а не только там где есть готовая реализация.
ptr128
23.08.2024 13:00Не понял, какие проблемы? Приведите хотя бы один пример, где невозможно заменить XML-RPC на gRPC.
на десяти разных ОС, включая сильно устаревшие
Вы знаете платформу, для которой нет C/C++ компилятора или бекенда для LLVM?
диких языков
В том же FORTRAN или COBOL куда проще реализовать вызов через gRPC, чем сериализацию и десериализацию XML.
alex0x08 Автор
23.08.2024 13:00Навскидку:
Lisp, TCL, окружения Linux старше 15 лет, практически все коммерческие Unix, промавтоматика, встраиваемые системы, где как раз имеет смысл миниатюризация всех используемых библиотек. Устаревшие Windows.
Разумеется там будет компилятор, но только устаревший, у которого проблемы компиляции даже не находятся поисковиками.
ptr128
23.08.2024 13:00Lisp, TCL, окружения Linux старше 15 лет, практически все коммерческие Unix, промавтоматика, встраиваемые системы
Сами погуглить не могли?
Разумеется там будет компилятор, но только устаревший, у которого проблемы компиляции даже не находятся поисковиками.
Кросскомпиляцию никто не отменял. А на K&R С, не говоря уже об упомянутых выше FORTRAN и COBOL, сериализацию и десериализацию XML сложнее будет реализовать, чем gRPC. Что я тоже показал выше.
alex0x08 Автор
23.08.2024 13:00Уважаемый ptr128, в интернете давно существует большая проблема с валидацией публикуемых данных, тем более когда речь заходит о сложных технических вещах.
То что вы «нагуглили» это разумеется замечательно, но это не истина в первой инстанции.
Если вы заметили, половина данной статьи состоит из конкретных примеров реально работающего кода на разных языках — все это я на самом деле собрал и запустил.
По этой причине могу подтвердить что оно действительно работает на момент написания статьи.
Если вы в силах повторить подобное для gRPC — пишите статью, коллеги оценят.
P.S.
Я не выступаю против gRPC, да и против вас лично ничего не имею, просто не хочу скатывания разумной технической дискуссии в очередной бессмысленный срач.
ptr128
23.08.2024 13:00То что вы «нагуглили» это разумеется замечательно, но это не истина в первой инстанции.
Так как большинство ссылок на github, истинность проверяется элементарно. А ссылки на сайты производителей коммерческих продуктов, вполне себе валидные по определению.
По этой причине могу подтвердить что оно действительно работает на момент написания статьи.
На платформах, не поддерживающих Java, вроде упомянутого выше ESP32?
Я просил пример, где, по Вашему мнению, gRPC не применим. Вы же выдали целый ворох групп. Вы действительно ожидали, что для каждой из систем, покрываемых этими общими группами, я приведу работающий код?
не хочу скатывания разумной технической дискуссии в очередной бессмысленный срач.
Простите, но уже скатились, предложив сериализацию и десериализацию XML на встраиваемых системах, где каждый байт оперативки на счету и CPU слабые. Надеюсь, понимаете, что gRPC требует на порядок меньше оперативной памяти и в разы меньше процессорных ресурсов, чем XML-RPC. Я знаю о чем говорю, так как когда мы переходили с REST на gRPC, то получили прирост производительности, в среднем, в 8-9 раз.
На этом предлагаю закончить.
alex0x08 Автор
23.08.2024 13:00Я вообще не понимаю причем тут сериализация, ее если что нет ни в моей реализации ни в большинстве использованных клиентских библиотек.
Там просто парсер XML-запроса и генерация ответа в виде строки.
И то и другое отлично ограничивается по используемым ресурсам.
так как когда мы переходили с REST на gRPC, то получили прирост производительности, в среднем, в 8-9 раз.
Не знаю что у вас за случай, но очень сомневаюсь что причина лишь в одном только протоколе.
Вы бы тогда скорее снижением объема трафика хвастались - размеры передаваемых данных по бинарному протоколу разумеется заметно меньше.
ptr128
23.08.2024 13:00Там просто парсер XML-запроса и генерация ответа в виде строки.
Первое - и есть десериализация. Второе - сериализация. А то что примеры в публикации не содержат в запросах и ответах ни массивов, ни массивов структур - это уже точно не ко мне.
сомневаюсь что причина лишь в одном только протоколе
Сомневаться - дело хорошее. Плохо, когда человек при этом ничего не делает. Хотя найти подтверждения моих слов очень легко. Например в этой публикации в тестах получили разницу в 7-10 раз, что вполне согласуется с моими данными. Код там есть, так что можете проверить сами.
Вы бы тогда скорее снижением объема трафика хвастались - размеры передаваемых данных по бинарному протоколу разумеется заметно меньше.
Во-первых, объем траффика тут тоже играет роль, так как если внутри ЦОД может быть до 400 гигабит на паре двухпортовых адаптеров, то между клиентом и сервером gRPC часто будет десятигигабитка, а порой даже гигабитка. Во-вторых, объем данных - это еще нагрузка на TLS, сериализацию и десериализацию. В-третьих, сам по себе HTTP/2 существенно эффективней HTTP/1.1. Тем более при использовании двунаправленного потокового gRPC и пакетной конвеерной обработки. А у нас запросы в Protobuf на несколько мегабайт с ответами на гигабайт - вполне себе обычны. В REST это было на порядок больше, так как в основном там массивы чисел. В XML-RPC было бы еще в 2-3 раза больше из-за его многословности в массивах.
alex0x08 Автор
23.08.2024 13:00Первое - и есть десериализация. Второе - сериализация. А то что примеры в публикации не содержат в запросах и ответах ни массивов, ни массивов структур - это уже точно не ко мне.
Не хочу уподобляться местным обитателям и придираться к точности терминов, но полагаю что под сериализацией/десериализацией XML подразумевалось использование как минимум DOM и какого-то механизма связывания с объектами языка - DTO, POJO и так далее.
Так вот всего этого тут нет, один только потоковый парсер.
"Массивы и массивы структур" были в тестах, но поскольку никаких проблем замечено не было — убрал.
Для C/C++ версий код был сильно объемнее, по очевидным причинам.
Тем более при использовании двунаправленного потокового gRPC и пакетной конвеерной обработки.
Двунаправленный потоковый вызов процедур? Вы фактически превратили атомарную по своей сути систему вызовов во что-то вроде видеострима. Полагаю следующим этапом придется добавлять какой-то контроль целостности данных, повторную отправку, докачку при обрыве и все прочие подобные радости.
А у нас запросы в Protobuf на несколько мегабайт с ответами на гигабайт - вполне себе обычны.
Не очень понимаю зачем все это было делать в рамках RPC протокола.
Если возникает большой объем передаваемых данных - существует вполне стандартный механизм для их передачи: через отдельную ссылку на скачивание.
В передаваемом ответе фигурирует лишь ссылка на скачивание, само скачивание клиент осуществляет отдельным запросом вне логики RPC.
Это вообщем-то стандартная практика даже для SOAP и REST.
В REST это было на порядок больше, так как в основном там массивы чисел.
Опять же не зная задачи могу ошибиться, но есть банальный работающий способ для такого: формируется текстовый файл с такими массивами чисел, который затем сжимается в архив и передается в виде бинарного файла.
Цифры сжимаются очень хорошо, на больших объемах будет существенный выигрыш.
С другой стороны происходит распаковка и чтение.
Если данных много то лучше формировать CSV с построчной разбивкой а не пытаться "сериализовать" все сразу и целиком.
ptr128
23.08.2024 13:00+2полагаю что под сериализацией/десериализацией XML подразумевалось
После такого, я уже точно не вижу смысла в продолжнии дискуссии. Почитайте, хотя бы в тут. Любой RPC вызов - это сериализация объектов на одной стороне и десериализация на другой.
Остальное даже комментировать не буду, так как не вижу понимания того, что такое конвейер, протокол, контракт, их версии и для чего они нужны.
alex0x08 Автор
23.08.2024 13:00Ссылка на википедию после всего описанного? Серьезно?
Вам правда что-ли какие-то там лайки важнее общения по делу?
rukhi7
23.08.2024 13:00+1Собственно вся библиотека реализующая как клиентскую так и серверную стороны XML-RPC состоит из трех файлов
И это на фоне кучи умных слов и абревиатур. Если всё так просто то в чем же была сложность???
А ведь чтобы сформулировать решение надо сформулировать в чем же была проблема, но вместо этой формулировки нам придётся довольствоваться эмоциями автора которые в общем то сводятся к восклицанию:
Ах как это сложно <тут названия - RPC,...>
И предлагается решение в виде:
А вот есть библиотека, с ней это (то почему сложно, и про что мы ни слова не увидели) становится не сложно!
Ну круто же!
Но по моему то что было сложным так и осталось нерешённым, но это совсем другая история, видимо.
alex0x08 Автор
23.08.2024 13:00И это на фоне кучи умных слов и абревиатур. Если всё так просто то в чем же была сложность???
В чем сложность вызова удаленных процедур?
Сейчас расскажу.
Допустим у вас клиент и сервер, с которого вызываются процедуры написаны на разных технологиях, что чаще всего и бывает.
Один умеет работать с Unicode, другой нет.
В этом месте JSON по-хорошему заканчивается, поскольку по стандарту любая клиентская реализация JSON должна поддерживать юникод.
А еще есть различия в типах данных, когда у вас один технологический стек считает что строка это набор символов, а другой что строка это массив байт. Где-то есть отдельный булевый тип, где-то его нет и нужно передавать 1\0. Есть отличия в трактовке дат, меток времени и длинных чисел.
Именно из‑за такого количества сложностей большинство RPC фреймворков объемные и сложные — они скрывают все эти детали под капотом и вы просто не задумываетесь о них при работе.
До судного дня Х, когда они вылезают на поверхность.
Фишка XML-RPC как протокола как раз и заключается в том что он не пытается скрывать всю эту сложность, четко обозначая минимально поддерживаемый набор типов.
Поэтому он до сих пор жив и используется, хотя огромная корпорация пытается его задушить с 1998го года.
Как-то так.
rukhi7
23.08.2024 13:00Допустим у вас клиент и сервер, с которого вызываются процедуры написаны на разных технологиях, что чаще всего и бывает.
Ну я где-то видел про то что клиент как то должен получить список функций которые он может вызывать, в первую очередь.
Представьте что этот список функций у клиента устарел, а вы начнёте решать проблемы Юникода, булевых типов, ... Вот чем и главное когда это закончится? Как вы думаете?
alex0x08 Автор
23.08.2024 13:00Скорее всего вы описываете Web Services Discovery — это такая попытка создания «DNS для вебсервисов», с моей точки зрения не очень удачная.
что этот список функций у клиента устарел, а вы начнёте решать проблемы
Куда чаще проблема заключается не в устаревании списка методов, а в изменении их сигнатуры без предупреждения.
Разумеется только техническими средствами это не решить, такое считается за ошибку и работа останавливается.
rukhi7
23.08.2024 13:00Разумеется только техническими средствами это не решить
В смысле не решить, не возможно контролировать? А если версии интерфейсов ввести например?
изменении их сигнатуры без предупреждения.
Вроде как изменение сигнатуры это и есть одно из проявлений устаревшего или просто не валидного интерфейса но есть и много других. Как раз в этом разнообразии и заключается одна сторона сложности (но только одна), на сколько я знаю.
alex0x08 Автор
23.08.2024 13:00В смысле не решить, не возможно контролировать?
В смысле что такое состояние нестыковки клиентского и сервисного интерфейсов в рамках одной системы (те когда вы контролируете и клиентскую и серверную сторону) должно трактоваться как серьезный системный сбой.
На практике это чаще всего будет означать что кто-то из участников процесса выложил не ту сборку или релиз оказался битым, а значит несовпадение сигнатуры вызова лишь последствия а не причина.
А если версии интерфейсов ввести например?
Если кратко любые версии интерфейсов не более чем иллюзия, не дающая никакой защиты от внутренних изменений.
Если длинно то вот моя статья на эту тему.
rukhi7
23.08.2024 13:00Скорее всего вы описываете Web Services Discovery — это такая попытка создания
Нет одно время я очень плотно работал с DirectX -ом и с другими дИректами. Я точно знаю что версии интерфейсов имеют смысл так как я их очень эффективно использовал. Это не значит что ваша статья плохая, но по моему это взгляд только с одной стороны.
Я бы тоже сам до этого никогда не дошёл, мне повезло поработать с системой где это было сделано до конца и правильно во всём множестве аспектов и ещё и с классной документацией.
Там много аспектов и надо ничего не упустить. В этом сложность.
alex0x08 Автор
23.08.2024 13:00Нет одно время я очень плотно работал с DirectX -ом и с другими дИректами. Я точно знаю что версии интерфейсов имеют смысл
Объясняю: две разных версии DirectX это две разных не связанных между собой сущности - две физически разных и независимых друг от друга библиотеки. Т.е одну версию можно удалить и другая от этого не сломается.
Две версии API вебсервиса это на самом деле одна сущность — одна программа (если так будет понятней), внутри которой на одном из уровней обязательно происходит смешение.
Даже если за разные версии API отвечают физически разные сервисы — данные у них все равно будут общие: общая база, общие сервисы, общее файловое хранилище и тд.
Полное разделение версий одного сервиса вплоть до данных я на практике не видел ни разу.
Так что две версии API это на самом деле две головы одной и той же гидры, только вместо отрастания по-новой (как в сказке), отрубание одной приведет к падению всего сервиса.
yokotoka
23.08.2024 13:00+1Почему не взяли gRPC? Там и библиотеки подо всё что только возможно из языков и эффективный быстрый бинарный формат protobuf с версионированием, и двунаправленные стримы данных... зачем в 2024 текстовый XML с диким оверхедом по процу и памяти при преобразовании бинарных данных в какой-нибудь base64 и неэффективному увеличению объема на треть в секциях CDATA?
Thrift у Apache ещё есть, например
Вы на них не обратили внимания?
olku
Json-rpc ещё проще, лапши меньше.
alex0x08 Автор
Вот вам несколько выдержек из официальной спецификации на JSON, вдруг решите свой парсер написать.
Про юникод:
Про цифры:
Про совместимость:
Так что JSON сложнее и объемнее чем кажется, причем любая его реализация фактически подразумевает undefined behavior, поскольку стандарт описывает и гарантирует лишь синтаксис.
Что касается упомянутого протокола JSON-RPC, то помимо описанных выше проблем есть еще вот такое:
И ниже про обработку ответа:
Получается что никакая реализация JSON-RPC не может быть stateless, поскольку эти самые id нужно где-то хранить.
Эта на первый взгляд мелочь обязательно всплывет при попытке масштабирования такого сервиса.
olku
Не хочу спорить. Мне кажется, вы не применяли на практике. Вручную не надо ничего парсить и хранить айди. Ну только если очень хочется.
alex0x08 Автор
Если уж в описании самого протокола (цитату из которого я приводил выше) написано, что для запроса и ответа нужно формирование ID - тут никаких споров быть не может.
Вот для примера реализация JSON‑RPC на Java, в этом месте происходит чтение ID запроса, вот тут и ниже по коду он используется для формирования ответа.
Разумеется за все это отвечает фреймворк с реализацией JSON-RPC а не клиентский код, так что "Вручную не надо ничего парсить и хранить айди" действительно не надо - это сделают за вас и в обязательном порядке.
olku
Там "фреймворк" на три дтошки. В общем, если будете реализовывать RPC на практике, не забудьте про альтернативу XML. Она проще.
alex0x08 Автор
Этот "фреймворк на три дтошки" мало того что тянет за собой парсер JSON в виде зависимости, так еще и накладывает определенные обязательства по разработке, одно из которых я описал выше.
Если у вас современная система, веб и популярный язык вроде Java/C# в JSON RPC нет никакого смысла — возьмите обычный REST, для которого JSON объекты лишь один из доступных типов данных.
Самый обычный REST позволит например часть данных брать из HTTP-заголовков, использовать все методы HTTP а один только POST, загрузку/скачивание бинарных файлов без цирка с сериализацией.
Зачем связывать себя по рукам и ногам жесткой схемой RPC просто так?
Иначе будет как у товарища выше по переписке, где он гоняет гигабайты данных через RPC протокол и жалуется на производительность.
Мне лично XML-RPC нужен в виде универсального инструмента для сложных и неадекватных условий, разумеется я не применяю его для всего вообще и например корпоративную разработку веду на самом обычном Java + Spring.