Книга: Сетевое программирование. От основ до приложений
Назад: Глава 8. Управление сокетами
Дальше: Глава 10. Управление сетевыми интерфейсами

Глава 9. Вспомогательные данные

Невзирая на все отвлекающие факторы, которые несут в себе технологии, важно уметь превращать информацию в знание, знание — в понимание, а понимание — в мудрость.

Э. Дейсктра, выступление перед выпускниками Колледжа естественных наук Техасского университета в Остине, 1996

Введение

Включение некоторых опций, например IP_RECVORIGDSTADDR, вызывает отправку сообщений, которые можно получить, вызвав функцию recvmsg(). Это «вспомогательные данные», или ancillary data. Эта глава будет посвящена работе с такими данными. Мы узнаем о типах вспомогательных данных, их структуре и применении в практических сценариях.

Вспомогательные данные — это данные, которые были добавлены к основным данным и используют тот же вид транспорта, но при этом несут информацию, не относящуюся к основным данным пакета. Например, TTL передается в той же дейтаграмме, что и основные данные.

Иногда вспомогательные данные называют управляющими данными.

В рамках сокетов основное назначение вспомогательных данных — передача управляющей информации. Например, Linux использует механизм вспомогательных данных для передачи расширенных ошибок, опций IP или файловых дескрипторов через сокеты домена UNIX.

Важно, что эти данные проверяются ядром, тогда как обычные данные — это просто данные, полученные от другой конечной точки и не проверяемые никак, за исключением контрольной суммы, если она предусмотрена в протоколе.

Внимание! Не стоит думать, что «вспомогательные данные» не важны. В английском языке они называются ancillary data. Слово ancillary имеет значение «поддержка, необходимая для основной деятельности». Поэтому эти данные управляющие, а не вспомогательные.

Функции sendmsg() и recvmsg()

Функции sendmsg() и recvmsg() представляют собой полноценный API для отправки и приема данных, расширяющий возможности описанных ранее функций. Он может заменить любую функцию приема и передачи данных.

Главные отличия этих функций:

• Возможность отправлять несколько единиц данных протокола за один вызов, что может улучшить производительность в нагруженных приложениях.

• В качестве аргумента им передается не буфер, а структура, которая содержит массив буферов.

• С помощью этих функций можно принимать и отправлять вспомогательные данные.

Для нас в контексте главы особый интерес представляет последний пункт. Только эти функции дают возможность обмениваться вспомогательными данными.

Рассмотрим прототипы функций:

#include <sys/socket.h>

 

ssize_t sendmsg(int socket, const msghdr *message, int flags);

ssize_t recvmsg(int socket, msghdr *message, int flags);

Параметры функций sendmsg() и recvmsg():

socket — файловый дескриптор сокета.

• message — указатель на структуру msghdr.

flags — определяет тип передачи или приема сообщения. Приложение может указать 0 или флаги, объединенные через побитовое «или».

Флаги, общие для приема и передачи:

MSG_OOB — отправить или принять внеполосные данные.

• Флаги для передачи данных, такие как MSG_EOR, MSG_NOSIGNAL, MSG_DONTROUTE и MSG_CONFIRM, были рассмотрены при описании функций recvfrom() и sendto().

Флаги для приема данных:

• Флаг MSG_PEEK был описан для recvfrom().

• MSG_WAITALL — немного иначе работает в сокетах типа SOCK_STREAM. Он запрашивает блокировку функции до тех пор, пока не будет возвращен полный объем данных. Но функция может вернуть меньший объем данных в следующих случаях:

• Используется сокет на основе сообщений.

• Соединение было разорвано.

• Поступил сигнал.

• Был указан флаг MSG_PEEK.

• В сокете произошла ошибка.

MSG_ERRQUEUE — получить ошибки из очереди. Будет работать при установленной опции сокета IP_RECVERR.

В некоторых версиях Linux для recvmsg() также могут быть доступны следующие флаги:

MSG_CMSG_CLOEXEC — установить флаг CLOEXEC на дескриптор, полученный с помощью операции SCM_RIGHTS. Работает для сокетов AF_UNIX.

MSG_TRUNC — для raw-сокетов и сокетов Netlink вернуть реальную длину пакета, даже если она превышает размер буфера.

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

Функции возвращают количество отправленных или принятых байтов либо –1. Но успешное завершение вызова sendmsg() не гарантирует доставку сообщения.

Возвращаемое значение –1 указывает только на локальные ошибки.

Внимание! Проблема этих функций в том, что они не переносимые. Например, в ОС Windows они отсутствуют, но есть аналогичные, зависящие от сервис-провайдера функции WSASendMsg() и WSARecvMsg(). Подробно они будут рассмотрены в главе 18.

Структура msghdr, показанная на рис. 9.1, определена в sys/socket.h следующим образом:

// Элементы массива для приема и передачи.

struct iovec

{

    // Указатель на данные.

    void *iov_base;

    // Количество байтов для передачи.

    size_t iov_len;

};

 

struct msghdr

{

    // Необязательный адрес.

    void *msg_name;

    // Размер адреса.

    socklen_t msg_namelen;

    // Массив с данными.

    struct iovec *msg_iov;

    // Количество элементов в msg_iov.

    size_t msg_iovlen;

    // Вспомогательные данные.

    void *msg_control;

    // Размер буфера вспомогательных данных.

    size_t msg_controllen;

    // Флаги принятого сообщения.

    // Игнорируются функцией sendmsg() при отправке.

    int msg_flags;

};

В каждой структуре iovec поле iov_base указывает на буфер, а поле iov_len содержит размер буфера в байтах. Некоторые из этих размеров могут быть нулевыми.

Функции send() и sendto() в Linux — это оболочки для sendmsg(), которые создают структуру msghdr.

Например, реализация sendmsg() для UDP позволяет использовать один заголовок UDP для каждого вызова sendmsg().

Если вы беспокоитесь о производительности, sendmsg() не даст серьезного выигрыша в случае передачи только одного iovec. Однако если вы объединяете буферы в пользовательском пространстве, это потенциально может принести некоторый выигрыш, хотя он и будет достигнут за счет переносимости кода.

