Заметки из Зазеркалья

03.02.2016

Развитие средств работы с двоичными данными

Реализовано в версии 8.3.9.1818.

Мы реализовали ряд низкоуровневых инструментов для работы с двоичными данными. Теперь вы можете решать такие задачи как:

  • Взаимодействие со специализированными устройствами по двоичному протоколу;
  • Разбор файлов и манипуляция файлами различных форматов;
  • Конвертация текстовых данных напрямую в двоичные данные, например, для отправки отчетов;
  • Работа с двоичными данными в памяти.

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

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

Основные типы для последовательной работы с данными

Назначение и взаимную связь новых объектов удобнее всего посмотреть на конкретном примере. Пример разбивает wav файл на одинаковые части размером 1000 байт.

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

Пример: Разбить WAV-файл на части

// Разбить WAV-файл на части.
ПотокИсходный          = ФайловыеПотоки.ОткрытьДляЧтения(ИмяФайла);
ЧтениеДанных           = Новый ЧтениеДанных(ПотокИсходный);
БуферЗаголовокИсходный = ЧтениеДанных.ПрочитатьВБуферДвоичныхДанных(44);

// Декодируем заголовок
РазмерДанных         = БуферЗаголовокИсходный.ПрочитатьЦелое32(4);    // chunkSize   
КоличествоКаналов    = БуферЗаголовокИсходный.ПрочитатьЦелое16(22);   // numChannels
ЧастотаДискретизации = БуферЗаголовокИсходный.ПрочитатьЦелое32(24);   // sampleRate
БайтовВСекунду       = БуферЗаголовокИсходный.ПрочитатьЦелое32(28);   // byteRate

// На основании информации из заголовка вычисляем размер
// фрагмента. В данном примере для простоты возьмем
// фиксированное число, т.к. к работе с бинарными данными
// это не относится.    

// Фрагменты - это массив значений типа РезультатЧтенияДанных
МассивФрагменты = ЧтениеДанных.РазделитьНаЧастиПо(1000);

НомерФрагмента = 0;
Для Каждого РезультатЧтенияФрагмент из МассивФрагменты Цикл
	БуферЗаголовокФрагмента = БуферЗаголовокИсходный.Копировать();
	
	РазмерФрагмента = РезультатЧтенияФрагмент.Размер + 40;
	БуферЗаголовокФрагмента.ЗаписатьЦелое32(4, РазмерФрагмента);
	
	НомерФрагмента = НомерФрагмента + 1;
	ИмяФайлаФрагмента = "C:\Фрагменты\" + Строка(НомерФрагмента) + ".wav";
	
	ПотокКонечный = ФайловыеПотоки.ОткрытьДляЗаписи(ИмяФайлаФрагмента); 
	
	ЗаписьДанных = Новый ЗаписьДанных(ПотокКонечный);
	ЗаписьДанных.ЗаписатьБуферДвоичныхДанных(БуферЗаголовокФрагмента);
	ЗаписьДанных.Записать(РезультатЧтенияФрагмент);
	ЗаписьДанных.Закрыть();
	
	ПотокКонечный.Закрыть();
	
КонецЦикла;

ПотокИсходный.Закрыть();

Основу для работы с двоичными данными составляет группа новых типов, которую можно обозначить словом «потоки». Таких типов три: Поток, ФайловыйПоток и ПотокВПамяти. В примере используется один из них, ФайловыйПоток, но остальные работают, по большому счёту, аналогично.

Потоки предназначены для последовательного чтения/записи больших объемов двоичных данных. Их преимущество заключается в том, что они позволяют работать с потоками данных произвольного объёма. Но вместе с этим они предоставляют лишь базовые возможности работы, такие как чтение из потока, запись в поток и изменение текущей позиции.

Потоки можно сконструировать по имени файла или из объекта ДвоичныеДанные. В примере поток конструируется по имени файла (ФайловыеПотоки.ОткрытьДляЧтения(ИмяФайла)) одним из методов объекта МенеджерФайловыхПотоков. Это новый способ конструирования, дальше мы расскажем о нём.

Затем в примере, для того, чтобы иметь более широкие возможности работы, из файлового потока конструируется объект ЧтениеДанных (Новый ЧтениеДанных(ПотокИсходный)). Этот объект позволяет уже читать отдельные байты, символы, числа. С его помощью можно прочитать строку с учётом кодировки, или прочитать данные до некоторого известного заранее маркера. Этот объект имеет своего «антипода», ЗаписьДанных, который конструируется аналогичным образом, но занимается не чтением, а записью данных. Поскольку эти объекты читают/пишут данные из/в потоки, то они также делают это последовательно, что позволяет работать с потоками произвольного объёма.

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

