XML-RPC: вызываем все, везде и сразу
У нас было пятьдесят операционных систем, десяток языков программирования и бесконечное множество библиотек и фреймворков всех сортов и расцветок,
а также кофе, немного времени и щепотка здравого смысла.
Не то чтобы это был необходимый запас для сетевой разработки, но раз уж начал коллекционировать дичь, то сложно остановиться...
Вызовы «взад-назад», туда и обратно
Что вы знаете об удаленных вызовах процедур?
Если вы из нового поколения разработчиков, то врядли слышали даже такой термин — в современных реалиях его заменили вездессущие REST и JSON.
Несмотря на то что REST это по большей части стандарт реализации методов API и не описывает всю конкретику и JSON при этом не единственный возможный формат данных.
Из читателей постарше многие вспомнят монструозный SOAP.
Вспомнят и немедленно вздрогнут — от воспоминаний многочисленных сбоев, генерации гор мусорного кода из метаданных WSDL, различий реализации (Microsoft vs все остальные) и еще всякого такого.
Еще более старые вспомнят COM+ и CORBA — еще более страшные, с еще большим набором проблем и геммороя при использовании.
Но есть кое-что объединяющее все поколения более-менее опытных разработчиков, а именно мысль:
вызывать из одной программы метод другой по сети — п#здец как сложно
Поэтому пытаться делать такое своими руками, без каких-либо фреймворков точно не стоит.
Ну что же, пришло время снова разрушать мифы, внимание на экран:
Всего ~500 строк Java-кода на серверную и клиентскую части и вы спокойно можете вызывать методы на любой ОС и в любом окружении.
Как обычно без каких-либо внешних библиотек и фреймворков.
Ну почему мне никто не рассказывал о таком во времена бессонных ночей #бли с проклятым SОАРом до кровавых мозолей.
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.
Да, теперь вы тоже знаете как оно появилось на свет.
Позднее Майкрософт начала широко рекламировать и внедрять SOAP, а изначальный XML-RPC был отвергнут
Выкупить успешный проект конкурента, скатить его в говно и закрыть — Microsoft проворачивал такое задолго до Гугла и эпохи стартапов.
Но увы (для 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, причем минимально возможных размеров и разумеется без всяких зависимостей.
Исходный код, вместе с примерами использования выложен на 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); } } ..
Тут происходит сопоставление названия тега к поддерживаемому типу из перечисления.
Например при разборе вот такого значения:
<value><i4>41</i4></value>
будет установлен тип DATA_TYPES.Integer.
Само же значение будет преобразовано согласно типу чуть ниже:
... 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; } } ..
Достойны упоминания еще работа с пулом исполнителей (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); ..
Если такая ошибка есть — 'исполнитель' в пул не возвращается.
Вызов POJO методов
Вторым интересным моментом в реализации является сам вызов метода через 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()); ..
Он разворачивает примитивные типы аргументов в их wrapped-версии:
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 Bean и CDI и все что угодно.
Вызов со стороны клиента выглядит так:
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 был бы неполным без примеров вызовов из других языков.
Ниже я покажу как выглядят вызовы к тестовому серверу XML-RPC из разных языков.
Не забываем про 500 строк исходного кода и разработку полностью с нуля.
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 присутствует во всех популярных дистрибутивах Linux:
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." }
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, которая присутствует в большинстве дистрибьютивов и ОС:
#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; }
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 lt; -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)
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, реализация достаточно сложная:
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)); });
{ "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 оказались немного заброшенными, хотя и рабочими.
Реализация с помощью библиотеки 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(quot;response, sum: {res["sum"]}, " + quot;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, несмотря на общие тенденции и «модность».
Очень простой протокол с фиксированными типами данных и минимальной реализацией способен решить очень много практических проблем, без погружения в особенности сериализации сложных типов данных, работы с датами или дробными числами.