При отправке данных поле адреса msg_name игнорируется для сокетов, ориентированных на соединение. Но в некоторых ОС в этом случае передача может завершиться с ошибкой EISCONN, поэтому лучше установить адрес в nullptr.

Для дейтаграммных сокетов, если был вызван connect(), этот адрес переопределит указанный ранее.

В случае приема данных в буфер, если msg_name ненулевой, он должен указывать на буфер, который нужно предварительно выделить: туда будет записан адрес отправителя, а msg_namelen будет перезаписан длиной этого адреса.

Рис. 9.1. Структура iovec

Внимание! Если поле адреса задано, поле msg_namelen перед вызовом всегда должно быть равным размеру адреса как при отправке, так и при приеме данных.

Поле msg_iov содержит 0 или более буферов с отправляемыми данными либо заполняемых принимаемыми данными. Поле msg_iovlen должно быть установлено равным числу буферов в массиве.

Максимальное число буферов в некоторых ОС можно узнать, используя функцию sysconf(), описываемую в главе 13.

Вспомогательные данные передаются в поле msg_control, длина которого задается в msg_controllen.

Поле msg_flags действительно лишь в случае получения данных, sendmsg() его игнорирует. Устанавливается оно при успешном завершении recvmsg(). ­Содержимое поля — флаги, объединенные через побитовое ИЛИ:

MSG_EOR — принят конец записи. Этот флаг сбрасывается, если возвращаемые данные не завершают логическую запись. Флаг должен быть поддержан конкретным протоколом. Обычно используется с сокетами типа SOCK_SEQPACKET. TCP, например, его не поддерживает, так как это потоковый протокол.

• MSG_TRUNC — завершающая часть дейтаграммы была отброшена из-за того, что дейтаграмма была больше предоставленного буфера. То есть ядро имеет больше данных для возврата, чем процесс выделил в сумме всех буферов, иначе сумма всех iov_len меньше размера данных. Этот флаг устанавливается, только если были усечены обычные данные.

• MSG_CTRUNC — часть управляющих данных была отброшена из-за нехватки места для них в буфере. У ядра есть больше вспомогательных данных для возврата, чем размер буфера, выделенный процессом, — msg_controllen меньше размера вспомогательных данных.

• MSG_OOB — были получены внеполосные данные. Этот флаг никогда не возвращается для внеполосных данных TCP. Другие семейства протоколов, например протоколы OSI, могут его вернуть.

• MSG_ERRQUEUE — указывает, что вместо данных была получена расширенная ошибка из очереди ошибок сокета.

• MSG_BCAST — дейтаграмма была получена как широковещательная передача канального уровня или на широковещательный IP-адрес. Хороший способ определить, была ли UDP-дейтаграмма отправлена на широковещательный адрес.

• MSG_MCAST — возвращается, если дейтаграмма была получена как многоадресная рассылка канального уровня.

MSG_NOTIFICATON — возвращается получателям SCTP, чтобы указать, что сообщение прочитано.

Данные из каждой области хранения, указанной msg_iov, отправляются по очереди.

В случае потокового сокета это будет поток данных на стороне получателя.

В случае дейтаграммного сокета буферы соберутся в одну дейтаграмму или сообщение, которое будет отправлено вызовом.

Поэтому если в буфере сокета нет места для хранения передаваемого сообщения, а в файловом дескрипторе сокета не установлено значение O_NONBLOCK, функция sendmsg() блокируется до тех пор, пока не освободится место.

Если же в передающем сокете нет места для хранения сообщения, которое должно быть передано, и дескриптор файла сокета имеет установленный O_NONBLOCK, функция sendmsg() не будет работать и вернет ошибку. В errno будет записано EMSGSIZE.

В Python существует несколько методов класса socket.socket, выполняющих то же, что и описанные выше функции:

_CMSG: TypeAlias = tuple[int, int, bytes]

_CMSGArg: TypeAlias = tuple[int, int, ReadableBuffer]

 

if sys.platform != "win32":

    # Функция сама выделит буфер.

    def recvmsg(self, bufsize: int, ancbufsize: int, flags: int) ->

        tuple[bytes, list[_CMSG], int, Any]

 

    # Принять данные в предварительно выделенный буфер.

    def recvmsg_into(self, buffers: Iterable[WriteableBuffer],

                     ancbufsize: int, flags: int) ->

        tuple[int, list[_CMSG], int, Any]

 

    def sendmsg(

        self,

        buffers: Iterable[typing.Buffer],

        ancdata: Iterable[_CMSGArg],

        flags: int,

        address: _Address,

    ) -> int

 

if sys.platform == "linux":

    # Требуется для работы с криптографией в ядре Linux.

    def sendmsg_afalg(

        self, msg: Iterable[typing.Buffer], *, op: int,

        iv: Any, assoclen: int, flags: int

    ) -> int

Видим, что эти методы не определены для ОС Windows. Но при необходимости из Python можно напрямую использовать упомянутые выше функции WSASendMsg() и WSARecvMsg().

В CPython данные методы на момент выхода книги могут быть реализованы и в ОС Windows.

Типы буфера для recvmsg_into() такие же, как и для recv_into(). Разница в том, что в рассматриваемом случае передается список буферов.

Параметр ancbufsize, задающий размер буфера вспомогательных данных, является обязательным. Получить его можно, используя CMSG-функции. О них мы поговорим далее. Остальные аргументы, в принципе, соответствуют аргументам функций C API.

Метод socket.sendmsg_afalg() используется для работы с криптографическими функциями ядра Linux через сокеты семейства AF_ALG. Он не имеет отношения к сети и в книге не рассматривается.

Обмен вспомогательными данными

Внимание! Желательно всегда передавать функции хотя бы один буфер размером хотя бы в один байт, то есть один символ, под обычные данные, даже если принимаются только вспомогательные данные. Иначе в некоторых ОС, например в определенных версиях MacOS X, можно получить ошибку.

