Во многом совместимые на уровне исходных кодов модели сокетов от 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);
Единственное полностью корректное отображение: fcntl(descriptor, F_SETFL, O_NONBLOCK) -> ioctlsocket(descriptor, FIONBIO, адрес переменной со значением O_NONBLOCK). Числовые значения флагов следует воспринимать относительно целевой системы (на BSD и Windows они разные).
При этом, на запрос типа fcntl(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() игнорируется вовсе.