Отличия сетевых вызовов Windows и Linux
Во многом совместимые на уровне исходных кодов модели сокетов от 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()
игнорируется вовсе.