Как было показано выше, управляющие данные можно отправлять и получать, используя поля msg_control и msg_controllen структуры msghdr.

Поле msg_control указывает на буфер для вспомогательных данных. Когда вызывается recvmsg() или sendmsg(), msg_controllen должен содержать длину буфера msg_control, включая заголовок в байтах.

При успешном завершении recvmsg() поле будет содержать длину последовательности управляющих сообщений.

Внимание! Если пространство, выделенное для приема входящих служебных данных, слишком мало, служебные данные усекаются до количества заголовков, которые уместятся в предоставленном буфере, и в msg.msg_flags устанавливается флаг MSG_CTRUNC.

Сообщения имеют вид:

struct cmsghdr

{

    // Количество байтов данных, включая заголовок.

    size_t cmsg_len;

    // Исходный протокол.

    int cmsg_level;

    // Тип, зависящий от протокола.

    int cmsg_type;

    // После заголовка идут данные:

    // unsigned char cmsg_data[];

};

В буфере находится сразу несколько таких сообщений, предваряемых заголовками, как показано на рис. 9.2.

Видим, что заголовок дополняется «полем» pad, то есть нулями, для того чтобы данные были расположены по выровненному адресу. Для получения данных это надо учитывать.

Рис. 9.2. Вспомогательные данные

Чтобы не вычислять адрес самостоятельно, для работы со вспомогательными данными используются макросы, описанные в man 3 cmsg и man 3 cmsg_space:

#include <sys/socket.h>

 

// Движение по заголовкам данных.

cmsghdr *CMSG_FIRSTHDR(msghdr *msgh);

cmsghdr *CMSG_NXTHDR(msghdr *msgh, cmsghdr *cmsg);

 

// Определить выравнивание.

size_t CMSG_ALIGN(size_t length);

// Определить необходимое пространство в буфере для блока.

size_t CMSG_SPACE(size_t length);

// Определить длину элемента.

size_t CMSG_LEN(size_t length);

 

// Получить указатель на первый байт данных.

unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

Не работайте напрямую с последовательностью структур cmsghdr. Всегда используйте макросы:

CMSG_FIRSTHDR() — получить указатель на первый заголовок cmsghdr в буфере вспомогательных данных, связанном с переданной структурой msghdr. Если данных нет — возвращает нулевой указатель.

CMSG_NXTHDR() — получить указатель на следующий заголовок cmsghdr. Макросу передается структура msghdr и структура cmsghdr, возвращенная CMSG_FIRSTHDR или предыдущим вызовом CMSG_NXTHDR. Если заголовков больше нет или в буфере недостаточно места, возвращает нулевой указатель.

CMSG_ALIGN() — округлить длину сообщения в большую сторону, до выровненного.

CMSG_SPACE() — получить размер буфера, который займет элемент, принятый через recvmsg(), включая дополнение в конце до выравнивания и заголовок. Размер буфера, который требуется для приема нескольких элементов, будет равен сумме значений, полученных из нескольких вызовов данного макроса.

• CMSG_LEN() — получить выровненную длину элемента вспомогательных данных для сохранения в поле cmsg_len структуры cmsghdr. Значение будет всегда меньше или равным CMSG_SPACE() для одинакового аргумента. Его аргумент — длина данных.

CMSG_DATA() — получить указатель на начало данных, идущих после заголовка cmsghdr. Данные по указателю не выровнены, и поэтому не на всех платформах может быть обеспечен доступ к каждому полю структуры. Приложения не должны приводить его к типу указателя, соответствующему типу полезной нагрузки. Следует копировать данные, например, через std::copy() или memcpy(), в объект либо из него.

В разных ОС макросы CMSG_SPACE и CMSG_LEN могут значительно различаться. Поэтому не стоит заменять один другим.

Ниже приведен упрощенный пример для Linux из /usr/include/bits/socket.h:

#define CMSG_DATA(cmsg) ((unsigned char *)((struct cmsghdr *)(cmsg) + 1))

 

#define CMSG_NXTHDR(mhdr, cmsg) __cmsg_nxthdr(mhdr, cmsg)

 

#define CMSG_FIRSTHDR(mhdr)                                           \

    ((size_t)(mhdr)->msg_controllen >= sizeof(struct cmsghdr)         \

    ? (struct cmsghdr *) (mhdr)->msg_control : (struct cmsghdr *) 0)

 

#define CMSG_ALIGN(len) (((len) + sizeof(size_t) — 1) \

    & (size_t) ~(sizeof(size_t) — 1))

 

#define CMSG_SPACE(len) (CMSG_ALIGN(len) \

    + CMSG_ALIGN(sizeof(struct cmsghdr)))

 

#define CMSG_LEN(len) (CMSG_ALIGN(sizeof(struct cmsghdr)) + (len))

CMSG_LEN использует выравнивание только для заголовка, тогда как CMSG_SPACE возвращает сумму выровненных размеров заголовка и длины, то есть размер буфера может быть потенциально больше, чем значение, возвращаемое CMSG_LEN.

Внимание! Для получения данных скопируйте их в предварительно выделенный объект. Не пытайтесь обращаться к ним по возвращенному указателю! На SPARC, PowerPC, DEC Alpha прямой доступ без копирования не будет работать. Скорее всего, на этих машинах будут работать ОС AIX и Solaris. Но в случае Linux вы также можете получить SIGBUS и аварийное завершение приложения.

В Python часть вышеуказанных макросов представлена следующими функциями модуля socket:

def CMSG_LEN(length: int) -> int

def CMSG_SPACE(length: int) -> int

Они могут вызвать исключение OverflowError, если значение длины выходит за размеры буфера. Остальные функции в Python не требуются: работа со вспомогательными данными реализована достаточно прозрачно.

Прием вспомогательных данных

Рассмотрим пример на C++, устанавливающий опцию IP_RECVTTL, в результате чего должны быть приняты не только данные получаемой дейтаграммы, но и ее TTL.

