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

Слева направо: клиент на 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

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

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

Комментарии (28)


  1. olku
    23.08.2024 13:00
    +1

    Json-rpc ещё проще, лапши меньше.


    1. alex0x08 Автор
      23.08.2024 13:00

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

      Про юникод:

      JSON syntax describes a sequence of Unicode code points. JSON also depends on Unicode in the hex numbers used in the \u escapement notation

      Про цифры:

      JSON is agnostic about the semantics of numbers. In any programming language, there can be a variety of number types of various capacities and complements, fixed or floating, binary or decimal. That can make interchange between different programming languages difficult. JSON instead offers only the representation of numbers that humans use: a sequence of digits.

      Про совместимость:

      It is expected that other standards will refer to this one, strictly adhering to the JSON syntax, while imposing semantics interpretation and restrictions on various encoding details. Such standards may require specific behaviours. JSON itself specifies no behaviour.

      Так что JSON сложнее и объемнее чем кажется, причем любая его реализация фактически подразумевает undefined behavior, поскольку стандарт описывает и гарантирует лишь синтаксис.

      Что касается упомянутого протокола JSON-RPC, то помимо описанных выше проблем есть еще вот такое:

      All transfer types are single objects, serialized using JSON.[1] A request is a call to a specific method provided by a remote system. It can contain three members:

      ..

      • id - A string or non-fractional number used to match the response with the request that it is replying to.[2] This member may be omitted if no response should be returned.[3]

      И ниже про обработку ответа:

      The receiver of the request must reply with a valid response to all received requests. A response can contain the members mentioned below.

      ..

      id - The id of the request it is responding to.

      Получается что никакая реализация JSON-RPC не может быть stateless, поскольку эти самые id нужно где-то хранить.

      Эта на первый взгляд мелочь обязательно всплывет при попытке масштабирования такого сервиса.


      1. olku
        23.08.2024 13:00

        Не хочу спорить. Мне кажется, вы не применяли на практике. Вручную не надо ничего парсить и хранить айди. Ну только если очень хочется.


        1. alex0x08 Автор
          23.08.2024 13:00

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

          Вот для примера реализация JSON‑RPC на Java, в этом месте происходит чтение ID запроса, вот тут и ниже по коду он используется для формирования ответа.

          Разумеется за все это отвечает фреймворк с реализацией JSON-RPC а не клиентский код, так что "Вручную не надо ничего парсить и хранить айди" действительно не надо - это сделают за вас и в обязательном порядке.


          1. olku
            23.08.2024 13:00

            Там "фреймворк" на три дтошки. В общем, если будете реализовывать RPC на практике, не забудьте про альтернативу XML. Она проще.


            1. alex0x08 Автор
              23.08.2024 13:00

              Этот "фреймворк на три дтошки" мало того что тянет за собой парсер JSON в виде зависимости, так еще и накладывает определенные обязательства по разработке, одно из которых я описал выше.

              Если у вас современная система, веб и популярный язык вроде Java/C# в JSON RPC нет никакого смысла — возьмите обычный REST, для которого JSON объекты лишь один из доступных типов данных.

              Самый обычный REST позволит например часть данных брать из HTTP-заголовков, использовать все методы HTTP а один только POST, загрузку/скачивание бинарных файлов без цирка с сериализацией.

              Зачем связывать себя по рукам и ногам жесткой схемой RPC просто так?

              Иначе будет как у товарища выше по переписке, где он гоняет гигабайты данных через RPC протокол и жалуется на производительность.

              Мне лично XML-RPC нужен в виде универсального инструмента для сложных и неадекватных условий, разумеется я не применяю его для всего вообще и например корпоративную разработку веду на самом обычном Java + Spring.


  1. ptr128
    23.08.2024 13:00

    Странно видеть описание протокола четвертьвековой давности. Например, gRPC у него выигрывает по всем статьям.


    1. alex0x08 Автор
      23.08.2024 13:00

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


      1. ptr128
        23.08.2024 13:00

        Не понял, какие проблемы? Приведите хотя бы один пример, где невозможно заменить XML-RPC на gRPC.

        на десяти разных ОС, включая сильно устаревшие

        Вы знаете платформу, для которой нет C/C++ компилятора или бекенда для LLVM?

        диких языков

        В том же FORTRAN или COBOL куда проще реализовать вызов через gRPC, чем сериализацию и десериализацию XML.


        1. alex0x08 Автор
          23.08.2024 13:00

          Навскидку:

          Lisp, TCL, окружения Linux старше 15 лет, практически все коммерческие Unix, промавтоматика, встраиваемые системы, где как раз имеет смысл миниатюризация всех используемых библиотек. Устаревшие Windows.

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


          1. ptr128
            23.08.2024 13:00

            Lisp, TCL, окружения Linux старше 15 лет, практически все коммерческие Unix, промавтоматика, встраиваемые системы

            Сами погуглить не могли?

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

            Кросскомпиляцию никто не отменял. А на K&R С, не говоря уже об упомянутых выше FORTRAN и COBOL, сериализацию и десериализацию XML сложнее будет реализовать, чем gRPC. Что я тоже показал выше.


            1. alex0x08 Автор
              23.08.2024 13:00

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

              То что вы «нагуглили» это разумеется замечательно, но это не истина в первой инстанции.

              Если вы заметили, половина данной статьи состоит из конкретных примеров реально работающего кода на разных языках — все это я на самом деле собрал и запустил.

              По этой причине могу подтвердить что оно действительно работает на момент написания статьи.

              Если вы в силах повторить подобное для gRPC — пишите статью, коллеги оценят.

              P.S.

              Я не выступаю против gRPC, да и против вас лично ничего не имею, просто не хочу скатывания разумной технической дискуссии в очередной бессмысленный срач.


              1. ptr128
                23.08.2024 13:00

                То что вы «нагуглили» это разумеется замечательно, но это не истина в первой инстанции.

                Так как большинство ссылок на github, истинность проверяется элементарно. А ссылки на сайты производителей коммерческих продуктов, вполне себе валидные по определению.

                По этой причине могу подтвердить что оно действительно работает на момент написания статьи.

                На платформах, не поддерживающих Java, вроде упомянутого выше ESP32?

                Я просил пример, где, по Вашему мнению, gRPC не применим. Вы же выдали целый ворох групп. Вы действительно ожидали, что для каждой из систем, покрываемых этими общими группами, я приведу работающий код?

                не хочу скатывания разумной технической дискуссии в очередной бессмысленный срач.

                Простите, но уже скатились, предложив сериализацию и десериализацию XML на встраиваемых системах, где каждый байт оперативки на счету и CPU слабые. Надеюсь, понимаете, что gRPC требует на порядок меньше оперативной памяти и в разы меньше процессорных ресурсов, чем XML-RPC. Я знаю о чем говорю, так как когда мы переходили с REST на gRPC, то получили прирост производительности, в среднем, в 8-9 раз.

                На этом предлагаю закончить.


                1. alex0x08 Автор
                  23.08.2024 13:00

                  Я вообще не понимаю причем тут сериализация, ее если что нет ни в моей реализации ни в большинстве использованных клиентских библиотек.

                  Там просто парсер XML-запроса и генерация ответа в виде строки.

                  И то и другое отлично ограничивается по используемым ресурсам.

                  так как когда мы переходили с REST на gRPC, то получили прирост производительности, в среднем, в 8-9 раз.

                  Не знаю что у вас за случай, но очень сомневаюсь что причина лишь в одном только протоколе.

                  Вы бы тогда скорее снижением объема трафика хвастались - размеры передаваемых данных по бинарному протоколу разумеется заметно меньше.


                  1. ptr128
                    23.08.2024 13:00

                    Там просто парсер XML-запроса и генерация ответа в виде строки.

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

                    сомневаюсь что причина лишь в одном только протоколе

                    Сомневаться - дело хорошее. Плохо, когда человек при этом ничего не делает. Хотя найти подтверждения моих слов очень легко. Например в этой публикации в тестах получили разницу в 7-10 раз, что вполне согласуется с моими данными. Код там есть, так что можете проверить сами.

                    Вы бы тогда скорее снижением объема трафика хвастались - размеры передаваемых данных по бинарному протоколу разумеется заметно меньше.

                    Во-первых, объем траффика тут тоже играет роль, так как если внутри ЦОД может быть до 400 гигабит на паре двухпортовых адаптеров, то между клиентом и сервером gRPC часто будет десятигигабитка, а порой даже гигабитка. Во-вторых, объем данных - это еще нагрузка на TLS, сериализацию и десериализацию. В-третьих, сам по себе HTTP/2 существенно эффективней HTTP/1.1. Тем более при использовании двунаправленного потокового gRPC и пакетной конвеерной обработки. А у нас запросы в Protobuf на несколько мегабайт с ответами на гигабайт - вполне себе обычны. В REST это было на порядок больше, так как в основном там массивы чисел. В XML-RPC было бы еще в 2-3 раза больше из-за его многословности в массивах.


                    1. alex0x08 Автор
                      23.08.2024 13:00

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

                      Не хочу уподобляться местным обитателям и придираться к точности терминов, но полагаю что под сериализацией/десериализацией XML подразумевалось использование как минимум DOM и какого-то механизма связывания с объектами языка - DTO, POJO и так далее.

                      Так вот всего этого тут нет, один только потоковый парсер.

                      "Массивы и массивы структур" были в тестах, но поскольку никаких проблем замечено не было — убрал.

                      Для C/C++ версий код был сильно объемнее, по очевидным причинам.

                      Тем более при использовании двунаправленного потокового gRPC и пакетной конвеерной обработки.

                      Двунаправленный потоковый вызов процедур? Вы фактически превратили атомарную по своей сути систему вызовов во что-то вроде видеострима. Полагаю следующим этапом придется добавлять какой-то контроль целостности данных, повторную отправку, докачку при обрыве и все прочие подобные радости.

                      А у нас запросы в Protobuf на несколько мегабайт с ответами на гигабайт - вполне себе обычны.

                      Не очень понимаю зачем все это было делать в рамках RPC протокола.

                      Если возникает большой объем передаваемых данных - существует вполне стандартный механизм для их передачи: через отдельную ссылку на скачивание.

                      В передаваемом ответе фигурирует лишь ссылка на скачивание, само скачивание клиент осуществляет отдельным запросом вне логики RPC.

                      Это вообщем-то стандартная практика даже для SOAP и REST.

                      В REST это было на порядок больше, так как в основном там массивы чисел.

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

                      Цифры сжимаются очень хорошо, на больших объемах будет существенный выигрыш.

                      С другой стороны происходит распаковка и чтение. 

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


                      1. ptr128
                        23.08.2024 13:00
                        +1

                        полагаю что под сериализацией/десериализацией XML подразумевалось

                        После такого, я уже точно не вижу смысла в продолжнии дискуссии. Почитайте, хотя бы в тут. Любой RPC вызов - это сериализация объектов на одной стороне и десериализация на другой.

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


                      1. alex0x08 Автор
                        23.08.2024 13:00

                        Ссылка на википедию после всего описанного? Серьезно?

                        Вам правда что-ли какие-то там лайки важнее общения по делу?


  1. rukhi7
    23.08.2024 13:00
    +1

    Собственно вся библиотека реализующая как клиентскую так и серверную стороны XML-RPC состоит из трех файлов

    И это на фоне кучи умных слов и абревиатур. Если всё так просто то в чем же была сложность???

    А ведь чтобы сформулировать решение надо сформулировать в чем же была проблема, но вместо этой формулировки нам придётся довольствоваться эмоциями автора которые в общем то сводятся к восклицанию:

    Ах как это сложно <тут названия - RPC,...>

    И предлагается решение в виде:

    А вот есть библиотека, с ней это (то почему сложно, и про что мы ни слова не увидели) становится не сложно!

    Ну круто же!

    Но по моему то что было сложным так и осталось нерешённым, но это совсем другая история, видимо.


    1. alex0x08 Автор
      23.08.2024 13:00

      И это на фоне кучи умных слов и абревиатур. Если всё так просто то в чем же была сложность???

      В чем сложность вызова удаленных процедур?

      Сейчас расскажу.

      Допустим у вас клиент и сервер, с которого вызываются процедуры написаны на разных технологиях, что чаще всего и бывает.

      Один умеет работать с Unicode, другой нет.

      В этом месте JSON по-хорошему заканчивается, поскольку по стандарту любая клиентская реализация JSON должна поддерживать юникод.

      А еще есть различия в типах данных, когда у вас один технологический стек считает что строка это набор символов, а другой что строка это массив байт. Где-то есть отдельный булевый тип, где-то его нет и нужно передавать 1\0. Есть отличия в трактовке дат, меток времени и длинных чисел.

      Именно из‑за такого количества сложностей большинство RPC фреймворков объемные и сложные — они скрывают все эти детали под капотом и вы просто не задумываетесь о них при работе.

      До судного дня Х, когда они вылезают на поверхность.

      Фишка XML-RPC как протокола как раз и заключается в том что он не пытается скрывать всю эту сложность, четко обозначая минимально поддерживаемый набор типов.

      Поэтому он до сих пор жив и используется, хотя огромная корпорация пытается его задушить с 1998го года.

      Как-то так.


      1. rukhi7
        23.08.2024 13:00

        Допустим у вас клиент и сервер, с которого вызываются процедуры написаны на разных технологиях, что чаще всего и бывает.

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

        Представьте что этот список функций у клиента устарел, а вы начнёте решать проблемы Юникода, булевых типов, ... Вот чем и главное когда это закончится? Как вы думаете?


        1. alex0x08 Автор
          23.08.2024 13:00

          Скорее всего вы описываете Web Services Discovery — это такая попытка создания «DNS для вебсервисов», с моей точки зрения не очень удачная.

          что этот список функций у клиента устарел, а вы начнёте решать проблемы

          Куда чаще проблема заключается не в устаревании списка методов, а в изменении их сигнатуры без предупреждения.

          Разумеется только техническими средствами это не решить, такое считается за ошибку и работа останавливается.


          1. rukhi7
            23.08.2024 13:00

            Разумеется только техническими средствами это не решить

            В смысле не решить, не возможно контролировать? А если версии интерфейсов ввести например?

            изменении их сигнатуры без предупреждения.

            Вроде как изменение сигнатуры это и есть одно из проявлений устаревшего или просто не валидного интерфейса но есть и много других. Как раз в этом разнообразии и заключается одна сторона сложности (но только одна), на сколько я знаю.


            1. alex0x08 Автор
              23.08.2024 13:00

              В смысле не решить, не возможно контролировать?

              В смысле что такое состояние нестыковки клиентского и сервисного интерфейсов в рамках одной системы (те когда вы контролируете и клиентскую и серверную сторону) должно трактоваться как серьезный системный сбой.

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

              А если версии интерфейсов ввести например?

              Если кратко любые версии интерфейсов не более чем иллюзия, не дающая никакой защиты от внутренних изменений.

              Если длинно то вот моя статья на эту тему.

               


              1. rukhi7
                23.08.2024 13:00

                Скорее всего вы описываете Web Services Discovery — это такая попытка создания

                Нет одно время я очень плотно работал с DirectX -ом и с другими дИректами. Я точно знаю что версии интерфейсов имеют смысл так как я их очень эффективно использовал. Это не значит что ваша статья плохая, но по моему это взгляд только с одной стороны.

                Я бы тоже сам до этого никогда не дошёл, мне повезло поработать с системой где это было сделано до конца и правильно во всём множестве аспектов и ещё и с классной документацией.

                Там много аспектов и надо ничего не упустить. В этом сложность.


                1. alex0x08 Автор
                  23.08.2024 13:00

                  Нет одно время я очень плотно работал с DirectX -ом и с другими дИректами. Я точно знаю что версии интерфейсов имеют смысл

                  Объясняю: две разных версии DirectX это две разных не связанных между собой сущности - две физически разных и независимых друг от друга библиотеки. Т.е одну версию можно удалить и другая от этого не сломается.

                  Две версии API вебсервиса это на самом деле одна сущность — одна программа (если так будет понятней), внутри которой на одном из уровней обязательно происходит смешение.

                  Даже если за разные версии API отвечают физически разные сервисы — данные у них все равно будут общие: общая база, общие сервисы, общее файловое хранилище и тд.

                  Полное разделение версий одного сервиса вплоть до данных я на практике не видел ни разу.

                  Так что две версии API это на самом деле две головы одной и той же гидры, только вместо отрастания по-новой (как в сказке), отрубание одной приведет к падению всего сервиса.


  1. yokotoka
    23.08.2024 13:00
    +1

    Почему не взяли gRPC? Там и библиотеки подо всё что только возможно из языков и эффективный быстрый бинарный формат protobuf с версионированием, и двунаправленные стримы данных... зачем в 2024 текстовый XML с диким оверхедом по процу и памяти при преобразовании бинарных данных в какой-нибудь base64 и неэффективному увеличению объема на треть в секциях CDATA?

    Thrift у Apache ещё есть, например

    Вы на них не обратили внимания?


    1. alex0x08 Автор
      23.08.2024 13:00

      а вы статью точно прочитали?