Оптимизация хранения и передачи XML с помощью Efficient XML Interchange (EXI)
опубликовано: ⏰ 10Небольшая статья о малоизвестном, а может и нет, формате представления XML. Давным-давно, когда мобильные устройства были маленькими и слабенькими, искали способ сэкономить на трафике и вычислительных ресурсах при передаче данных между клиентом и сервером с использованием XML. Один из вариантов – использование компактного представления в виде Efficient XML Interchange (EXI).
Введение
Сейчас есть достаточно много форматов данных для компактного хранения и передачи данных. Наиболее распространённый из них это JSON, но он не всегда подходит для задач где требуется строгая типизация или валидация данных. В этом случае можно использовать XML, который тоже является человекочитаемым форматом данных. Но несмотря на гибкость и наличие различных стандартов (XSD, XPath, XSLT), он не очень эффективен по размеру, потреблению ресурсов (потребление памяти, если не используется SAX) и по скорости парсинга. Вот тут и можно применить EXI.
Что не так с XML?
XML – это текстовый формат, который может быть легко прочитан человеком. Однако при работе с большими объёмами данных он может быть неэффективным по следующим причинам:
- Неоптимальный размер.
- Сложность парсинга.
- Проблема типизации.
При этом у него есть и плюсы:
- Простота чтения и понимания структуры данных, пускай и громоздкой в некоторых случаях.
- Поддержка XML Schema Definition (XSD) для описания структуры данных и валидации.
- Поддержка XPath и XSLT для поиска и преобразования данных.
Неоптимальный размер
Текстовые данные занимают больше места, чем двоичные. Да и сам XML может быть довольно «многословным» по сравнению с JSON (бывает и наоборот).
Например,
<sensor>
<id>device-001</id>
<temperature unit="C">24.5</temperature>
<timestamp>2026-03-26T10:00:00Z</timestamp>
</sensor>В текстовом виде это событие занимает около 136 байт. Из них полезная нагрузка (ID, значение, время) составляет примерно 30% (39 байт). Остальное – это имена тегов, атрибуты, скобки и служебные символы. Если вы передаёте такие данные каждые 5 секунд с миллиона устройств, избыточные 97 байт превращаются в гигабайты лишнего трафика. В условиях ограниченных каналов связи с плохой связью, например, когда данные передаются через сотовую сеть или спутниковые каналы, это может быть критично. Ситуация ещё более усугубляется при передаче больших объёмов данных.
Сложность парсинга
Парсинг XML более затратен по ресурсам, чем при работе с двоичными данными. Парсер должен читать поток символов, распознавать лексемы, строить дерево модели документа (DOM) или обрабатывать события (SAX), проверять кодировки (UTF-8, UTF-16). Это процесс, интенсивно использующий процессор. На слабых ЦПУ полный XML-парсер может занимать значительную часть доступных ресурсов.
Приведу пример – он очень старый; сейчас это скорее справедливо для микроконтроллёров. Мобильное приложение было написано для PDA (Windows CE) на JavaME и для коммуникации с сервером использовался протокол на базе XML. Сообщения не всегда доходили из-за нестабильной связи, и иногда само приложение падало от переполнения памяти. Приходилось искать возможные утечки памяти и править в VM (на тот момент уже заброшенной), и пересобирать свою версию, что было сомнительным удовольствием.
Проблема типизации
XML по своей природе текстовый. Значение <temperature>24.5</temperature> для парсера – это строка. Преобразование в число с плавающей точкой происходит на уровне приложения. В бинарных протоколах тип данных закодирован явно, что исключает ошибки преобразования и ускоряет обработку. Частично это может компенсироваться использованием XSD с валидацией входных данных.
EXI (Efficient XML Interchange)
EXI (Efficient XML Interchange) – это стандарт W3C (рекомендация W3C EXI 1.0). Его основная идея заключается в кодировании XML в компактное бинарное представление, которое значительно уменьшает размер данных и ускоряет обработку. EXI использует таблицы сопоставления (словарь) и кодирование с фиксированной длиной. Полное описание можно найти в стандарте W3C EXI 1.0.
По сути, EXI оперирует потоком байтов (аналог SAX), а не XML-документом, со следующими событиями:
Start DocumentEnd DocumentStart ElementEnd ElementAttributeCharactersNamespace DeclarationCommentProcessing InstructionDOCTYPEEntity ReferenceSelf Contained
Может работать в двух режимах:
- schema-less (без схемы) – кодировщик и декодировщик не имеют доступа к XSD-схеме. Они работают только с потоком событий. Имена элементов и атрибутов передаются в потоке, но сжимаются через встроенный словарь. Часто встречающиеся строки заменяются короткими индексами.
- schema-informed (со схемой) – обе стороны (клиент и сервер) имеют идентичную XSD-схему. На основе схемы строится грамматика (EXI Grammar). Грамматика определяет структуру документа. Поскольку она известна, имена элементов не передаются вообще. Вместо тега
<temperature>передается целочисленный индекс из грамматики (например,5).
| Критерий | schema-less | schema-informed |
|---|---|---|
| Типизация | Всё приводится к строке, если не указан другой тип. | Если схема говорит, что элемент temperature имеет тип xsd:float, значение кодируется сразу в бинарном формате IEEE 754, без преобразования в строку “24.5”. |
| Плюсы | Гибкость. Можно передавать любой XML, даже если схема неизвестна получателю заранее | Максимальная компактность (уменьшение в несколько раз), максимальная скорость, встроенная валидация. |
| Минусы | Размер пакета больше, чем в режиме со схемой. Нет строгой валидации типов на уровне транспорта. | Жёсткая привязка версий. Если схема изменится, старые клиенты не смогут прочитать новые сообщения (если не предусмотрены механизмы версионирования). |
| Сжатие | Обычно даёт уменьшение размера в 2–3 раза по сравнению с текстовым XML. | Ещё большая степень сжатия. |
Механизмы сжатия
Использование грамматик и битовая упаковка. Детали можно посмотреть в официальной спецификации, где всё описано довольно подробно. Сам подход схож с алгоритмами сжатия со словарями (например, LZ77), но с использованием EXI-грамматик и специальных правил для упаковки битов. Практически всегда используется в паре с EXI Schema-aware, так как он позволяет использовать схемы для определения типов данных и упрощения кодирования.
Например, в текстовом XML атрибут unit="C" занимает 9 байт. В EXI schema-aware (в числах могу ошибаться, но смысл, думаю, понятен):
- Факт наличия атрибута
unitкодируется 1 битом (флаг). - Значение
"C"может быть закодировано как индекс перечисления (если схема определяетunitкак enum), занимая 2 бита.
В итоге 3 бита против 72 бит.
Дополнительно могут использоваться классические алгоритмы сжатия, такие как Deflate (используемый в ZIP/GZIP), ко всему потоку EXI. Это дает максимальное сжатие, но требует ресурсов CPU.
Сравнение с другими форматами
| Характеристика | XML (Text) | JSON | Protobuf / gRPC | EXI (Schema-aware) | CBOR / MessagePack |
|---|---|---|---|---|---|
| Читаемость | Высокая | Высокая | Низкая (бинарный) | Низкая (бинарный) | Низкая (бинарный) |
| Размер (Payload) | 100% (база) | ~60-70% | ~20-30% | ~10-20% | ~20-30% |
| Скорость парсинга | Низкая | Средняя | Очень высокая | Очень высокая | Высокая |
| Схема / Контракт | XSD (строгая) | JSON Schema (слабая) | .proto (строгая) | XSD (строгая) | CDDL / Нет |
| Валидация | Встроенная | Внешняя | Встроенная | Встроенная при декодировании | Нет |
| Поддержка типов | Строгая (через XSD) | Слабая (все строки/числа) | Строгая | Строгая (наследует XSD) | Базовая |
| Версионирование | Сложное (Namespaces) | Гибкое | Сложное (необязательные поля) | Сложное (требует совместимости схем) | Гибкое |
| Экосистема | Огромная | Огромная | Большая (Google) | Нишевая (IoT, Auto, Energy) | Небольшая |
| Инструменты | Много | Много | Много | Ограничено (Java, C, С#, Python) | Много |
Доступные библиотеки для использования
Реализации стандарта доступны для некоторых популярных языков программирования:
- Java – наиболее зрелая реализация. Библиотека Exificient является эталонной. Она интегрируется с JAXB (Java Architecture for XML Binding).
- C/C++, .NET – библиотека OpenEXI.
- Python – библиотека
exiили pyexi. - JavaScript – есть реализации в Exificient.
Пример работы в Java
Далее небольшой пример на Java с XML и POJO классами, сгенерированными через JAXB. За исходный XML взят тот, что был в начале статьи (<sensor/>).
Задача – преобразовать POJO в формат EXI для хранения или передачи и получить из него обратно POJO.
Зависимости (Maven)
<dependencies>
<dependency>
<groupId>com.siemens.ct.exi</groupId>
<artifactId>exificient</artifactId>
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.5</version>
</dependency>
</dependencies>Преобразование POJO в EXI через JAXB
private static void marshal() throws Exception {
final var sensor = new Sensor();
sensor.setId("device-001");
sensor.setTemperature(24.5);
sensor.setTimestamp("2026-03-26T10:00:00Z");
try (var osEXI = new FileOutputStream(EXI_FILE_JAXB)) {
final var exiResult = new EXIResult(DefaultEXIFactory.newInstance());
exiResult.setOutputStream(osEXI);
final var factory = JAXBContext.newInstance(sensor.getClass());
final var marshaller = factory.createMarshaller();
marshaller.marshal(sensor, exiResult);
}
final var exiSize = Files.size(Paths.get(EXI_FILE_JAXB));
System.out.printf("JAXB (serialize): EXI size: %d bytes%n", exiSize);
}Преобразование EXI в POJO через JAXB
private static void unmarshal() throws Exception {
final var exiSource = new EXISource(DefaultEXIFactory.newInstance());
exiSource.setInputSource(new InputSource(EXI_FILE_JAXB));
final var factory = JAXBContext.newInstance(Sensor.class);
final var unmarshaller = factory.createUnmarshaller();
final var sensor = (Sensor) unmarshaller.unmarshal(exiSource);
System.out.printf("JAXB (deserialize): %s%n", sensor.toString());
}Прямое преобразование XML в EXI
private static void encode() throws IOException, EXIException, SAXException {
try (var osEXI = new FileOutputStream(EXI_FILE)) {
final var exiResult = new EXIResult(DefaultEXIFactory.newInstance());
exiResult.setOutputStream(osEXI);
// java.xml
final var xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.setContentHandler(exiResult.getHandler());
xmlReader.parse(XML_FILE); // parse XML input
}
final var xmlSize = Files.size(Paths.get(XML_FILE));
final var exiSize = Files.size(Paths.get(EXI_FILE));
System.out.printf("XML size: %d bytes%n", xmlSize);
System.out.printf("EXI size: %d bytes%n", exiSize);
System.out.printf("Ratio: %.2f%%%n", (double) exiSize / xmlSize * 100);
/*
Encode:
XML size: 136 bytes
EXI size: 81 bytes
Ratio: 59.56%
*/
}Финальный размер EXI более чем вполовину меньше оригинала XML.
Прямое преобразование EXI в XML
private static void decode() throws Exception {
final var exiSource = new EXISource(DefaultEXIFactory.newInstance());
exiSource.setInputSource(new InputSource(EXI_FILE));
// java.xml
final var transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(exiSource, new StreamResult(XML_FILE_DECODE));
final var xmlSize = Files.size(Paths.get(XML_FILE_DECODE));
final var exiSize = Files.size(Paths.get(EXI_FILE));
System.out.println("Decode:");
System.out.printf("\t XML size: %d bytes%n", xmlSize);
System.out.printf("\t EXI size: %d bytes%n", exiSize);
System.out.printf("\t Ratio: %.2f%%%n", (double) xmlSize / exiSize * 100);
/*
Decode:
XML size: 211 bytes
EXI size: 81 bytes
Ratio: 260.49%
*/
}ℹ️ Важно
При использовании JAXB бизнес-логика не меняется. Работа по-прежнему осуществляется через JAXBContext и с обычными Java-объектами.
Риски использования EXI
Наверное, самая большая проблема – “нечитаемость” формата. Сообщения или документы уже нельзя просто открыть в текстовом редакторе. Как решение, можно сделать инструмент для отладки с декодированием EXI по аналогии с exificient-gui. Например, это может быть необходимо для логирования в тестовых средах. На промышленных это может быть отключено из соображений безопасности и производительности. Если этого не сделать, то увеличивается время на диагностику инцидентов (MTTR).
Возможны проблемы с версионированием. В JSON можно просто добавить новое поле, и старые клиенты его проигнорируют, если десериализация настроена на такое поведение. В EXI со схемой изменение схемы может сломать совместимость. Как можно обойти:
- Использовать
xsd:anyдля расширяемых частей документа. - Никогда не удалять поля из схемы, только делать их опциональными (как и в protobuf).
- Использовать пространства имён (как с обычным XML) для версионирования (например,
v1,v2).
Заключение
Далеко не всегда есть смысл использовать EXI при наличии достаточных вычислительных ресурсов и хорошей пропускной способности сети. Если требуется снизить трафик и необходимое место под хранение данных, то даже обычные алгоритмы сжатия текста семейства PPM* могут быть более эффективны.
В приложениях tekBlueprint очень много данных хранятся в формате XML, и документы могут быть довольно объёмными. Компоненты между приложениями и tekBlueprint.kbase также передаются в виде XML. Поэтому EXI рассматривал в качестве промежуточного формата хранения.
ℹ️ Примечание
Пример проекта доступен в GitHub.