Код хорошо иллюстрирует вызов recvmsg(), использование макросов и обход вспомогательных данных с целью поиска необходимого сообщения.

Создадим сокет, привяжем к адресу и установим на сокет требуемую опцию:

const socket_wrapper::SocketWrapper sock_wrap;

 

try

{

    assert(argv[1]);

    // Создать UDP-сокет и привязать адрес.

    auto servinfo = socket_wrapper::get_serv_info(argv[1], SOCK_DGRAM);

 

    socket_wrapper::Socket sock = {servinfo->ai_family, servinfo->ai_socktype,

                                   servinfo->ai_protocol};

 

    if (bind(sock, servinfo->ai_addr, servinfo->ai_addrlen) < 0)

    {

 

        throw std::system_error(sock_wrap.get_last_error_code(),

                                std::system_category(), "bind");

    }

 

    // Значение опции IPRECV_TTL — включено.

    int recv_ttl = 1;

 

    // Включить опцию IP_RECVTTL.

    if (setsockopt(sock, IPPROTO_IP, IP_RECVTTL, &recv_ttl,

                   sizeof(recv_ttl)) != 0)

    {

        throw std::system_error(sock_wrap.get_last_error_code(),

                                std::system_category(), "Set IP_RECVTTL");

    }

Размер выделенного буфера — 4 байта, поскольку в setsockopt() для передачи флагов, как правило, используется int. Поэтому безопасно выделять буфер именно такого размера.

Максимальный размер буфера является конфигурируемым. В Linux он задается в /proc/sys/net/core/optmem_max. В современных версиях ядра по умолчанию он равен примерно 20 Кбайт.

Определим размер буфера для обычных и вспомогательных данных:

    // Буфер для обычных данных.

    std::array<char, 255> data_buff;

 

    // Буфер для вспомогательных данных.

    std::array<char, CMSG_SPACE(sizeof(uint32_t))> ancil_data_buff;

 

    // Буфер для основных данных пока один: указатель на выделенный буфер

    // и его размер в байтах.

    iovec iov[1] = { { &data_buff[0], data_buff.size() } };

 

    // Необходимо заполнить структуру msgh.

    msghdr msgh =

    {

        .msg_name = nullptr, .msg_namelen = 0,

        // Буферы для основных данных и количество этих буферов.

        .msg_iov = iov, .msg_iovlen = 1,

        // Буфер вспомогательных данных и его размер в байтах.

        .msg_control = &ancil_data_buff[0],

        .msg_controllen = ancil_data_buff.size(),

        .msg_flags = 0

    };

В цикле осуществим прием данных, используя recvmsg(), после чего обойдем блоки вспомогательных данных, если таковые имеются:

    // Принять данные.

    for (ssize_t n = recvmsg(sock, &msgh, 0); n; n = recvmsg(sock, &msgh, 0))

    {

        if (n < 0)

        {

             throw std::system_error(sock_wrap.get_last_error_code(),

                                     std::system_category(), "recvmsg");

        }

 

        std::cout

            << n

            << " bytes was read: "

            << std::string(data_buff.begin(), data_buff.begin() + n)

            << std::endl;

 

        // Указатель на управляющие данные.

        cmsghdr *cmsg = nullptr;

 

        // Идти по элементам управляющих данных.

        for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg != nullptr;

             cmsg = CMSG_NXTHDR(&msgh, cmsg))

        {

            // Как было сказано выше, установка опции IP_RECVTTL

            // будет приводить к отправке сообщения

            // IP_TTL, передаваемого со вспомогательными данными.

            if (IPPROTO_IP == cmsg->cmsg_level && IP_TTL == cmsg->cmsg_type)

            {

                uint32_t received_ttl = 0;

                // Помним, что сообщение необходимо скопировать.

                std::copy(CMSG_DATA(cmsg),

                          CMSG_DATA(cmsg) + sizeof(received_ttl),

                          &received_ttl);

 

                std::cout

                    << "IP_RECVTTL was received: " << received_ttl

                    << std::endl;

 

                break;

            }

        }

Макрос CMSG_FIRSTHDR() инициализирует структуру cmsg в цикле первым управляющим сообщением и CMSG_NXTHDR() для получения всех последующих.

Указатель на данные получаем, используя макрос CMSG_DATA(), после чего копируем данные, как было сказано выше. Только после этого к ним можно осуществлять доступ.

Если нужных элементов в списке не оказалось, мы увидим, что cmsg имеет нулевое значение:

        if (nullptr == cmsg)

        {

            // Цикл в конце списка, и CMSG_NXTHDR() вернул nullptr,

            // а сообщения IP_TTL не было.

            // Значит, произошла ошибка.

            throw std::system_error(sock_wrap.get_last_error_code(),

                                    std::system_category(),

                                    "IP_RECVTTL receiving");

 

        }

        break;

    }

}

Запустим Netcat и отправим строку на сервер:

nc -u localhost 12345

RECV_TTL testing!

В результате увидим, что пришло значение TTL, равное 64:

build/bin/b01-ch09-ancil-data-recv 12345

18 bytes was read: RECV_TTL testing!

 

IP_RECVTTL was received: 64

Если отключить опцию IP_RECVTTL, сообщение приходить не будет.

Стоит помнить, что перед каждым вызовом recvmsg() необходимо правильно инициализировать поля структуры, ей передаваемой.

Аналогичный код на Python:

import socket

import sys

 

# Не все константы есть в модуле socket, некоторые приходится объявлять.

IP_RECVTTL = 12

 

