Элементарные сокеты
Глава 3Введение в сокеты
3.1. Введение
Эта глава начинается с описания программного интерфейса приложения (API) сокетов. Мы начнем со структур адресов сокетов, которые будут встречаться почти в каждом примере на протяжении всей книги. Эти структуры можно передавать в двух направлениях: от процесса к ядру и от ядра к процессу. Последний случай — пример аргумента, через который передается возвращаемое значение, и далее в книге мы встретимся с другими примерами таких аргументов.
Перевод текстового представления адреса в двоичное значение, входящее в структуру адреса сокета, осуществляется функциями преобразования адресов. В большей части существующего кода IPv4 используются функции
inet_addr
и inet_ntoa
, но две новых функции inet_pton
и inet_ntop
работают и с IPv4, и с IPv6.Одной из проблем этих функций является то, что они зависят от протокола, так как для них имеет значение тип преобразуемого адреса — IPv4 или IPv6. Мы разработали набор функций, названия которых начинаются с
sock_
, работающих со структурами адресов сокетов независимо от протокола. Эти функции мы и будем использовать, чтобы сделать наш код не зависящим от протокола.3.2. Структуры адреса сокетов
Большинство функций сокетов используют в качестве аргумента указатель на структуру адреса сокета. Каждый набор протоколов определяет свою собственную структуру адреса сокетов. Имена этих структур начинаются с
sockaddr_
и заканчиваются уникальным суффиксом для каждого набора протоколов.Структура адреса сокета IPv4
Структура адреса сокета IPv4, обычно называемая структурой адреса сокета Интернета, именуется
sockaddr_in
и определяется в заголовочном файле
. В листинге 3.1[1] представлено определение POSIX.Листинг 3.1. Структура адреса сокета Интернета (IPv4): sockaddr_in
struct in_addr {
in_addr_t s_addr; /* 32-разрядный адрес IPv4 */
/* сетевой порядок байтов */
};
struct sockaddr_in {
uint8_t sin_len; /* длина структуры (16) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-разрядный номер порта TCP или UDP */
/* сетевой порядок байтов */
struct in_addr sin_addr; /* 32-разрядный адрес IPv4 */
/* сетевой порядок байтов */
char sin_zero[8]; /* не используется */
};
Есть несколько моментов, касающихся структур адреса сокета в целом, которые мы покажем на примере.
■ Элемент длины
sin_len
появился в версии 4.3BSD-Reno, когда была добавлена поддержка протоколов OSI (см. рис. 1.6). До этой реализации первым элементом был sin_family
, который исторически имел тип unsigned short
(целое без знака). Не все производители поддерживают поле длины для структур адреса сокета, и в POSIX, например, не требуется наличия этого элемента. Типы данных, подобные uint8_t
, введены в POSIX (см. табл. 3.1). Наличие поля длины упрощает обработку структур адреса сокета с переменной длиной.■ Даже если поле длины присутствует, нам не придется устанавливать и проверять его значение, пока мы не имеем дела с маршрутизирующими сокетами (см. главу 18). Оно используется внутри ядра процедурами, работающими со структурами адресов сокетов из различных семейств протоколов (например, код таблицы маршрутизации).
ПРИМЕЧАНИЕЧетыре функции, передающие структуру адреса сокета от процесса к ядру, — bind, connect, sendto и sendmsg — используют функцию sockargs в реализациях, ведущих происхождение от Беркли [128, с. 452]. Эта функция копирует структуру адреса сокета из процесса и затем явно присваивает элементу sin_len значение размера структуры, переданной в качестве аргумента этим четырем функциям. Пять функций, передающих структуру адреса сокета от ядра к процессу, — accept, recvfrom, recvmsg, getpeername и getsockname — устанавливают элемент sin_len перед возвращением управления процессу.
К сожалению, обычно не существует простого теста, выполняемого в процессе компиляции и определяющего, задает ли реализация поле длины для своих структур адреса сокета. В нашем коде мы тестируем собственную константу HAVE_SOCKADDR_SA_LEN (см. листинг Г.2), но для того чтобы определить, задавать эту константу или нет, требуется откомпилировать простую тестовую программу, использующую необязательный элемент структуры, и проверить, успешно ли выполнена компиляция. В листинге 3.3 мы увидим, что от реализаций IPv6 требуется задавать SIN6_LEN, если структура адреса сокета имеет поле длины. В некоторых реализациях IPv4 (например, Digital Unix) поле длины предоставляется для приложений, основанных на параметре времени компиляции (например, _SOCKADDR_LEN). Это свойство обеспечивает совместимость с другими, более ранними программами.
■ POSIX требует наличия только трех элементов структуры:
sin_family
, sin_addr
и sin_port
. POSIX-совместимая реализация может определять дополнительные элементы структуры, и это норма для структуры адреса сокета Интернета. Почти все реализации добавляют элемент sin_zero, так что все структуры адреса сокета имеют размер как минимум 16 байт.■ Типы элементов
s_addr
, sin_family
и sin_port
мы указываем согласно POSIX. Тип данных in_addr_t
соответствует целому числу без знака длиной как минимум 32 бита, in_port_t
— целому числу без знака длиной как минимум 16 бит, a sa_family_t
— это произвольное целое число без знака. Последнее обычно представляет собой 8-разрядное целое без знака, если реализация поддерживает поле длины, либо 16-разрядное целое без знака, если поле длины не поддерживается. В табл. 3.1 перечислены эти три типа данных POSIX вместе с некоторыми другими типами данных POSIX, с которыми мы встретимся.
Таблица 3.1. Типы данных, требуемые POSIX
Тип данных Описание Заголовочный файл int8_t 8-разрядное целое со знаком uint8_t 8-разрядное целое без знака int16_t 16-разрядное целое со знаком uint16_t 16-разрядное целое без знака int32_t 32-разрядное целое со знаком uint32_t 32-разрядное целое без знака sa_family_t семейство адресов структуры адреса сокета socklen_t длина структуры адреса сокета, обычно типа uint32_t in_addr_t IPv4-адрес, обычно типа uint32_t in_port_t порт TCP или UDP, обычно типа uint16_t
■ Вы также встретите типы данных
u_char
, u_short
, u_int
и u_long
, которые не имеют знака. POSIX определяет их с замечанием, что они устарели. Они предоставляются в целях обратной совместимости.■ И адрес IPv4, и номер порта TCP и UDP всегда хранятся в структуре в соответствии с порядком байтов, определенным в сети (сетевой порядок байтов — network byte order). Об этом нужно помнить при использовании этих элементов (более подробно о разнице между порядком байтов узла и порядком байтов в сети мы поговорим в разделе 3.4).
■ К 32-разрядному адресу IPv4 можно обратиться двумя путями. Например, если
serv
— это структура адреса сокета Интернета, то serv.sin_addr
указывает на 32-разрядный адрес IPv4 как на структуру in_addr
, в то время как serv.sin_addr.s_addr
указывает на тот же 32-разрядный адрес IPv4 как на значение типа in_addr_t
(обычно это 32-разрядное целое число без знака). Нужно следить за корректностью обращения к адресам IPv4, особенно при использовании их в качестве аргументов различных функций, потому что компиляторы часто передают структуры не так, как целочисленные переменные.ПРИМЕЧАНИЕПричина того, что sin_addr является структурой, а не просто целым числом без знака, носит исторический характер. В более ранних реализациях (например, 4.2BSD) структура in_addr определялась как объединение (union) различных структур, чтобы сделать возможным доступ к каждому из четырех байтов 32-разрядного IPv4-адреса, а также к обоим входящим в него 16-разрядным значениям. Эта возможность использовалась в адресах классов А, В и С для выборки соответствующих байтов адреса. Но с появлением подсетей и последующим исчезновением различных классов адресов (см. раздел А.4) и введением бесклассовой адресации (classless addressing) необходимость в объединении структур отпала. В настоящее время большинство систем отказались от использования объединения и просто определяют in_addr как структуру, содержащую один элемент типа in_addr_t.
■ Элемент
sin_zero
не используется, но мы всегда устанавливаем его в нуль при заполнении одной из этих структур. Перед заполнением структуры мы всегда обнуляем все ее элементы, а не только sin_zero
.ПРИМЕЧАНИЕВ большинстве случаев при использовании этой структуры не требуется, чтобы элемент sin_zero был равен нулю, но, например, при привязке конкретного адреса IPv4 (а не произвольного интерфейса) этот элемент обязательно должен быть нулевым [128, с. 731-732].
■ Структуры адреса сокета используются только на данном узле: сама структура не передается между узлами, хотя определенные поля (например, поля IP-адреса и порта) используются для соединения.
Универсальная структура адреса сокета
Структуры адреса сокета всегда передаются по ссылке при передаче в качестве аргумента для любой функции сокета. Но функции сокета, принимающие один из этих указателей в качестве аргумента, должны работать со структурами адреса сокета из любого поддерживаемого семейства протоколов.
Проблема в том, как объявить тип передаваемого указателя. Для ANSI С решение простое:
void*
является указателем на неопределенный (универсальный) тип (generic pointer type). Но функции сокетов существовали до появления ANSI С, и в 1982 году было принято решение определить универсальную структуру адреса сокета (generic socket address structure) в заголовочном файле
, которая показана в листинге 3.2.Листинг 3.2. Универсальная структура адреса сокета: sockaddr
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; /* семейство адресов: константа AF_xxx */
char sa_data[14]; /* адрес, специфичный для протокола */
};
Функции сокетов определяются таким образом, что их аргументом является указатель на общую структуру адреса сокета, как показано в прототипе функции
bind
(ANSI С):int bind(int, struct sockaddr*, socklen_t);
При этом требуется, чтобы для любых вызовов этих функций указатель на структуру адреса сокета, специфичную для протокола, был преобразован в указатель на универсальную структуру адреса сокета. Например:
struct sockaddr_in serv; /* структура адреса сокета IPv4 */
/* заполняем serv{} */
bind(sockfd, (struct sockaddr*)&serv, sizeof(serv));
Если мы не выполним преобразование (
struct sockaddr*
), компилятор С сгенерирует предупреждение в форме "Warning: passing arg 2 of 'bind' from incompatible pointer type"
(Передается указатель несовместимого типа). Здесь мы предполагаем, что в системных заголовочных файлах имеется прототип ANSI С для функции bind.С точки зрения разработчика приложений, универсальная структура адреса сокета используется только для преобразования указателей на структуры адресов конкретных протоколов.
ПРИМЕЧАНИЕВспомните, что в нашем заголовочном файле unp.h (см. раздел 1.2) мы определили SA как строку "struct sockaddr", чтобы сократить код, который мы написали для преобразования этих указателей.
С точки зрения ядра основанием использовать в качестве аргументов указатели на универсальные структуры адреса сокетов является то, что ядро должно получать указатель вызывающей функции, преобразовывать его в struct sockaddr, а затем по значению элемента sa_family определять тип структуры. Но разработчику приложений было бы проще работать с указателем void*, поскольку это избавило бы его от необходимости выполнять явное преобразование указателя.
Структура адреса сокета IPv6
Структура адреса сокета IPv6 задается при помощи включения заголовочного файла
, как показано в листинге 3.3.Листинг 3.3. Структура адреса сокета IPv6: sockaddr_in6
struct in6_addr {
uint8_t s6_addr[16]; /* 128-разрядный адрес IPv6 */
/* сетевой порядок байтов */
};
#define SIN6_LEN /* требуется для проверки во время компиляции */
struct sockaddr_in6 {
uint8_t sin_len; /* длина этой структуры (24) */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* номер порта транспортного уровня */
/* сетевой порядок байтов */
uint32_t sin6_flowinfo; /* приоритет и метка потока */
/* сетевой порядок байтов */
struct in6_addr sin6_addr; /* IPv6-адрес */
/* сетевой порядок байтов */
uint32_t sin6_scope_id; /* набор интерфейсов */
};
ПРИМЕЧАНИЕРасширения API сокетов для IPv6 описаны в RFC 3493 [36].
Отметим следующие моменты относительно листинга 3.3:
■ Константа
SIN6_LEN
должна быть задана, если система поддерживает поле длины для структур адреса сокета.■ Семейством IPv6 является
AF_INET6
, в то время как семейство IPv4 — AF_INET
.■ Элементы в структуре упорядочены таким образом, что если структура
sockaddr_in6
выровнена по 64 битам, то так же выровнен и 128-разрядный элемент sin6_addr
. На некоторых 64-разрядных процессорах доступ к данным с 64-разрядными значениями оптимизирован, если данные выровнены так, что их адрес кратен 64.■ Элемент
sin6_flowinfo
разделен на три поля:□ 20 бит младшего порядка — это метка потока;
□ следующие 12 бит зарезервированы.
Поле метки потока и поле приоритета рассматриваются в описании рис. А.2. Отметим, что использование поля приоритета еще не определено.
■ Элемент
sin6_scope_id
определяет контекст, в котором действует контекстный адрес (scoped address). Чаще всего это бывает индекс интерфейса для локальных адресов (см. раздел А.5).Новая универсальная структура адреса сокета
Новая универсальная структура адреса сокета была определена как часть API сокетов IPv6 с целью преодолеть некоторые недостатки существующей структуры
sockaddr
. В отличие от структуры sockaddr
, новая структура sockaddr_storage
достаточно велика для хранения адреса сокета любого типа, поддерживаемого системой. Новая структура задается подключением заголовочного файла
, часть которого показана в листинге 3.4.Листинг 3.4. Структура хранения адреса сокета sockaddr_storage
struct sockaddr_storage {
uint8_t ss_len; /* длина этой структуры (зависит от реализации) */
sa_family_t ss_family; /* семейство адреса. AF_xxx */
/* зависящие от реализации элементы, обеспечивающие:
а) выравнивание, достаточное для выполнения требований по выравниванию всех
типов адресов сокетов, поддерживаемых системой;
б) достаточный объем для хранения адреса сокета любого типа,
поддерживаемого системой. */
};
Тип
sockaddr_storage
— это универсальная структура адреса сокета, отличающаяся от struct sockaddr
по следующим параметрам:1. Если к структурам адресов сокетов, поддерживаемым системой, предъявляются требования по выравниванию, структура
sockaddr_storage
выполняет самое жесткое из них.2. Структура
sockaddr_storage
достаточно велика для размещения любой структуры адреса сокета, поддерживаемой системой.Заметьте, что поля структуры
sockaddr_storage
непрозрачны для пользователя, за исключением ss_family
и ss_len
(если таковые заданы). Структура sockaddr_storage
должна преобразовываться в структуру адреса соответствующего типа для обращения к содержимому остальных полей.Сравнение структур адреса сокетов
На рис. 3.1 показано сравнение пяти структур адресов сокетов, с которыми мы встретимся в тексте, предназначенных для IPv4, IPv6, доменного сокета Unix (см. листинг 15.1), канального уровня (см. листинг 18.1) и хранения. Подразумевается, что все структуры адреса сокета содержат 1-байтовое поле длины, поле семейства также занимает 1 байт и длина любого поля, размер которого ограничен снизу, в точности равна этому ограничению.
Рис. 3.1. Сравнение различных структур адресов сокетов
Две структуры адреса сокета имеют фиксированную длину, а структура доменного сокета Unix и структура канального уровня — переменную. При обработке структур переменной длины мы передаем функциям сокетов указатель на структуру адреса сокета, а в другом аргументе передаем длину этой структуры. Под каждой структурой фиксированной длины мы показываем ее размер в байтах (для реализации 4.4BSD).
ПРИМЕЧАНИЕСама структура sockaddr_un имеет фиксированную длину, но объем информации в ней — длина полного имени (pathname) — может быть переменным. Передавая указатели на эти структуры, следует соблюдать аккуратность при обработке поля длины — как длины в структуре адреса сокета (если поле длины поддерживается данной реализацией), так и длины данных, передаваемых ядру и принимаемых от него.
Этот рисунок служит также иллюстрацией стиля, которого мы придерживаемся в этой книге: названия структур на рисунках всегда выделяются полужирным шрифтом, а за ними следуют фигурные скобки.
Ранее отмечалось, что в реализации 4.3BSD Reno ко всем структурам адресов сокетов было добавлено поле длины. Если бы поле длины присутствовало в оригинальной реализации сокетов, то не возникло бы необходимости передавать аргумент длины функциям сокетов (третий аргумент функций bind и connect). Вместо этого размер структуры мог бы храниться в поле длины структуры.
3.3. Аргументы типа «значение-результат»
Мы отмечали, что когда структура адреса сокета передается какой-либо из функций сокетов, она всегда передается по ссылке, то есть в качестве аргумента передается указатель на структуру. Длина структуры также передается в качестве аргумента. Но способ, которым передается длина, зависит от того, в каком направлении передается структура: от процесса к ядру или наоборот.
1. Три функции
bind
, connect
и sendto
передают структуру адреса сокета от процесса к ядру. Один из аргументов этих функций — указатель на структуру адреса сокета, другой аргумент — это целочисленный размер структуры, как показано в следующем примере:struct sockaddr_in serv;
/* заполняем serv{} */
connect(sockfd, (SA*)&serv, sizeof(serv));
Поскольку ядру передается и указатель, и размер структуры, на которую он указывает, становится точно известно, какое количество данных нужно скопировать из процесса в ядро. На рис. 3.2 показан этот сценарий.
Рис. 3.2. Структура адреса сокета, передаваемая от процесса к ядру
В следующей главе мы увидим, что размер структуры адреса сокета в действительности имеет тип
socklen_t
, а не int
, но POSIX рекомендует определять socklen
_t как uint32_t
.2. Четыре функции
accept
, recvfrom
, getsockname
и getpeername
передают структуру адреса сокета от ядра к процессу, то есть в направлении, противоположном предыдущему случаю. Этим функциям передается указатель на структуру адреса сокета и указатель на целое число, содержащее размер структуры, как показано в следующем примере:struct sockaddr_un cli; /* домен Unix */
socklen_t len;
len = sizeof(cli); /* len - это значение */
getpeername(unixfd, (SA*)&cli, &len);
/* значение len могло измениться */
Причина замены типа для аргумента «длина» с целочисленного на указатель состоит в том, что «длина» эта является и значением при вызове функции (сообщает ядру размер структуры, так что ядро при заполнении структуры знает, где нужно остановиться), и результатом, когда функция возвращает значение (сообщает процессу, какой объем информации ядро действительно сохранило в этой структуре). Такой тип аргумента называется аргументом типа «значение-результат» (value-result argument). На рис. 3.3 представлен этот сценарий.
Рис. 3.3. Структура адреса сокета, передаваемая от ядра к процессу
Пример аргументов типа «значение-результат» вы увидите в листинге 4.2.
Если при использовании аргумента типа «значение-результат» для длины структуры структура адреса сокета имеет фиксированную длину (см. рис. 3.1), то значение, возвращаемое ядром, будет всегда равно этому фиксированному размеру: 16 для
sockaddr_in
IPv4 и 24 для sockaddr_in6
IPv6. Для структуры адреса сокета переменной длины (например, sockaddr_un
домена Unix) возвращаемое значение может быть меньше максимального размера структуры (вы увидите это в листинге 15.2).ПРИМЕЧАНИЕМы говорили о структурах адресов сокетов, передаваемых между процессом и ядром. Для такой реализации, как 4.4BSD, где все функции сокетов являются системными вызовами внутри ядра, это верно. Но в некоторых реализациях, особенно в System V, функции сокетов являются лишь библиотечными функциями, которые выполняются как часть обычного пользовательского процесса. То, как эти функции взаимодействуют со стеком протоколов в ядре, относится к деталям реализации, которые обычно нас не волнуют. Тем не менее для простоты изложения мы будем продолжать говорить об этих структурах как о передаваемых между процессом и ядром такими функциями, как bind и connect. (В разделе В.1 вы увидите, что реализации System V действительно передают пользовательские структуры адресов сокетов между процессом и ядром, но как часть сообщений потоков STREAMS.)
Существует еще две функции, передающие структуры адресов сокетов: это recvmsg и sendmsg (см. раздел 14.5). Однако при их вызове поле длины не является отдельным аргументом функции, а передается как одно из полей структуры.
В сетевом программировании наиболее общим примером аргумента типа «значение-результат» может служить длина возвращаемой структуры адреса сокета. Вы встретите и другие аргументы типа «значение-результат»:
■ Три средних аргумента функции
select
(раздел 6.3).■ Аргумент «длина» для функции
getsockopt
(см. раздел 7.2).■ Элементы
msg_namelen
и msg_controllen
структуры msghdr
при использовании с функцией recvmsg
(см. раздел 14.5).■ Элемент
ifc_len
структуры ifconf
(см. листинг 17.1).■ Первый из двух аргументов длины в функции
sysctl
(см. раздел 18.4).3.4. Функции определения порядка байтов
Рассмотрим 16-разрядное целое число, состоящее из двух байтов. Возможно два способа хранения этих байтов в памяти. Такое расположение, когда первым идет младший байт, называется прямым порядком байтов (little-endian), а когда первым расположен старший байт — обратным порядком байтов (big-endian). На рис. 3.4 показаны оба варианта.
Рис. 3.4. Прямой и обратный порядок байтов для 16-разрядного целого числа
Сверху на этом рисунке изображены адреса, возрастающие справа налево, а снизу — слева направо. Старший бит (most significant bit, MSB) является в 16-разрядном числе крайним слева, а младший бит (least significant bit, LSB) — крайним справа.
ПРИМЕЧАНИЕТермины «прямой порядок байтов» и «обратный порядок байтов» указывают, какой конец многобайтового значения — младший байт или старший — хранится в качестве начального адреса значения.
К сожалению, не существует единого стандарта порядка байтов, и можно встретить системы, использующие оба формата. Способ упорядочивания байтов, используемый в конкретной системе, мы называем порядком байтов узла (host byte order). Программа, представленная в листинге 3.5, выдает порядок байтов узла.
Листинг 3.5. Программа для определения порядка байтов узла
//intro/byteorder.c
1 #include "unp.h"
2 int
3 main(int argc, char **argv)
4 {
5 union {
6 short s;
7 char c[sizeof(short)];
8 } un;
9 un.s = 0x0102;
10 printf("%s: ", CPU_VENDOR_OS);
11 if (sizeof(short) == 2) {
12 if (un.c[0] == 1 && un.c[1] == 2)
13 printf("big-endian\n");
14 else if (un.c[0] == 2 && un.c[1] == 1)
15 printf("little-endian\n");
16 else
17 printf("unknown\n");
18 } else
19 printf('sizeof(short) = %d\n", sizeof(short));
20 exit(0);
21 }
Мы помещаем двухбайтовое значение
0x0102
в переменную типа short
(короткое целое) и проверяем значения двух байтов этой переменной: с[0]
(адрес А на рис. 3.4) и c[1]
(адрес А + 1 на рис. 3.4), чтобы определить порядок байтов.Константа
CPU_VENDOR_OS
определяется программой GNU (аббревиатура «GNU» раскрывается рекурсивно — GNU's Not Unix) autoconf
в процессе конфигурации, необходимой для выполнения программ из этой книги. В этой константе хранится тип центрального процессора, а также сведения о производителе и реализации операционной системы. Ниже представлены некоторые примеры вывода этой программы при запуске ее в различных системах (см. рис. 1.7).freebsd4 % byteorder
i386-unknown-freebsd4.8: little-endian
macosx % byteorder
powerpc-apple-darwin6.6: big-endian
freebsd5 % byteorder
sparc64-unknown-freebsd5.1: big-endian
aix % byteorder
powerpc-ibm-aix5.1.0.0: big-endian
hpux % byteorder
hppa1.1-hp-ux11 11: big-endian
linux % byteorder
i586-pc-linux-gnu: little-endian
solaris % byteorder
sparc-sun-solaris2.9: big-endian
Все, что было сказано об определении порядка байтов 16-разрядного целого числа, конечно, справедливо и в отношении 32-разрядного целого.
ПРИМЕЧАНИЕСуществуют системы, в которых возможен переход от прямого к обратному порядку байтов либо при перезапуске системы (MIPS 2000), либо в любой момент выполнения программы (Intel i860).
Разработчикам сетевых приложений приходится обрабатывать различия в определении порядка байтов, поскольку в сетевых протоколах используется сетевой порядок байтов (network byte order). Например, в сегменте TCP есть 16- разрядный номер порта и 32-разрядный адрес IPv4. Стеки отправляющего и принимающего протоколов должны согласовывать порядок, в котором передаются байты этих многобайтовых полей. Протоколы Интернета используют обратный порядок байтов.
Теоретически реализация Unix могла бы хранить поля структуры адреса сокета в порядке байтов узла, а затем выполнять необходимые преобразования при перемещении полей в заголовки протоколов и обратно, позволяя нам не беспокоиться об этом. Но исторически и с точки зрения POSIX определяется, что для некоторых полей в структуре адреса сокета порядок байтов всегда должен быть сетевым. Поэтому наша задача — выполнить преобразование из порядка байтов узла в сетевой порядок и обратно. Для этого мы используем следующие четыре функции:
#include
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
Обе функции возвращают значение, записанное в сетевом порядке байтов
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);
Обе функции возвращают значение, записанное в порядке байтов узла
В названиях этих функций
h
обозначает узел, n
обозначает сеть, s
— тип short, l
— тип long. Термины short и long являются наследием времен реализации 4.2BSD Digital VAX. Следует воспринимать s
как 16-разрядное значение (например, номер порта TCP или UDP), а l
— как 32-разрядное значение (например, адрес IPv4). В самом деле, в 64-разрядной системе Digital Alpha длинное целое занимает 64 разряда, а функции htonl
и ntohl
оперируют 32-разрядными значениями (несмотря на то, что используют тип long
).Используя эти функции, мы можем не беспокоиться о реальном порядке байтов на узле и в сети. Для преобразования порядка байтов в конкретном значении следует вызвать соответствующую функцию. В системах с таким же порядком байтов, как в протоколах Интернета (обратным), эти четыре функции обычно определяются как пустой макрос.
Мы еще вернемся к проблеме определения порядка байтов, обсуждая данные, содержащиеся в сетевом пакете, и сравнивая их с полями в заголовках протокола, в разделе 5.18 и упражнении 5.8.
Мы до сих пор не определили термин байт. Его мы будем использовать для обозначения 8 бит, поскольку практически все современные компьютерные системы используют 8-битовые байты. Однако в большинстве стандартов Интернета для обозначения 8 бит используется термин октет. Началось это на заре TCP/IP, поскольку большая часть работы выполнялась в системах типа DEC-10, в которых не применялись 8-битовые байты. Еще одно важное соглашение, принятое в стандартах Интернета, связано с порядком битов. Во многих стандартах вы можете увидеть «изображения» пакетов, подобные приведенному ниже (это первые 32 разряда заголовка IPv4 из RFC 791):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
В этом примере приведены четыре байта в том порядке, в котором они передаются по проводам. Крайний слева бит является наиболее значимым. Однако нумерация начинается с нуля, который соответствует как раз наиболее значимому биту. Вам необходимо получше ознакомиться с этой записью, чтобы не испытывать трудностей при чтении описаний протоколов в RFC.
ПРИМЕЧАНИЕТипичной ошибкой среди программистов сетевых приложений начала 80-х, разрабатывающих код на рабочих станциях Sun (Motorola 68000 с обратным порядком байтов), было забыть вызвать одну из указанных четырех функций. На этих рабочих станциях программы работали нормально, но при переходе на машины с прямым порядком байтов они переставали работать.
3.5. Функции управления байтами
Существует две группы функций, работающих с многобайтовыми полями без преобразования данных и без интерпретации их в качестве строк языка С с завершающим нулем. Они необходимы нам при обработке структур адресов сокетов, поскольку такие поля этих структур, как IP-адреса, могут содержать нулевые байты, но при этом не являются строками С. Строки с завершающим нулем обрабатываются функциями языка С, имена которых начинаются с аббревиатуры
str
. Эти функции подключаются с помощью файла
.Первая группа функций, названия которых начинаются с
b
(от слова «byte» — «байт»), взяты из реализации 4.2BSD и все еще предоставляются практически любой системой, поддерживающей функции сокетов. Вторая группа функций, названия которых начинаются с mem
(от слова «memory» — память), взяты из стандарта ANSI С и доступны в любой системе, обеспечивающей поддержку библиотеки ANSI С.Сначала мы представим функции, которые берут начало от реализации Беркли, хотя в книге мы будем использовать только одну из них —
bzero
. (Дело в том, что она имеет только два аргумента и ее проще запомнить, чем функцию memset
с тремя аргументами, как объяснялось в разделе 1.2.) Две другие функции, bcopy
и bcmp
, могут встретиться вам в существующих приложениях.#include
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
Возвращает: 0 в случае равенства, ненулевое значение в случае неравенства
ПРИМЕЧАНИЕМы впервые встречаемся со спецификатором const. В приведенном примере он служит признаком того, что значения, на которые указывает указатель, то есть src, ptr1 и ptr2, не изменяются функцией. Другими словами, область памяти, на которую указывает указатель со спецификатором const, считывается функцией, но не изменяется.
Функция
bzero
обнуляет заданное число байтов в указанной области памяти. Мы часто используем эту функцию для инициализации структуры адреса сокета нулевым значением. Функция bcopy
копирует заданное число байтов из источника в место назначения. Функция bcmp
сравнивает две произвольных последовательности байтов и возвращает нулевое значение, если две байтовых строки идентичны, и ненулевое — в противном случае.Следующие функции являются функциями ANSI С:
#include
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
Возвращает: 0 в случае равенства, значение <0 или >0 в случае неравенства (см. текст)
Функция
memset
присваивает заданному числу байтов значение с. Функция memcpy
аналогична функции bcopy
, но имеет другой порядок двух аргументов. Функция bcopy
корректно обрабатывает перекрывающиеся поля, в то время как поведение функции memcpy
не определено, если источник и место назначения перекрываются. В случае перекрывания полей должна использоваться функция ANSI С memmove
(упражнение 30.3).ПРИМЕЧАНИЕЧтобы запомнить порядок аргументов функции memcpy, подумайте о том, что он совпадает с порядком аргументов в операторе присваивания (справа — оригинал, слева — копия).
dest = src;
Последним аргументом этой функции (как и всех ANSI-функций memXXX) всегда является длина области памяти.
Функция
memcmp
сравнивает две произвольных последовательности байтов и возвращает нуль, если они идентичны. В противном случае знак возвращаемого значения определяется знаком разности между первыми несовпадающими байтами, на которые указывают ptr1 и ptr2. Предполагается, что сравниваемые байты принадлежат к типу unsigned char
.3.6. Функции inet_aton, inet_addr и inet_ntoa
Существует две группы функций преобразования адресов, которые мы рассматриваем в этом и следующем разделах. Они выполняют преобразование адресов Интернета из строк ASCII (удобных для человеческого восприятия) в двоичные значения с сетевым порядком байтов (эти значения хранятся в структурах адресов сокетов).
1. Функции
inet_aton
, inet_ntoa
и inet_addr
преобразуют адрес IPv4 из точечно-десятичной записи (например, 206.168.112.96) в 32-разрядное двоичное значение в сетевом порядке байтов. Возможно, вы встретите эти функции в многочисленных существующих программах.2. Более новые функции
inet_pton
и inet_ntop
работают и с адресами IPv4, и с адресами IPv6. Эти функции, описываемые в следующем разделе, мы используем в книге.#include
int inet_aton(const char *strptr, struct in_addr *addrptr);
Возвращает: 1, если строка преобразована успешно, 0 в случае ошибки
in_addr_t inet_addr(const char *strptr);
Возвращает: 32-разрядный адрес IPv4 в сетевом порядке байтов: INADDR_NONE в случае ошибки
char *inet_ntoa(struct in_addr inaddr);
Возвращает: указатель на строку с адресом в точечно-десятичной записи
Первая из названных функций,
inet_aton
, преобразует строку, на которую указывает strptr
, в 32-разрядное двоичное число, записанное в сетевом порядке байтов, передаваемое через указатель addrptr
. При успешном выполнении возвращаемое значение равно 1, иначе возвращается нуль.ПРИМЕЧАНИЕФункция inet_aton обладает одним недокументированным свойством: если addrptr — пустой указатель (null pointer), функция все равно выполняет проверку допустимости адреса, содержащегося во входной строке, но не сохраняет результата.
Функция
inet_addr
выполняет то же преобразование, возвращая в качестве значения 32-разрядное двоичное число в сетевом порядке байтов. Проблема при использовании этой функции состоит в том, что все 232 возможных двоичных значений являются действительными IP-адресами (от 0.0.0.0 до 255.255.255.255), но в случае возникновения ошибки функция возвращает константу INADDR_NONE
(обычно представленную двоичным числом, состоящим из 32 бит, установленных в единицу). Это означает, что точечно-десятичная запись 255.255.255.255 (ограниченный адрес для широковещательной передачи IPv4, см. раздел 18.2) не может быть обработана этой функцией, поскольку ее двоичное значение выглядит как указание на сбой при выполнении функции.ПРИМЕЧАНИЕХарактерной проблемой, сопровождающей выполнение функции inet_addr, может стать то, что, как утверждается в некоторых руководствах, в случае ошибки она возвращает значение -1 вместо INADDR_NONE. С некоторыми компиляторами это может вызвать проблемы при сравнении возвращаемого значения функции (значение без знака) с отрицательной константой.
На сегодняшний день функция
inet_addr
является нерекомендуемой, или устаревшей, и в создаваемом коде вместо нее должна использоваться функция inet_aton
. Еще лучше использовать более новые функции, описанные в следующем разделе, работающие и с IPv4, и с IPv6.Функция
inet_ntoa
преобразует 32-разрядный двоичный адрес IPv4, хранящийся в сетевом порядке байтов, в точечно-десятичную строку. Строка, на которую указывает возвращаемый функцией указатель, находится в статической памяти. Это означает, что функция не допускает повторного вхождения, то есть не является повторно входимой (reentrant), что мы обсудим в разделе 11.14. Наконец, отметим, что эта функция принимает в качестве аргумента структуру, а не указатель на структуру.ПРИМЕЧАНИЕФункции, принимающие структуры в качестве аргументов, встречаются редко. Более общим способом является передача указателя на структуру.
3.7. Функции inet_pton и inet_ntop
Эти функции появились с IPv6 и работают как с адресами IPv4, так и с адресами IPv6. Их мы и будем использовать в книге. Символы
p
и n
обозначают соответственно формат представления и численный формат. Формат представления адреса часто является строкой ASCII, а численный формат — это двоичное значение, входящее в структуру адреса сокета. #include int inet_pton(int family, const char *strptr, void *addrptr);
Возвращает: 1 в случае успешного выполнения функции: 0, если входная строка имела неверный формат представления; -1 в случае ошибки
const char *inet_ntop(int family, const void *addrptr,
char *strptr, size_t len);
Возвращает: указатель на результат, если выполнение функции прошло успешно. NULL в случае ошибки
Значением аргумента
family
для обеих функций может быть либо AF_INET
, либо AF_INET6
. Если family
не поддерживается, обе функции возвращают ошибку со значением переменной errno
, равным EAFNOSUPPORT
.Первая функция пытается преобразовать строку, на которую указывает
strptr
, сохраняя двоичный результат с помощью указателя addrptr
. При успешном выполнении ее возвращаемое значение равно 1. Если входная строка находится в неверном формате представления для заданного семейства (family
), возвращается нуль.Функция
inet_ntop
выполняет обратное преобразование: из численного формата (addrptr
) в формат представления (strptr
). Аргумент len
— это размер принимающей строки, который передается, чтобы функция не переполнила буфер вызывающего процесса. Чтобы облегчить задание этого размера, в заголовочный файл
включаются следующие определения:#define INET_ADDRSTRLEN 16 /* для точечно-десятичной записи IPv4-адреса */
#define INET6_ADDRSTRLEN 46 /* для шестнадцатеричной записи IPv6-адреса */
Если аргумент
len
слишком мал для хранения результирующего формата представления вместе с символом конца строки (terminating null), возвращается пустой указатель и переменной errno
присваивается значение ENOSPC
.Аргумент
strptr
функции inet_ntop
не может быть пустым указателем. Вызывающий процесс должен выделить память для хранения преобразованного значения и задать ее размер. При успешном выполнении функции возвращаемым значением является этот указатель.На рис. 3.5 приведена схема действия пяти функций, описанных в этом и предыдущем разделах.
Рис. 3.5. Функции преобразования адресов
Пример
Даже если ваша система еще не поддерживает IPv6, вы можете использовать новые функции, заменив вызовы вида
foo.sin_addr.s_addr = inet_addr(cp);
на
inet_pton(AF_INET, cp, &foo.sin_addr);
а также заменив вызовы вида
ptr = inet_ntoa(foo.sin_addr);
на
char str[INET_ADDRSTRLEN];
ptr = inet_ntop(AF_INET, &foo.sin_addr, str, sizeof(str));
В листинге 3.6 представлено простое определение функции
inet_pton
, поддерживающее только IPv4, а в листинге 3.7 — версия inet_ntop
, поддерживающая только IPv4.Листинг 3.6. Простая версия функции inet_pton, поддерживающая только IPv4
//libfree/inet_pton_ipv4.c
10 int
11 inet_pton(int family, const char *strptr, void *addrptr)
12 {
13 if (family == AF_INET) {
14 struct in_addr in_val;
15 if (inet_aton(strptr, &in_val)) {
16 memcpy(addrptr, &in_val, sizeof(struct in_addr));
17 return (1);
18 }
19 return (0);
20 }
21 errno = EAFNOSUPPORT;
22 return (-1);
23 }
Листинг 3.7. Простая версия функции inet_ntop, поддерживающая только IPv4
//libfree/inet_ntop_ipv4.c
8 const char *
9 inet_ntop(int family, const void *addrptr, char *strptr, size_t len)
10 {
11 const u_char *p = (const u_char*)addrptr;
12 if (family == AF_INET) {
13 char temp[INET_ADDRSTRLEN];
14 snprintf(temp, sizeof(temp), "%d.%d.%d.%d",
15 p[0], p[1], p[2], p[3]);
16 if (strlen(temp) >= len) {
17 errno = ENOSPC;
18 return (NULL);
19 }
20 strcpy(strptr, temp);
21 return (strptr);
22 }
23 errno = EAFNOSUPPORT;
24 return (NULL);
25 }
3.8. Функция sock_ntop и связанные с ней функции
Основная проблема, связанная с функцией
inet_ntop
, состоит в том, что вызывающий процесс должен передать ей указатель на двоичный адрес. Этот адрес обычно содержится в структуре адреса сокета, поэтому вызывающему процессу необходимо знать формат структуры и семейство адресов. Следовательно, чтобы использовать эту функцию, для IPv4 нужно написать код следующего вида:struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
или для IPv6 такого вида:
struct sockaddr_in6 addr6:
inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
Как видите, код становится зависящим от протокола.
Чтобы решить эту проблему, напишем собственную функцию и назовем ее
sock_ntop
. Она получает указатель на структуру адреса сокета, исследует эту структуру и вызывает соответствующую функцию для того, чтобы возвратить формат представления адреса.#include "unp.h"
char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen);
Возвращает: непустой указатель, если функция выполнена успешно, NULL в случае ошибки
sockaddr
указывает на структуру адреса сокета, длина которой равна значению addrlen
. Функция sock_ntop
использует свой собственный статический буфер для хранения результата и возвращает указатель на этот буфер.Формат представления — либо точечно-десятичная форма записи адреса IPv4, либо шестнадцатеричная форма записи адреса IPv6, за которой следует завершающий символ (мы используем точку, как в программе
netstat
), затем десятичный номер порта, а затем завершающий нуль. Следовательно, размер буфера должен быть равен как минимум INET_ADDRSTRLEN
плюс 6 байт для IPv4 (16 + 6 - 22) либо INET6_ADDRSTRLEN
плюс 6 байт для IPv6 (46 + 6 - 52).ПРИМЕЧАНИЕОбратите внимание, что при статическом хранении результата функция не допускает повторного вхождения (не является повторно входимой) и не может быть использована несколькими программными потоками (не является безопасной в многопоточной среде — thread-safe). Более подробно мы поговорим об этом в разделе 11.18. Мы допустили такое решение для этой функции, чтобы ее было легче вызывать из простых программ, приведенных в книге.
В листинге 3.8 представлена часть исходного кода, обрабатывающая семейство
AF_INET
.Листинг 3.8. Наша функция sock_ntop
//lib/sock_ntop.c
5 char *
6 sock_ntop(const struct sockaddr *sa, socklen_t salen)
7 {
8 char portstr[7];
9 static char str[128]; /* макс. длина для доменного сокета Unix */
10 switch (sa->sa_family) {
11 case AF_INET: {
12 struct sockaddr_in *sin = (struct sockaddr_in*)sa;
13 if (inet_ntop(AF_INET, &sin->sin_addr. str, sizeof(str)) == NULL)
14 return (NULL);
15 if (ntohs(sin->sin_port) != 0) {
16 snprintf(portstr, sizeof(portstr), ntohs(sin->sin_port));
17 strcat(str, portstr);
18 }
19 return (str);
20 }
Для работы со структурами адресов сокетов мы определяем еще несколько функций, которые упростят переносимость нашего кода между IPv4 и IPv6.
#include "unp.h"
int sock_bind_wild(int sockfd, int family);
Возвращает: 0 в случае успешного выполнения функции, -1 в случае ошибки
int sock_cmp_addr(const struct sockaddr *sockaddr1,
const struct sockaddr *sockaddr2, socklen_t addrlen);
Возвращает: 0, если адреса относятся к одному семейству и совпадают, ненулевое значение в противном случае
int sock_cmp_port(const struct sockaddr *sockaddr1,
const struct sockaddr *sockaddr2, socklen_t addrlen);
Возвращает: 0, если адреса относятся к одному семейству и порты совпадают, ненулевое значение в противном случае
int sock_get_port(const struct sockaddr *sockaddr, socklen_t addrlen);
Возвращает: неотрицательный номер порта для адресов IPv4 или IPv6, иначе -1
char *sock_ntop_host(const struct sockaddr *sockaddr, socklen_t addrlen);
Возвращает: непустой указатель в случае успешного выполнения функции, NULL в случае ошибки
void sock_set_addr(const struct sockaddr *sockaddr,
socklen_t addrlen, void *ptr);
void sock_set_port(const struct sockaddr *sockaddr,
socklen_t addrlen, int port);
void sock_set_wild(struct sockaddr *sockaddr, socklen_t addrlen);
Функция
sock_bind_wild
связывает универсальный адрес и динамически назначаемый порт с сокетом. Функция sock_cmp_addr
сравнивает адресные части двух структур адреса сокета, а функция sock_cmp_port
сравнивает номера их портов. Функция sock_get_port
возвращает только номер порта, а функция sock_ntop_host
преобразует к формату представления только ту часть структуры адреса сокета, которая относится к узлу (все, кроме порта, то есть IP-адрес узла). Функция sock_set_addr
присваивает адресной части структуры значение, указанное аргументом ptr
, а функция sock_set_port
задает в структуре адреса сокета только номер порта. Функция sock_set_wild
задает адресную часть структуры через символы подстановки. Как обычно, мы предоставляем для всех этих функций функции- обертки, которые возвращают значение, отличное от типа void, и в наших программах обычно вызываем именно обертки. Мы не приводим в данной книге исходный код для этих функций, так как он свободно доступен (см. предисловие).3.9. Функции readn, writen и readline
Потоковые сокеты (например, сокеты TCP) демонстрируют с функциями
read
и write
поведение, отличное от обычного ввода-вывода файлов. Функция read
или write
на потоковом сокете может ввести или вывести немного меньше байтов, чем запрашивалось, но это не будет ошибкой. Причиной может быть достижение границ буфера для сокета в ядре. Все, что требуется в этой ситуации — чтобы процесс повторил вызов функции read
или write
для ввода или вывода оставшихся байтов. (Некоторые версии Unix ведут себя аналогично при записи в канал (pipe) более 4096 байт.) Этот сценарий всегда возможен на потоковом сокете при выполнении функции read
, но с функцией write
он обычно наблюдается, только если сокет неблокируемый. Тем не менее вместо write
мы всегда вызываем функцию writen
на тот случай, если в данной реализации возможно возвращение меньшего количества данных, чем мы запрашиваем.Введем три функции для чтения и записи в потоковый сокет.
#include "unp.h"
ssize_t readn(int filedes, void *buff, size_t nbytes);
ssize_t writen(int filedes, const void *buff, size_t nbytes);
ssize_t readline(int filedes, void *buff, size_t maxlen);
Все функции возвращают: количество считанных или записанных байтов, -1 в случае ошибки
В листинге 3.9 представлена функция
readn
, в листинге 3.10 — функция writen
, а в листинге 3.11 — функция readline
.Листинг 3.9. Функция readn: считывание n байт из дескриптора
//lib/readn.c
1 #include "unp.h"
2 ssize_t /* Считывает n байт из дескриптора */
3 readn(int fd, void *vptr, size_t n)
4 {
5 size_t nleft;
6 ssize_t nread;
7 char *ptr;
8 ptr = vptr;
9 nleft = n;
10 while (nleft > 0) {
11 if ((nread = read(fd, ptr, nleft)) < 0) {
12 if (errno == EINTR)
13 nread = 0; /* и вызывает снова функцию read() */
14 else
15 return (-1);
16 } else if (nread == 0)
17 break; /* EOF */
18 nleft -= nread;
19 ptr += nread;
20 }
21 return (n - nleft); /* возвращает значение >= 0 */
22 }
Листинг 3.10. Функция writen: запись n байт в дескриптор
//lib/writen.c
1 #include "unp.h"
2 ssize_t /* Записывает n байт в дескриптор */
3 writen(int fd, const void *vptr, size_t n)
4 {
5 size_t nleft;
6 ssize_t nwritten;
7 const char *ptr;
8 ptr = vptr;
9 nleft = n;
10 while (nleft > 0) {
11 if ((nwritten = write(fd, ptr, nleft)) <= 0) {
12 if (errno == EINTR)
13 nwritten = 0; /* и снова вызывает функцию write() */
14 else
15 return (-1); /* ошибка */
16 }
17 nleft -= nwritten;
18 ptr += nwritten;
19 }
20 return (n);
21 }
Листинг 3.11. Функция readline: считывание следующей строки из дескриптора, по одному байту за один раз
//test/readline1.с
1 #include "unp.h"
/* Ужасно медленная версия, приводится только для примера */
2 ssize_t
3 readline(int fd, void *vptr, size_t maxlen)
4 {
5 ssize_t n, rc;
6 char c, *ptr;
7 ptr = vptr;
8 for (n = 1; n < maxlen; n++) {
9 again:
10 if ((rc = read(fd, &c, 1)) == 1) {
11 *ptr++ = c;
12 if (c == '\n')
13 break; /* записан символ новой строки, как в fgets() */
14 } else if (rc == 0) {
15 if (n == 1)
16 return (0); /* EOF, данные не считаны */
17 else
18 break; /* EOF, некоторые данные были считаны */
19 } else {
20 if (errno == EINTR)
21 goto again;
22 return (-1); /* ошибка, errno задается функцией read() */
23 }
24 }
25 *ptr = 0; /* завершаем нулем, как в fgets() */
26 return (n);
27 }
Если функция чтения или записи (
read
или write
) возвращает ошибку, то наши функции проверяют, не совпадает ли код ошибки с EINTR (прерывание системного вызова сигналом, см. раздел 5.9). В этом случае прерванная функция вызывается повторно. Мы обрабатываем ошибку в этой функции, чтобы не заставлять процесс снова вызвать read
или write
, поскольку целью наших функций является предотвращение обработки нехватки данных вызывающим процессом.В разделе 14.3 мы покажем, что вызов функции
recv
с флагом MSG_WAITALL
позволяет обойтись без использования отдельной функции readn
.Заметим, что наша функция
readline
вызывает системную функцию read
один раз для каждого байта данных. Это очень неэффективно, поэтому мы и написали в примечании «Ужасно медленно!». Возникает соблазн обратиться к стандартной библиотеке ввода-вывода (stdio
). Об этом мы поговорим через некоторое время в разделе 14.8, но учтите, что это может привести к определенным проблемам. Буферизация, предоставляемая stdio
, решает проблемы с производительностью, но при этом создает множество логистических сложностей, которые в свою очередь порождают скрытые ошибки в приложении. Дело в том, что состояние буферов stdio
недоступно процессу. Рассмотрим, например, строчный протокол взаимодействия клиента и сервера, причем такой, что могут существовать разные независимые реализации клиентов и серверов (достаточно типичное явление; например, множество веб-браузеров и веб-серверов были разработаны независимо в соответствии со спецификацией HTTP). Хороший стиль программирования заключается в том, что эти программы должны не только ожидать от своих собеседников соблюдения того же протокола, но и контролировать трафик на возможность получения непредвиденного трафика. Подобные нарушения протокола должны рассматриваться как ошибки, чтобы программисты имели возможность находить и устранять неполадки в коде, а также обнаруживать попытки взлома систем. Обработка некорректного трафика должна давать приложению возможность продолжать работу. Буферизация stdio
мешает достижению перечисленных целей, поскольку приложение не может проверить наличие непредвиденных (некорректных) данных в буферах stdio
в любой конкретный момент.Существует множество сетевых протоколов, основанных на использовании строк текста: SMTP, HTTP, FTP, finger. Поэтому соблазн работать со строками будет терзать вас достаточно часто. Наш совет: мыслить в терминах буферов, а не строк. Пишите код таким образом, чтобы считывать содержимое буфера, а не отдельные строки. Если же ожидается получение строки, ее всегда можно поискать в считанном буфере.
В листинге 3.12 приведена более быстрая версия функции
readline
, использующая свой собственный буфер (а не буферизацию stdio
). Основное достоинство этого буфера состоит в его открытости, благодаря чему вызывающий процесс всегда знает, какие именно данные уже приняты. Несмотря на это, использование readline
все равно может вызвать проблемы, как мы увидим в разделе 6.3. Системные функции типа select
ничего не знают о внутреннем буфере readline
, поэтому неаккуратно написанная программа с легкостью может очутиться в состоянии ожидания в вызове select
, при том, что данные уже будут находиться в буферах readline
. По этой причине сочетание вызовов readn
и readline
не будет работать так, как этого хотелось бы, пока функция readn
не будет модифицирована с учетом наличия внутреннего буфера.Листинг 3.12. Улучшенная версия функции readline
//lib/readline.c
1 #include "unp.h"
2 static int read_cnt;
3 static char *read_ptr;
4 static char read_buf[MAXLINE];
5 static ssize_t
6 my_read(int fd, char *ptr)
7 {
8 if (read_cnt <= 0) {
9 again:
10 if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
11 if (errno == EINTR)
12 goto again;
13 return(-1);
14 } else if (read_cnt == 0)
15 return(0);
16 read_ptr = read_buf;
17 }
18 read_cnt--;
19 *ptr = *read_ptr++;
20 return(1);
21 }
22 ssize_t
23 readline(int fd, void *vptr, size_t maxlen)
24 {
25 ssize_t n, rc;
26 char c, *ptr;
27 ptr = vptr;
28 for (n = 1; n < maxlen; n++) {
29 if ((rc = my_read(fd, &c)) == 1) {
30 *ptr++ = c;
31 if (c== '\n')
32 break; /* Записан символ новой строки, как в fgets() */
33 } else if (rc == 0) {
34 *ptr = 0;
35 return(n - 1); /* EOF, считано n-1 байт данных */
36 } else
37 return(-1); /* ошибка, read() задает значение errno */
38 }
39 *ptr = 0; /* завершающий нуль, как в fgets() */
40 return(n);
41 }
42 ssize_t
43 readlinebuf(void **vptrptr)
44 {
45 if (read_cnt)
46 *vptrptr = read_ptr;
47 return(read_cnt);
48 }
2-21
Внутренняя функция my_read
считывает до MAXLINE
символов за один вызов и затем возвращает их по одному.29
Единственное изменение самой функции readline
заключается в том, что теперь она вызывает функцию my_read
вместо read
.42-48
Новая функция readlinebuf
выдает сведения о состоянии внутреннего буфера, что позволяет вызывающим функциям проверить, нет ли в нем других данных, помимо уже принятой строки.ПРИМЕЧАНИЕК сожалению, использование переменных типа static в коде readline.c для поддержки информации о состоянии при последовательных вызовах приводит к тому, что функция больше не является безопасной в многопоточной системе (thread-safe) и повторно входимой (reentrant). Мы обсуждаем это в разделах 11.18 и 26.5. Мы предлагаем версию, безопасную в многопоточной системе, основанную на собственных данных программных потоков, в листинге 26.5.
3.10. Резюме
Структуры адресов сокетов являются неотъемлемой частью каждой сетевой программы. Мы выделяем для них место в памяти, заполняем их и передаем указатели на них различным функциям сокетов. Иногда мы передаем указатель на одну из этих структур функции сокета, и она сама заполняет поля структуры. Мы всегда передаем эти структуры по ссылке (то есть передаем указатель на структуру, а не саму структуру) и всегда передаем размер структуры в качестве дополнительного аргумента. Когда функция сокета заполняет структуру, длина также передается по ссылке, и ее значение может быть изменено функцией, поэтому мы называем такой аргумент «значение-результат» (value-result).
Структуры адресов сокетов являются самоопределяющимися, поскольку они всегда начинаются с поля
family
, которое идентифицирует семейство адресов, содержащихся в структуре. Более новые реализации, поддерживающие структуры адресов сокетов переменной длины, также содержат поле, которое определяет длину всей структуры.Две функции, преобразующие IP-адрес из формата представления (который мы записываем в виде последовательности символов ASCII) в численный формат (который входит в структуру адреса сокета) и обратно, называются
inet_pton
и inet_ntop
. Эти функции являются зависящими от протокола. Более совершенной методикой является работа со структурами адресов сокетов как с непрозрачными (opaque) объектами, когда известны лишь указатель на структуру и ее размер. Мы разработали набор функций sock_
, которые помогут сделать наши программы не зависящими от протокола. Создание наших не зависящих от протокола средств мы завершим в главе 11 функциями getaddrinfo
и getnameinfo
.Сокеты TCP предоставляют приложению поток байтов, лишенный маркеров записей. Возвращаемое значение функции read может быть меньше запрашиваемого, но это не обязательно является ошибкой. Чтобы упростить считывание и запись потока байтов, мы разработали три функции
readn
, writen
и readline
, которые и используем в книге. Однако сетевые программы должны быть написаны в расчете на работу с буферами, а не со строками.Упражнения
1. Почему аргументы типа «значение-результат», такие как длина структуры адреса сокета, должны передаваться по ссылке?
2. Почему и функция
readn
, и функция writen
копируют указатель void*
в указатель char*
?3. Функции
inet_aton
и inet_addr
характеризуются традиционно нестрогим отношением к тому, что они принимают в качестве точечно-десятичной записи адреса IPv4: допускаются от одного до четырех десятичных чисел, разделенных точками; также допускается задавать шестнадцатеричное число с помощью начального 0x
или восьмеричное число с помощью начального 0 (выполните команду telnet 0xe
, чтобы увидеть поведение этих функций). Функция inet_pton
намного более строга в отношении адреса IPv4 и требует наличия именно четырех чисел, разделенных точками, каждое из которых является десятичным числом от 0 до 255. Функция inet_pton
не разрешает задавать точечно- десятичный формат записи адреса, если семейство адресов — AF_INET6
, хотя существует мнение, что это можно было бы разрешить, и тогда возвращаемое значение было бы адресом IPv4, преобразованным к виду IPv6 (см. рис. А.6). Напишите новую функцию inet_pton_loose
, реализующую такой сценарий: если используется семейство адресов AF_INET
и функция inet_pton
возвращает нуль, вызовите функцию inet_aton
и посмотрите, успешно ли она выполнится. Аналогично, если используется семейство адресов AF_INET6
и функция inet_pton
возвращает нуль, вызовите функцию inet_aton
, и если она выполнится успешно, возвратите адрес IPv4, преобразованный к виду IPv6.Глава 4