Заголовок файла получается в виде объекта БуферДвоичныхДанных. Это важный объект, но о нём мы скажем чуть позже. А тело файла делится на части равного размера (ЧтениеДанных.РазделитьНаЧастиПо(1000)), которые получаются в виде объектов РезультатЧтенияДанных. Этот тип никаких особенных возможностей не предоставляет, а в основном просто хранит прочитанные данные.

Далее в примере для каждой такой части файла создаётся ФайловыйПоток для записи, и на его основе конструируется ЗаписьДанных. Запись данных записывает в поток новый заголовок, новое тело, и закрывает его. В результате, по окончании цикла, получается набор из нескольких файлов.

Побайтовые операции

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

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

В качестве иллюстрации возможностей буфера двоичных данных можно привести пример поворота картинки на 90 градусов.

Пример: поворот изображения

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

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

// Получаем байты изображения в объект БуферДвоичныхДанных.
БуферИзображение = ПолучитьБайтыИзображения();

// Исходим из того, что размеры изображения в пикселах нам известны.
Ширина = 200;
Высота = 100;

// Создаем новый массив байтов для перевернутого изображения.
// Предположим, что каждый пиксел представлен 4 байтами. Т.к. в данном примере 
// мы не меняем сами пикселы, а только изменяем их порядок - внутреннее 
// преставление пиксела для нас неважно.
БуферПеревернутоеИзображение = Новый БуферДвоичныхДанных(Ширина * Высота * 4);

// Поворачиваем исходное изображение на 90 градусов. Для этого пробегаемся
// по всем пикселам исходного изображения, вычисляем новый индекс пиксела
// в перевернутом изображении, и записываем пиксел в новый массив байтов.
Для ИндексШирина = 0 по Ширина-1 Цикл
	Для ИндексВысота = 0 по Высота - 1 Цикл
		ИндексПиксела      = 4 * (ИндексШирина + ИндексВысота * Ширина);
		НовыйИндексПиксела = 4 * (ИндексВысота + ИндексШирина * Ширина);
		
		// Используем ПолучитьСрез(), а не Прочитать() для того, чтобы избежать
		// лишнего копирования, т.к. исходный буфер у нас не изменяется.
		Пиксел = БуферИзображение.ПолучитьСрез(ИндексПиксела, 4);
		БуферПеревернутоеИзображение.Записать(НовыйИндексПиксела, Пиксел);
		
	КонецЦикла;
	
КонецЦикла;

Синхронная и асинхронная работа

Потоки (Поток, ФайловыйПоток, ПотокВПамяти), ЧтениеДанных, ЗаписьДанных, РезультатЧтенияДанных имеют пары синхронных и асинхронных методов. Например, Записать() – НачатьЗапись(), Закрыть() – НачатьЗакрытие().

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

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

Менеджеры и асинхронные конструкторы

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

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

Конечно, одноимённый метод Соединить() есть и у самого объекта БуферДвоичныхДанных. С его помощью можно соединить данный буфер с другим. Но для этого нужно использовать контекст данного буфера, что бывает не всегда удобно.

МенеджерФайловыхПотоков мы создали для других целей. Для потоков, как мы уже говорили, необходима возможность как синхронной, так и асинхронной работы. И если для синхронных методов мы сделали их асинхронные аналоги, то для конструкторов такой способ не подходит. Конструкторы объектов могут работать только синхронно. Поэтому для того, чтобы была возможность асинхронного конструирования, мы реализовали менеджер с асинхронными методами, которые, по сути, разными способами конструируют объект ФайловыйПоток.

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

Новый ФайловыйПоток(<ИмяФайла>, <РежимОткрытия>, <УровеньДоступа>, <РазмерБуфера>)

использовать подходящий метод с одним параметром:

ОткрытьДляДописывания(<ИмяФайла>)

Пример чтения и записи составного (multipart) HTTP-сообщения

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

Пример чтения составного (multipart) HTTP-сообщения

// Процедура - Чтение multipart HTTP
//
// Параметры:
//  Заголовки          - Соответствие   - Заголовки запроса.
//  ДвоичныеДанныеТело - ДвоичныеДанные - Тело запроса.
//
Процедура Чтение_MultipartHTTP(Заголовки, ДвоичныеДанныеТело) Экспорт
	
	Разделитель = ПолучитьРазделительСоставногоСообщения(Заголовки);
	
	Маркеры = Новый Массив();
	Маркеры.Добавить("==" + Разделитель);
	Маркеры.Добавить("==" + Разделитель + Символы.ПС);
	Маркеры.Добавить("==" + Разделитель + Символы.ВК);
	Маркеры.Добавить("==" + Разделитель + Символы.ВК + Символы.ПС);
	Маркеры.Добавить("==" + Разделитель + "==");
	
	Адрес        = Неопределено;
	Текст        = Неопределено;
	Изображение1 = Неопределено;
	Изображение2 = Неопределено;
	
	ЧтениеДанных = Новый ЧтениеДанных(ДвоичныеДанныеТело);
	
	Размер = ДвоичныеДанныеТело.Размер();
	
	// Переходим к началу первой части.
	ЧтениеДанных.ПропуститьДо(Маркеры);
	
	// Далее в цикле читаем все части.
	Пока Истина Цикл
		РезультатЧтенияЧасть = ЧтениеДанных.ПрочитатьДо(Маркеры);
		
		Если Не РезультатЧтенияЧасть.МаркерНайден Тогда
			
			// Неправильно сформированное сообщение.
			Прервать;
			
		КонецЕсли;
		
		ЧтениеЧасти = Новый ЧтениеДанных(РезультатЧтенияЧасть.ОткрытьПотокДляЧтения());
		ЗаголовкиЧасти = ПрочитатьЗаголовки(ЧтениеЧасти);
		ИмяЧасти       = ПолучитьИмяСообщения(ЗаголовкиЧасти);
		
		Если ИмяЧасти = "DestAddress" Тогда
			Адрес = ЧтениеЧасти.ПрочитатьСимволы();
			
		ИначеЕсли ИмяЧасти = "MessageText" Тогда
			Текст = ЧтениеЧасти.ПрочитатьСимволы();
			
		ИначеЕсли ИмяЧасти = "image1" Тогда
			Изображение1 = ЧтениеЧасти.Прочитать().ПолучитьДвоичныеДанные();
			
		ИначеЕсли ИмяЧасти = "image2" Тогда
			Изображение2 = ЧтениеЧасти.Прочитать().ПолучитьДвоичныеДанные();
			
		КонецЕсли;
		
		Если РезультатЧтенияЧасть.ИндексМаркера = 4 Тогда
			
			// Прочитали последнюю часть.
			Прервать;
			
		КонецЕсли;
		
	КонецЦикла;
	
КонецПроцедуры

// Функция - Прочитать заголовки
//
// Параметры:
//  ЧтениеЧасти	 - ЧтениеДанных - Часть запроса.
// 
// Возвращаемое значение:
//  Соответствие - Заголовки части.
//
Функция ПрочитатьЗаголовки(ЧтениеЧасти)
	
	Заголовки = Новый Соответствие();
	
	Пока Истина Цикл
		Стр = ЧтениеЧасти.ПрочитатьСтроку();
		
		Если Стр = "" Тогда
			
			Прервать;
			
		КонецЕсли;
		
		Части = СтрРазделить(Стр, ":");
		ИмяЗаголовка = СокрЛП(Части[0]);
		Значение     = СокрЛП(Части[1]);
		
		Заголовки.Вставить(ИмяЗаголовка, Значение);
		
	КонецЦикла;
	
	Возврат Заголовки;
	
КонецФункции

// Функция - Получить разделитель составного сообщения
//
// Параметры:
//  Заголовки	 - Соответствие - Заголовки запроса.
// 
// Возвращаемое значение:
//  Строка - Разделитель составного сообщения
//
Функция ПолучитьРазделительСоставногоСообщения(Заголовки)
	ТипСодержимого = Заголовки.Получить("Content-Type");
	
	Свойства = СтрРазделить(ТипСодержимого, ";", Ложь);
	Граница = Неопределено;
	
	Для Каждого Свойство Из Свойства Цикл
		Части = СтрРазделить(Свойство, "=", Ложь);
		ИмяСвойства = СокрЛП(Части[0]);
		
		Если ИмяСвойства <> "boundary" Тогда
			
			Продолжить;
			
		КонецЕсли;
		
		Граница = СокрЛП(Части[1]);
		
		Прервать;
		
	КонецЦикла;
		
	Возврат Граница;
	
КонецФункции

// Функция - Получить имя сообщения
//
// Параметры:
//  Заголовки	 - Соответствие - Заголовки части.
// 
// Возвращаемое значение:
//  Строка - Имя сообщения.
//
Функция ПолучитьИмяСообщения(Заголовки)

	Описание = заголовки.Получить("Content-Disposition");
	
	Свойства = СтрРазделить(Описание, ";", Ложь);
	Имя = Неопределено;
	
	Для Каждого Свойство Из Свойства Цикл
		Части = СтрРазделить(Свойство, "=", Ложь);
		ИмяСвойства = СокрЛП(Части[0]);
		
		Если ИмяСвойства <> "name" Тогда
			
			Продолжить;
			
		КонецЕсли;
		
		Имя = СокрЛП(Части[1]);
		
		Прервать;
		
	КонецЦикла;
		
	Возврат Имя;
	
КонецФункции

Пример записи составного (multipart) HTTP-сообщения

// Содание тестового multipart HTTP-сообщения
// 
// Возвращаемое значение:
//  Структура - {Заголовки, ДвоичныеДанные}
//
Функция Запись_MultipartHTTP() Экспорт
	
	// Создаем вложенные сообщения.
	ДвоичныеДанныеАдрес = СоздатьСообщение_Текст("DestAddress", "brutal-vasya@example.com");	
		
	ДвоичныеДанныеТекст = СоздатьСообщение_Текст("MessageText", 
		"Привет, Василий!" + Символы.ПС + 
		"Твой ручной лев, которого ты оставил у меня на прошлой неделе, разодрал весь мой диван." + Символы.ПС +
		"Пожалуйста забери его скорее!" + Символы.ПС +
		"Во вложении две фотки с последствиями.");
		
	ДвоичныеДанныеПингвины = СоздатьСообщение_Изображение("image1", "penguins.jpg", БиблиотекаКартинок.Пингвины);
	ДвоичныеДанныеКоала    = СоздатьСообщение_Изображение("image2", "coala.jpg",    БиблиотекаКартинок.Коала);
	
	// Формируем основное составное сообщение.
	Разделитель = "Asrf456BGe4h";
	
	Результат = Новый Структура();
	Заголовки = Новый Соответствие();
	
	Результат.Вставить("Заголовки", Заголовки);
	Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Разделитель);
	
	ПотокТело = Новый ПотокВПамяти();
	ЗаписьДанных = Новый ЗаписьДанных(ПотокТело);
	
	ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
	ЗаписьДанных.Записать(ДвоичныеДанныеАдрес);
	
	ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
	ЗаписьДанных.Записать(ДвоичныеДанныеТекст);
	
	ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
	ЗаписьДанных.Записать(ДвоичныеДанныеПингвины);


	ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
	ЗаписьДанных.Записать(ДвоичныеДанныеКоала);
	
	ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель + "==");

	ЗаписьДанных.Закрыть();
	
	ДвоичныеДанныеТело = ПотокТело.ЗакрытьИПолучитьДвоичныеДанные();
	
	Результат.Вставить("Тело", ДвоичныеДанныеТело);
	
	Возврат Результат;
	
КонецФункции

// Возвращает HTTP-сообщение в виде ДвоичныеДанные
//
// Параметры:
//  ИмяСообщения - Строка - 
//  Текст        - Строка -
// 
// Возвращаемое значение:
//  ДвоичныеДанные - HTTP-сообщение
//
Функция СоздатьСообщение_Текст(ИяСообщения, ДвоичныеДанныеТекст)
	
	Поток = Новый ПотокВПамяти();
	ЗаписьДанных = Новый ЗаписьДанных(Поток);
	
	// Заголовки.
	ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=" + ИмяСообщения);
	ЗаписьДанных.ЗаписатьСтроку("");
	
	// Тело.
	ЗаписьДанных.ЗаписатьСтроку(ДвоичныеДанныеТекст);
	
	ЗаписьДанных.Закрыть();
	
	Возврат Поток.ЗакрытьИПолучитьДвоичныеДанные();
	
КонецФункции

// Возвращает HTTP-сообщение в виде ДвоичныеДанные
//
// Параметры:
//  ИмяСообщения - Строка - 
//  ИмяФайла	 - Строка - 
//  Картинка	 - Картинка - 
// 
// Возвращаемое значение:
//  ДвоичныеДанные - HTTP-сообщение
//
Функция СоздатьСообщение_Изображение(ИмяСообщения, ИмяФайла, Картинка)
	
	Поток = Новый ПотокВПамяти();
	ЗаписьДанных = Новый ЗаписьДанных(Поток);
	
	// Заголовки.
	ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=" + ИмяСообщения + "; filename=" + ИмяФайла);
	ЗаписьДанных.ЗаписатьСтроку("Content-Type: image/jpeg");
	ЗаписьДанных.ЗаписатьСтроку("");

	// Тело.
	ЗаписьДанных.Записать(картинка.ПолучитьДвоичныеДанные());
	
	ЗаписьДанных.Закрыть();
	
	Возврат Поток.ЗакрытьИПолучитьДвоичныеДанные();
	
КонецФункции

Теги: разработка  8.3.9