Куда вы хотите пойти сегодня?
Слоган Microsoft в 1994–2002 гг.
В этой главе мы перейдем к рассмотрению, вероятно, наиболее важной для сетевой разработки в ОС Windows темы — ее сокетного API.
По аналогии с предыдущими главами книги рассмотрим, какие типы сокетов поддерживаются в ОС Windows, на примерах разберем процесс их создания, а также обмен данными без установки соединения, его завершение и работу с использованием соединения со стороны как сервера, так и клиента.
Кроме того, разберемся, чем отличается работа с внеполосными данными. Коснемся обработки возможных ошибок, а также начнем рассмотрение использования не только блокирующих, но и неблокирующих операций ввода-вывода, но продолжим изучать эту тему в следующих книгах.
Затем изучим специфические особенности работы с WinSock и некоторые различия между сокетными вызовами Windows и POSIX — чтобы проще реализовывать кросс-платформенные приложения.
Помимо этого мы начнем рассмотрение более сложной темы — провайдеров сокетов. Узнаем, как работать с провайдерами транспорта. Эта информация нужна для лучшего понимания работы сетевой подсистемы в ОС Windows и может быть полезна разработчикам для создания сложных приложений, которые могут осуществлять обработку любых данных протокола требуемым способом, при этом сохраняя интерфейс сокетов.
Также в этой главе мы немного затронем вопрос реализации провайдеров имен.
Перейдем к основам работы с сокетами в ОС Windows. Начнем с создания нового сокета и привязки адреса к нему.
Рассмотрим, как POSIX-совместимый API, так и специфичный для ОС Windows.
Хотя в ОС Windows сокет — это не файловый дескриптор, он, как и тип дескриптора, является непрозрачным, то есть приложения могут обращаться к нему только через функции WinSock API.
Для сокетов это отдельный тип SOCKET, определенный в winsock2.h. В WinSock для создания сокета можно использовать две функции: socket() и WSASocket().
Функция socket() POSIX-совместима, ведет себя как было описано ранее и имеет тот же интерфейс, что и функция socket() в Linux:
SOCKET WSAAPI socket(int af, int type, int protocol);
Параметры функции socket():
• af — семейство адресов протокола. Аналогично таковому в POSIX:
• AF_INET или AF_INET6 — для интернет-сокетов.
• AF_UNIX — для Unix-сокетов.
• AF_UNSPEC — можно задать автоматический выбор семейства по значению протокола, но делать это не рекомендуется.
• type — тип сокета:
• SOCK_STREAM — для потоковых сокетов, например TCP.
• SOCK_DGRAM — для сокетов, ориентированных на сообщения.
• SOCK_RAW — для сырых сокетов, которые полноценно поддерживаются не во всех версиях ОС Windows.
• SOCK_RDM — надежная передача дейтаграмм. Используется для PGM.
• SOCK_SEQPACKET — последовательность пакетов, сохраняющих границы или поток дейтаграмм.
• protocol — протокол. Зависит от семейства адресов и провайдера сокетов. Например, для Bluetooth определен протокол BTHPROTO_RFCOMM. Мы рассмотрим только протоколы Internet:
• IPPROTO_ICMP — «обычный» ICMP. Подходит для семейств AF_UNSPEC, AF_INET, AF_INET6 и типа сокета SOCK_RAW либо не заданного.
• IPPROTO_IGMP — протокол управления группами. Семейства и тип сокетов как для ICMP.
• IPPROTO_ICMPV6 — ICMP для IPv6. Семейства и тип сокетов как для ICMP.
• IPPROTO_TCP — семейства AF_INET и AF_INET6, тип — SOCK_STREAM.
• IPPROTO_UDP — семейства AF_INET и AF_INET6, тип — SOCK_DGRAM.
Подробное описание функции см. в главе 1, а для конкретных типов сокетов — в других предыдущих главах.
Функция WSASocket() — ее расширенный вариант в Windows:
SOCKET WSAAPI WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
Она имеет три дополнительных параметра:
• lpProtocolInfo — указатель на структуру WSAPROTOCOL_INFO, в которой хранятся характеристики нового сокета. Если этот параметр ненулевой, созданный сокет будет привязан к описанному этой структурой провайдеру. Используется при создании нового дескриптора сокета функцией WSADuplicateSocket(). Чтобы определить, какие параметры должны быть взяты от существующего экземпляра сокета, необходимо подставить на место семейства адресов, типа или протокола константу FROM_PROTOCOL_INFO.
• g — это идентификатор существующей группы сокетов:
• 0 — группы не используются.
• 0x01 или SG_UNCONSTRAINED_GROUP — создать неограниченную группу сокетов и сделать новый сокет первым ее членом.
• 0x02 или SG_CONSTRAINED_GROUP — создать ограниченную группу сокетов и сделать новый сокет первым членом.
• В ином случае — идентификатор группы, в которую будет включен сокет.
• dwFlags — набор флагов, используемых для указания дополнительных атрибутов сокета:
• WSA_FLAG_OVERLAPPED — сокет будет поддерживать перекрывающиеся операции ввода-вывода. Перекрывающийся ввод-вывод позволяет выполнять несколько операций одновременно. Он будет рассмотрен в книге 2.
• WSA_FLAG_MULTIPOINT_C_ROOT, WSA_FLAG_MULTIPOINT_C_LEAF, WSA_FLAG_MULTIPOINT_D_ROOT, WSA_FLAG_MULTIPOINT_D_LEAF — данные флаги задают тип сокета в сеансе многоточечной рассылки: c_root, c_leaf, d_root, d_leaf — если многоадресная рассылка поддерживается транспортным провайдером.
• WSA_FLAG_ACCESS_SYSTEM_SECURITY — разрешить установку дескриптора безопасности сокета. В этом случае для сокета можно установить список правил доступа SACL, по которым будут создаваться исключения и предупреждения с записью в журнал безопасности при нарушении прав доступа. Может быть установлен, если у вызывающего есть право ACCESS_SYSTEM_SECURITY.
• WSA_FLAG_NO_HANDLE_INHERIT — сокет не будет наследоваться дочерними процессами.
Основное различие между этими функциями в том, что WSASocket() позволяет задать дополнительные флаги и указатель на структуру провайдера, который будет обслуживать сокет.
Если ошибки не возникает, функции возвращают дескриптор, ссылающийся на сокет. В противном случае возвращается значение INVALID_SOCKET.
Пример вызова:
auto sock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, nullptr, 0,
WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == sock)
{
// Ошибка.
}
При создании сокета можно управлять его группой, атрибутами безопасности и флагом наследования.
Группы сокетов требуются для того, чтобы задать входящим сокетам атрибуты, например относительные приоритеты сокетов в группе или спецификацию QoS для группы.
Ограниченная группа сокетов может состоять только из сокетов, ориентированных на соединение, и требует, чтобы все сокеты имели одинаковые значения типа и протокола, а соединения всех сокетов группы устанавливались с одинаковым адресом.
По умолчанию идентификатор группы равен 0, то есть сокет не принадлежит никакой группе.
Идентификаторы существующих групп сокетов уникальны для всех процессов данного провайдера транспорта. Группа и связанный идентификатор существуют, пока не закрыт последний сокет в группе.
Когда группа создана, ее идентификатор можно получить с помощью функции getsockopt():
SOCKET sock = WSASocket(...);
GROUP group_id = 0;
socklen_t gid_size = sizeof(group_id);
if (getsockopt(sock, SOL_SOCKET, SO_GROUP_ID, &group_id, &gid_size) != 0)
{
// Ошибка.
}
Точно так же получить или установить групповой приоритет можно, используя опцию SO_GROUP_PRIORITY уровня SOL_SOCKET. Как он будет использоваться, зависит от провайдера транспорта.
Константы SG_UNCONSTRAINED_GROUP = 0x01 и SG_CONSTRAINED_GROUP = 0x02 в настоящее время не определены в общедоступном заголовочном файле, поэтому и указаны числа.
После создания сокета адрес привязывается с помощью функции bind():
#include <winsock2.h>
int WSAAPI bind(SOCKET s, const sockaddr *name, int namelen);
Параметры функции bind():
• s — дескриптор сокета.
• name — адрес для связывания. Например, IP-адрес, порт и протокол.
• namelen — это просто размер передаваемой структуры адреса в байтах.
В случае ошибки bind() возвращает SOCKET_ERROR, в случае успеха — 0.
Значимых отличий между POSIX- и WinSock2-версиями функций bind() нет. Передача адреса типа in6addr_any либо INADDR_ANY приведет к тому, что для подключений будут использоваться все сетевые адаптеры.
Если порт нулевой, провайдер транспорта установит случайный неиспользуемый порт, выбирая его так же, как эфемерный.
Максимальное значение порта задается в ключе реестра HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\MaxUserPort.
Диапазон значений портов, начиная с Windows Vista, составляет 49152-65535.
Ранее — 1025-5000.
Начиная с версии Windows 10 Insider build 17063, в ОС Windows появились сокеты UNIX-domain, описанные в главе 3. Их наличие позволяет создавать кросс-платформенный межпроцессный API. Windows 11 поддерживает такие сокеты по умолчанию.
Проверить их поддержку можно следующим образом:
C:\> sc query afunix
SERVICE_NAME: afunix
TYPE : 1 KERNEL_DRIVER
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
Внимание! Такая проверка может потребовать административных прав.
Большая часть поддержки Unix-сокетов реализуется в ядре драйвером afunix.sys. Выше мы видим, что драйвер загружен и работает.
Сам файл сокета является пользовательской точкой повторной обработки NTFS.
Для определения адреса Unix-сокета используется структура sockaddr_un. В реализации для Windows имя, семантика и определение адреса Unix-сокета такие же, как в Linux.
Существует три разных формата адресации для сокетов Unix:
• Привязанные к файловой системе. Элемент структуры sun_path содержит путь в файловой системе. Кодировка символов — UTF-8. Путь оканчивается нулем.
• Абстрактные. Первый символ в sun_path — 0. Реализация не поддерживает функцию автоматической привязки, то есть такую, при которой абстрактный адрес генерируется автоматически.
• Безымянные. Сокет привязан к пути без имени, например это сокеты, которые могут быть созданы функцией socketpair(). Хотя данный тип сокета и поддерживается драйвером, API WinSock его не поддерживает.
Когда сокет связан с действительным путем, в файловой системе создается файл сокета. Приложение должно разорвать связь, прежде чем к тому же адресу может быть привязан другой сокет. Перед повторным использованием файл сокета необходимо удалить.
Передача вспомогательных данных, например файловых дескрипторов через SCM_RIGHTS или учетных данных через SCM_CREDENTIALS, не поддерживается.
Передать дескрипторы можно, используя функцию WSADuplicateSocket(), которая описана в разделе «Управление дескриптором» главы 18.
Также не поддерживается функция socketpair().
Для работы с Unix-сокетами в WinSock2 требуется подключить соответствующий заголовочный файл:
#include <afunix.h>
Тип сокета необходимо установить в SOCK_DGRAM или SOCK_SEQPACKET. В остальном работа с ними ведется так же, как с любым другим типом сокетов.
Так же как в Unix-подобных системах, данные сокеты помогают установить безопасную связь. Ограничить круг процессов, которые могут использовать сокет, можно, задав права доступа к файлу сокета или каталогу, в котором он находится.
Raw-сокеты позволяют манипулировать базовым транспортом, и в Microsoft посчитали, что их наличие создаст проблемы с безопасностью. Поэтому только члены группы администраторов могут использовать сокеты типа SOCK_RAW.
Начиная с Windows Vista, не имея прав, создать такой сокет не получится, а в более ранних версиях пользователи, не являющиеся администраторами, при работе с таким сокетом будут получать от bind() ошибку WSAEACCES. Чтобы обойти это ограничение в Windows, можно отключить проверку, создав следующую переменную реестра типа DWORD со значением 1:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Afd\Parameters
\DisableRawSecurity
После этого raw-сокет возможно использовать так же, как в Linux. Особенностью raw-сокетов является то, что при их создании IP-заголовок будет включен в возвращаемые при приеме данные, независимо от того, установлена ли опция IP_HDRINCL.
Приложение должно само определить длину IP-заголовка, чтобы найти полезную нагрузку в буфере.
Кроме того, raw-сокеты, начиная с Windows XP, имеют дополнительный набор ограничений, введенный в целях безопасности:
• Запрет на использование TCP поверх raw-сокетов. Для raw-сокета типа IPPROTO_TCP не получится вызвать bind(), хотя для сокетов типа IPPROTO_IP, IPPROTO_UDP или IPPROTO_SCTP это возможно.
• Запрет на отправку UDP-дейтаграмм с недопустимым исходным адресом. Исходный IP-адрес любой исходящей UDP-дейтаграммы должен существовать для интерфейса, через который производится отправка, иначе дейтаграмма удаляется.
Зачастую из-за этих ограничений разработчики снифферов и сканеров реализуют свои драйверы или свой провайдер транспорта.
Пример работы с raw-сокетами вы можете посмотреть в главе 4, изучив код утилиты ping. Она кросс-платформенная и будет работать в ОС Windows.
В WinSock доступны следующие типы ввода-вывода для сокетов:
• Блокирующий. Вызов не вернет управление до получения или отправки данных.
• Неблокирующий, или асинхронный. Сначала отправляется запрос к ядру, после чего поток выполняется далее.
Для неблокирующего ввода-вывода существует несколько вариантов:
• С использованием select() и WSAPoll(). Выполняется периодический опрос дескрипторов на готовность.
• Перекрывающийся, или overlapped. По завершении исполнения вызова активируется событие либо отправляется сообщение в порт завершения ввода-вывода.
• APC — асинхронный вызов процедур. По завершении операции будет вызвана процедура, указатель на которую передавался функции при вызове.
• Оконные сообщения. При наступлении события WinSock отправляет сообщения в окно с указанным хэндлом. Эти сообщения можно читать и фильтровать с помощью функции WSAAsyncSelect(), вызов которой также переводит сокет в неблокирующий режим. Эта опция устарела, и вместо нее Microsoft предлагает использовать перекрывающийся, или overlapped, ввод-вывод.
• Уведомления через объекты событий. Функция WSAEventSelect() позволяет установить события на дескриптор сокета. Эти события активируются при наступлении определенных условий, например завершении чтения.
Сокетный API, как правило, не использует APC. В случае WinSock функция WSAAccept() использует процедуру обратного вызова для сигнализации о завершении.
Пример функции, использующей APC, — SetWaitableTimer(). Также с помощью APC реализуются функции ReadFileEx() и WriteFileEx().
Следует помнить, что приложения Windows основаны на асинхронном фреймворке. Этот фреймворк — оконная система и «оконная функция», в которой выполняется цикл обработки сообщений окна. Работа с этим циклом — естественный для Windows сценарий, то есть «основной режим» WinAPI — асинхронный.
Блокирующий режим используется только в самых простых малонагруженных приложениях, а также для создания прототипов будущих приложений. Использующие блокирующий режим приложения хуже масштабируются, часто создают для обработки ввода-вывода один или два потока на соединение и потребляют лишнее процессорное время на переключение между потоками. Однако блокирующий режим естественен и прост, и поэтому он использовался в предыдущих главах и будет использоваться в книге и далее. Неблокирующий ввод-вывод значительно сложнее, поэтому к нему мы перейдем только в книге 2.
В главе 3 мы уже рассмотрели протокол UDP и API-сокетов, поддерживающий работу по протоколам без установки соединения.
В ОС Windows существует как POSIX-совместимый вариант API, так и своя реализация для работы с такими сокетами. Прототип и поведение совместимых функций аналогичны описанным ранее. Используя эти функции, после создания нового сокета и привязки его к интерфейсу через bind() уже можно осуществлять обмен данными.
Поскольку соединение отсутствует, принимающий сокет может получать дейтаграммы, исходящие от любой машины в сети.
Специфичная для ОС Windows функция приема дейтаграмм — WSARecvFrom(). Она отличается только возможностью производить асинхронный ввод-вывод:
#include <winsock2.h>
int WSAAPI recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
int WSAAPI WSARecvFrom(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
sockaddr *lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
Параметры, отсутствующие у совместимой функции:
• lpOverlapped — необязательный указатель на структуру WSAOVERLAPPED.
• lpCompletionRoutine — указатель на функцию завершения, вызываемую после завершения операции приема.
Эти параметры игнорируются для сокетов, не использующих перекрывающийся ввод-вывод.
Из флагов приема данных поддерживаются следующие:
• MSG_PEEK — не удалять данные из буфера.
• MSG_OOB — принимать внеполосные данные.
• MSG_PARTIAL — указывает, что буфер содержит только часть сообщения.
Для отправки данных предназначена совместимая функция sendto() и специфичная для ОС Windows WSASendTo():
#include <winsock2.h>
int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
int WSAAPI WSASendTo(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const sockaddr *lpTo,
int iTolen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
Различие между ними то же: возможность использовать перекрывающийся ввод-вывод.
Так же как в POSIX-варианте, поддерживаются флаги MSG_DONTROUTE, MSG_OOB и MSG_PARTIAL. Однако флаг MSG_DONTROUTE игнорируется провайдером от Microsoft. Вместо него можно использовать ioctl SIO_ASSOCIATE_HANDLE.
Внимание! Флаги могут не только управлять поведением функций, но и быть установлены функциями после вызова как индикаторы. Таким является флаг MSG_PARTIAL.
Флаг MSG_PARTIAL специфичен для ОС Windows и предназначен только для сокетов, ориентированных на сообщения.
В функциях приема этот флаг говорит о том, что получено частичное сообщение. Остальные части сообщения будут получены следующими вызовами функций приема.
Принимать такое сообщение необходимо до первой операции, в которой MSG_PARTIAL не установлен, то есть принят конец сообщения.
Для функций передачи флаг указывает, что lpBuffers содержит только частичное сообщение. Транспортами, которые не поддерживают передачу частичных сообщений, будет возвращаться код ошибки WSAEOPNOTSUPP.
В случае отложенного завершения значение, на которое указывает lpFlags, не обновляется. Приложение должно вызвать функцию WSAGetOverlappedResult() и проверить флаги, на которые указывает параметр lpdwFlags.
Возвращаемые значения функций различны:
• WSARecvFrom() и WSASendTo() возвращают 0 в случае успеха, значение SOCKET_ERROR при ошибке и WSA_IO_PENDING, если запущена перекрывающаяся операция.
• recvfrom() и sendto() — количество принятых или переданных данных соответственно.
Так же как в случае связи без установки соединения, ОС Windows поддерживает совместимые функции и имеет специфичные.
На рис. 16.1 показано соответствие между процессами работы клиента и сервера, а также соответствие функций WinSock 2 и POSIX-совместимых.
Рис. 16.1. Процессы работы клиента и сервера в WinSock
Клиент намного проще, чем сервер, и требует меньше шагов для установки успешного подключения. Его API состоит только из функции подключения.
Совместимая функция connect() имеет прототип, похожий на тот, который присутствует в Linux:
#include <winsock2.h>
int WSAAPI connect(SOCKET s, const sockaddr *name, int namelen);
Параметры функции connect():
• s — дескриптор сокета;
• name — указатель на структуру адреса;
• namelen — размер адреса.
Разница в том, что длина имени имеет тип int, а не size_t. Возвращаемое значение — 0 в случае успеха или SOCKET_ERROR в случае неудачи.
Функция GetLastError() вернет WSAECONNREFUSED, если отсутствует процесс, прослушивающий данный порт. Если же сервер недоступен, будет возвращено WSAETIMEDOUT. Полный список ошибок см. в MSDN.
Функция WSAConnect() имеет гораздо больше возможностей, чем совместимая функция connect():
#include <qos.h>
#include <winsock2.h>
int WSAAPI WSAConnect(
SOCKET s,
const sockaddr *name,
int namelen,
LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS,
LPQOS lpGQOS
);
Параметры, специфичные для функции WSAConnect():
• lpCallerData — указатель на пользовательские данные, которые должны быть переданы в сокет удаленного абонента при установлении соединения.
• lpCalleeData — указатель на пользовательские данные, которые должны быть получены из сокета удаленного абонента при установлении соединения.
• lpSQOS — указатель на структуру, которая содержит параметры QoS.
• lpGQOS — зарезервировано для будущего использования с группами сокетов. Если применимо, указатель на структуры QoS для группы сокетов. Этот параметр должен быть нулевым.
В случае успеха WSAConnect() вернет 0, в противном случае — SOCKET_ERROR. Коды ошибки такие же, как и для connect(), кроме специфичных для функции — WSAEOPNOTSUPP и WSAEPROTONOSUPPORT.
Параметры lpCallerData и lpCalleeData предназначены для указания на данные, которые будут переданы или получены при соединении соответственно. Если они имеют нулевое значение, пользовательские данные не передаются и не принимаются.
В lpCalleeData данные могут быть записаны, если они были переданы сервером на этапе подключения и операция подключения завершилась успешно.
Для неблокирующих сокетов о завершении данной операции говорит уведомление FD_CONNECT.
Атрибут len структуры WSABUF, на который указывает параметр lpCalleeData, должен содержать длину буфера, выделенного приложением. Если он равен 0, данные не будут переданы или не были получены.
Эту функцию имеет смысл использовать вместе с функцией WSAAccept(), которая описана далее.
Во время подключения приложение может использовать параметры lpSQOS и lpGQOS, чтобы переопределить предыдущие параметры качества обслуживания. Если параметр lpSQOS ненулевой, он должен содержать указатель на структуру, определяющую параметры QoS для обоих направлений. Если провайдер транспорта не может выполнить запрос на качество обслуживания, будет возвращена ошибка.
Для однонаправленных сокетов параметры QoS отправки или получения будут игнорироваться.
Параметр lpGQOS нужен, чтобы установить параметры QoS для группы, и действует, только если сокет является лидером группы сокетов.
Структуры, которые используются для работы с QoS:
#include <qos.h>
#include <ws2def.h>
typedef struct _flowspec
{
// Разрешенная скорость, с которой могут передаваться данные.
ULONG TokenRate;
// Максимальное количество байтов для данного направления потока,
// независимо от времени.
ULONG TokenBucketSize;
// Верхний разрешенный предел на передачу в байтах в секунду.
ULONG PeakBandwidth;
// Максимальное количество микросекунд между передачей и получением бита.
ULONG Latency;
// Количество микросекунд между максимальной и минимальной задержкой
// пакета.
ULONG DelayVariation;
// Уровень обслуживания для согласования потока:
// SERVICETYPE_NOTRAFFIC, SERVICETYPE_BESTEFFORT и прочие.
SERVICETYPE ServiceType;
// Максимальный разрешенный размер пакета в байтах.
ULONG MaxSduSize;
// Минимальный размер пакета, для которого обеспечивается
// качество обслуживания.
ULONG MinimumPolicedSize;
} FLOWSPEC, *PFLOWSPEC, *LPFLOWSPEC;
typedef struct _QualityOfService
{
// QoS для отправки.
FLOWSPEC SendingFlowspec;
// QoS для приема.
FLOWSPEC ReceivingFlowspec;
// Параметры, специфичные для провайдера.
WSABUF ProviderSpecific;
} QOS, *LPQOS;
Для приложений, которые будут работать только на версиях ОС Windows Vista и более поздних, Microsoft предлагает использовать более удобные функции: WSAConnectByName() и WSAConnectByList(). Первая из них позволяет выполнять подключение к указанной службе по имени узла и службы, при необходимости автоматически получая адресную информацию:
#include <winsock2.h>
bool WSAConnectByName(
SOCKET s,
LPTSTR nodename,
LPTSTR servicename,
LPDWORD LocalAddressLength,
LPSOCKADDR LocalAddress,
LPDWORD RemoteAddressLength,
LPSOCKADDR RemoteAddress,
const timeval *timeout,
LPWSAOVERLAPPED Reserved
);
Параметры функции WSAConnectByName():
• s — дескриптор сокета.
• nodename — строка, которая содержит имя, а также IPv4- или IPv6-адрес узла, к которому требуется подключиться.
• servicename — строка, которая содержит имя службы или порт узла для подключения.
• LocalAddressLength — указатель на размер буфера LocalAddress. Перед вызовом длина устанавливается равной размеру буфера. После успешного завершения функции сюда будет записан новый размер адреса.
• LocalAddress — указатель на структуру SOCKADDR, получающую адрес локального абонента. Если параметр нулевой, LocalAddressLength тоже игнорируется.
• RemoteAddressLength — указатель на размер буфера RemoteAddress в байтах. После успешного завершения вызова значение переменной будет переписано размером адреса, установленного в RemoteAddress.
• RemoteAddress — указатель на структуру SOCKADDR, которая получает адрес удаленного абонента.
• timeout — указатель на переменную, содержащую количество миллисекунд, в течение которых функция будет ожидать ответ от удаленного приложения перед прерыванием вызова.
• Reserved — параметр зарезервирован и должен быть установлен в nullptr.
В буфер локального адреса будет записано то же, что вернет функция getsockname(), а в буфер удаленного адреса — то же, что функция getpeername().
Вторая функция подключается к адресам из переданного списка. Процесс похож на то, что необходимо делать после вызова getaddrinfo() — перебирать адреса, пытаясь установить соединение. Но функция WSAConnectByList() также пытается установить соединение, используя адреса с наибольшим шансом на успех, что не только гарантирует, что соединение будет установлено, если оно вообще возможно, но и минимизирует время на его установление.
Эти адреса функция получает через вызов IP Helper API CreateSortedAddressPairs().
Рассмотрим прототип WSAConnectByList():
#include <winsock2.h>
typedef struct _SOCKET_ADDRESS_LIST
{
// Количество структур в массиве Address.
int iAddressCount;
// Массив адресов.
SOCKET_ADDRESS Address[1];
} SOCKET_ADDRESS_LIST, *PSOCKET_ADDRESS_LIST, FAR *LPSOCKET_ADDRESS_LIST;
bool WSAConnectByList(
SOCKET s,
PSOCKET_ADDRESS_LIST SocketAddress,
LPDWORD LocalAddressLength,
LPSOCKADDR LocalAddress,
LPDWORD RemoteAddressLength,
LPSOCKADDR RemoteAddress,
const timeval *timeout,
LPWSAOVERLAPPED Reserved
);
Параметры функции WSAConnectByList():
• s — дескриптор сокета.
• SocketAddress — указатель на структуру SOCKET_ADDRESS_LIST, которая представляет возможные адреса и порты для подключения к узлу.
• timeout — время в миллисекундах, в течение которого ожидается ответ от удаленного приложения перед прерыванием вызова.
Если тайм-аут имеет нулевое значение, WSAConnectByList() завершится либо после успешного установления соединения, либо после неудачной попытки подключения для всех возможных пар «локальный — удаленный адрес».
Параметры LocalAddressLength, LocalAddress, RemoteAddressLength, RemoteAddress и Reserved аналогичны таковым в функции WSAConnectByName().
Обе функции возвращают логическое значение — истину в случае успеха.
Рассмотрим API, который предоставляет ОС Windows для реализации серверных приложений.
Прототип функции listen() такой же, как и в Unix-подобных системах:
#include <winsock2.h>
int WSAAPI listen(SOCKET s, int backlog);
Параметры функции listen():
• s — дескриптор сокета;
• backlog — максимальное количество соединений, ожидающих в очереди.
Возвращаемое значение такое же, как и у POSIX-функции: 0 в случае успеха и SOCKET_ERROR при неудаче.
Работа с очередью соединений несколько отличается. Если очередь соединений заполнена, следующий запрос клиента вернет ошибку WSAECONNREFUSED.
Ограничения параметра backlog
Параметр backlog неявно ограничен значением, определяемым базовым провайдером. Недопустимые значения заменяются ближайшим допустимым.
Если установлено значение SOMAXCONN, провайдер транспорта установит размер очереди на «максимально разумное» значение.
Если установлено значение SOMAXCONN_HINT(N), значение backlog будет равно N с поправкой на диапазон (200, 65535), причем оно может быть больше, чем SOMAXCONN.
SOMAXCONN_HINT поддерживается только провайдером Microsoft TCP/IP.
Запрос на соединение от клиента принимается с помощью функций accept() или WSAAccept().
Windows accept() почти аналогична POSIX-совместимой версии, но отличается типом параметра addrlen: в POSIX версии это socklen_t:
#include <winsock2.h>
SOCKET WSAAPI accept(SOCKET s, sockaddr *addr, int *addrlen);
Параметры функции accept():
• s — дескриптор сокета.
• addr — указатель на буфер с адресом подключающегося объекта, определяемый семейством адресов.
• addrlen — указатель на длину буфера с адресом.
Гораздо больший интерес представляет функция WSAAccept(), которая может работать как в асинхронном, так и в синхронном режиме:
#include <winsock2.h>
int CALLBACK ConditionFunc(
LPWSABUF lpCallerId,
LPWSABUF lpCallerData,
LPQOS lpSQOS,
LPQOS lpGQOS,
LPWSABUF lpCalleeId,
LPWSABUF lpCalleeData,
GROUP FAR *g,
DWORD_PTR dwCallbackData
);
SOCKET WSAAPI WSAAccept(
SOCKET s,
sockaddr *addr,
LPINT addrlen,
LPCONDITIONPROC lpfnCondition,
DWORD_PTR dwCallbackData
);
Параметры, специфичные для функции WSAAccept():
• lpfnCondition — адрес функции обратного вызова. Она может принимать или отклонять соединение.
• dwCallbackData — пользовательские данные, передаваемые функции lpfnCondition.
Обе функции возвращают дескриптор клиентского сокета в случае успеха или INVALID_SOCKET в случае неудачи. Помимо этого они записывают по указателю в addr адрес принятого клиента, а в addrlen — размер этого адреса, если эти указатели ненулевые. В остальном функция WSAAccept() будет работать так же, как и accept(), если не указан параметр lpfnCondition — условная функция. Она может вернуть следующие значения:
• CF_ACCEPT — функция WSAAccept() создаст новый сокет.
• CF_REJECT — соединение будет отклонено.
• CF_DEFER — принятие решения отложено, и никаких действий для этого соединения выполнено не будет.
Пока запрос на отложенное подключение находится в начале очереди отложенных задач, провайдер не выдает дополнительных указаний для ожидающих запросов на подключение.
Помимо отклонения или принятия нового подключения условная функция может не только прочитать, но и установить для сокета различные параметры, например задать группу сокета по указателю, который был ей передан:
• lpCallerId — буфер с адресом подключившегося клиента, в случае INET-сокетов — sockaddr.
• lpCallerData — данные, отправленные клиентом при его подключении.
• lpSQOS, lpGQOS — параметры QoS.
• lpCalleeId — локальный адрес.
• lpCalleeData — данные, которые сервер отправляет клиенту при его подключении.
• g — управление группой сокета. Параметр может принимать следующие значения:
• 0 — групповые операции не выполняются.
• Существующий идентификатор группы — сокет будет добавлен в группу.
• SG_UNCONSTRAINED_GROUP — создать неограниченную группу сокетов и сделать сокет первым членом.
• SG_CONSTRAINED_GROUP — создать ограниченную группу сокетов и сделать сокет первым членом.
• dwCallbackData — пользовательские данные, переданные как параметр WSAAccept().
Использование данной функции в приложениях вместе с данными, отправляемыми WSAConnect(), дает возможность быстро отфильтровать большое число подключений по пользовательскому критерию. Это может быть полезно для высоконагруженных приложений, о которых мы поговорим в книге 2.
Функции recv() и send(), рассмотренные в главе 5, имеют POSIX-совместимый прототип:
#include <winsock2.h>
int recv(SOCKET s, char *buf, int len, int flags);
int WSAAPI send(SOCKET s, const char *buf, int len, int flags);
Параметр длины буфера имеет тип int, а не size_t, а также отличается тип возвращаемого значения.
Функция recv() поддерживает флаги MSG_PEEK, MSG_OOB, MSG_WAITALL, если их поддерживает провайдер. Функция send() поддерживает флаги MSG_DONTROUTE и MSG_OOB. Описание флагов см. в главах 4 и 8.
Обе функции возвращают количество отправленных байтов, 0 или –1 в случае ошибки.
Специфичные для ОС Windows функции дают возможность использовать перекрывающийся ввод-вывод и частичные дейтаграммы:
#include <winsock2.h>
int WSAAPI WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
int WSAAPI WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
Параметры, специфичные для функций в ОС Windows:
• lpBuffers — указатель на массив буферов, каждый из которых описывается структурой WSABUF, представленной в разделе «Общие структуры данных».
• dwBufferCount — количество буферов в массиве.
• lpNumberOfBytesRecvd и lpNumberOfBytesSent — указатели на количество полученных или отправленных байтов соответственно. Будут установлены, только если операции завершаются немедленно.
• lpFlags — указатель на флаги.
• lpOverlapped — указатель на структуру WSAOVERLAPPED. Используется для перекрывающегося ввода-вывода.
• lpCompletionRoutine — указатель на функцию завершения. Используется для перекрывающегося ввода-вывода.
Функции принимают и передают данные в наборе буферов. С точки зрения функциональности от POSIX-версий они отличаются возможностью использовать перекрывающийся ввод-вывод.
Функция WSASend() поддерживает дополнительный флаг MSG_PARTIAL, а WSARecv() — флаг MSG_PEEK для получения данных из буфера, не удаляя их, а также флаг MSG_PUSH_IMMEDIATE, который предназначен для потоковых сокетов. Он сообщает провайдеру транспорта, что не следует задерживать выполнение частично заполненных ожидающих запросов на получение данных. То есть приложение будет получать входящие данные как можно скорее, не дожидаясь данных, которые находятся в пути. Обе функции возвращают 0 в случае успеха и WSA_SOCKETERROR при ошибке. В остальном они ведут себя как WSARecvFrom() и WSASendTo() соответственно.
По окончании работы с сокетом можно завершить соединение и освободить все ресурсы, связанные с этим дескриптором сокета, используя для этого POSIX-совместимую функцию shutdown():
#include <winsock2.h>
int WSAAPI shutdown(SOCKET s, int how);
Параметры функции shutdown():
• s — дескриптор сокета.
• how — флаг запрещаемых операций:
• SD_RECEIVE — отключить операции приема данных.
• SD_SEND — отключить операции передачи данных.
• SD_BOTH — отключить и то и другое.
Функция завершит соединение, но так же, как в POSIX, не закроет дескриптор.
Кроме прочих существуют функции WSASendDisconnect(), WSARecvDisconnect() и DisconnectEx(). Их мы рассмотрим в книге 2.
Для освобождения дескриптора требуется вызвать функцию closesocket():
int WSAAPI closesocket(SOCKET s);
Так же как и функция close() в POSIX, данная функция при необходимости сама вызывает завершение соединения и удаляет объект, если закрываемый дескриптор — последний, который ссылается на объект сокета.
Обе функции вернут 0 в случае успеха и SOCKET_ERROR в случае неудачи.
Внеполосные данные мы рассмотрели в главе 6. В документации Windows для WinSock написано, что c внеполосными данными он работает так же, как BSD-системы, однако это поведение можно изменить с помощью опции сокета TCP_EXPEDITED_1122.
Это флаг, включение которого задает поведение согласно RFC 1122 «Requirements for Internet Hosts».
Обратная ей опция TCP_BSDURGENT задает поведение «как у BSD-сокетов», то есть согласно RFC 793 «Transmission Control Protocol».
Если какая-либо из этих опций задана, выключить ее нельзя.
Прежде всего данные опции определяют, куда указывает метка внеполосных данных в совместимых BSD-сокетах, и при использовании этой опции. Варианты показаны на рис. 16.2.
Рис. 16.2. Разница между типами OOB-данных
Согласно RFC 793 указатель срочности указывает на порядковый номер октета, следующего за срочными данными. Однако в RFC 1122 говорится, что это ошибка и указатель должен указывать на последний октет срочных данных. Проблема в том, что ОС Windows поддерживает только один байт внеполосных данных, тогда как RFC 1122 говорит, что TCP должен поддерживать последовательности байтов срочных данных любой длины.
Кроме того, не прописано, будет ли ОС Windows буферизовать последующие внеполосные данные. Поэтому если программа читает данные медленно и в это время поступает еще один байт срочных данных, один из байтов может быть потерян.
На практике ОС Windows буферизует срочные данные. Однако использование внеполосной сигнализации TCP в WinSock ненадежно.
Хотя некоторые сокетные вызовы совместимы, можно заметить, что между ними и WinSock API существует достаточно различий. Эти различия необходимо учитывать при реализации кросс-платформенных приложений. Для удобства приведем их одним списком. Мы опишем различия не только в уже изученном API, но и в том, которое мы рассмотрим в следующих главах и книгах.
Очевидно, что отличаются состав и местоположение заголовочных файлов. Кроме этого, отличается расположение файлов конфигурации:
ОС | Файл узлов | Файл служб | Файл протоколов |
POSIX | /etc/hosts | /etc/services | /etc/protocols |
Windows | %SystemRoot%\System32\drivers\etc\hosts | %SystemRoot%\System32\drivers\etc\service | %SystemRoot%\System32\drivers\etc\protocols |
Важным отличием WinSock является необходимость инициализации в начале работы и очистки при завершении работы.
Различия в составе вызовов WinAPI:
• В ОС Windows закрытие сокетов выполняется функцией closesocket().
• Функции read() и write()для WinSock-сокетов не работают. Хотя могут работать ReadFile() и WriteFile().
• Специфичные для ОС Windows функции, такие как WSAAccept(), WSAConnect() и т.п. Их аргументы и возвращаемые значения, как правило, не соответствуют их POSIX-аналогам.
• Функции-расширения от Microsoft: TransmitFile(), GetAcceptExSockaddrs() и прочие. Часть из них мы рассмотрим в книге 2.
• Функция ioctl(), которая позволяет задавать параметры устройств, в ОС Windows представлена функциями ioctlsocket(), WSAIoCtl() и DeviceIoControl(). Функция fcntl() отсутствует.
• Функции poll() в POSIX соответствует функция WSAPoll() в ОС Windows.
Различия в типах и значениях параметров и возвращаемых значениях:
• Числовые значения констант, обозначающих пространства адресов, например AF_INET, в разных ОС разные.
• Константы в getsockopt() и setsockopt() имеют разные значения.
• В POSIX тип дескриптора — int, в ОС Windows — макрос SOCKET раскрывается в тип void*.
• Дескрипторы в Windows — положительные числа. Нежелательно делать проверку на –1, лучше сравнивать дескриптор с константой INVALID_SOCKET.
• Возвращаемое значение функций, специфичных для ОС Windows, не стандартизовано.
• Все вызовы POSIX возвращают –1, устанавливая переменную errno. В ОС Windows POSIX-совместимые вызовы возвращают дополнение до единицы — макрос SOCKET_ERROR.
• Константы errno и коды ошибок в ОС Windows имеют разные значения. Дополнительную информацию об ошибке возможно получить с помощью вызова функции WSAGetLastError().
• Структуры addrinfo, используемые в функции getaddrinfo(), различаются: ai_addr и ai_canonname имеют разные смещения относительно начала структуры: их поменяли местами. При заполнении, а также сохранении и восстановлении структур это может вызывать проблемы.
• Функции обмена данными sendto(), recvfrom(), send(), recv() и т.п. в POSIX и в POSIX-совместимых Windows-функциях имеют различные типы аргументов. Поэтому если требуется передать большой объем данных, максимальный размер фрагмента стоит рассчитывать по меньшему типу.
• Флаги в функциях обмена данными имеют абсолютно разные значения. Поэтому всегда следует использовать только именованные константы, а не числа и проверять, что флаг существует в разных ОС.
• Значения флагов в полях структуры pollfd, используемые функциями poll() и WSAPoll(), различаются.
Внимание! Хотя на 32-битной системе проблем при работе не возникает, на 64-разрядной системе тип SOCKET от Microsoft в два раза больше по размеру, чем int, что может иметь значение при переносе приложений на другие платформы.
Структура POSIX для использования в select():
typedef struct
{
long fds_bits[FD_SETSIZE / 8 * sizeof(long)];
} fd_set;
Это отличается от структуры fd_set для функции select() в WinSock:
typedef struct fd_set
{
unsigned fd_count;
SOCKET fd_array[FD_SETSIZE];
} FDSET, *PFDSET;
Различий достаточно много, и, вероятно, чтобы их нивелировать, разумно использовать разные библиотеки. Рассматривать библиотеки мы начнем в книге 2 и продолжим в книге 3.
Рассмотренный нами API для приложений носит название WSA. Кроме него Windows Sockets предоставляют API для расширения сокетной библиотеки — SPI, Service Provider Interface.
Функции WSA, как правило, выполняют минимум работы по диспетчеризации и согласованию результатов, а основные задачи переадресуют различным провайдерам, показанным на рис. 16.3. Это проистекает из самой организации сокетного API: вариантов пространств имен, протоколов и даже стеков в нем множество.
Существует два типа провайдеров:
• Провайдеры транспорта для выполнения задач по обмену данными.
• Провайдеры пространств имен для разрешения адресов.
Провайдеры транспорта выполняют работу по реальному обмену данными, например, провайдер транспорта MS TCP реализует TCP. Таких провайдеров может быть несколько, и они могут предоставлять несколько реализаций с разной функциональностью.
То же справедливо для функций и провайдеров, выполняющих разрешение имен.
Рис. 16.3. Провайдеры транспорта и пространств имен
Как правило, задавать провайдер не требуется явно, но в редких случаях необходимо выбирать реализацию, например, когда программист самостоятельно реализует протокол.
Примеры пространств имен, которые поддерживает Windows: DNS для Internet, доменные службы Active Directory, X.500. Они значительно различаются между собой.
Провайдеры реализуются в DLL с одной экспортируемой точкой входа, которая вызывается функцией инициализации: WSPStartup() или NSPStartup().
Иные функции провайдера доступны для Ws2_32.dll через таблицу диспетчеризации провайдера. Библиотеки провайдеров загружаются Ws2_32.dll при необходимости и выгружаются, когда больше не требуются.
Мы коснемся API для работы с провайдерами лишь в той мере, в какой он необходим для понимания работы с WSA.
Функции, работающие с провайдерами транспортов, имеют разные префиксы:
• WSP — Windows Sockets Providers;
• WPU — Windows Sockets Provider Upcall;
• WSC — Windows Sockets Configuration.
Функции для провайдеров пространств имен начинаются с NSP, что означает Namespace Provider.
Рассмотрим интерфейс WinSock, изображенный на рис. 16.4. Приложение вызывает функции WinSock2 API из библиотеки Ws2_32.dll. Сама библиотека делегирует большую часть работы провайдерам.
Для выполнения операций по разрешению имен Ws2_32.dll вызывает сервисы провайдеров пространств имен, а для передачи сообщений — провайдеров транспорта.
Библиотека Mswsock.dll — это сокетные расширения от Microsoft, которые, начиная с ОС Windows Vista, были интегрированы с WinSock.
Провайдер TCP/IP реализован в библиотеке Wshtcpip.dll. Он взаимодействует с драйвером AFD, который позволяет отправлять и принимать сообщения по сети. Драйвер работает с разными протоколами и делегирует работу с TCP/IP соответствующему драйверу.
TCPIP.sys предоставляет интерфейс TLNPI — Transport Layer Network Provider Interface. Ранее предоставлялся интерфейс TDI — Transport Driver Interface.
Чтобы не переписывать старый код и сохранить обратную совместимость, для поддержки TDI в новых ядрах реализован TDX.sys — драйвер-преобразователь из TDI в TLNPI.
Между провайдером транспорта и пользовательским API, реализованным в Ws2_32.dll, работает интерфейс сервисного провайдера — WinSock Transport SPI. Он похож на Winsock API, так как все основные функции сокетов отображаются в функции провайдеров транспортов.
Например, функции socket() и WSASocket() сопоставляются с WSPSocket(), connect() и WSAConnect() — с WSPConnect(), send() и WSASend() — с WSPSend() и т.д. Но его функции начинаются с префикса WSP и доступны через адреса в таблице, которую заполняет провайдер.
Когда в API существует как новая версия функции WinSock, так и более старая версия, в SPI будет отражаться только новая версия.
Рис. 16.4. Вызов сетевых функций
Некоторые вспомогательные функции, такие как htonl(), htons(), ntohl() и ntohs(), реализованы в Ws2_32.dll и не реализуются провайдерами.
Функции манипулирования такими объектами, как события и ожидания, например WSACreateEvent(), WSASetEvent(), WSAWaitForMultipleEvents(), сопоставляются непосредственно со службами Windows и тоже не реализованы в SPI.
Нас интересуют не провайдеры как таковые, а протоколы, которые они поддерживают. Центральной структурой данных, объединяющей WSA и WSP, является структура WSAPROTOCOL_INFO, описывающая протокол. Прежде всего эта структура используется для загрузки провайдера, а затем — для привязки сокетов к этому провайдеру.
Рассмотрим ее первую часть, которая описывает службы и протокол:
typedef struct _WSAPROTOCOL_INFOW
{
// Битовая маска, описывающая службы, предоставляемые протоколом.
DWORD dwServiceFlags1;
// Зарезервированные поля.
DWORD dwServiceFlags2;
DWORD dwServiceFlags3;
DWORD dwServiceFlags4;
// Набор флагов c информацией о том, как протокол представлен
// в каталоге Winsock.
DWORD dwProviderFlags;
// GUID провайдера, реализующего протокол.
GUID ProviderId;
// Уникальный идентификатор, назначаемый WinSock каждой
// структуре WSAPROTOCOL_INFO.
DWORD dwCatalogEntryId;
Значения флагов в dwServiceFlags1 устанавливаются при регистрации провайдера и могут быть следующими:
• XP1_CONNECTIONLESS — предоставляет обмен данными без установления соединения, то есть это дейтаграммный протокол. Если данный флаг не установлен, протокол поддерживает передачу данных с установлением соединения.
• XP1_GUARANTEED_DELIVERY — гарантирует, что все отправленные данные будут доставлены.
• XP1_GUARANTEED_ORDER — гарантирует поступление данных в том порядке, в каком они были отправлены. Также гарантируется отсутствие дублирования.
• XP1_MESSAGE_ORIENTED — учитывает границы порций данных, то есть протокол ориентирован на сообщения.
• XP1_PSEUDO_STREAM — протокол, ориентированный на сообщения, но границы сообщений игнорируются для всех квитанций. Это удобно, когда приложение не хочет, чтобы кадрирование сообщения выполнялось протоколом.
• XP1_GRACEFUL_CLOSE — поддерживает двухфазное закрытие, как в TCP. Если данный флаг не установлен, будут выполняться только аварийные закрытия.
• XP1_EXPEDITED_DATA — поддерживаются внеполосные данные.
• XP1_CONNECT_DATA — поддерживаются данные, отправляемые при подключении.
• XP1_DISCONNECT_DATA — поддерживаются данные, отправляемые при отключении.
• XP1_SUPPORT_BROADCAST — поддерживается широковещание.
• XP1_SUPPORT_MULTIPOINT — поддерживается многоточечный или многоадресный режим.
• XP1_MULTIPOINT_CONTROL_PLANE — указывает, является ли плоскость управления корневой (1) или нет (0).
• XP1_MULTIPOINT_DATA_PLANE — указывает, является ли плоскость данных корневой (1) или некорневой (0).
• XP1_QOS_SUPPORTED — поддерживается качество обслуживания.
• XP1_INTERRUPT — зарезервирован.
• XP1_UNI_SEND — протокол может только отправлять данные.
• XP1_UNI_RECV — протокол может только получать данные.
• XP1_IFS_HANDLES — дескрипторы сокетов, возвращаемые провайдером, являются дескрипторами инсталлируемой файловой системы — IFS.
• XP1_PARTIAL_MESSAGE — флаг MSG_PARTIAL поддерживается WSASend() и WSASendTo().
• XP1_SAN_SUPPORT_SDP — протокол обеспечивает поддержку системных локальных сетей System area networks, или SAN.
Если флаги XP1_UNI_SEND или XP1_UNI_RECV не установлены, протокол двунаправленный.
Функциональная плоскость применительно к сетям — это абстрактное представление о том, где происходят определенные процессы.
, — это часть сети, которая управляет пересылкой данных. Например, процесс создания таблицы маршрутизации считается частью плоскости управления.
Плоскость данных выполняет непосредственную пересылку данных.
Чаще эти термины можно встретить при описании работы маршрутизаторов.
Флаги в dwProviderFlags устанавливаются аналогично флагам выше:
• PFL_MULTIPLE_PROTO_ENTRIES — указывает, что это одна из нескольких записей одного протокола, реализующего несколько вариантов поведения. Например, протокол на принимающей стороне может вести себя как ориентированный на сообщения или как ориентированный на поток.
• PFL_RECOMMENDED_PROTO_ENTRY — рекомендуемая или наиболее часто используемая запись для протокола, реализующего несколько вариантов поведения.
• PFL_HIDDEN — указывает Ws2_32.dll, что этот протокол не должен возвращаться в буфер результатов, созданный WSAEnumProtocols(). То есть приложение, использующее Windows Sockets 2, не увидит его запись.
• PFL_MATCHES_PROTOCOL_ZERO — указывает, что значение нуля в параметре протокола функций socket() и WSASocket() соответствует этой записи протокола. То есть это протокол, используемый по умолчанию.
• PFL_NETWORKDIRECT_PROVIDER — провайдер поддерживает прямой доступ к сети.
Оставшаяся часть структуры также содержит параметры протокола:
// Структура, которая содержит цепочку протоколов.
WSAPROTOCOLCHAIN ProtocolChain;
// Идентификатор версии протокола.
int iVersion;
// Семейство адресов как в функции socket(): AF_INET, AF_INET6 и т.д.
int iAddressFamily;
// Максимальная длина адреса в байтах.
int iMaxSockAddr;
// Минимальная длина адреса в байтах.
int iMinSockAddr;
// Тип сокета как в функции socket():
// SOCK_STREAM, SOC_DGRAM, SOCK_RAW и т.д.
int iSocketType;
// Протокол как в функции socket(): IPPROTO_TCP, IPPROTO_UDP и т.д.
int iProtocol;
// Максимальное значение, которое может быть добавлено к iProtocol
// при передаче значения параметра протокола функции socket().
// Равен 0, если протокол не допускает использования
// диапазона идентификаторов.
int iProtocolMaxOffset;
// Порядок байтов, используемый протоколом:
// BIGENDIAN (0) или LITTLEENDIAN (1).
int iNetworkByteOrder;
// Тип используемой схемы безопасности.
// Если протокол не использует средства безопасности, содержит
// SECURITY_PROTOCOL_NONE (0).
int iSecurityScheme;
// Максимальный размер сообщения, который может отправить узел.
// 0 — потоковый протокол.
// 1 — максимальный размер исходящего сообщения зависит от MTU.
// 0xffffffff — протокол, ориентированный на сообщения, но ограничения
// на размер сообщений нет.
DWORD dwMessageSize;
// Зарезервировано сервис-провайдером.
DWORD dwProviderReserved;
// Имя протокола, например "MSAFD Tcpip [UDP/IP]".
// Максимально допустимое количество символов — WSAPROTOCOL_LEN
// (255 символов).
// Это поле содержит отличие данной структуры от WSAPROTOCOL_INFOA.
// В общем случае необходимо считать, что оно имеет тип TCHAR.
WCHAR szProtocol[WSAPROTOCOL_LEN + 1];
} WSAPROTOCOL_INFOW, *LPWSAPROTOCOL_INFOW;
Большинство функций, относящихся к провайдерам, кроме высокоуровневых функций их перечисления, объявлены в заголовочном файле ws2spi.h.
GUID, необходимый для регистрации провайдера, можно сгенерировать, используя команду PowerShell:
PS C:\> [guid]::NewGuid()
Guid
----
2f2a48b5-9981-46c2-a63b-80322484ca86
Существует несколько классов протоколов:
• Многоуровневые протоколы, или Layered Protocols, реализуют функции связи высокого уровня, используя существующий транспортный стек для обмена данными. Примером этого типа протокола является TLS. Он добавляет протокол для выполнения аутентификации и выбора схемы шифрования. Работает он поверх надежного транспорта, например TCP.
• Базовые протоколы способны выполнять обмен данными с удаленным абонентом. Пример таких протоколов — TCP или UDP.
• Цепочка протоколов — это несколько многоуровневых протоколов, объединенных вместе и привязанных к базовому протоколу. То есть цепочка протоколов — это полный стек, который может быть использован для обмена данными.
Структура цепочки протоколов:
typedef struct _WSAPROTOCOLCHAIN
{
// Длина цепочки:
// 0 — многоуровневый протокол, 1 — базовый,
// иные значения — цепочка протоколов.
int ChainLen;
// Идентификатор в каталоге — dwCatalogEntryId.
DWORD ChainEntries[MAX_PROTOCOL_CHAIN];
} WSAPROTOCOLCHAIN, *LPWSAPROTOCOLCHAIN;
На рис. 16.5 видно, что приложения могут непосредственно использовать только базовые протоколы и цепочки протоколов.
Рис. 16.5. Многоуровневые и основные протоколы
Внимание! Layered Service Providers и API для работы с ним устарели и не рекомендованы к использованию. В современных приложениях вместо них лучше использовать Windows Filtration Platform.
Многоуровневые протоколы и реализующие их многоуровневые провайдеры, или Layered Service Providers — LSP, устарели. Для их создания в структуре WSAPROTOCOL_INFO просто указывался путь к первому многоуровневому провайдеру в цепочке, и эта структура передавалась в его метод WSPStartup(). Многоуровневый провайдер обрабатывал часть функций, а для тех функций, которые не относятся к нему, вызывал функции нижележащего провайдера.
Подобные LSP ранее использовались для регулирования скорости закачки, фильтрации содержимого и прочего. Например, антивирус NOD32 использовал их для сканирования трафика, а прокси-сервер ProxyCap — для организации прозрачной отправки данных через прокси.
Информацию о доступных провайдерах транспорта вместе с протоколами предоставляет функция WSAEnumProtocols():
int WSAAPI WSAEnumProtocols(
LPINT lpiProtocols,
LPWSAPROTOCOL_INFO lpProtocolBuffer,
LPDWORD lpdwBufferLength
);
Параметры функции WSAEnumProtocols():
• lpiProtocols — завершенный нулем массив идентификаторов протоколов. Параметр опциональный. Если он нулевой, будет возвращена информация обо всех доступных протоколах, иначе — только о тех, что перечислены в массиве.
• lpProtocolBuffer — буфер, который будет заполнен массивом структур WSAPROTOCOL_INFO.
• lpdwBufferLength — количество байтов в переданном буфере. Если размер буфера недостаточен, функция запишет сюда его необходимый размер.
В случае успеха функция возвращает число протоколов в буфере, иначе — SOCKET_ERROR.
Посмотрим, какие провайдеры есть в системе. Для этого вызовем функцию их перечисления и выведем свойства.
Первый вызов почти наверняка завершится с ошибкой из-за нехватки места в буфере структур:
...
int main()
{
const socket_wrapper::SocketWrapper sw;
// Вектор для хранения провайдеров протоколов.
std::vector<WSAPROTOCOL_INFO> protocol_info(1);
DWORD real_buffer_len = protocol_info.size() * sizeof(WSAPROTOCOL_INFO);
// Первый вызов функции наверняка завершится с ошибкой из-за
// недостаточного размера буфера.
auto info_count = WSAEnumProtocols(nullptr, protocol_info.data(),
&real_buffer_len);
Требуемый размер буфера вернется в переменной real_buf_len. Расширим буфер и повторим вызов:
if (SOCKET_ERROR == info_count)
{
if (int e_code = WSAGetLastError(); e_code != WSAENOBUFS)
{
// Какая-то другая ошибка.
std::cerr << "WSAEnumProtocols failed with error: "
<< e_code << std::endl;
return EXIT_FAILURE;
}
else
{
std::cerr << "WSAEnumProtocols failed with error: WSAENOBUFS "
<< e_code << std::endl;
std::cout << "Increasing buffer size to "
<< real_buffer_len << std::endl;
// Увеличить размер буфера до того, который вернула функция.
protocol_info.resize(real_buffer_len /
sizeof(WSAPROTOCOL_INFO) + 1);
real_buffer_len = protocol_info.size() * sizeof(WSAPROTOCOL_INFO);
// Повторно вызвать функцию с буфером необходимого размера.
info_count = WSAEnumProtocols(nullptr, protocol_info.data(),
&real_buffer_len);
if (SOCKET_ERROR == info_count)
{
std::cerr << "WSAEnumProtocols failed with error: "
<< WSAGetLastError() << std::endl;
return EXIT_FAILURE;
}
}
}
Остается идти по вектору и выводить свойства разных провайдеров. Сначала выведем их общие данные, такие как название, протокол, номер в каталоге и т.п.:
std::cout << "WSAEnumProtocols succeeded with protocol count = "
<< info_count << std::endl;
for (size_t i = 0; i < info_count; ++i)
{
// Вывести данные сокетного провайдера.
std::cout
<< "Winsock Catalog Provider Entry " << i << "\n"
<< "Catalog Entry ID: "
<< protocol_info[i].dwCatalogEntryId << "\n"
<< "Version: " << protocol_info[i].iVersion << "\n"
<< "Entry type: "
<< ((1 == protocol_info[i].ProtocolChain.ChainLen) ?
"Base Service Provider" : "Layered Chain Entry")
<< "\n"
<< "Protocol: " << protocol_info[i].szProtocol << "\n"
<< "Protocol Chain length: "
<< protocol_info[i].ProtocolChain.ChainLen << "\n"
<< std::endl;
std::wstring guid_string(40, 0);
// Преобразовать GUID провайдера в строку и вывести GUID.
if (!StringFromGUID2(protocol_info[i].ProviderId,
reinterpret_cast<LPOLESTR>(guid_string.data()),
guid_string.size() — 1))
{
std::cerr << "StringFromGUID2 failed" << std::endl;
}
else
{
std::wcout << "Provider GUID: " << guid_string << std::endl;
}
Выведем оставшиеся поля:
std::cout
<< "Address Family: " << protocol_info[i].iAddressFamily << "\n"
<< "Max Socket Address Length: "
<< protocol_info[i].iMaxSockAddr << "\n"
<< "Min Socket Address Length: "
<< protocol_info[i].iMinSockAddr << "\n"
<< "Socket Type: " << protocol_info[i].iSocketType << "\n"
<< "Socket Protocol: " << protocol_info[i].iProtocol << "\n"
<< "Socket Protocol Max Offset: "
<< protocol_info[i].iProtocolMaxOffset << "\n"
<< "Network Byte Order: "
<< protocol_info[i].iNetworkByteOrder << "\n"
<< "Security Scheme: " << protocol_info[i].iSecurityScheme << "\n"
<< "Max Message Size: " << protocol_info[i].dwMessageSize << "\n"
<< std::endl;
}
return EXIT_SUCCESS;
}
Запустим пример. Провайдеров в системе, где запускалась утилита, порядка 11 штук, поэтому сократим вывод и покажем лишь несколько примеров:
D:\build\bin> b01-ch16-enum-protocols.exe
WSAEnumProtocols failed with error: WSAENOBUFS 10055
Increasing buffer size to 4464
WSAEnumProtocols succeeded with protocol count = 12
Winsock Catalog Provider Entry 0
Catalog Entry ID: 1001
Version: 2
Entry type: Base Service Provider
Protocol: MSAFD Tcpip [TCP/IP]
Protocol Chain length: 1
Provider GUID: {E70F1AA0-AB8B-11CF-8CA3-00805F48A192}
Address Family: 2
Max Socket Address Length: 16
Min Socket Address Length: 16
Socket Type: 1
Socket Protocol: 6
Socket Protocol Max Offset: 0
Network Byte Order: 0
Security Scheme: 0
Max Message Size: 0
Первый в списке — провайдер TCP по умолчанию от Microsoft. Он предоставляет TCP поверх IPv4. Следующий — провайдер UDP поверх IPv4 от Microsoft:
Winsock Catalog Provider Entry 1
Catalog Entry ID: 1002
Version: 2
Entry type: Base Service Provider
Protocol: MSAFD Tcpip [UDP/IP]
Protocol Chain length: 1
Provider GUID: {E70F1AA0-AB8B-11CF-8CA3-00805F48A192}
Address Family: 2
Max Socket Address Length: 16
Min Socket Address Length: 16
Socket Type: 2
Socket Protocol: 17
Socket Protocol Max Offset: 0
Network Byte Order: 0
Security Scheme: 0
Max Message Size: 65527
TCP-провайдер, который предоставляет TCP поверх IPv6, реализован отдельно:
Winsock Catalog Provider Entry 2
Catalog Entry ID: 1004
Version: 2
Entry type: Base Service Provider
Protocol: MSAFD Tcpip [TCP/IPv6]
Protocol Chain length: 1
Provider GUID: {F9EAB0C0-26D4-11D0-BBBF-00AA006C34E4}
Address Family: 23
Max Socket Address Length: 28
Min Socket Address Length: 28
Socket Type: 1
Socket Protocol: 6
Socket Protocol Max Offset: 0
Network Byte Order: 0
Security Scheme: 0
Max Message Size: 0
Аналогично для UDP и других протоколов. Другие провайдеры предоставляют работу с Bluetooth, поддержку связи по Hyper-V и т.п.
Вот, например, уже упомянутый провайдер сокетов AF_UNIX:
Winsock Catalog Provider Entry 4
Catalog Entry ID: 1007
Version: 2
Entry type: Base Service Provider
Protocol: AF_UNIX
Protocol Chain length: 1
Provider GUID: {A00943D9-9C2E-4633-9B59-0057A3160994}
Address Family: 1
Max Socket Address Length: 110
Min Socket Address Length: 2
Socket Type: 1
Socket Protocol: 0
Socket Protocol Max Offset: 0
Network Byte Order: 0
Security Scheme: 0
Max Message Size: 0
От функции WSAEnumProtocols() отличается сервисная функция WSCEnumProtocols(), которая возвращает полный список протоколов, в том числе тех, для которых установлен флаг PFL_HIDDEN в поле dwProviderFlags структуры WSAPROTOCOL_INFO, и фиктивных провайдеров LSP, у которых нулевая длина цепочки в ProtocolChain.ChainLen:
int WSCEnumProtocols(
LPINT lpiProtocols,
LPWSAPROTOCOL_INFO lpProtocolBuffer,
LPDWORD lpdwBufferLength,
LPINT lpErrno
);
Параметры функции те же, что и для WSA-аналога, кроме указателя на код ошибки — lpErrno.
Перечисление выполняется в порядке установки провайдеров. Это тот же порядок, в каком рассматриваются протоколы, когда запрашивается создание нового сокета.
Функция WSCWriteProviderOrder() из Sporder.dll позволяет установить новый порядок:
#include <sporder.h>
int WSCWriteProviderOrder(
LPDWORD lpwdCatalogEntryId,
DWORD dwNumberOfEntries);
Параметры функции WSCWriteProviderOrder():
• lpwdCatalogEntryId — массив идентификаторов провайдеров, который определяет их порядок.
• dwNumberOfEntries — количество идентификаторов в массиве.
Существует утилита Sporder.exe, позволяющая переупорядочить каталог провайдеров.
Чтобы транспортный протокол стал доступен приложениям, он должен быть установлен и зарегистрирован в WinSock.
Чтобы зарегистрировать новый провайдер, требуется предоставить одну или несколько заполненных структур WSAPROTOCOL_INFO и вызвать функцию установки провайдера:
#include <ws2spi.h>
int WSCInstallProvider(
LPGUID lpProviderId,
const WCHAR *lpszProviderDllPath,
const LPWSAPROTOCOL_INFOW lpProtocolInfoList,
DWORD dwNumberOfEntries,
LPINT lpErrno
);
int WSCInstallProviderAndChains(
LPGUID lpProviderId,
const LPWSTR lpszProviderDllPath,
const LPWSTR lpszLspName,
DWORD dwServiceFlags,
LPWSAPROTOCOL_INFOW lpProtocolInfoList,
DWORD dwNumberOfEntries,
LPDWORD lpdwCatalogEntryId,
LPINT lpErrno
);
Параметры функций WSCInstallProvider() и WSCInstallProviderAndChains:
• lpProviderId — уникальный идентификатор провайдера.
• lpszProviderDllPath — путь к DLL провайдера.
• lpszLspName — указатель на имя провайдера.
• dwServiceFlags — флаги для LSP. Всего определен только один флаг — XP1_IFS_HANDLES. Его назначение можно узнать в MSDN; напоминаем, что использовать данный API для установки LSP не рекомендуется.
• lpProtocolInfoList — указатель на массив структур WSAPROTOCOL_INFO. Каждая структура определяет протокол, семейство адресов и тип сокета, поддерживаемые данным провайдером.
• dwNumberOfEntries — количество структур в массиве.
• lpdwCatalogEntryId — указатель на недавно установленную «фиктивную» запись для провайдера транспорта в базе данных конфигурации системы Winsock 2. Этот идентификатор используется для установки записей каталога LSP.
• lpErrno — указатель на ошибку.
В случае успеха функции возвращают 0, в случае ошибки — SOCKET_ERROR, а код ошибки записывают по указателю в lpErrno.
Функция WSCDeinstallProvider() выполняет удаление провайдера:
int WSCDeinstallProvider(LPGUID lpProviderId, LPINT lpErrno);
Функция WPUCreateSocketHandle() создает новый дескриптор сокета для указанного провайдера транспорта.
Функция принимает два параметра: дескриптор сокета и указатель на код ошибки.
#include <ws2spi.h>
SOCKET WPUCreateSocketHandle(
DWORD dwCatalogEntryId,
DWORD_PTR dwContext,
LPINT lpErrno
);
Параметры функции WPUCreateSocketHandle():
• dwCatalogEntryId — идентификатор записи каталога.
• dwContext — значение контекста.
• lpErrno — указатель на код ошибки.
Функция возвращает новый дескриптор сокета в случае успеха или INVALID_SOCKET, если произошла ошибка.
Функция WPUCloseSocketHandle() закрывает существующий дескриптор сокета, созданный функцией WPUCreateSocketHandle():
int WPUCloseSocketHandle(SOCKET s, LPINT lpErrno);
Функция возвращает 0, если ошибки не произошло, или SOCKET_ERROR, если возникла ошибка. В этом случае код ошибки доступен в указателе на него.
Кратко рассмотрим API провайдеров пространств имен. Их основное назначение — работа по разрешению адресов, то есть запрос функций типа getaddrinfo() на разрешение www.google.com в IP-адрес будет выполнен провайдером пространств имен.
Такой подход не нов и похож на подход в современных Unix-подобных системах, в которых GLibC реализует сложный многоуровневый резолвер.
Следующие функции возвращают доступные провайдеры пространств имен:
#include <winsock2.h>
typedef struct _WSANAMESPACE_INFO
{
// Уникальный идентификатор провайдера.
GUID NSProviderId;
// Поддерживаемые провайдером пространства имен: NS_DNS, NS_EMAIL и т.п.
DWORD dwNameSpace;
// Если истина — провайдер активен.
bool fActive;
// Версия провайдера.
DWORD dwVersion;
// Отображаемый идентификатор.
LPTSTR lpszIdentifier;
// Далее идут данные, зависящие от провайдера в WSANAMESPACE_INFOEX.
// BLOB ProviderSpecific;
} WSANAMESPACE_INFO, *PWSANAMESPACE_INFO, *LPWSANAMESPACE_INFO;
int WSAAPI WSAEnumNameSpaceProviders(
LPDWORD lpdwBufferLength,
LPWSANAMESPACE_INFO lpnspBuffer
);
int WSAAPI WSAEnumNameSpaceProvidersEx(
LPDWORD lpdwBufferLength,
LPWSANAMESPACE_INFOEX lpnspBuffer
);
Параметры функций WSAEnumNameSpaceProviders() и WSAEnumNameSpaceProvidersEx():
• lpdwBufferLength — количество байтов в буфере, в который возвращается информация о провайдерах. Сюда же будет записано необходимое количество байтов, если размер буфера недостаточен.
• lpnspBuffer — массив структур WSANAMESPACE_INFO или WSANAMESPACE_INFOEX, расположенных последовательно.
В случае успеха функции возвращают количество структур в буфере, иначе — SOCKET_ERROR, а номер ошибки можно получить, вызвав WSAGetLastError().
В 64-битных ОС Windows существуют 32-битные функции, такие как WSCInstallNameSpace32() и WSCEnumNameSpaceProvidersEx32().
Эти функции оперируют 32-битным каталогом провайдеров, который разделен с 64-битным.
Данные функции нужны в целях совместимости.
Управлять порядком перечисления можно с помощью следующей функции:
#include <sporder.h>
int WSCWriteNameSpaceOrder(LPGUID lpProviderId, DWORD dwNumberOfEntries);
Параметры функции WSCWriteNameSpaceOrder():
• lpProviderId — массив идентификаторов провайдеров, который определяет их порядок.
• dwNumberOfEntries — количество идентификаторов в массиве.
Функция вернет 0 в случае успеха или код ошибки в случае неудачи.
В то время как провайдеры транспорта предоставляют услуги обмена данными, провайдеры имен предоставляют услуги конвертации IP-адресов и доменных имен.
Провайдеры должны быть зарегистрированы в пространстве имен. Существует несколько типов таких пространств.
Статические пространства имен требуют, чтобы все службы были зарегистрированы заранее, то есть при создании пространства имен. Примеры такого пространства — файлы узлов, протоколов и служб, используемые во многих реализациях TCP/IP.
В ОС Windows они находятся в каталоге %SystemRoot%\system32\drivers\etc.
Постоянные пространства имен позволяют службам регистрироваться в процессе исполнения. Сохраняют регистрационную информацию в постоянном хранилище, где она остается до тех пор, пока служба не запросит ее удаление. Такие пространства имен типичны для служб каталогов, например X.500 и DNS.
Хотя существует программный способ разрешения имен DNS с помощью сокетов Windows, провайдер пространства имен DNS в Windows не поддерживает регистрацию новых DNS-имен в постоянном хранилище с помощью WinSock.
Динамические пространства имен позволяют службам регистрироваться в процессе исполнения. Они не сохраняют регистрационную информацию в постоянное хранилище. Такие пространства часто полагаются на широковещательные рассылки, чтобы указать на доступность сетевой службы.
Пример такого пространства — протокол привязки имен NBP, применяемый в AppleTalk, или пространство PNRP — протокол разрешения одноранговых имен.
Устанавливают провайдер пространства имен функции WSCInstallNameSpace() и WSCInstallNameSpaceEx().
Рассмотрим их прототипы:
#include <ws2spi.h>
int WSCInstallNameSpace(
LPWSTR lpszIdentifier,
LPWSTR lpszPathName,
DWORD dwNameSpace,
DWORD dwVersion,
LPGUID lpProviderId
);
int WSCInstallNameSpaceEx(
LPWSTR lpszIdentifier,
LPWSTR lpszPathName,
DWORD dwNameSpace,
DWORD dwVersion,
LPGUID lpProviderId,
LPBLOB lpProviderSpecific
);
Параметры функций WSCInstallNameSpace() и WSCInstallNameSpaceEx():
• lpszIdentifier — указатель на строку-идентификатор провайдера.
• lpszPathName — путь к DLL провайдера. В строке поддерживается подстановка переменных, например %SystemRoot%.
• dwNameSpace — идентификатор пространства имен, которое поддерживается данным провайдером.
• dwVersion — версия провайдера.
• lpProviderId — уникальный идентификатор провайдера.
• lpProviderSpecific — данные, зависящие от провайдера.
Для провайдеров, способных поддерживать несколько пространств имен, эти функции необходимо вызывать для каждого поддерживаемого пространства имен, и каждый раз должен предоставляться уникальный идентификатор провайдера.
Функция WSCUnInstallNameSpace() удаляет провайдер:
int WSCUnInstallNameSpace(LPGUID lpProviderId);
В случае успеха функции возвращают ноль, иначе — SOCKET_ERROR.
Похоже, что данные в атрибуте ProviderSpecific используются единственным провайдером имен — NS_EMAIL, который позволяет работать с именами электронной почты.
Структура NAPI_PROVIDER_INSTALLATION_BLOB, определенная в nsemail.h, позволяет задать факт поддержки регулярных выражений и список доменов при установке провайдера.
А структура NAPI_DOMAIN_DESCRIPTION_BLOB, возвращаемая перечислением, содержит домен, с которым работает этот провайдер имен.
Все функции установки и удаления провайдеров требуют прав администратора. Также администратор или пользователь, имеющий сходные права, может включать и выключать провайдеры имен:
#include <ws2spi.h>
int WSCEnableNSProvider(LPGUID lpProviderId, bool fEnable);
Параметры функции WSCEnableNSProvider():
• lpProviderId — GUID провайдера.
• fEnable — включить или выключить провайдер.
Возвращаемые значения те же, что и для функций выше.
Когда приложение создает новый сокет, который будет связан с провайдером, WinSock инициирует загрузку DLL провайдера, после чего вызывает функцию WSPStartup():
#include <ws2spi.h>
int WSPStartup(
WORD wVersionRequested,
LPWSPDATA lpWSPData,
LPWSAPROTOCOL_INFOW lpProtocolInfo,
WSPUPCALLTABLE upcallTable,
LPWSPPROC_TABLE lpProcTable
);
Параметр wVersionRequested содержит максимальную версию SPI, которая может быть использована в вызове.
Параметр upcallTable входной и содержит адреса функций, которые используются провайдером, а в lpProcTable провайдер записывает адреса вызовов:
WSPUPCALLTABLE upcallTable =
{
// Инициализировать таблицу адресами вызовов.
};
// Переменная, в которую будут сохранены вызовы.
LPWSPPROC_TABLE lpProcTable;
wVersionRequested = MAKEWORD(2, 2);
if (WSPStartup(wVersionRequested, &WSPData, lpProtocolBuffer,
upcallTable, lpProcTable) != 0)
{
return;
}
// Проверить, что провайдер поддерживает версию WinSock 2.2.
if (LOBYTE(WSPData.wVersion) != 2 || HIBYTE(WSPData.wVersion) != 2)
{
// Нужный провайдер не был найден.
LPWSPCleanup();
return;
}
Провайдер содержит счетчик ссылок для каждого процесса, а WSPStartup() может вызываться несколько раз, и поэтому для каждого успешного вызова должен быть вызван WSPCleanup().
В процессе инициализации Ws2_32.dll извлекает таблицу диспетчеризации, полученную в качестве параметра WSPStartup(), из атрибута lpProcTable.
Для библиотек, приложений и провайдеров транспорта могут существовать разные версии, от которых зависит набор функций, параметров структур данных и т.п.
Согласование версии обрабатывается функцией WSPStartup().
Ws2_32.dll передает провайдеру наивысшие номера версий, с которыми совместима библиотека.
Провайдер сравнивает версии с собственным поддерживаемым диапазоном номеров версий и возвращает значение из перекрывающейся части диапазона, обычно максимально возможное.
Если диапазоны не перекрываются, то есть стороны несовместимы, функция возвращает ошибку.
При каждом вызове WSPStartup() может указываться любой номер версии, поддерживаемой библиотекой провайдера, но если процесс вызывает WSPStartup() несколько раз, провайдер может использовать только последний указатель на таблицу.
Использование таблицы вызовов более удобно: можно сделать один вызов, чтобы обнаружить весь набор функций. Кроме того, это позволяет цепочкам провайдеров работать более эффективно.
Таблицы выглядят следующим образом:
typedef struct WSPData
{
WORD wVersion;
WORD wHighVersion;
WCHAR szDescription[WSPDESCRIPTION_LEN + 1];
} WSPDATA, *LPWSPDATA;
// Адреса функций, используемых провайдером.
typedef struct _WSPUPCALLTABLE
{
LPWPUCLOSEEVENT lpWPUCloseEvent;
LPWPUCLOSESOCKETHANDLE lpWPUCloseSocketHandle;
LPWPUCREATEEVENT lpWPUCreateEvent;
LPWPUCREATESOCKETHANDLE lpWPUCreateSocketHandle;
...
LPWPUSETEVENT lpWPUSetEvent;
LPWPUOPENCURRENTTHREAD lpWPUOpenCurrentThread;
LPWPUCLOSETHREAD lpWPUCloseThread;
} WSPUPCALLTABLE, *LPWSPUPCALLTABLE;
Адреса функций провайдера, которые он возвращает для использования в API:
typedef struct _WSPPROC_TABLE
{
LPWSPACCEPT lpWSPAccept;
LPWSPADDRESSTOSTRING lpWSPAddressToString;
LPWSPASYNCSELECT lpWSPAsyncSelect;
LPWSPBIND lpWSPBind;
...
LPWSPSEND lpWSPSend;
LPWSPSENDDISCONNECT lpWSPSendDisconnect;
LPWSPSENDTO lpWSPSendTo;
LPWSPSETSOCKOPT lpWSPSetSockOpt;
LPWSPSHUTDOWN lpWSPShutdown;
LPWSPSOCKET lpWSPSocket;
LPWSPSTRINGTOADDRESS lpWSPStringToAddress;
} WSPPROC_TABLE, *LPWSPPROC_TABLE;
Точно так же инициализация производится для провайдеров имен:
#include <ws2spi.h>
typedef struct _NSP_ROUTINE
{
// Размер структуры в байтах.
DWORD cbSize;
// Номера версии спецификации, поддерживаемой сервис-провайдером.
DWORD dwMajorVersion;
DWORD dwMinorVersion;
// Указатели на функции.
LPNSPCLEANUP NSPCleanup;
LPNSPLOOKUPSERVICEBEGIN NSPLookupServiceBegin;
LPNSPLOOKUPSERVICENEXT NSPLookupServiceNext;
LPNSPLOOKUPSERVICEEND NSPLookupServiceEnd;
LPNSPSETSERVICE NSPSetService;
LPNSPINSTALLSERVICECLASS NSPInstallServiceClass;
LPNSPREMOVESERVICECLASS NSPRemoveServiceClass;
LPNSPGETSERVICECLASSINFO NSPGetServiceClassInfo;
LPNSPIOCTL NSPIoctl;
} NSP_ROUTINE, *LPNSP_ROUTINE;
int WSAAPI NSPStartup(
LPGUID lpProviderId,
LPNSP_ROUTINE lpnspRoutines
);
После каждого вызова NSPStartup() также должен вызываться NSPCleanup(), который возвращает провайдер имен в таблице lpnspRoutines.
Провайдер имен версии 2 будет использовать структуру NSPV2_ROUTINE. Суть та же, но указатели на функции отличаются. Мы не будем ее рассматривать.
В силу ограниченного размера книги не будем рассматривать в ней полную реализацию провайдеров.
Провайдеры требуется реализовывать достаточно редко, но их код имеет свои особенности. Например, провайдеры должны предоставлять достаточно большое число вызовов, которые используются вышележащим API.
Сокетный API для обмена данными имеет как POSIX-совместимые функции, так и реализованные только в WinSock.
Чтобы создать новый сокет в ОС Windows, используются функции socket() и WSASocket(). Последняя из них является специфичной для Windows и позволяет указывать дополнительные флаги, а также использовать заданный провайдер транспорта, который будет обслуживать сокет.
Для сокетов в ОС Windows поддерживаются все основные адресные семейства, которые есть в Unix-подобных системах. Некоторые типы сокетов, например raw-сокеты, имеют достаточно много ограничений, а использовать их могут только члены группы администраторов.
В ОС Windows 10 появились также Unix-domain-сокеты, что теперь позволяет создавать IPC, переносимые между Unix-подобными системами и ОС Windows.
Привязка адреса к сокету выполняется так же, как в POSIX API, вызовом функции bind().
Сокеты Windows позволяют выполнять блокирующий и неблокирующий обмен данными. Вариантов реализации неблокирующего ввода-вывода в Windows существует несколько, их мы рассмотрим в книге 2.
Для связи без установки соединения используются как уже знакомые нам функции recvfrom() и sendto(), так и специфичные для ОС Windows — WSARecvFrom() и WSASendTo(). Последние в данном случае отличаются только возможностью производить асинхронный ввод-вывод.
Для связи с установлением соединения на стороне клиента используются функции connect() и WSAConnect(), а также более удобные WSAConnectByName() и WSAConnectByList().
Чтобы перевести сокет в состояние прослушивания, используется функция listen(), а для приема запроса на соединение от клиента — функции accept() и WSAAccept(). Получение данных на подключенном сокете выполняется функциями recv() и WSARecv(), а отправка — функциями send(), WSASend().
Обмен данными реализован в провайдерах транспорта, которые являются разделяемыми библиотеками. А разрешение имен реализуют провайдеры имен.
Из-за того что провайдеры должны быть зарегистрированы в системе, существуют API для установки провайдеров, их перечисления и удаления.
Сокеты можно связывать с провайдерами, WinSock при этом инициирует загрузку DLL-провайдера, после чего вызывает функцию WSPStartup().
1. Когда необходимо использовать функцию WSASocket() для создания нового сокета в WinSock? А когда — функцию socket()?
2. Почему для функции bind() не существует WSA-аналога, например WSABind()?
3. Могут ли в ОС Windows работать приложения, использующие Unix-сокеты, без значительных изменений?
4. Чем отличается работа с raw-сокетами в ОС Windows от работы с ними же в Linux?
5. Каковы основные отличия блокирующего ввода-вывода на сокетах от неблокирующего? Какие плюсы и минусы есть у каждого?
6. Для чего нужны функции WSARecvFrom() и WSASendTo()? В чем их отличие от recvfrom() и sendto()?
7. Для чего нужен флаг MSG_PARTIAL в функции WSASendTo()? Каковы его особенности?
8. В чем преимущества и недостатки функций WSAConnectByName() и WSAConnectByList() по сравнению с connect()?
9. Как правильно выбрать значение параметра backlog функции listen() в ОС Windows?
10. Что позволяет делать функция WSAAccept() по сравнению с accept()?
11. Какая функция закрывает сокет в WinSock?
12. В чем особенности работы с внеполосными данными в ОС Windows?
13. Назовите три характерных отличия POSIX-сокетов от Windows-сокетов. Как нивелировать эти отличия при разработке приложения?
14. Чем отличается тип SOCKET на 64-разрядной системе от типа дескриптора в POSIX?
15. Зачем нужны провайдеры транспорта? А провайдеры пространств имен?
16. Все ли функции сокетного API Windows реализуются провайдерами? Если нет, то какие не реализуются? Почему?
17. В каких случаях требуется реализация собственных провайдеров?
18. Какие классы протоколов могут поддерживать провайдеры транспорта?
19. Стоит ли использовать LSP при разработке новых приложений?
20. Для чего нужна функция NSPStartup() и когда она будет вызвана?
21. В примере, выводящем провайдеры, мы вывели их списком. Определите, какие именно провайдеры были выведены и за что они отвечают.
22. Создайте клиент, используя функции WinSock, ориентированные на соединение. Клиент должен отправлять данные из командной строки.
23. Создайте сервер, используя функции WinSock, ориентированные на соединение. Сервер должен выводить на экран всю информацию, которая приходит от клиента.