Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

8.6. Порядок байтов

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

По крайней мере один вопрос решен: все современные машины имеют 8-битовые байты. Однако все объекты, большие байта, представляются на разных машинах по-разному, поэтому полагаться на какие-то опре­деленные свойства было бы ошибкой. Короткие целые числа (обычно 16 битов, или 2 байта) могут иметь младший байт, расположенный как по меньшему адресу (little-endian, младшеконечное расположение), чем старший, так и по большему (big-endian, старшеконечное). Выбор ва­рианта произволен, а некоторые машины вообще поддерживают обе модели.

Итак, несмотря на то, что и старшеконечные и младшеконечные ма­шины рассматривают память как последовательность слов, расположен­ных в одном и том же порядке, байты внутри слова они интерпретируют различно. На приведенной диаграмме четыре байта, начинающиеся с по­зиции 0, представляют шестнадцатеричное целое 0x11223344 для стар-шеконечников и 0x44332211 — для младшеконечников.

Для того чтобы увидеть порядок байтов в действии, запустите сле­дующую программу:

/* byteorder: отображает байты длинного целого */

int main(void)

{

unsigned long x;

unsigned char *p;

int i;

/* 11 22 33 44 => big-endian */

/. 44 33 22 11 => little-endian */

/* x = 0x1122334455667788UL; для 64-битового long */

x = 0x11223344UL;

p = (unsigned char *) &x;

for (i = 0; i < sizeof(long); i++)

printf("%x ", *p++);

printf(“\n”);

return 0:

}

На 32-битовом старшеконечнике на экран будет выведено

11 22 33 44

на младшеконечнике —

44 33 22 11

а на PDP-11 (16-битовая машина, все еще встречающаяся во встроенных системах) результатом будет

22 11 44 33

На машинах с 64-битовым типом long мы можем рассмотреть константу большей длины и увидеть те же результаты.

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

fwrite(&x, sizeof(x), 1, stdout);

делает явным образом. Небезопасно писать (отправлять) int (или short, или long) на одном компьютере и читать это число как int на другом.

Например, если компьютер-передатчик пишет с помощью

unsigned short x;

fwrite(&x, sizeof(x), 1, stdout);

а компьютер-приемник производит чтение так:

unsigned short x;

fread(&x. sizeof(x), 1, stdin);

то, если эти компьютеры имеют разный порядок байтов, значение х будет воспроизведено неправильно. Например, если отправлено было число 0x1000, то прочитано оно будет как 0x0010.

Эта проблема часто решается посредством условной компиляции и перестановки байтов, то есть примерно так:

? short x;

? fread(&x, sizeof(x), 1, stdin);

? #ifdef BIG-ENDIAN

? /* осуществляем перестановку байтов */

? х = ((x&0xFF) << 8) | ((х>>8) & 0xFF);

? #endif

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

Если ситуация выглядит невесело для short, то для более длинных типов она еще хуже — для них существует больше способов перепутать байты. А если к этому добавить еще всяческие преобразования между членами структур, ограничения на выравнивание и абсолютно таин­ственный порядок байтов в старых машинах, то проблема покажется просто неразрешимой.

Используйте при обмене данными фиксированный порядок байтов.

Решение проблемы все же существует. Записывайте байты в каноничес­ком порядке, используя переносимый код:

unsigned short x;

putchar(x >> 8); /* пишем старший байт */

putchar(x & 0xFF); /* пишем младший байт */

и считывайте их обратно побайтово, собирая первоначальные значения:

unsigned short x;

х = getchar() << 8; /* читаем старший байт */

х |= getchar() & 0xFF; /* читаем младший байт */

Этот подход можно перенести и на структуры: записывайте значе­ния членов структур в определенной последовательности, побайтово, без выравнивания. Неважно, какой порядок байтов вы изберете, — по­дойдет любой, лишь бы передатчик и приемник согласовали порядок байтов в передаче и количество байтов в каждом объекте. В следующей главе мы покажем несколько инструментов для упаковки и распаков­ки данных.

Побайтовая обработка может показаться довольно дорогим удоволь­ствием, но в сравнении с вводом-выводом, при котором необходима упа­ковка и распаковка данных, потери времени окажутся незначитель­ными. Представьте себе систему X Window, в которой клиент пишет данные с тем порядком байтов, что используется у него на машине, а сер­вер должен распаковывать все, что клиент посылает. На клиентском конце достигнута экономия в несколько инструкций, но сервер стано­вится больше и сложнее, поскольку ему приходится обрабатывать одно­временно данные с разными порядками байтов: ведь клиенты вполне могут иметь различные типы машин. В общем, сложностей куда больше. Кроме того, не забывайте, что это графическая оболочка, где расходы на распаковку байтов будут с лихвой перекрыты выполнением той графи­ческой операции, которую они содержат.

Система X Window воспринимает данные от клиента с любым поряд­ком байтов: считается, что это забота сервера. А система Plan 9, наобо­рот, сама задает порядок байтов для отправки сообщений файл-серверу (или графическому серверу), а данные запаковываются и распаковыва­ются неким универсальным кодом вроде приведенного выше. На прак­тике определить временные затраты во время исполнения оказывается практически невозможно; можно только сказать, что на фоне ввода-вы­вода упаковка данных оказывается малозаметной.

Java — язык более высокого уровня, чем С и C++, в нем порядок бай­тов скрыт совсем. Библиотеки представляют интерфейс Serializable, который определяет, как элементы данных пакуются для передачи.

Однако, если вы пишете на С или C++, всю работу придется выпол­нять самостоятельно. Главное, что можно сказать про побайтовую обра­ботку: она решает имеющуюся проблему для всех машин с 8-битовыми байтами, причем решает без участия №ifdef. Мы еще вернемся к этой теме в следующей главе.

Итак, лучшим решением нередко оказывается преобразование информации в текстовый формат, который (не считая проблемы CRLF) является абсолютно переносимым: не существует никаких неопределен­ностей с его представлением. Однако и текстовый формат не является панацеей. Время и размер для некоторых программ могут быть критич­ны, кроме того, часть данных, особенно числа с плавающей точкой, мо­гут потерять точность при округлениях в процессе передачи через printf и scanf. Если вам надо передавать числа с плавающей точкой, особо за­ботясь о точности, убедитесь, что у вас есть хорошая библиотека фор­матированного ввода-вывода; такие библиотеки существуют, просто ее может не оказаться конкретно в вашей системе. Особенно сложно представлять числа с плавающей точкой для переноса их в двоичном формате, но при должном внимании текст может эту проблему решить.

Существует один тонкий момент, связанный с использованием стан­дартных функций для обработки двоичных файлов, — эти файлы необ­ходимо открывать в двоичном режиме:

FILE *fin;

fin = fopen(binary__file, "rb");

с = getc(fin);

Если 'b' опущено, то в большинстве систем Unix это ни на что не повли­яет, но в системах Windows первый встретившийся во вводе байт Control-Z (восьмеричный 032, шестнадцатеричный 1А) прервет чтение (такое происходило у нас с программой strings из главы 5). В тоже вре­мя при использовании двоичного режима для чтения текстовых файлов вам придется вставлять символы \r во ввод и убирать их из вывода.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]