Итак, пришло время вернуться к теме «Yet another unpack»))). А точнее, к теме обработки данных в формате gzip/zlib/deflate. За свою практику мне пришлось всего дважды столкнуться с необходимостью обработки данных, сжатых алгоритмом Deflate. Первый раз случился несколько лет назад, и я тогда воспользовался внешней компонентой, опубликованной здесь, на Инфостарте (//infostart.ru/1c/tools/487987/). Спасибо автору, но, к сожалению, в то время его компонента работала только на 32-разрядной платформе Windows, и пришлось вскоре от нее отказаться, т.к. 32-разрядный сервер 1С нынче мало где встретишь. (Признаюсь, я тогда докатился до того, что написал свою реализацию алгоритма распаковки на языке 1С, работало правильно, но дико медленно, всё-таки 1С – язык не для подобных задач). Второй раз случился недавно, и я решил закрыть этот гештальт окончательно.
Задача имеет следующий вид: на вход поступают данные в формате XML, сжатые в GZIP (ещё и закодированные поверх в Base64, но это пока опустим). Данные имеют табличную форму и их требуется загрузить в таблицу БД (в справочник, в регистр сведений – не суть). Объем данных в поступающем файле довольно большой, и может исчисляться мегабайтами даже в сжатом виде (на самом деле, данные мне приходят не из файла, а из ответа веб-сервиса, но, опять же, не суть).
XML имеет низкую информационную плотность, и прекрасно жмётся более, чем в 10 раз. Будучи распакованным в памяти целиком, он может превратиться в «бомбу» размером в сотню мегабайт, что нанесет ощутимый удар по ресурсам памяти сервера. Конечно, можно было бы выполнить распаковку из файла в файл, а потом уже читать распакованный XML из файла в потоковом режиме, но перекачивать десятки мегабайт через дисковую память – не очень красивое решение с точки зрения производительности, да и вообще. Хотелось бы реализовать прямую распаковку в памяти, но в потоковом режиме, т. е. блоками разумного размера, чтобы объем используемой памяти не зависел от общего объема входных данных.
Решение должно работать на любых серверных архитектурах платформы 1С. Неизвестно, какой сервер будет использовать наш заказчик, он может оказаться как на Windows, так и на Linux.
Отсюда следуют два ключевых требования к внешней компоненте:
-
Обработка данных в потоковом режиме.
-
Поддержка всех серверных архитектур 1С: как Windows, так и Linux.
В нашем распоряжении есть популярная библиотека zlib (авторы Jean-loup Gailly и Mark Adler, https://www.zlib.net/). Написана на C, поставляется в виде исходных текстов и может быть собрана под любую целевую архитектуру. Собственно, на этой библиотеке и построен архиватор gzip.
Базовые функции zlib ориентированы как раз на потоковую обработку (сжатие и распаковку) данных блоками произвольного размера: именно то, что нам нужно. Все функции этой библиотеки для работы с gzip-файлами являются уже вторичной надстройкой над базовыми потоковыми функциями.
Перехожу к главному.
Внешняя компонента Zstream является оберткой для библиотеки zlib и открывает доступ к ее базовым функциям сжатия и распаковки. Она разработана по технологии 1C Native API и поддерживает следующие архитектуры 1С:
-
Windows i386
-
Windows x86_64
-
Linux i386
-
Linux x86_64
-
Linux arm64
Решение ориентировано, в первую очередь, для работы на сервере. Можно использовать его и на клиенте, но не на веб-клиенте (согласно документации 1С, веб-клиент не умеет обмениваться с ВК объектами типа ДвоичныеДанные).
(Энтузиасты подскажут, что вместо двоичных данных можно было бы передавать строки, закодированные в Base64 – энтузиастам флаг в руки, исходники ВК я выложил).
Библиотека zlib встроена как статическая (это значит - никаких дополнительных .dll и .so).
Компонента разработана по принципу «ничего лишнего». Архитектурно она представляет собой потоковый процессор и у нее всего два основных метода: Записать и Прочитать.
Пример подключения и установки режимов (на сервере):
ПодключитьВнешнююКомпоненту(Местоположение, "Zstream",
ТипВнешнейКомпоненты.Native, ТипПодключенияВнешнейКомпоненты.Изолированно);
ПроцессорПотока = Новый("AddIn.Zstream.Zstream");
ПроцессорПотока.РежимОбработки = ПроцессорПотока.РежимОбработки_Кодирование;
ПроцессорПотока.ТипКодировки = ПроцессорПотока.ТипКодировки_Gzip;
Пример обработки потока:
Пока Истина Цикл
ВходнойБлок = ... // ДвоичныеДанные - очередная порция входных данных
КонецВвода = ... // Булево - признак конца входного потока
ПроцессорПотока.Записать(ВходнойБлок,, КонецВвода);
Пока Истина Цикл
ВыходнойБлок = ПроцессорПотока.Прочитать(РазмерБлока); // ДвоичныеДанные - очередная порция выходных данных
Если ВыходнойБлок <> Неопределено Тогда
... // используем полученные данные
Иначе
Прервать;
КонецЕсли;
КонецЦикла;
Если ПроцессорПотока.КонецПотока ИЛИ ПроцессорПотока.Ошибка Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Рассмотрим подробнее.
После создания объекта Zstream нужно установить режим обработки потока (свойство РежимОбработки): кодирование (сжатие) или декодирование (распаковка). Также нужно установить тип кодировки (свойство ТипКодировки): Gzip, Zlib или Deflate.
Если мы будем распаковывать данные и нам заранее неизвестен входной формат данных (но мы точно знаем, что в деле замешана zlib), можно установить тип кодировки Авто – это приведет к попытке автоматически определить формат по заголовку gzip или zlib. Но если входные данные окажутся в формате Deflate, который вообще не содержит никакого заголовка, будет возвращена ошибка.
Если мы собираемся сжимать данные, то дополнительно можно установить уровень сжатия (свойство УровеньСжатия). Для кодировки Gzip можно установить некоторые сведения gzip-заголовка: имя и дату/время исходного файла (свойства ИмяФайла и ДатаФайла). Для распаковки данных всего этого делать не нужно. Но при распаковке можно, наоборот, прочитать имя и дату/время исходного файла, обращаясь к тем же свойствам объекта (при условии, что входной формат - gzip).
Для установки свойств объекта используются «встроенные константы» - специальные свойства объекта, доступные только для чтения и возвращающие некоторые предопределенные значения. Для установки свойств следует использовать именно свойства-константы (нельзя рассчитывать на то, что возвращаемые ими значения не изменятся в следующей версии ВК). Вот список свойств-констант:
-
РежимОбработки_Кодирование - Число
-
РежимОбработки_Декодирование - Число
-
ТипКодировки_Deflate - Число
-
ТипКодировки_Gzip - Число
-
ТипКодировки_Zlib - Число
-
ТипКодировки_Авто - Число
-
УровеньСжатия_ПоУмолчанию - Число
-
УровеньСжатия_БезСжатия - Число
-
УровеньСжатия_ЛучшаяСкорость - Число
-
УровеньСжатия_ЛучшееСжатие - Число
После того, как мы настроили потоковый процессор, можно начать пропускать через него поток данных. Для этого мы записываем данные на вход процессора блоками произвольного размера, а на выходе процессора читаем обработанные данные, опять же, блоками произвольного размера.
Входной поток данных мы получаем откуда угодно: из файла, из базы данных, из ответа веб-сервиса. Получаем данные блоками в виде объектов ДвоичныеДанные. Размер блока может быть любым: от сколь угодно малого (вплоть до блока размером 1 байт, но это будет серьезным ударом по производительности) до сколь угодно большого (ограничивается только объемом доступной памяти).
Для подачи данных на вход процессора используется метод Записать:
Записать(<Данные>, <Размер>, <КонецВвода>)
<Данные> - ДвоичныеДанные - блок данных в виде объекта ДвоичныеДанные.
<Размер> - Число - Размер блока данных в байтах. Имеет смысл указывать, только если нужно записать меньше данных, чем есть в объекте ДвоичныеДанные. Если не указано, то записывается весь объем данных.
<КонецВвода> - Булево - Признак последнего блока во входном потоке. При достижении конца входного потока необходимо передать в этом параметре значение Истина.
Метод Записать передает в обработку очередной блок данных входного потока. Если предыдущий блок данных не был обработан до конца, установка нового блока не приведет к потере данных: новый блок данных присоединяется к предыдущему. Однако, злоупотребление этим может привести к нежелательному росту объема используемой памяти. Поэтому лучше вычитать все данные предыдущего блока прежде, чем передавать в обработку следующий.
Разрешается вызывать метод с пустым блоком данных и значением КонецВвода = Истина для установки признака конца потока.
Для чтение обработанных данных с выхода процессора используется метод Прочитать:
Прочитать(<ПредельныйРазмер>)
<ПредельныйРазмер> - Число - Предельный размер получаемого блока данных в байтах. Возвращаемый блок данных не будет превышать заданный размер, но может оказаться меньше него. Если передать значение 0, то размер возвращаемого блока будет определен автоматически. Значение по умолчанию: 0.
Возвращаемое значение:
– ДвоичныеДанные – Блок данных в виде объекта ДвоичныеДанные, если имеются данные для чтения. Блок данных может оказаться меньше заданного предельного размера, для проверки фактического размера следует использовать метод ДвоичныеДанные.Размер().
- Неопределено – Если нет доступных данных для чтения.
Существует несколько причин, по которым метод может вернуть значение Неопределено. Определить причину можно, анализируя свойства объекта КонецПотока и Ошибка.
-
КонецПотока = Ложь: закончились данные для обработки. Следует записать очередной блок данных на вход процессора и продолжить чтение.
-
КонецПотока = Истина: входной поток данных обработан полностью, достигнут конец потока (был вызван метод Записать с признаком КонецВвода = Истина). Все дальнейшие попытки чтения будут безрезультатны.
-
Ошибка = Истина: при обработке данных произошла ошибка. Все дальнейшие попытки чтения будут безрезультатны. Ошибка может возникнуть при распаковке данных и обычно свидетельствует о повреждении входных данных.
Также существует еще один метод – Сбросить(). Он выполняет сброс объекта к начальному состоянию, чтобы можно было перейти к обработке нового потока данных, не пересоздавая объект Zstream. Метод не имеет параметров и возвращаемого значения.
Подробное описание методов и свойств ВК можно найти в прилагаемом файле readme.txt.
В комплекте с ВК идет демонстрационно-тестовая внешняя обработка ZstreamTest.epf. Она демонстрирует потоковое сжатие и распаковку «файл-в-файл». Помимо этого, она демонстрирует возможность подключения выхода потокового процессора к входу объекта ЧтениеXML. О том, как это делается, я как-нибудь расскажу в отдельной статье (но это не точно). Желающие могут разобраться самостоятельно.
Сразу после запуска внешней обработки необходимо нажать кнопку «Установить и подключить ВК», после чего становятся доступны ее функции. Кстати, демо-обработка использует ВК на клиенте (ну лень было мне возиться с передачей файлов с клиента на сервер и обратно). Так что не пытайтесь запускать её на веб-клиенте.
В поле «Имя исходного файла» выберите исходный файл для сжатия. Выберите тип кодировки, уровень сжатия и размер блока. Поля «Имя сжатого файла» и «Имя распакованного файла» заполнятся автоматически, но вы можете их редактировать. Нажмите кнопку «Сжать файл» для сжатия файла. Нажмите кнопку «Распаковать файл» для распаковки файла. Кнопка «Получить данные заголовка» читает только информацию из заголовка gzip.
Тестирование для Windows 10 выполнялось на платформе 1С 8.3.24.1342 (i386 и x86_64). Тестирование для Linux выполнялось на Ubuntu 24.04.1 LTS (GNU/Linux 5.15.167.4-microsoft-standard-WSL2 x86_64), 1С 8.3.24.1761 (x86_64). Тестирование НЕ выполнялось для архитектур Linux i386 и arm64, ввиду отсутствия таковых в ближнем доступе. Лишь на кроссплатформенность GNU C++ надеюсь и уповаю.
Также выкладываю исходники ВК (нам нечего скрывать!). Компонента написана на C++. Серийных маньяков разработчиков внешних компонент могут заинтересовать следующие моменты:
-
Класс CAddInNative, куда я вынес ряд вещей, которые, как правило, реализуются одинаковым образом в любой ВК, независимо от ее назначения.
-
Автоматическая система сборки. Позволяет выполнять сборку одной командой сразу под все архитектуры. Да-да, одной командой сразу под Windows и Linux. Как? Читайте в readme.txt.