Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение20 мин
Количество просмотров2.1K

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

Слева направо: клиент на Python, запущенный standalone‑сервер, клиент на Java, в самом низу клиент на C++, слившиеся в едином порыве сетевого вызова.
Слева направо: клиент на Python, запущенный standalone‑сервер, клиент на Java, в самом низу клиент на C++, слившиеся в едином порыве сетевого вызова.

Вызовы туда и обратно

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

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

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

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

Оживляем давно умершеечиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.

Теги:
Хабы:
Всего голосов 9: ↑6 и ↓3+4
Комментарии29

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань