Работа с блоками данных в файлах
Зачем нужны блоки?
Блоки нужны для того, чтобы сжимать данные по частям. Это упрощает процесс сжатия и позволяет сжимать данные даже если они поступают не целиком, а частями.
Как это реализовано?
Представьте, что у вас есть длинный текст, который нужно сжать. Вместо того чтобы работать со всем текстом сразу, мы разделяем его на небольшие части (блоки). В нашем случае размер блока равен размеру буфера - это небольшой участок памяти (обычно 4096 или 8192 байт), который используется для временного хранения данных при чтении файла.
Каждый такой блок обрабатывается отдельно:
- Сначала программа берет первый блок и сжимает его
- Затем переходит ко второму блоку и сжимает его
- И так далее, пока все блоки не будут обработаны
Преимущества такого подхода:
- Если файл очень большой, нам не нужно загружать его целиком в память компьютера
- Если при передаче данных произошла ошибка, можно повторно передать только испорченный блок, а не весь файл
- Можно начать обработку данных, даже если еще не получен весь файл целиком
В итоге все сжатые блоки собираются вместе, образуя один сжатый файл. При этом каждый блок можно будет потом распаковать независимо от других.
Указываем размер блока
Указание размера блока перед самими данными - это важная техническая необходимость. Давайте разберем причины:
- Чтение данных при распаковке
- Без размера блока распаковщик не будет знать, сколько байт нужно прочитать для следующего блока
- Нельзя определить границы блоков только по содержимому, так как сжатые данные могут содержать любые значения байтов
- Пример проблемы без размеров блоков:
// Неправильно - без размеров блоков compressed1 := []byte{0x85, 0x41} // "AAAAA" compressed2 := []byte{0x83, 0x42} // "BBB" // При записи: 0x85 0x41 0x83 0x42 // При чтении: как узнать, где заканчивается первый блок?
- Правильная реализация с размерами:
// Правильно - с размерами блоков block1 := []byte{0x85, 0x41} // "AAAAA" block1Size := uint16(len(block1)) // 2 байта // Записываем: [размер block1] [данные block1] [размер block2] [данные block2] // При чтении: // 1. Читаем 2 байта размера // 2. Читаем указанное количество байт данных // 3. Переходим к следующему блоку
- Преимущества блочной структуры:
- Возможность обработки файлов любого размера
- Эффективное использование памяти
- Возможность параллельной обработки блоков
- Устойчивость к повреждениям (повреждение одного блока не влияет на другие)
- Формат записи:
[1 байт: длина имени файла]
[N байт: имя файла]
[2 байта: размер блока 1]
[N байт: данные блока 1]
[2 байта: размер блока 2]
[N байт: данные блока 2]
...
- Пример работы с блоками:
// При сжатии blockSize := uint16(len(compressed)) writer.Write([]byte{byte(blockSize >> 8), byte(blockSize)}) // Записываем размер writer.Write(compressed) // Записываем данные // При распаковке blockSizeBytes := make([]byte, 2) reader.Read(blockSizeBytes) // Читаем размер blockSize := uint16(blockSizeBytes[0])<<8 | uint16(blockSizeBytes[1]) compressedBlock := make([]byte, blockSize) reader.Read(compressedBlock) // Читаем данные
- Альтернативные подходы:
- Использование разделителей блоков (менее надёжно, так как разделитель может встретиться в данных)
- Фиксированный размер блоков (неэффективно для сжатия)
- Запись размера в конце блока (требует дополнительного прохода при чтении)
Таким образом, запись размера блока - это необходимый элемент формата сжатых данных, обеспечивающий корректное чтение и надёжность работы архиватора.