if len(sys.argv) < 2:

    print(f'{sys.argv[0]} <port>')

    sys.exit(1)

 

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

    sock.bind(('', int(sys.argv[1])))

 

    # Установить опцию IP_RECVTTL.

    sock.setsockopt(socket.IPPROTO_IP, IP_RECVTTL, 1)

 

    ttl = 0

    # Поле TTL 32-битное, то есть имеет размер 4 байта.

    ttl_size = 4

 

    # 255 — произвольное значение буфера для обычных данных.

    # Минимальный размер буфера для вспомогательных данных всегда нужно

    # рассчитывать через CMSG_LEN.

    # В Python нет структуры msghdr, поэтому вернется кортеж.

    msg, ancdata, flags, addr = sock.recvmsg(255, socket.CMSG_LEN(ttl_size))

 

    print(f'{len(msg)} bytes was read: {msg.decode()}')

    # Вспомогательные данные представлены списком кортежей.

    for cmsg_level, cmsg_type, cmsg_data in ancdata:

        if cmsg_level == socket.IPPROTO_IP and socket.IP_TTL == cmsg_type:

            # То, что в C API делает макрос CMSG_DATA().

            ttl_pointer = cmsg_data[:len(cmsg_data) -

                                    (len(cmsg_data) % ttl_size)]

            # Получить целое значение из байтов.

            # Порядок байтов указан как LE, поскольку значения в полях

            # дейтаграммы представлены в сетевом порядке.

            ttl = int.from_bytes(ttl_pointer, byteorder='little')

            print(f'IP_RECVTTL was received: {ttl}')

            break

Прием вспомогательных данных на потоковых сокетах «разрывает поток». То есть когда вспомогательные и обычные данные были отправлены за несколько вызовов sendmsg(), даже если recvmsg() сможет принять их в буфер сразу, он все равно будет возвращать управление каждый раз после приема вспомогательных данных, переданных в отдельном вызове sendmsg(). Данные, переданные вместе со вспомогательными, могут быть записаны в буфер сразу.

Создание и отправка вспомогательных данных

Внимание! При отправке вспомогательных данных должен быть отправлен хотя бы один байт обычных данных. В Linux это необходимо для успешной отправки вспомогательных данных через потоковый сокет домена UNIX.

При отправке вспомогательных данных через дейтаграммный сокет домена UNIX в Linux такой необходимости нет. Однако переносимые приложения должны отправлять по крайней мере один байт реальных данных при отправке вспомогательных данных через дейтаграммные сокеты.

Иногда пользователю требуется не только принимать вспомогательные данные от сетевой подсистемы, но и отправлять самому. В главе 8 был рассмотрен пример с CRIU. Однако классический сценарий — отправка дескрипторов в процесс, связанный через сокет PF_UNIX. Мы рассмотрим его ниже, в разделе об управляющих данных Unix-сокетов.

Отправка выполняется через функцию sendmsg(), и при работе с ней важно правильно использовать CMSG-макросы.

Для получения значения длины отправляемых данных используется макрос CMSG_LEN(), а для установки значения поля msg_controllen — макрос CMSG_SPACE():

// Некий дескриптор будет играть роль отправляемых данных.

int fd = open(...);

 

// Заголовок блока сообщений.

msghdr msg = { 0 };

 

// Буфер с "данными".

std::vector<char> buf(1);

 

// Создание буфера для вспомогательных данных.

// Размер сообщения учитывает дополнение.

// Поэтому используется CMSG_SPACE.

std::vector<char> ancil_buf(CMSG_SPACE(sizeof(fd)));

 

...

 

msg.msg_control = &ancil_buf[0];

 

// Установка размера:

msg.msg_controllen = CMSG_SPACE(sizeof(fd));

// Или так:

msg.msg_controllen = ancil_buf.size();

// Заголовок первого сообщения.

cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);

 

// Установить различные параметры.

cmsg->cmsg_level = SOL_SOCKET;

cmsg->cmsg_type = SCM_RIGHTS;

 

// В сообщении отправляется дескриптор, для чего получаем его размер.

cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

 

// Сюда будут записаны данные.

*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = fd;

 

// Отправить данные.

if (sendmsg(socket, &msg, 0) < 0)

    ...

Вспомогательные данные Internet-сокетов

Для приема следующих сообщений в cmsg_level необходимо установить уровень IPPROTO_IP:

IP_ORIGDSTADDR — исходный адрес получателя дейтаграммы. Сообщение содержит структуру адреса sockaddr_in. Отправку включает опция IP_RECVORIGDSTADDR.

• IP_PKTINFO — содержит информацию о входящем пакете. Одноименная опция включает отправку данного сообщения. Применима только для сокетов, ориентированных на дейтаграммы. Сообщение содержит структуру in_pktinfo, описанную ниже. Отправка этого сообщения включается одноименной опцией.

• IP_RECVERR — в случае ошибки сокета можно получить расширенную информацию, вызвав recvmsg() с установленным флагом MSG_ERRQUEUE. Будет возвращен массив ошибок из структур sock_extended_err.

• SCM_SECURITY — контекст безопасности SELinux однорангового сокета. Данные представляют собой строку с завершающим нулем, содержащую контекст безопасности. Получатель должен выделить для этих данных не менее NAME_MAX байт в части данных вспомогательного сообщения. Отправку включает опция SO_PASSSEC.

IP_TOS — содержит байт с полем «тип обслуживания» из заголовка IP-пакета. Будет принят через recvmsg(), если на сокете включена опция IP_RECVTOS. Значение TOS может быть установлено через опцию IP_TOS.

Структура для получения информации о входящем пакете:

struct in_pktinfo

{

    // Индекс сетевого интерфейса.

    unsigned int ipi_ifindex;

    // Локальный адрес.

    struct in_addr ipi_spec_dst;

    // Адрес назначения из заголовка.

    struct in_addr ipi_addr;

};

Структура ошибки:

#define SO_EE_ORIGIN_NONE    0

#define SO_EE_ORIGIN_LOCAL   1

#define SO_EE_ORIGIN_ICMP    2

#define SO_EE_ORIGIN_ICMP6   3

 

struct sock_extended_err

{

    // Номер ошибки в очереди.

    uint32_t ee_errno;

    // Код происхождения ошибки: SO_EE_ORIGIN_ICMP или SO_EE_ORIGIN_LOCAL.

    uint8_t ee_origin;

    // Тип.

    uint8_t ee_type;

    // Код.

    uint8_t ee_code;

    // Нулевое заполнение.

    uint8_t ee_pad;

    // Дополнительная информация.

