Не забывайте о порядке байтов
| | |
В современных компьютерах целые числа хранятся по-разному, в зависимости от архитектуры. Рассмотрим 32-разрядное число 305419896 (0x12345678). Четыре байта этого числа могут храниться двумя способами: сначала два старших байта {такой порядок называется тупоконечным - big endian)
12 34 56 78
или сначала два младших байта (такой порядок называется остроконечным - little endian)
78 56 34 12
Примечание: Термины «тупоконечный» и «остроконечный» ввел Коэн [Cohen П1981], считавший, что споры о том, какой формат лучше, сродни распрям лилипутов из романа Свифта «Путешествия Гулливера», которые вели бесконечные войны, не сумев договориться, с какого конца следует разбивать яйцо — с тупого или острого. Раньше были в ходу и другие форматы, но практически во все современных машинах применяется либо тупоконечный, либо остроконечный порядок байтов.
Определить формат, применяемый в конкретной машине, можно с помощью следующей текстовой программы, показывающей, как хранится число 0х12345б78 (листинг 3.34).
Листинг 3.34. Программа для определения порядка байтов
endian.c
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include "etcp.h"
4 int main( void )
5 {
6 u_int32_t x = 0x12345678; /* 305419896 */
7 unsigned char *xp = ( char * )&x;
9 printf( "%0x %0x %0x %0x\n",
10 xp[ 0 ], xp[ 1 ], xp[ 2 ], xp[ 3 ] );
11 exit( 0 );
12 }
Если запустить эту программу на компьютере с процессором Intel, то получится:
bsd: $ endian
78 56 34 12
bsd: $
Отсюда ясно видно, это - остроконечная архитектура.
Конкретный формат хранения иногда в шутку называют полом байтов. Он важен, поскольку остроконечные и тупоконечные машины (а равно те, что используют иной порядок) часто общаются друг с другом по протоколам TCP/IP- Поскольку такая информация, как адреса отправления и назначения, номера портов, длина датаграмм, размеры окон и т.д., представляется в виде целых чисел, необходимо, чтобы обе стороны интерпретировали их одинаково.
Чтобы обеспечить взаимодействие компьютеров с разными архитектурами» все целочисленные величины, относящиеся к протоколам, передаются в сетевом порядке байтов, который по определению является тупоконечным. По большей части, обо всем заботятся сами протоколы, но сетевые адреса, номера портов, а иногда и другие данные, представленные в заголовках, вы задаете сами. И всякий раз необходимо преобразовывать их в сетевой порядок.
Для этого служат две функции, занимающиеся преобразованием из машинного порядка байт в сетевой и обратно. Представленные ниже объявления этих функций заимствованы из стандарта POSIX. В некоторых версиях UNIX эти объявления находятся не в файле netinet/in.h. Типы uint32_t и uint16_t приняты в POSIX соответственно для без знаковых 32- и 16-разрядных целых. В некоторых реализациях эти типы могут отсутствовать. Тем не менее функции htonl и ntohl всегда принимают и возвращают беззнаковые 32-разрядные целые числа, будь то UNIX или Winsock. Точно так же функции htons и ntohs всегда принимают и возвращают беззнаковые 16-разрядные целые.
Примечание: Буквы «l» и «s» в конце имен функций означают long (длинное) и short (короткое). Это имело смысл, так как первоначально данные функции появились в системе 4.2BSD, разработанной для 32-разрядной машины, где длинное целое принимали равным 32 бит, а короткое - 16. С появлением 64-разрядных машин это уже не так важно, поэтому следует помнить, что 1-функции работают с 32-разрядными числами, которые не обязательно представлены как long, а s-функции - с 16разрядными числами, которые не обязательно представлены в виде short. Удобно считать, что 1-функции предназначены для преобразования длинных полей в заголовках протокола, а s-функции - коротких полей.
#include <netinet/in.h> /* UNIX */
#include <winsock2 .h> /* Winsock */
uint32_t htonl( uint32_t host32 );
uint16_t htons( uint16_t host16 );
Обе функции возвращают целое число в сетевом порядке.
uint32_t ntohl( uint32_t network32 ) ;
uint16_t ntohs( uint16_t network16 );
Обе функции возвращают целое число в машинном порядке.
Функции htonl и htons преобразуют целое число из машинного порядка байт в сетевой, тогда как функции ntohl и ntohs выполняют обратное преобразование. Заметим, что на «тупоконечных» машинах эти функции ничего не делают И обычно определяются в виде макросов:
#define htonl(x) (x)
На «остроконечных» машинах (и для иных архитектур) реализация функций зависит от системы. Не надо задумываться, на какой машине вы работаете, поскольку эти функции всегда делают то, что нужно.
Применение этих функций обязательно только для полей, используемых протоколами. Пользовательские данные для протоколов IP, UDP и TCP выглядят как множество неструктурированных байтов, так что неважно, записаны целые числа в сетевом или машинном порядке. Тем не менее функции ntoh* и hton* стоит применять при передаче любых данных, поскольку тем самым вы обеспечиваете возможность совместной работы машин с разной архитектурой. Даже если сначала предполагается, что приложение будет работать только на одной платформе обязательно настанет день, когда его придется переносить на другую платформу. Тогда дополнительные усилия окупятся с лихвой.
Примечание: В общем случае проблема преобразования данных между машинами с разными архитектурами сложна. Многие программисты решают ее, преобразуя все числа в код ASCII (или, возможно, в код EBCDIC для больших машин фирмы IBM). Другой подход связан с использованием компоненты XDR (External Data Representation -внешнее представление данных), входящей в состав подсистемы вызова удаленных процедур (RFC - remote procedure call), разработанной фирмой Sun. Компонента XDR определена в RFC 1832 [Srinivasan 1995] и представляет собой набор правил для кодирования данных различных типов, а также язык, описывающий способ кодирования. Хотя предполагалось, что XDR будет применяться как часть RPC, можно пользоваться этим механизмом в ваших программах. В книге [Stevens 1999] обсуждается XDR и его применение без RPC.
И, наконец, следует помнить, что функции разрешения имен, такие как gethostbyname и getservbyname (совет 29), возвращают значения, представленные в сетевом порядке. Поэтому следующий неправильный код
struct servant *sp;
struct sockaddr_in *sap;
sp = getservbyname( name, protocol );
sap->sin_port = htons( sp->s_port );
приведет к ошибке, если исполняется не на «тупоконечной» машине.