experiments
July 7

XML-RPC:  вызываем все, везде и сразу


У нас было пятьдесят операционных систем, десяток языков программирования и бесконечное множество библиотек и фреймворков всех сортов и расцветок,
а также кофе, немного времени и щепотка здравого смысла.

Не то чтобы это был необходимый запас для сетевой разработки, но раз уж начал коллекционировать дичь, то сложно остановиться...

Слева направо: клиент на Python, standalone-сервер, клиент на Java, в самом низу - клиент на C++

Вызовы «взад-назад», туда и обратно

Что вы знаете об удаленных вызовах процедур?

Только честно и без гугла?

Если вы из нового поколения разработчиков, то врядли слышали даже такой термин — в современных реалиях его заменили вездессущие 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;
     }
  }
 ..

И.. все.

Это вся обработка 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); 
..

Если такая ошибка есть — 'исполнитель' в пул не возвращается.

Вызов 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;
}

Вот так выглядит 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 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));
});

Готовый 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 оказались немного заброшенными, хотя и рабочими.

Реализация с помощью библиотеки 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, несмотря на общие тенденции и «модность».

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

Пользуйтесь ;)