    uint32_t ee_info;

    // Прочие данные.

    uint32_t ee_data;

    // Дальше могут следовать еще данные.

};

 

// Указатель на адрес узла.

struct sockaddr *SO_EE_OFFENDER(struct sock_extended_err *);

Значения полей, кроме ee_errno и ee_origin, зависят от протокола:

ee_origin имеет значение:

• SO_EE_ORIGIN_ICMP для ошибок, полученных в виде пакета ICMP;

• SO_EE_ORIGIN_LOCAL для локально сгенерированных ошибок.

• ee_type и ee_code — устанавливаются из полей типа и кода заголовка ICMP.

• ee_info — содержит обнаруженный MTU для ошибок EMSGSIZE.

• Макрос SO_EE_OFFENDER возвращает указатель на адрес узла, вызвавшего ошибку, и этот адрес — структура sockaddr_in. Поле sin_family адреса имеет значение AF_UNSPEC, когда источник неизвестен.

Когда ошибка произошла в сети, все параметры IP, такие как IP_OPTIONS, IP_TTL, разрешенные для сокета и содержащиеся в пакете ошибки, передаются в качестве управляющих сообщений. При этом полезная нагрузка пакета все равно будет получена.

TCP не имеет очереди ошибок, поэтому сообщение MSG_ERRQUEUE нельзя использовать для сокетов SOCK_STREAM. Опция IP_RECVERR действительна для TCP, но все ошибки возвращаются в errno после вызова функций или при получении значения опции сокета SO_ERROR.

Для raw-сокетов IP_RECVERR разрешает передачу всех полученных ошибок ICMP в приложение, в противном случае сообщения об ошибках передаются только для подключенных сокетов.

Вспомогательные данные Unix-сокетов

Внимание! Если вы получаете любые управляющие данные из Unix-сокетов через recvmsg(), всегда обрабатывайте сообщение SCM_RIGHTS, даже если не используете его данные, и закрывайте все отправленные дескрипторы. Иначе атакующий субъект может забить таблицу дескрипторов вашего процесса.

Как и опции, вспомогательные типы сообщений имеют уровень SOL_SOCKET в cmsg_level по историческим причинам.

Рассмотрим эти сообщения:

SCM_RIGHTS — отправить или получить набор дескрипторов открытых файлов от другого процесса. Если буфер, используемый для приема вспомогательных данных, содержащих файловые дескрипторы, слишком мал, вспомогательные данные усекаются или отбрасываются, а лишние файловые дескрипторы автоматически закрываются. Если процесс превысит лимит по числу открытых дескрипторов — RLIMIT_NOFILE, лишние дескрипторы будут закрыты. В ядре есть константа SCM_MAX_FD со значением примерно 255, определяющая максимальное ограничение на количество файловых дескрипторов в массиве. Попытка отправить массив больше этого предела вызовет ошибку. Пример для этого сообщения был приведен выше.

• SCM_CREDENTIALS — отправить или получить учетные данные UNIX. Можно использовать для аутентификации: данные, указанные отправителем, проверяются ядром. Учетные данные передаются как структура ucred, определенная в sys/socket.h. Чтобы получить сообщение, на сокете должна быть включена опция SO_PASSCRED.

• SCM_SECURITY — получить контекст безопасности SELinux однорангового сокета. Описано в опциях Internet-сокетов.

Префикс "SCM_" означает Socket Control Message.

Структура для сообщения SCM_CREDENTIALS:

struct ucred

{

    // Идентификатор процесса-отправителя.

    pid_t pid;

    // Идентификатор пользователя процесса-отправителя.

    uid_t uid;

    // Идентификатор группы процесса-отправителя.

    gid_t gid;

};

Отправка и получение файловых дескрипторов

Отправлять и получать файловые дескрипторы может быть нужно, например, для того, чтобы передать часть работы с клиентами сервера порожденному для параллельной обработки процессу или в случае, когда сервер получает клиентский сокет через accept() и хочет делегировать работу с клиентом дочернему процессу.

Дескриптор, в том числе сокета, представляет собой индекс в таблице, содержащей ссылки на объекты ядра и некоторые флаги. У каждого процесса своя таблица дескрипторов и свой набор дескрипторов, которые имеют значение только в его границах, что видно на рис. 9.3.

Рис. 9.3. Таблицы дескрипторов и файлов

Можно создавать новый дескриптор одного и того же объекта, управлять наследованием дескрипторов и передавать дескрипторы из процесса в процесс, работающий в том же экземпляре ОС. В Python, например, для этого существуют методы сокета:

def socket.dup() -> socket.socket

def socket.get_inheritable() -> bool

def socket.set_inheritable(inheritable: bool)

Метод dup() дублирует сокет — дубликат не будет наследоваться в потомках. Оставшиеся два метода служат для получения и установки флага наследования.

Передавать дескрипторы между разными процессами может только операционная система. Она создает новые индексы в таблице дескрипторов процесса, которому дескрипторы отправляются, и связывает их с объектами ядра, с которым связаны дескрипторы в отправляющем процессе.

В Python для обмена дескрипторами реализованы функции в модуле socket:

# Отправить дескрипторы.

def send_fds(sock, buffers, fds[, flags[, address]]) -> int

# Принять дескрипторы.

def recv_fds(sock, bufsize, maxfds[, flags]) -> \

    (data, file_descriptors, msg_flags, address)

Посмотрим на их код:

def send_fds(sock, buffers, fds, flags=0, address=None):

    """ send_fds(sock, buffers, fds[, flags[, address]]) -> integer

 

    Отправляет список файловых дескрипторов поверх сокета AF_UNIX.

    """

 

    # Дескрипторы — массив целых чисел.

    return sock.sendmsg(buffers, [(_socket.SOL_SOCKET, _socket.SCM_RIGHTS,

                                  array.array("i", fds))])

Для отправки дескрипторов используется операция, инициируемая сообщением SCM_RIGHTS.

