Планета гаджетов / технологий
Содержание
В этой статье мы поговорим о том, как можно считывать и записывать двоичные данные. В руководстве мы будем использовать псевдокод, но вы можете писать на любом удобном языке программирования, который поддерживает базовые операции ввода/вывода.
Если вы не знаете о битовых операциях, то в коде встретите непонятные символы, в частности: &
, |
, <<
и >>
. Это стандартные битовые операции для работы с двоичным представлением чисел, доступные в большинстве языков программирования.
Прежде чем начать, давайте разберем два важных определения: порядок байтов (endiannes) и потоки.
Порядок байтов определяет — как это ни странно — порядок байтов (простите за тавтологию). Предположим, что у нас есть 16-битное число со значением 0x1020. В двоичном виде число может быть представлено по-разному: байт 0x20, а следом за ним байт со значением 0x10 (это обратный порядок байтов), или 0x10, после которого стоит байт 0x20 (это прямой порядок байтов).
Потоки — это подобные массивам объекты, которые содержат последовательность байтов (а в некоторых случаях бит). Двоичные данные считываются и записываются в эти потоки.
Давайте начнем с определения некоторых полей. В идеале все они должны быть в секции private:
__stream // объект, подобный массиву и содержащий байты __endian // порядок данных в потоке __length // количество байт в потоке __position // положение следующего байта для чтения из потока |
А вот так может выглядеть конструктор нашего класса:
class DataInput( stream, endian ) { __stream = stream __endian = endian __length = stream.length |
Следующие функции будут читать из потока целые беззнаковые числа:
// чтение 8-битного беззнакового целого числа // выбрасываем исключение, если больше нет байтов для считывания if( __position >= __length ) { throw new Exception( «…» ) // возвращаем значение байта и увеличиваем положение следующего байта для его корректного считывания return __stream[ __position ++ ] // чтение 16-битного беззнакового целого числа // так как число состоит из нескольких байт, то обрабатываем 2 случая if( __endian == BIG_ENDIAN ) { //прямой порядок байтов value |= readU8() << 8 value |= readU8() << 0 // обратный порядок байтов value |= readU8() << 0 value |= readU8() << 8 // чтение 24-битного беззнакового целого числа if( __endian == BIG_ENDIAN ) { value |= readU8() << 16 value |= readU8() << 8 value |= readU8() << 0 value |= readU8() << 0 value |= readU8() << 8 value |= readU8() << 16 // чтение 32-битного беззнакового целого числа if( __endian == BIG_ENDIAN ) { value |= readU8() << 24 value |= readU8() << 16 value |= readU8() << 8 value |= readU8() << 0 value |= readU8() << 0 value |= readU8() << 8 value |= readU8() << 16 value |= readU8() << 24 |
Эти функции будут считывать знаковые числа:
// чтение 8-битного знакового целого числа // считываем беззнаковое число // смотрим старший бит (означающий знак числа) if( value >> 7 == 1 ) { // используем дополнительный код для конвертирования значения value = ~( value ^ 0xFF ) // чтение 16-битного знакового целого числа if( value >> 15 == 1 ) { value = ~( value ^ 0xFFFF ) // чтение 24-битного знакового целого числа if( value >> 23 == 1 ) { value = ~( value ^ 0xFFFFFF ) // чтение 32-битного знакового целого числа if( value >> 31 == 1 ) { value = ~( value ^ 0xFFFFFFFF ) |
Как и в примере выше, начнем с определения полей нашего класса. Различий будет не много, но они все же будут. Как уже говорилось, в идеале все представленные ниже поля должны быть в секции private:
__stream // объект, подобный массиву и содержащий байты __endian // порядок данных в потоке __position // положение следующего байта для записи в поток |
Вот так будет выглядеть конструктор класса:
class DataOutput( stream, endian ) { __stream = stream __endian = endian |
Следующие функции будут записывать в поток целые беззнаковые числа:
// запись 8-битного беззнакового целого числа function writeU8( value ) { // следующая строчка обеспечивает беззнаковость // добавляем значение в поток и увеличиваем положение следующего байта __stream[ __position ++ ] = value // запись 16-битного беззнакового целого числа function writeU16( value ) { // скорректируем число в зависимости от порядка байтов if( __endian == BIG_ENDIAN ) { //прямой порядок writeU8( value >> 8 ) writeU8( value >> 0 ) // обратный порядок writeU8( value >> 0 ) writeU8( value >> 8 ) // запись 24-битного беззнакового целого числа function writeU24( value ) { if( __endian == BIG_ENDIAN ) { writeU8( value >> 16 ) writeU8( value >> 8 ) writeU8( value >> 0 ) writeU8( value >> 0 ) writeU8( value >> 8 ) writeU8( value >> 16 ) // запись 32-битного беззнакового целого числа function writeU32( value ) { if( __endian == BIG_ENDIAN ) { writeU8( value >> 24 ) writeU8( value >> 16 ) writeU8( value >> 8 ) writeU8( value >> 0 ) writeU8( value >> 0 ) writeU8( value >> 8 ) writeU8( value >> 16 ) writeU8( value >> 24 ) |
А теперь осталось реализовать несколько функций, записывающих знаковые числа, однако можно использовать аналогичные методы, которые работают с беззнаковыми числами. Но для полноты API лучше всего определить эти методы:
// запись 8-битного беззнакового целого числа function writeS8( value ) { // запись 16-битного беззнакового целого числа function writeS16( value ) { // запись 24-битного беззнакового целого числа function writeS24( value ) { // запись 32-битного беззнакового целого числа function writeS32( value ) { |
И на этом все! Теперь вы знаете, как можно осуществить чтение и запись двоичных данных.
Перевод статьи «How to Read and Write Binary Data for Your Custom File Formats»