В этой статье мы поговорим о том, как можно считывать и записывать двоичные данные. В руководстве мы будем использовать псевдокод, но вы можете писать на любом удобном языке программирования, который поддерживает базовые операции ввода/вывода.
Битовые операции
Если вы не знаете о битовых операциях, то в коде встретите непонятные символы, в частности: &, |, << и >>. Это стандартные битовые операции для работы с двоичным представлением чисел, доступные в большинстве языков программирования.
Порядок байтов и потоки
Прежде чем начать, давайте разберем два важных определения: порядок байтов (endiannes) и потоки.
Порядок байтов определяет — как это ни странно — порядок байтов (простите за тавтологию). Предположим, что у нас есть 16-битное число со значением 0x1020. В двоичном виде число может быть представлено по-разному: байт 0x20, а следом за ним байт со значением 0x10 (это обратный порядок байтов), или 0x10, после которого стоит байт 0x20 (это прямой порядок байтов).
Потоки — это подобные массивам объекты, которые содержат последовательность байтов (а в некоторых случаях бит). Двоичные данные считываются и записываются в эти потоки.
Считывание двоичных данных
Давайте начнем с определения некоторых полей. В идеале все они должны быть в секции private:
__stream // объект, подобный массиву и содержащий байты
__endian // порядок данных в потоке
__length // количество байт в потоке
__position // положение следующего байта для чтения из потока
А вот так может выглядеть конструктор нашего класса:
Следующие функции будут читать из потока целые беззнаковые числа:
// чтение 8-битного беззнакового целого числа
function readU8() {
// выбрасываем исключение, если больше нет байтов для считывания
if( __position >= __length ) {
throw new Exception( "..." )
}
// возвращаем значение байта и увеличиваем положение следующего байта для его корректного считывания
return __stream[ __position ++ ]
}
// чтение 16-битного беззнакового целого числа
function readU16() {
value = 0
// так как число состоит из нескольких байт, то обрабатываем 2 случая
if( __endian == BIG_ENDIAN ) { //прямой порядок байтов
value |= readU8() << 8
value |= readU8() << 0
} else {
// обратный порядок байтов
value |= readU8() << 0
value |= readU8() << 8
}
return value
}
// чтение 24-битного беззнакового целого числа
function readU24() {
value = 0
if( __endian == BIG_ENDIAN ) {
value |= readU8() << 16
value |= readU8() << 8
value |= readU8() << 0
} else {
value |= readU8() << 0
value |= readU8() << 8
value |= readU8() << 16
}
return value
}
// чтение 32-битного беззнакового целого числа
function readU32() {
value = 0
if( __endian == BIG_ENDIAN ) {
value |= readU8() << 24
value |= readU8() << 16
value |= readU8() << 8
value |= readU8() << 0
} else {
value |= readU8() << 0
value |= readU8() << 8
value |= readU8() << 16
value |= readU8() << 24
}
return value
}
Эти функции будут считывать знаковые числа:
// чтение 8-битного знакового целого числа
function readS8() {
// считываем беззнаковое число
value = readU8()
// смотрим старший бит (означающий знак числа)
if( value >> 7 == 1 ) {
// используем дополнительный код для конвертирования значения
value = ~( value ^ 0xFF )
}
return value
}
// чтение 16-битного знакового целого числа
function readS16() {
value = readU16()
if( value >> 15 == 1 ) {
value = ~( value ^ 0xFFFF )
}
return value
}
// чтение 24-битного знакового целого числа
function readS24() {
value = readU24()
if( value >> 23 == 1 ) {
value = ~( value ^ 0xFFFFFF )
}
return value
}
// чтение 32-битного знакового целого числа
function readS32() {
value = readU32()
if( value >> 31 == 1 ) {
value = ~( value ^ 0xFFFFFFFF )
}
return value
}
Запись двоичных данных
Как и в примере выше, начнем с определения полей нашего класса. Различий будет не много, но они все же будут. Как уже говорилось, в идеале все представленные ниже поля должны быть в секции private:
__stream // объект, подобный массиву и содержащий байты
__endian // порядок данных в потоке
__position // положение следующего байта для записи в поток
Следующие функции будут записывать в поток целые беззнаковые числа:
// запись 8-битного беззнакового целого числа
function writeU8( value ) {
// следующая строчка обеспечивает беззнаковость
value &= 0xFF
// добавляем значение в поток и увеличиваем положение следующего байта
__stream[ __position ++ ] = value
}
// запись 16-битного беззнакового целого числа
function writeU16( value ) {
value &= 0xFFFF
// скорректируем число в зависимости от порядка байтов
if( __endian == BIG_ENDIAN ) { //прямой порядок
writeU8( value >> 8 )
writeU8( value >> 0 )
} else {
// обратный порядок
writeU8( value >> 0 )
writeU8( value >> 8 )
}
}
// запись 24-битного беззнакового целого числа
function writeU24( value ) {
value &= 0xFFFFFF
if( __endian == BIG_ENDIAN ) {
writeU8( value >> 16 )
writeU8( value >> 8 )
writeU8( value >> 0 )
} else {
writeU8( value >> 0 )
writeU8( value >> 8 )
writeU8( value >> 16 )
}
}
// запись 32-битного беззнакового целого числа
function writeU32( value ) {
value &= 0xFFFFFFFF
if( __endian == BIG_ENDIAN ) {
writeU8( value >> 24 )
writeU8( value >> 16 )
writeU8( value >> 8 )
writeU8( value >> 0 )
} else {
writeU8( value >> 0 )
writeU8( value >> 8 )
writeU8( value >> 16 )
writeU8( value >> 24 )
}
}
А теперь осталось реализовать несколько функций, записывающих знаковые числа, однако можно использовать аналогичные методы, которые работают с беззнаковыми числами. Но для полноты API лучше всего определить эти методы:
// запись 8-битного беззнакового целого числа
function writeS8( value ) {
writeU8( value )
}
// запись 16-битного беззнакового целого числа
function writeS16( value ) {
writeU16( value )
}
// запись 24-битного беззнакового целого числа
function writeS24( value ) {
writeU24( value )
}
// запись 32-битного беззнакового целого числа
function writeS32( value ) {
writeU32( value )
}
Вывод
И на этом все! Теперь вы знаете, как можно осуществить чтение и запись двоичных данных.
В 2024 году Python сохраняет лидирующие позиции среди языков программирования благодаря широкому применению. JavaScript и Java остаются популярными для фронтенд-разработки и корпоративных приложений
PGlite — компактная JavaScript-библиотека на базе WebAssembly, позволяющая запускать полноценный сервер PostgreSQL прямо в браузере. Весит всего 3 МБ, поддерживает расширения, такие как pgvector, и упрощает разработку и тестирование приложений.
Сравнение TypeScript и JavaScript. Показываем основные отличия друг от друга. Рассматриваем преимущества и недостатки Сравнение TypeScript и JavaScript ✔ Tproger