def recv_fds(sock, bufsize, maxfds, flags=0):

    """ recv_fds(sock, bufsize, maxfds[, flags]) ->

        (data, list of file descriptors, msg_flags, address)

 

    Принимает до maxfds файловых дескрипторов, возвращает данные и список

    дескрипторов.

    """

 

    # Массив целых чисел.

    fds = array.array("i")

    msg, ancdata, flags, addr = sock.recvmsg(bufsize,

                                             _socket.CMSG_LEN(maxfds *

                                                              fds.itemsize))

    for cmsg_level, cmsg_type, cmsg_data in ancdata:

        if (cmsg_level == _socket.SOL_SOCKET and

            cmsg_type == _socket.SCM_RIGHTS):

            # Создание массива целых чисел из массива байтов.

            fds.frombytes(cmsg_data[:len(cmsg_data) -

                                    (len(cmsg_data) % fds.itemsize)])

 

    return msg, list(fds), flags, addr

В C++ процесс отправки данных чуть сложнее.

Сначала необходимо подготовить данные, как показано на рис. 9.4:

1. Создать буфер, размер которого равен сумме CMSG_SPACE() для всех управляющих сообщений, которые необходимо отправить.

2. Поле msghdr.msg_controllen нужно установить равным размеру буфера.

3. Используя макросы CMSG_FIRSTHDR() и CMSG_NXTHDR(), пройти по всем заголовкам cmsg, инициализируя:

• поля cmsg_level и cmsg_type;

• поле cmsg_len, используя для получения его значения макрос CMSG_LEN() от длины отправляемых данных;

• другие поля заголовка cmsghdr и данные возможно инициализировать через макрос CMSG_DATA().

Рис. 9.4. Подготовка буфера к отправке

Затем подготовленный буфер нужно передать функции sendmsg(). Нам потребуется вспомогательная функция, которая вернет по номеру дескриптора путь. Реализуем ее через обращение к /proc:

std::filesystem::path get_path_from_fd(int fd)

{

    char filename[PATH_MAX];

 

    // Все дескрипторы процесса есть в /proc, о которой будет рассказано

    // в главе 13.

    const std::string link = "/proc/self/fd/" + std::to_string(fd);

    // Дескриптор в /proc — симлинк, который ссылается на реальный файл

    // или указывает на специальный, однако это не относится

    //к нашему сценарию.

    // Читая симлинк, получаем имя файла.

    if (readlink(link.c_str(), filename, sizeof(filename)) < 0)

        throw std::system_error(errno, std::system_category(), "readlink");

 

    return std::filesystem::path(filename);

}

О /proc будет рассказано подробнее в главе 13.

Теперь реализуем отправку дескриптора. Отправим только одиночный дескриптор, но массив дескрипторов отправляется аналогично:

void send_descriptors(int socket, int fd)

{

    msghdr msg = {};

 

    // Буфер с "данными".

    std::array<char, 1> buf;

 

    // Создание буфера для вспомогательных данных.

    std::array<char, CMSG_SPACE(sizeof(fd))> ancil_buf;

 

    iovec io = { .iov_base = &buf[0], .iov_len = buf.size() };

 

    msg.msg_iov = &io;

    msg.msg_iovlen = 1;

    msg.msg_control = &ancil_buf[0];

    msg.msg_controllen = ancil_buf.size();

 

    cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);

    cmsg->cmsg_level = SOL_SOCKET;

    cmsg->cmsg_type = SCM_RIGHTS;

 

    // Отправить единственный дескриптор, для чего получить его размер.

    cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

    // Он будет записан сюда.

    *reinterpret_cast<int *>(CMSG_DATA(cmsg)) = fd;

    // Отправка.

    if (sendmsg(socket, &msg, 0) < 0)

        throw std::system_error(errno, std::generic_category(), "sendmsg");

}

Рассмотрим функцию для приема дескрипторов. Она принимает данные и управляющую информацию через recvmsg():

int receive_descriptors(int socket)

{

    msghdr msgh = {};

 

    std::array<char, 255> buffer;

    iovec io = { .iov_base = &buffer[0], .iov_len = buffer.size() };

    msgh.msg_iov = &io;

    msgh.msg_iovlen = 1;

 

    std::array<char, buffer_size> c_buffer;

    msgh.msg_control = &c_buffer[0];

    msgh.msg_controllen = c_buffer.size();

 

    // Принять сообщение, которое содержит обычные и вспомогательные данные.

    if (recvmsg(socket, &msgh, 0) < 0)

        throw std::system_error(errno, std::generic_category(), "recvmsg");

 

    int received_descriptor = -1;

Обрабатывает вспомогательные данные в цикле:

    // Обработка вспомогательных данных.

    for (cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh); cmsg != nullptr;

         cmsg = CMSG_NXTHDR(&msgh, cmsg))

    {

        if (SOL_SOCKET == cmsg->cmsg_level && SCM_RIGHTS == cmsg->cmsg_type)

        {

            if (received_descriptor < 0)

            {

                // Помним, что безопаснее выполнить копирование.

                std::copy(CMSG_DATA(cmsg), CMSG_DATA(cmsg) +

                          sizeof(received_descriptor),

                          &received_descriptor);

 

                std::cout

                    << "Descriptor was received: " << received_descriptor

                    << std::endl;

            }

            else

            {

                // Если пришли лишние дескрипторы, закрыть их.

                int fd;

                std::copy(CMSG_DATA(cmsg), CMSG_DATA(cmsg) + sizeof(fd), &fd);

                close(fd);

            }

        }

    }

 

    if (-1 == received_descriptor)

    {

        throw std::logic_error("Descriptor receiving error");

    }

 

    return received_descriptor;

}

Процесс-родитель, к примеру, создает файл для чтения и записи, после чего отправляет его дескриптор потомку:

void parent(int sock)

