Отличия сетевых вызовов Windows и Linux

image
Во многом совместимые на уровне исходных кодов модели сокетов от Berkeley и Microsoft, на практике оказываются не такими уж кросплатформенными.

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


Тип сокетов
  • BSD: int
  • Win: void * // макрос SOCKET

Пока разрядность процессора 32 бита, проблем при взаимоотображении не возникает. На 64 битах у Windows тип SOCKET оказывается в 2 раза большим по размеру.

Дескриптор сокета на BSD ничем не отличается от дескриптора файла, а значит, некоторые системные вызовы принимают одновременно описатели и сокетов и файлов (например, такие-вот «редко» используемые вызовы, как close(), fcntl() и ioctl()).
Еще есть побочный эффект, проявляющий себя в довольно подлых ситуациях, заключающийся в том, что в системах с поддержкой модели Berkeley числовое значение описателя сокета — обычно маленькое число (меньше 100), и подряд создающиеся дескрипторы различаются на 1. В модели Microsoft такой описатель сразу больше 200 (примерно), а подряд создающиеся описатели различаются на sizeof(SOCKET).


Обработка ошибок
  • BSD: Вызовы возвращают -1, устанавливается глобальная перменная errno.
  • Win: Вызовы возвращают -1 (макрос SOCKET_ERROR), статус получаем с помощью WSAGetLastError().

Константы errno и коды ошибок Windows имеют абсолютно разные значения.


Создание сокетов:

socket(int af, int type, int protocol);

Константы для первого аргумента имеют абсолютно разные значения на BSD и Windows. Для второго пока совпадают.


Настройка сокетов
  • BSD:

    getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t *option_len);
    setsockopt(int sockfd, int level, int option_name, void const *option_value, socklen_t option_len)

  • Win:

    getsockopt(SOCKET sock, int level, int option_name, void *option_value, socklen_t *option_len);
    setsockopt(SOCKET sock, int level, int option_name, void const *option_value, socklen_t option_len)

Константы флагов для второго и третьего аргументов имеют абсолютно разные значения на BSD и Windows.


Настройка сокетов 2
  • BSD: fcntl(int fd, int cmd, ...);
  • Win: ioctlsocket(SOCKET sock, long cmd, long unsigned *arg);

Единственное полностью корректное отображение: fcnlt(descriptor, F_SETFL, O_NONBLOCK) -> ioctlsocket(descriptor, FIONBIO, адрес переменной со значением O_NONBLOCK). Числовые значения флагов следует воспринимать относительно целевой системы (на BSD и Windows они разные).
При этом, на запрос типа fcnlt(descriptor, F_GETFL), можно возвращать 0 или O_RDWR.


Настройка сокетов 3
  • BSD: ioctl(int fd, int cmd, ...);
  • Win: ioctlsocket(SOCKET sock, long cmd, long unsigned *arg);

Случаев реального использования ioctl() с сокетом в качестве первого аргумента пока не выявлено.


Работа с DNS

getaddrinfo(char const *node, char const *service, struct addrinfo const *hints, struct addrinfo **res)
  • BSD:

    struct addrinfo
    {
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    socklen_t ai_addrlen;
    struct sockaddr *ai_addr;
    char *ai_canonname;
    struct addrinfo *ai_next;
    };

  • Win:

    typedef struct addrinfo
    {
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    char *ai_canonname;
    struct sockaddr_ *ai_addr;
    struct addrinfo_ *ai_next;
    } ADDRINFOA, *PADDRINFOA;

Просмотрите внимательно инварианты этих структур. ai_addr и ai_canonname имеют разные смещения относительно начала структуры. Разработчики их просто поменяли местами (перепутали?).


Пересылка данных
  • BSD:

    recv(int sockfd, void *buffer, size_t length, int flags);
    recvfrom(int sockfd, void *buffer, size_t length, int flags, struct sockaddr *from, socklen_t *fromlen);
    send(int sockfd, void const *buffer, size_t length, int flags);
    sendto(int sockfd, void const *buffer, size_t length, int flags, struct sockaddr const *to, socklen_t tolen);

  • Win:

    recv(SOCKET sock, void *buffer, size_t length, int flags);
    recvfrom(SOCKET sock, void *buffer, size_t length, int flags, struct sockaddr *from, socklen_t *fromlen);
    send(SOCKET sock, void const *buffer, size_t length, int flags);
    sendto(SOCKET sock, void const *buffer, size_t length, int flags, struct sockaddr const *to, socklen_t tolen);

Флаги для четвертого аргумента имеют абсолютно разные значения на BSD и Windows.


Ожидание операций
  • BSD: poll(struct pollfd *fds, nfds_t nfds, int timeout);

    struct pollfd
    {
    int fd;
    short events;
    short revents;
    };

  • Win: WSAPoll(struct pollfd *fds, nfds_t nfds, int timeout);

    typedef struct pollfd
    {
    SOCKET sock;
    WORD events;
    WORD revents;
    } WSAPOLLFD, *PWSAPOLLFD;

Константы флагов для второго и третьего инвариантов структуры pollfd имеют абсолютно разные значения на BSD и Windows. WSAPoll() есть только в Windows 6й версии (Vista) и старше.


Ожидание операций 2
  • BSD: select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);

    typedef struct
    {
    long fds_bits[FD_SETSIZE / 8 * sizeof(long)];
    } fd_set;

  • Win: select(int nfds, FDSET *readfds, FDSET *writefds, FDSET *errorfds, struct timeval *timeout);

    typedef struct fd_set
    {
    unsigned fd_count;
    SOCKET fd_array[FD_SETSIZE];
    } FDSET, *PFDSET;

Проблема в select возникает при взаимоотображении структуры fd_set. Вспомним как работает select(). Данный вызов принимает три множества сокетов: для проверки на чтение, запись и ошибки на протяжении некоторого времени. Добавить свой сокет на проверку в одно из этих множеств можно макросом FD_SET(socket, set), проверить на установленость — FD_ISSET(socket, set), удалить один сокет из множества — FD_CLR(socket, set), удалить все — FD_ZERO(set). После вызова, select() оставляет в соответствующих множествах только те сокеты, которые, на протяжении указанного последним аргументом таймаута приобрели ожидаемое состояние.

Для BSD помещение некоторого сокета в некоторое множество заключается во взведении в последнем такого бита, порядковый номер которого численно равен дескриптору сокета. FD_SETSIZE обычно равно 1024. Первый аргумент select() представляет собой максимальное числовое значение дескриптора сокета, входящего в любое из трех множеств плюс один. Учитывая, что установка бита в массиве fds_bits производится исключительно без проверки диапазона, становится ясно, что при значении дескриптора сокета >= FD_SETSIZE поведение программы неопределено. Такая, несколько ненадежная реализация select — пережиток скупых на память компьютеров. Кстати, вот в каком случае важно непрямое преобразование int -> SOCKET и обратно.

Для Windows помещение некоторого сокета в некоторое множество заключается во вставке его в массив fd_array по индексу fd_count и дальнейшем увеличении последнего. FD_SETSIZE обычно равно 64. При этом, первый аргумент select() игнорируется вовсе.
  • +23
  • 13,2k
  • 1
Поделиться публикацией

Комментарии 1

    +1
    Вот такое своеобразное понимание стандартов у Микрософта. Сколько копий поломано было… и главное нервов.

    А за статью спасибо :)

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

    Самое читаемое