{

    std::cout << "Parent started" << std::endl;

    // Пусть это будет временный файл в /tmp.

    char f_tmpl[] = "/tmp/descr_send_exampleXXXXXX";

    // Функция возвращает дескриптор открытого временного файла.

    int file_fd = mkstemp(f_tmpl);

 

    if (file_fd < 0)

        throw std::system_error(errno, std::generic_category(),

                                "Parent opening local file");

    // По дескриптору получим путь к файлу.

    const std::filesystem::path local_file(

        std::move(get_path_from_fd(file_fd)));

 

    std::cout

        << "Parent opened file with descriptor = " << file_fd

        << " [" << local_file << "]"

        << std::endl;

    // Отправим этот дескриптор потомку.

    send_descriptors(sock, file_fd);

    // Сразу закроем файл и удалим. В реальном приложении с ним

    // будет выполняться какая-то работа.

    close(file_fd);

    std::filesystem::remove(local_file);

 

    std::cout << "Parent exited" << std::endl;

}

В свою очередь, потомок использует дескриптор, чтобы прочитать данные:

void child(int sock)

{

    std::cout << "Child" << std::endl;

    // Получить дескриптор.

    int fd = receive_descriptors(sock);

    std::cout

        << "Child received descriptor = " << fd

        << " [" << get_path_from_fd(fd) << "]"

        << std::endl;

    // Цикл чтения/записи и другая работа с дескриптором, как если бы он

    // был открыт в этом процессе.

    std::array<char, 256> buffer;

    ssize_t nbytes;

 

    std::cout << "Reading from a file in the child..." << std::endl;

    for (ssize_t nbytes = read(fd, buffer.data(), buffer.size()); nbytes > 0;)

    {

        write(1, buffer.data(), nbytes);

    }

 

    close(fd);

    std::cout << "Child exited" << std::endl;

}

В функции main() создается пара связанных Unix-сокетов и два процесса. Лишние дескрипторы закрываются, так как родитель только передает, а потомок только читает:

int main(int argc, char **argv)

{

    socket_wrapper::SocketWrapper sock_wrap;

 

    try

    {

        // Создать пару сокетов.

        int sp[2];

        if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sp) != 0)

            throw std::system_error(sock_wrap.get_last_error_code(),

                                    std::generic_category(),

                                    "socketpair");

        // Создать потомка.

        int pid = fork();

        if (pid > 0)

        {

            // Выполняется процесс-родитель.

            close(sp[1]);

            parent(sp[0]);

        }

        else

        {

            // Выполняется потомок.

            close(sp[0]);

            child(sp[1]);

        }

    }

    catch (const std::exception& e)

    {

        std::cerr << e.what() << std::endl;

        return EXIT_FAILURE;

    }

    catch (...)

    {

        std::cerr << "Unknown exception!" << std::endl;

        return EXIT_FAILURE;

    }

 

    return EXIT_SUCCESS;

}

Результат показывает, что номера дескрипторов в родителе и потомке разные, как и ожидалось:

build/bin/b01-ch09-send-recv-descriptors

Parent started

Child

Parent opened file with descriptor = 4 ["/tmp/descr_send_exampleZ0KEzy"]

Parent exited

Descriptor was received: 3

Child received descriptor = 3 [/tmp/descr_send_exampleZ0KEzy (deleted)]

Reading from a file in the child...

Child exited

Видим, что дескрипторы независимы: если родитель закрывает дескриптор, в потомке тот же файл будет открыт, пока его явно не закроет потомок. Тем не менее по всем параметрам дескриптор потомка дублирует родительский дескриптор.

Также видим, что у потомка в названии файла присутствует слово «(deleted)». Это говорит о том, что файл был удален. И действительно, мы его удалили в родительском процессе. Но данный код выполнялся под управлением Linux, и эту ситуацию можно считать нормальной: «удаленные» файлы не будут видны в файловой системе. Однако физически они будут присутствовать, если хотя бы один процесс имеет связанный дескриптор.

Файлы будут удаляться не при каждом запуске приложения: синхронизация отсутствует, и родительский процесс может просто не успеть удалить файл до того, как с ним начнет работать дочерний процесс.

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

Резюме

В этой главе мы подробно рассмотрели механизм отправки и получения управляющей информации, которая также известна как «вспомогательные данные».

В Unix-подобных системах для обмена вспомогательными данными используются функции sendmsg() и recvmsg(). В ОС Windows — WSASendMsg() и WSARecvMsg(). Для обработки вспомогательных данных система предоставляет набор макросов.

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

В Linux обычно механизм вспомогательных данных используется для передачи расширенных ошибок и опций IP. А для сокетов домена AF_UNIX — файловых дескрипторов. Часто управляющие сообщения отправляются при изменении опций сокета.

Вопросы и задания

1. Что такое вспомогательные данные в сетевом программировании?

2. Для чего ядро ОС проверяет вспомогательные данные?

3. Можно ли использовать вспомогательные данные для передачи пользовательских данных или через них может быть передана только системная информация?

4. Какие функции используются для отправки и получения вспомогательных данных? Могут ли эти функции использоваться в любой ОС?

5. Каким образом вспомогательные данные связаны с опциями сокета?

6. Почему рекомендуется использовать макросы при работе со структурами cmsghdr?

7. В чем разница макросов CMSG_SPACE() и CMSG_LEN()?

8. Как создать буфер для отправки или приема вспомогательных данных?

9. Назовите пример вспомогательных данных сокета семейства AF_INET. Как получить эти данные?

10. Что случится, если места в буфере недостаточно для приема вспомогательных данных?

11. Могут ли вспомогательные данные отправляться без обычных данных или только вместе с ними?

12. Зачем для передачи файловых дескрипторов между процессами использовать вспомогательные данные?

13. Какие особенности необходимо учитывать при отправке массива дескрипторов через сокеты?

14. Когда нужно обрабатывать сообщение SCM_RIGHTS?

15. Какие операционные системы поддерживают механизм вспомогательных данных?

16. В примере о вспомогательных данных UNIX-сокетов мы отправили один дескриптор от родительского процесса к потомку. Доработайте пример таким образом, чтобы отправлялось несколько дескрипторов.

Назад: Глава 8. Управление сокетами
Дальше: Глава 10. Управление сетевыми интерфейсами