В ближайшие 10 лет я не вижу большого коммерческого потенциала у интернета.
Билл Гейтс, из выступления на COMDEX, 1994
В этой главе мы рассмотрим особенности API для работы с адресами в ОС Windows. Мы уделим внимание структурам данных и функциям, используемым в WinSock для выполнения операций с IDN, с IPv4- и IPv6-адресами, таким как преобразование имен в адреса и обратно или разбор строки адреса.
Также мы рассмотрим функции WinSock, позволяющие получать информацию о локальных и удаленных точках подключения.
Затем, по аналогии с главой 2, мы рассмотрим, как работать с DNS и переводить доменные имена в IP-адреса и обратно.
Мы также познакомимся с базой служб, что позволит запрашивать сетевые ресурсы у DNS-серверов и регистрировать свои ресурсы, которые мы хотим предоставлять. Кроме того, коснемся функций регистрации служб в базе и удаления их из базы.
В конце главы затронем тонкие моменты работы с DNS и настройку резолвера.
Структура для адресации в сокете аналогична той, что используется в POSIX-сокетах и была рассмотрена в главе 2.
Приложения указывают IPv4-адрес и порт для обмена через структуру sockaddr_in:
typedef struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;
Структура sockaddr_in6 тоже аналогична описанной в главе 6. Но структуры адресов для конкретных протоколов определяются иначе.
Для IPv4 структура определена в winsock2.h:
struct in_addr
{
union
{
// IPv4-адрес в формате четырех символов u_char.
struct
{
u_char s_b1;
u_char s_b2;
u_char s_b3;
u_char s_b4;
} S_un_b;
// IPv4-адрес в формате двух u_shorts.
struct
{
u_short s_w1;
u_short s_w2;
} S_un_w;
// Адрес IPv4 в формате u_long.
u_long S_addr;
} S_un;
};
А для IPv6 в in6addr.h:
typedef struct in6_addr
{
union
{
u_char Byte[16];
u_short Word[8];
} u;
} IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR;
Структура sockaddr_storage определена в ws2def.h. Она аналогична POSIX-структуре, но имеет дополнительное имя типа — SOCKADDR_STORAGE.
Существует еще одна крайне похожая на sockaddr_storage структура — sockaddr_storage_xp.
Она требуется для работы функций WSK — WinSock Kernel, то есть сокетов в режиме ядра.
Мы данной работы в режиме ядра не касаемся и эту структуру не рассматриваем.
Некоторые функции, например WSASetService(), работают с адресной структурой CSADDR_INFO, которая определена следующим образом:
#include <nspapi.h>
#include <winsock2.h>
// Адреса, которые возвращаются с ответом.
typedef struct _CSADDR_INFO
{
// Локальный адрес, например, установленный bind().
SOCKET_ADDRESS LocalAddr;
// Адрес удаленного абонента, который установлен, например connect().
SOCKET_ADDRESS RemoteAddr;
// Тип сокета, например SOCK_STREAM.
int iSocketType;
// Используемый протокол, например IPPROTO_TCP.
int iProtocol;
} CSADDR_INFO, *PCSADDR_INFO, *LPCSADDR_INFO;
Эту структуру также может быть удобно использовать для описания соединения между узлами.
Рис. 15.1. Структура адреса CSADDR_INFO
Как видно из рис. 15.1, структуры, которые хранят локальный и удаленный адреса, здесь тоже нестандартные:
typedef struct sockaddr
{
#if (_WIN32_WINNT < 0x0600)
u_short sa_family;
#else
// Семейство адресов
ADDRESS_FAMILY sa_family;
#endif
// Данные адреса.
char sa_data[14];
} SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;
typedef struct _SOCKET_ADDRESS
{
// Указатель на адрес.
LPSOCKADDR lpSockaddr;
// Длина адреса.
int iSockaddrLength;
} SOCKET_ADDRESS, *PSOCKET_ADDRESS, *LPSOCKET_ADDRESS;
Для преобразования адресов WinSock API предоставляет большое количество функций, разобраться в которых сразу может быть сложно. Попробуем рассмотреть большую их часть в этом разделе.
WinSock также содержит расширенные функции, обозначаемые суффиксом Ex.
Например GetAddrInfoEx() — аналог рассмотренной далее GetAddrInfo().
Помимо того что данные функции содержат некоторые дополнительные параметры, например, GetAddrInfoEx() позволяет выбирать провайдер имен, отличный от DNS, они часто используются как асинхронные. Разбирать их в этой главе мы не будем — это одна из тем книги 2.
В WinAPI существует набор функций для работы с числовыми значениями IP-адресов. Большая часть из этих функций не документирована, но они могут быть полезны. Здесь мы рассмотрим некоторые из них.
Для сравнения IP-адресов могут быть использованы следующие функции:
#include <mstcpip.h>
// Сравнить адреса одного семейства.
bool INET_ADDR_EQUAL(ADDRESS_FAMILY af, const void* a, const void* b);
// Сравнить невыровненные адреса.
bool INET_UNALIGNED_ADDR_EQUAL(ADDRESS_FAMILY af,
const void* a, const void* b);
// Здесь используется универсальная структура SOCKADDR,
// и семейства адресов сравниваются в первую очередь.
bool INETADDR_ISEQUAL(const SOCKADDR *a, const SOCKADDR *b)
Параметры функций сравнения:
• af — семейство адресов: AF_INET либо AF_INET6;
• a и b — сравниваемые адреса.
Функции для проверки IP-адреса на то, инициализирован ли он или является подстановочным адресом для всех интерфейсов:
// Это any-адрес, то есть адрес всех сетевых интерфейсов (0.0.0.0)?
bool INET_IS_ADDR_UNSPECIFIED(ADDRESS_FAMILY af, const void* a);
bool INET_IS_UNALIGNED_ADDR_UNSPECIFIED(ADDRESS_FAMILY af, const void* a);
// То же самое для SOCKADDR.
bool INETADDR_ISUNSPECIFIED(const SOCKADDR *a);
bool INETADDR_ISANY(const SOCKADDR *a);
// Вернуть any-адрес.
const UCHAR* INET_ADDR_UNSPECIFIED(ADDRESS_FAMILY af);
// Установить переданный по указателю адрес в any.
void INETADDR_SETANY(PSOCKADDR a);
Проверка на то, что это адрес локальной петли, и получение этого адреса:
// Это адрес локальной петли?
bool INET_IS_ADDR_LOOPBACK(ADDRESS_FAMILY af, const void* a);
bool INETADDR_ISLOOPBACK(const SOCKADDR *a);
// Установить значение адреса в адрес локальной петли.
void INETADDR_SETLOOPBACK(PSOCKADDR a);
Проверка на то, что адрес широковещательный или адресует группу многоадресной рассылки:
// Это широковещательный адрес?
bool INET_IS_ADDR_BROADCAST(ADDRESS_FAMILY af, const void* a);
// Это адрес многоадресной рассылки?
bool INET_IS_ADDR_MULTICAST(ADDRESS_FAMILY af, const void* a);
Определение длины адресов:
// Определить длину адреса, то есть размер структур IN_ADDR или IN6_ADDR
// для указанного семейства.
size_t INET_ADDR_LENGTH(ADDRESS_FAMILY af);
// Определить длину структуры SOCKADDR_IN6 или SOCKADDR_IN.
size_t INET_SOCKADDR_LENGTH(ADDRESS_FAMILY af);
Получение и установка адресного поля из структуры, которая содержит адрес и порт:
// Вернуть поле адреса: sin_addr или sin6_addr.
PUCHAR INETADDR_ADDRESS(const SOCKADDR* a);
// Установить поле адреса.
VOID INETADDR_SET_ADDRESS(PSOCKADDR a, const UCHAR *Address);
Получение из той же структуры порта:
// Вернуть поле порта.
USHORT INETADDR_PORT(const SOCKADDR *a);
// Установить порт.
void INETADDR_SET_PORT(PSOCKADDR a, USHORT Port);
Определение того, что переданный IPv6-адрес лежит в диапазоне IPv4-адресов.
// Это IPv6-адрес из диапазона IPv4-адресов?
bool INETADDR_ISV4MAPPED(const SOCKADDR *a);
Данные функции, хотя и не документированы, используются уже давно и, вероятно, будут сохранены в API. Какие-то из них можно использовать в коде приложений.
Для получения IP-адресов из строки рекомендуется использовать функции, описанные в главе 2, особенно в кросс-платформенном коде:
#include <ws2tcpip.h>
int WSAAPI inet_pton(int family, PCSTR pszAddrString, PVOID pAddrBuf);
Параметры функции inet_pton():
• family — семейство адресов: AF_INET либо AF_INET6.
• pszAddrString — указатель на строку-источник, содержащую текстовое представление IP-адреса.
• pAddrBuf — указатель на буфер, в котором хранится числовое представление IP-адреса в сетевом порядке байтов.
В случае успеха, так же как и inet_pton() в POSIX, функция вернет 1, а буфер pAddrBuf будет содержать IP-адрес в сетевом порядке байтов.
PCSTR WSAAPI inet_ntop(
int family,
const void *pAddr,
PSTR pStringBuf,
size_t stringBufSize
);
Параметры функции inet_ntop():
• pAddr — указатель на IP-адрес в сетевом байте для преобразования в строку.
• pStringBuf — указатель на буфер, в который сохраняется строковое представление IP-адреса, завершающееся нулем. Размер буфера должен быть не менее 16 символов для IPv4-адреса и не менее 46 символов для IPv6.
• stringBufSize — длина буфера pStringBuf.
Возвращаемое значение такое же, как и для POSIX inet_ntop().
Их Windows-аналоги — InetPton() и InetNtop() — были рассмотрены в разделе об ANSI- и Unicode-версиях функций.
Внимание! Данные функции поддерживают только Internet-адреса, то есть IPv4 и IPv6.
Функция inet_addr(), используется для формирования IPv4-адреса:
WINSOCK_API_LINKAGE unsigned long WSAAPI inet_addr(const char FAR * cp);
Параметр cp — строка, заканчивающаяся нулем, представляющая числа адреса в «точечной» нотации.
Числа могут быть десятичными, восьмеричными или шестнадцатеричными. Причем их возможно смешивать и задавать не все компоненты.
Функция вернет значение типа unsigned long. Например:
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
...
// Число, содержащее IP-адрес.
unsigned long ul_addr = inet_addr(argv[1]);
// Различные варианты ошибок.
switch (ul_addr)
{
case INADDR_NONE:
case INADDR_ANY:
{
WSACleanup();
throw ...
}
}
Обратная функция:
char FAR * WSAAPI inet_ntoa(in_addr in);
Она преобразует IP-адрес в строковый вид. Так же как и в POSIX, в ОС Windows данные функции являются устаревшими.
В некоторых версиях ОС Windows это могут быть не функции, а макросы. В любом случае пользоваться ими нежелательно по следующим причинам:
• они поддерживают только адреса IPv4 и помечены как нерекомендованные;
• они не являются переносимыми.
Еще одна функция, которая преобразует сетевой адрес из текстовой формы в двоичную:
#include <winsock2.h>
int WSAAPI WSAStringToAddress(
LPSTR addressString,
int addressFamily,
LPWSAPROTOCOL_INFO lpProtocolInfo,
LPSOCKADDR lpAddress,
LPINT lpAddressLength
);
Параметры функции WSAStringToAddress():
• addressString — строка адреса в стандартной текстовой форме.
• addressFamily — семейство адресов, к которому принадлежит адрес в строке addressString.
• lpProtocolInfo — необязательный указатель на структуру LPWSAPROTOCOL_INFOA либо LPWSAPROTOCOL_INFOW. В общем случае на LPWSAPROTOCOL_INFO. Если этот параметр нулевой, вызов направляется провайдеру первого протокола, поддерживающего заданное семейство адресов.
• lpAddress — указатель на структуру, в которой будет сохранен адрес.
• lpAddressLength — указатель на длину буфера lpAddress. Если вызов функции успешен, в переменную, на которую указывает этот параметр, будет записан размер структуры sockaddr, возвращенный в параметре lpAddress.
Функция вернет 0 в случае успеха, а в случае ошибки — SOCKET_ERROR.
Если размер буфера недостаточен, функция завершается с кодом ошибки WSAEFAULT, который можно получить, вызвав WSAGetLastError(), как будет рассмотрено в главе 24. А в переменную, на которую указывает lpAddressLength, сохраняется требуемый размера буфера.
Это позволяет динамически подбирать размер при необходимости.
Существует и обратная функция, преобразующая адрес из структуры sockaddr в печатную форму:
int WSAAPI WSAAddressToString(
LPSOCKADDR lpsaAddress,
DWORD dwAddressLength,
LPWSAPROTOCOL_INFO lpProtocolInfo,
LPSTR lpszAddressString,
LPDWORD lpdwAddressStringLength
);
Параметры функции WSAAddressToString():
• lpsaAddress — указатель на структуру sockaddr, которая будет преобразована в строку.
• dwAddressLength — длина адреса в структуре sockaddr, то есть sizeof(sockaddr). Размер отличается в разных протоколах.
• lpProtocolInfo — указатель на структуру WSAPROTOCOL_INFO конкретного провайдера. Если этот параметр нулевой, вызов направляется провайдеру первого протокола, поддерживающего заданное семейство адресов.
• lpszAddressString — указатель на буфер, в который будет сохранен адрес.
• lpdwAddressStringLength — размер буфера адреса. После успешного завершения переменная, на которую указывает параметр, будет содержать размер строки, включая завершающий ее нулевой символ.
Возвращаемые значения те же, что и у предыдущей функции.
Особенность этих функций в том, что вместо указателя на буфер они принимают указатель на структуру sockaddr, в которую сохраняют результат. Кроме того, они позволяют явно выбирать провайдер протокола, используемый для преобразования адресов.
Данные функции поддерживают IPv6, начиная с Windows XP SP1.
Начиная с версии Windows Vista, в IP Helper существует заголовочный файл ip2string.h, в котором объявлены более гибкие функции преобразования адресов в печатную форму и обратно:
// Строка в IPv4.
NTSYSAPI NTSTATUS RtlIpv4StringToAddress(
PCSTR s,
BOOLEAN strict,
PCSTR *terminator,
in_addr *addr
);
// Строка в IPv6.
NTSYSAPI NTSTATUS RtlIpv6StringToAddress(
PCSTR s,
PCSTR *terminator,
in6_addr *addr
);
Параметры функций RtlIpv4StringToAddress() и RtlIpv6StringToAddress():
• s — строка, которая содержит IP-адрес.
• strict — флаг формата. Если значение истинно, строка должна быть IPv4-адресом, представленным строго в десятичном формате из четырех чисел, разделенных точками. В противном случае допускается любая из форм строкового представления IPv4-адреса: десятичная, восьмеричная или шестнадцатеричная.
• terminator — параметр, который получает указатель на символ, завершающий преобразованную строку.
• addr — указатель на структуру, в которую будет сохранено двоичное представление адреса.
Существуют и расширенные варианты данных функций:
// Преобразовать строку в IPv4.
NTSYSAPI NTSTATUS RtlIpv4StringToAddressEx(
PCSTR addressString,
BOOLEAN strict,
in_addr *address,
PUSHORT port
);
// Преобразовать строку в IPv6.
NTSYSAPI NTSTATUS RtlIpv6StringToAddressExA(
PCSTR addressString,
in6_addr *address,
PULONG scopeId,
PUSHORT port
);
Параметры функций RtlIpv4StringToAddressEx() и RtlIpv6StringToAddressExA():
• addressString — строка, содержащая IP-адрес.
• strict — см. в описании RtlIpv4StringToAddress() выше.
• address — указатель на выходную переменную для сохранения адреса.
• port — указатель на переменную, в которую должен сохраниться номер порта. Он сохраняется в сетевом порядке байтов. Если в строке он не указан, порт устанавливается равным 0.
• scopeId — указатель на идентификатор области IPv6-адреса. Если строка адреса не содержит строкового представления идентификатора области, в этом параметре возвращается 0.
Функции возвращают STATUS_SUCCESS в случае успеха, STATUS_INVALID_PARAMETER в случае некорректного значения параметра адреса либо иную ошибку в других случаях. Инициализации WinSock эти функции не требуют.
Для работы с Ethernet-адресами в том же файле объявлены следующие функции:
// Преобразование Ethernet-адреса в строку.
NTSYSAPI PSTR RtlEthernetAddressToString(
const DL_EUI48 *addr,
PSTR s
);
// Преобразование Ethernet-адреса из строки.
NTSYSAPI NTSTATUS RtlEthernetStringToAddress(
PCSTR s,
PCSTR *terminator,
DL_EUI48 *addr
);
Параметры функций RtlEthernetAddressToString() и RtlEthernetStringToAddress():
• s — строка адреса. Входной или выходной буфер. Формат строки — шесть пар шестнадцатеричных символов, разделенных дефисами, например: F4-CE-46-2D-90-8C.
• terminator — указатель на символ, завершающий преобразованную строку.
• addr — Ethernet-адрес в сетевом порядке байтов.
У функции, преобразующей адрес в строку, возвращаемое значение — адрес буфера в случае успеха или нулевой указатель в случае ошибки. Обратная функция возвращает такие же значения, как и функции преобразования IP-адресов, описанные выше.
Структура Ethernet-адреса определена в заголовочном файле netiodef.h:
// Уникальный идентификатор в рамках организации
// — Organizationally Unique Identifier.
union _DL_OUI
{
UINT8 Byte[3];
// Первый байт. 0bxxxxxxLG.
struct
{
// Младший значащий бит.
UINT8 Group : 1;
UINT8 Local : 1;
};
};
typedef union _DL_OUI DL_OUI, *PDL_OUI;
// Идентификатор расширения — Extension Identifier — для адресов EUI-48.
union _DL_EI48
{
UINT8 Byte[3];
};
typedef union _DL_EI48 DL_EI48, *PDL_EI48;
// Адрес EUI-48.
union _DL_EUI48
{
UINT8 Byte[6];
struct
{
DL_OUI Oui;
DL_EI48 Ei48;
};
};
typedef union _DL_EUI48 DL_EUI48, *PDL_EUI48;
Но включать данный файл обычно не стоит, так как структура непрозрачная и объявлена в нескольких других заголовочных файлах.
Данные функции удобны, но не переносимы. Кроме того, функции, начинающиеся с Rtl, то есть Run-Time Library, не включены в Windows SDK, поскольку они содержатся в библиотеке Ntdll.dll, иными словами, являются оболочками над вызовами ядра. На это же указывает макрос NTSYSAPI.
Макрос NTSYSAPI при использовании функции раскрывается в __declspec(dllimport).
Целесообразность их использования зависит от того, планируется ли в будущем переносить код на другую ОС.
В IP Helper API имеется несколько полезных функций для конвертации маски в длину префикса и длины в маску:
#include <netioapi.h>
NETIOAPI_API ConvertLengthToIpv4Mask(ULONG MaskLength, PULONG Mask);
NETIOAPI_API ConvertIpv4MaskToLength(ULONG Mask, PUINT8 MaskLength);
Параметры функций ConvertLengthToIpv4Mask() и ConvertIpv4MaskToLength():
• MaskLength — длина префикса;
• Mask — маска.
Функции вернут NO_ERROR в случае успеха.
Вызывать функцию для конвертации длины префикса в маску удобно следующим образом:
union
{
// Маска как число.
ULONG ul;
// Маска по байтам.
uint8_t b[4];
} mask;
ConvertLengthToIpv4Mask(netmask_length, &mask.ul);
Строку, которая содержит адрес и порт, можно разобрать, используя системный API:
#include <iphlpapi.h>
typedef struct NET_ADDRESS_INFO_
{
NET_ADDRESS_FORMAT Format;
union
{
struct
{
WCHAR Address[DNS_MAX_NAME_BUFFER_LENGTH];
WCHAR Port[6];
} NamedAddress;
SOCKADDR_IN Ipv4Address;
SOCKADDR_IN6 Ipv6Address;
SOCKADDR IpAddress;
};
} NET_ADDRESS_INFO, *PNET_ADDRESS_INFO;
DWORD ParseNetworkString(
const WCHAR *NetworkString,
DWORD Types, PNET_ADDRESS_INFO AddressInfo,
USHORT *PortNumber, BYTE *PrefixLength);
Параметры функции ParseNetworkString():
• NetworkString — строка для разбора.
• Types — тип строки:
• NET_STRING_IP_ADDRESS — IPv4-адрес в стандартной десятичной нотации с точками или адрес IPv6 в стандартной шестнадцатеричной нотации. Идентификатор области IPv6 может присутствовать, а порт или префикс — нет. Соответствует NET_STRING_IPV4_ADDRESS | NET_STRING_IPV6_ADDRESS.
• NET_STRING_IP_ADDRESS_NO_SCOPE — адрес IPv4 или адрес IPv6 без идентификатора области. Соответствует NET_STRING_IPV4_ADDRESS | NET_STRING_IPV6_ADDRESS_NO_SCOPE.
• NET_STRING_IP_SERVICE — служба IPv4 или служба IPv6. Порт обязателен в строке. Идентификатор области IPv6 может присутствовать, префикс — нет. Соответствует NET_STRING_IPV4_SERVICE | NET_STRING_IPV6_SERVICE.
• NET_STRING_IP_SERVICE_NO_SCOPE — служба IPv4 или служба IPv6. Порт обязателен в строке. Идентификатор области IPv6 и префикс не должны присутствовать в строке. Соответствует NET_STRING_IPV4_SERVICE | NET_STRING_IPV6_SERVICE_NO_SCOPE.
• NET_STRING_IP_NETWORK — сеть IPv4 или IPv6. Префикс в нотации CIDR требуется как часть сетевой строки, а порт или идентификатор области — нет. Соответствует NET_STRING_IPV4_NETWORK | NET_STRING_IPV6_NETWORK.
• NET_STRING_ANY_ADDRESS — адрес IPv4, адрес IPv6 или имя DNS. Идентификатор области IPv6 может присутствовать в строке, сетевой порт или префикс — нет. Соответствует типам NET_STRING_NAMED_ADDRESS | NET_STRING_IP_ADDRESS.
• NET_STRING_ANY_ADDRESS_NO_SCOPE — адрес IPv4, адрес IPv6 или имя DNS. Идентификатор области IPv6, порт и префикс не должны присутствовать в строке. Соответствует типам NET_STRING_NAMED_ADDRESS | NET_STRING_IP_ADDRESS_NO_SCOPE.
• NET_STRING_ANY_SERVICE — служба IPv4, IPv6 или имя DNS. Порт должен присутствовать в строке. Идентификатор области IPv6 может присутствовать, а префикс — нет. Соответствует типам NET_STRING_NAMED_SERVICE | NET_STRING_IP_SERVICE.
• NET_STRING_ANY_SERVICE_NO_SCOPE — указывает на службу IPv4, IPv6 или имя DNS. Порт требуется в строке, а идентификатора области IPv6 и префикса в ней быть не должно. Соответствует NET_STRING_NAMED_SERVICE | NET_STRING_IP_SERVICE_NO_SCOPE.
• AddressInfo — сюда будет записан разобранный адрес.
• PortNumber — по указателю в эту переменную будет записан порт в порядке байтов узла. Если порта в строке нет, будет записан 0.
• PrefixLength — сюда будет записана длина префикса.
В случае успеха функция вернет ERROR_SUCCESS. Она может вернуть ERROR_INSUFFICIENT_BUFFER, если размер буфера недостаточен для записи адреса.
Для работы с интернационализированными доменными именами, описанными в RFC 3490 «Internationalizing Domain Names in Applications (IDNA)», существует несколько функций WinAPI.
Рассмотрим их прототипы:
#include <winnls.h>
// Преобразовать IDN в Punycode.
int IdnToUnicode(
DWORD dwFlags,
LPCWSTR lpASCIICharStr,
int cchASCIIChar,
LPWSTR lpUnicodeCharStr,
int cchUnicodeChar
);
// Преобразовать IDN или другую метку в форму NamePrep,
// указанную в RFC 3491.
int IdnToNameprepUnicode(
DWORD dwFlags,
LPCWSTR lpUnicodeCharStr,
int cchUnicodeChar,
LPWSTR lpNameprepCharStr,
int cchNameprepChar
);
// Преобразовать IDN в Punycode.
int IdnToAscii(
DWORD dwFlags,
LPCWSTR lpUnicodeCharStr,
int cchUnicodeChar,
LPWSTR lpASCIICharStr,
int cchASCIIChar
);
Параметры функций IdnToUnicode(), IdnToNameprepUnicode(), IdnToAscii():
• dwFlags — флаги, определяющие параметры преобразования:
• IDN_ALLOW_UNASSIGNED — разрешить включение неназначенных кодовых точек во входную строку. Если они присутствуют и не разрешены, функция вернет ошибку ERROR_INVALID_NAME. Флаг позволяет функции обрабатывать символы, которые в настоящее время недопустимы в IDN, но могут быть разрешены в более поздних версиях стандарта IDNA.
• IDN_USE_STD3_ASCII_RULES — отфильтровать символы ASCII, которые не разрешены в именах STD3. Единственными символами ASCII, разрешенными во входной строке, являются буквы, цифры и дефис. Строка не может начинаться или заканчиваться дефисом.
• IDN_EMAIL_ADDRESS — если флаг установлен, в случае ошибки в адресе e-mail будет возвращен резервный адрес, если он доступен, например <local>@gmail.com.
• IDN_RAW_PUNYCODE — отключить проверку и сопоставление Punycode.
• lpASCIICharStr — указатель на буфер, который получает строку, состоящую только из набора символов ASCII.
• cchASCIIChar — размер входной ASCII-строки либо размер буфера для выходной ASCII-строки в lpASCIICharStr.
• lpUnicodeCharStr — указатель на Unicode-строку, представляющую IDN в кодировке UTF-16.
• cchUnicodeChar — размер входной Unicode-строки, переданной в lpUnicodeCharStr, либо размер выходного буфера для Unicode-строки.
Сначала в функции передается входной буфер, затем — выходной. Если в качестве выходного буфера передан нулевой указатель и в соответствующее значение длины установлено в 0, функции вернут требуемый размер выходного буфера.
Иначе в случае успеха будет возвращено количество символов в буфере результата, а в случае ошибки — 0.
Внимание! POSIX-функции возвращают 0 в случае успеха, но данные функции возвращают 0 в случае ошибки.
Чтобы получить расширенную информацию об ошибке, приложение может вызвать функцию GetLastError() с одним из следующих кодов ошибки:
• ERROR_INSUFFICIENT_BUFFER — предоставленный размер буфера был недостаточно большим или равным 0.
• ERROR_INVALID_FLAGS — значения, предоставленные для флагов, недействительны.
• ERROR_INVALID_NAME — указано недопустимое имя.
• ERROR_INVALID_PARAMETER — одно из значений параметра было недопустимым.
• ERROR_NO_UNICODE_TRANSLATION — в строке обнаружен недопустимый код Unicode.
В главах 24 и 25 мы подробнее рассмотрим обработку ошибок и исключительных ситуаций, в частности данную функцию.
Функции, описанные в главе 2, доступны и в ОС Windows. В большинстве случаев уместно использовать их. Но для настройки и тонкой работы с DNS существует специфичный Windows API.
В составе WinSock имеется структура hostent, которая описана в главе 2. Она похожа на структуру в POSIX, но параметры семейства адресов имеют тип short, а не int:
typedef struct hostent
{
// Официальное имя узла, FQDN.
char *h_name;
// Псевдонимы.
char **h_aliases;
// Семейство адресов.
short h_addrtype;
// Длина адреса.
short h_length;
// Список адресов.
char **h_addr_list;
} HOSTENT, *PHOSTENT, *LPHOSTENT;
С данной структурой работают функции gethostbyaddr() для получения структуры по адресу узла и gethostbyname() по его имени.
Напоминаем, что данные функции являются устаревшими, поэтому использовать их не рекомендуется. Но при необходимости их описание можно посмотреть в MSDN.
Функции для обхода базы узлов в WinSock отсутствуют.
Прототипы функций для получения адресов с использованием DNS:
#include <ws2tcpip.h>
int WSAAPI getaddrinfo(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult
);
int WSAAPI GetAddrInfoW(
PCWSTR pNodeName,
PCWSTR pServiceName,
const ADDRINFOW *pHints,
PADDRINFOW *ppResult
);
#define GetAddrInfoA getaddrinfo
Существует также функция GetAddrInfoEx(), которая помимо работы в асинхронном режиме, дает возможность задавать таймаут ответа.
Функция getaddrinfo() имеет POSIX-совместимый прототип:
• pNodeName — имя узла либо строка, которая содержит его IPv4-адрес в «точечной» нотации либо IPv6-адрес в шестнадцатеричной нотации.
• pServiceName — строка, которая содержит имя службы или порт.
• pHints — указатель на структуру, аналогичную addrinfo.
• ppResult — список возвращенных адресов.
Функция также возвращает 0 при успехе и код ошибки в ином случае.
В nspapi.h объявлена функция GetAddressByName(), которая запрашивает пространство имен, чтобы получить информацию о сетевом адресе для указанной сетевой службы.
Данная функция является устаревшей и не используется в WinSock2. Поэтому мы ее рассматривать не будем.
Структура ADDRINFOA немного отличается от POSIX-структуры addrinfo:
struct addrinfo
{
// AI_PASSIVE, AI_CANONNAME и т.д.
int ai_flags;
// AF_UNSPEC, AF_INET, AF_INET6, AF_NETBIOS, AF_BTH, AF_IRDA.
int ai_family;
// SOCK_STREAM, SOCK_DGRAM.
int ai_socktype;
// IPPROTO_TCP, IPPROTO_UDP, IPPROTO_RM или IPPROTO_PGM для надежной
// широковещательной передачи.
int ai_protocol;
// Размер ai_addr в байтах.
size_t ai_addrlen;
// Полное каноническое имя. Здесь присутствует отличие:
// в POSIX-структуре данное поле расположено после ai_addr.
char *ai_canonname;
// struct sockaddr_in или _in6.
struct sockaddr *ai_addr;
// Следующий узел списка.
struct addrinfo *ai_next;
} ADDRINFOA, *PADDRINFOA;
Разница в том, что поле ai_canonname расположено перед ai_addr, тогда как в POSIX-структуре — после.
Структура ADDRINFOW, которая является типом аргумента функции GetAddrInfoW(), отличается тем, что для канонического имени в ней используется строка широких символов:
typedef struct addrinfoW
{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
// Каноническое имя в UNICODE.
PWSTR ai_canonname;
struct sockaddr *ai_addr;
struct addrinfoW *ai_next;
} ADDRINFOW, *PADDRINFOW;
В поле ai_flags поддерживаются флаги, которые есть в POSIX, но отличаются от тех, которые есть в Linux. Флаги AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST, AI_NUMERICSERV, AI_V4MAPPED работают, как описано в главе 2.
Флаг AI_ADDRCONFIG говорит о том, что GetAddrInfoW() будет выполнять разрешение имен только в том случае, если настроен глобальный адрес, а не адрес локальной петли.
Флаги AI_IDN, AI_CANONIDN, AI_IDN_ALLOW_UNASSIGNED, AI_IDN_USE_STD3_ASCII_RULES не поддерживаются.
Windows также имеет несколько флагов, которые используются только в этой ОС, причем в разных версиях они разные. Следующие флаги поддерживаются, начиная с Windows Vista, и относятся только к адресам электронной почты, то есть к провайдеру пространства имен NS_EMAIL:
• AI_NON_AUTHORITATIVE — если флаг установлен, провайдер возвращает результаты как с авторитетных, так и с неавторитетных DNS-серверов. В противном случае — только с авторитетных.
• AI_SECURE — если установлен бит AI_SECURE, провайдер будет возвращать результаты, полученные безопасно, чтобы свести к минимуму возможность спуфинга.
• AI_RETURN_PREFERRED_NAMES — если флаг установлен, провайдер вернет предпочтительные имена для публикации. В параметре pNodeName не должно указываться имя.
Авторитетный DNS-сервер является конечным владельцем IP-адреса домена, поиск которого производится, и хранит информацию о записях DNS.
Авторитетный DNS-сервер обычно является последним шагом в цепочке рекурсивных запросов IP-адреса и содержит информацию, специфичную для доменного имени, которое он обслуживает.
Кроме него могут существовать, например, кэширующие DNS-серверы.
Еще несколько флагов поддерживаются, начиная с Windows 7 и 8:
• AI_FQDN — если установлен этот флаг и указана только одна компонента имени, функция getaddrinfo() вернет полное доменное имя в атрибуте ai_canonname результирующей структуры addrinfo. При установке флага AI_CANONNAME возвращается каноническое имя из записи DNS, которое может отличаться от FQDN. Поэтому флаги AI_FQDN и AI_CANONNAME нельзя устанавливать одновременно: это приведет к ошибке EAI_BADFLAGS.
• AI_FILESERVER — подсказка провайдеру пространства имен о том, что запрашиваемое имя хоста используется в сценарии совместного доступа к файлам. Провайдер пространства имен может игнорировать этот флаг.
• AI_DISABLE_IDN_ENCODING — отключить автоматическое кодирование IDN в Punycode.
Удалять выделенные структуры нужно, используя те же функции, что и в POSIX:
void WSAAPI freeaddrinfo(PADDRINFOA pAddrInfo);
void WSAAPI FreeAddrInfoW(PADDRINFOW pAddrInfo);
#define FreeAddrInfoA freeaddrinfo
#ifdef UNICODE
#define FreeAddrInfo FreeAddrInfoW
#else
#define FreeAddrInfo FreeAddrInfoA
#endif
Для преобразования кода ошибки getaddrinfo() в строку используется уже описанная в главе 2 функция gai_strerror().
В Windows эта функция представляет собой вызов FormatMessage() с флагом FORMAT_MESSAGE_FROM_SYSTEM. То есть ошибки getaddrinfo() здесь не отличаются от других системных ошибок.
Пример использования данной функции на практике приведен в главе 2 — это резолвер, который успешно компилируется и работает в ОС Windows.
Иногда требуется зарегистрировать имя некоторой службы и связанные адреса в базе. Для этого используется следующая функция:
#include <ws2tcpip.h>
int WSAAPI SetAddrInfoEx(
PCTSTR pName,
PCTSTR pServiceName,
SOCKET_ADDRESS *pAddresses,
DWORD dwAddressCount,
LPBLOB lpBlob,
DWORD dwFlags,
DWORD dwNameSpace,
LPGUID lpNspId,
timeval *timeout,
LPOVERLAPPED lpOverlapped,
LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine,
LPHANDLE lpNameHandle
);
Параметры функции SetAddrInfoEx():
• pName — имя, под которым должны быть зарегистрированы адреса, либо имя для удаления адреса.
• pServiceName — имя службы, ассоциированное с именем в pName.
• pAddresses — указатель на необязательный массив адресов.
• dwAddressCount — число адресов в массиве. Если 0, все адреса для pName будут удалены.
• lpBlob — данные, специфичные для провайдера.
• dwFlags — флаги, специфичные для провайдера.
• dwNameSpace — идентификатор пространства имен, например:
• NS_ALL — все активные неймспейсы;
• NS_DNS — данные, получаемые от DNS, в том числе IP-адреса;
• NS_EMAIL — пространство имен электронной почты.
• lpNspId — необязательный идентификатор конкретного провайдера имен, который используется для работы с адресами.
• timeout — тайм-аут ожидания, по завершении которого вызов будет завершен принудительно.
• lpOverlapped — необязательный указатель на структуру, используемую для перекрывающегося ввода-вывода.
• lpCompletionRoutine — необязательный указатель на процедуру, вызываемую при успешном завершении операции.
• lpNameHandle — параметр, требуемый для асинхронных операций. Зарезервирован и должен быть равен 0.
В случае успеха функция возвращает 0, в случае неудачи — код ошибки. Регистрацию и отмену регистрации на момент написания книги поддерживает только провайдер NS_EMAIL, начиная с Windows Vista.
Эта функция не поддерживает асинхронные операции, и относящиеся к ним параметры не используются.
Остальные провайдеры, в том числе работающие в пространстве имен NS_DNS, данную функциональность не поддерживают.
Для обратного преобразования используется функция getnameinfo():
#include <ws2tcpip.h>
// Максимальный размер для буфера, на который указывает pServiceBuffer.
#define NI_MAXSERV 32
// Максимальный размер для буфера, на который указывает pNodeBuffer.
#define NI_MAXHOST 1025
int WSAAPI getnameinfo(
const SOCKADDR *pSockaddr,
socklen_t sockaddrLength,
PCHAR pNodeBuffer,
DWORD nodeBufferSize,
PCHAR pServiceBuffer,
DWORD serviceBufferSize,
int flags
);
int WSAAPI GetNameInfoW(
const SOCKADDR *pSockaddr,
socklen_t sockaddrLength,
PWCHAR pNodeBuffer,
DWORD nodeBufferSize,
PWCHAR pServiceBuffer,
DWORD serviceBufferSize,
int flags
);
#define GetNameInfoA getnameinfo
Макросы, содержащие максимальный размер, также определены в ws2tcpip.h.
Прототипы функций почти не отличаются от POSIX-вариантов, рассмотренных в главе 2. Значения флагов также одинаковые, но флаг NI_NUMERICSCOPE отсутствует.
Возвращаемые значения также совместимы. Данные функции поддерживаются, начиная с Windows XP SP2.
В ОС Windows имеется возможность получать данные сетевых служб подобно тому, как описано в главе 2. Однако для этого используется иной API.
К базе можно делать запросы, описанные структурой WSAQUERYSET:
#include <winsock2.h>
typedef struct _WSAQuerySet
{
// Размер структуры: sizeof(WSAQUERYSET).
DWORD dwSize;
// Имя службы. Может содержать подстановочные символы.
LPTSTR lpszServiceInstanceName;
// Идентификатор класса. Обязателен для запроса.
LPGUID lpServiceClassId;
// Версия провайдера имен.
LPWSAVERSION lpVersion;
// Необязательный комментарий. Игнорируется запросом.
LPTSTR lpszComment;
// Пространство имен: NS_ALL, NS_DNS, NS_EMAIL и т.п.
// Может использоваться запросом.
DWORD dwNameSpace;
// Необязательный GUID провайдера имен. Если он передан в запросе,
// будет запрошен только указанный провайдер.
LPGUID lpNSProviderId;
// Указатель на необязательную начальную точку запроса
// в иерархическом пространстве имен.
LPTSTR lpszContext;
// Размер в байтах массива ограничений протокола.
DWORD dwNumberOfProtocols;
// Опциональный список протоколов.
// Возвращены будут только службы, которые используют эти протоколы.
LPAFPROTOCOLS lpafpProtocols;
// Указатель на необязательную строку запроса.
LPTSTR lpszQueryString;
// Количество элементов в массиве lpcsaBuffer. Не используется запросом.
DWORD dwNumberOfCsAddrs;
// Указатель на массив структур CSADDR_INFO. Не используется запросом,
// но может быть использован в ответе вместе с полем выше.
LPCSADDR_INFO lpcsaBuffer;
// Не используется запросом.
DWORD dwOutputFlags;
// Данные, специфичные для провайдера.
LPBLOB lpBlob;
} WSAQUERYSET, *PWSAQUERYSET, *LPWSAQUERYSET;
Структура WSAQUERYSET изображена на рис. 15.2. В эту же структуру будет возвращаться результат запроса.
Рис. 15.2. Структура WSAQUERYSET
В lpBlob хранится рассмотренная ранее структура hostent. Формат ее хранения показан на рис. 15.3.
Указатели внутри нее — это смещения данных от начала атрибута lpBlobData. Поэтому чтобы получить данные как через обычные указатели, необходимо добавить к ним базовый адрес структуры.
В массивах h_aliases и h_addr_list указатели также являются смещениями, каждое 32-битное поле в месте ссылки будет состоять из смещений.
Рис. 15.3. Структура hostent в BLOB структуры WSAQUERYSET
Для провайдеров имен версии 2 используется структура WSAQUERYSET2.
Она отличается от WSAQUERYSET только тем, что в новой структуре отсутствует поле lpServiceClassId.
Используемые протоколы описываются в поле lpafpProtocols, которое содержит массив структур AFPROTOCOLS:
#include <nspapi.h>
#include <winsock2.h>
// Структура протоколов, которыми могут ограничиваться запросы.
typedef struct _AFPROTOCOLS
{
// Семейство, например AF_INET.
int iAddressFamily;
// Протокол, например IPPROTO_TCP.
int iProtocol;
} AFPROTOCOLS, *PAFPROTOCOLS, *LPAFPROTOCOLS;
Функция WSALookupServiceBegin() выполняет первый запрос:
#include <winsock2.h>
int WSAAPI WSALookupServiceBegin(
LPWSAQUERYSET lpqsRestrictions,
DWORD dwControlFlags,
LPHANDLE lphLookup
);
Параметры функции WSALookupServiceBegin():
• lpqsRestrictions — критерии поиска.
• dwControlFlags — флаги, управляющие глубиной поиска:
• LUP_DEEP — сделать рекурсивный запрос и вернуть не только первый уровень.
• LUP_CONTAINERS или LUP_NOCONTAINERS — возвращать только контейнеры или не возвращать контейнеры. Обычно этот флаг используется для того, чтобы запрашивать устройства, например, при работе с Bluetooth.
• LUP_NEAREST — упорядочить результаты по метрике расстояния. Как будет рассчитана метрика, зависит от провайдера.
• LUP_RETURN_NAME — получить имя в lpszServiceInstanceName.
• LUP_RETURN_TYPE — получить тип в lpServiceClassId.
• LUP_RETURN_VERSION — получить версию в lpVersion.
• LUP_RETURN_COMMENT — получить комментарий в lpszComment.
• LUP_RETURN_ADDR — получить адреса в lpcsaBuffer.
• LUP_RETURN_BLOB — получить данные провайдера в lpBlob.
• LUP_RETURN_ALIASES — возвращать информацию о псевдонимах при следующих запросах, выполняемых через WSALookupServiceNext(). Каждый возвращаемый псевдоним будет иметь установленный флаг RESULT_IS_ALIAS.
• LUP_RETURN_QUERY_STRING — получить строку запроса.
• LUP_RETURN_ALL — установить все флаги LUP_RETURN_.
• LUP_FLUSHPREVIOUS — указывает провайдеру отбросить последний набор результатов, который был слишком велик для буфера, и перейти к следующему набору результатов. Используется в качестве значения параметра dwControlFlags в WSALookupServiceNext().
• LUP_FLUSHCACHE — игнорировать кэш, если провайдер его использует. Например, в случае DNS будет сделан запрос к серверу, даже если запись уже получена.
• LUP_RES_SERVICE — указывает, находится ли основной ответ в RemoteAddr- или в LocalAddr- атрибутах структуры CSADDR_INFO.
• lphLookup — переменная, в которую будет сохранен дескриптор, используемый при вызове WSALookupServiceNext().
Некоторые пространства имен, такие как распределенная система каталогов Whois++, поддерживают задание в строке lpszQueryString расширенных SQL-запросов.
Эта функция «открывает базу служб». При успешном выполнении она возвращает 0 и записывает в lphLookup дескриптор, который будет использован при вызовах WSALookupServiceNext() для получения последовательности результатов:
int WSAAPI WSALookupServiceNext(
HANDLE hLookup,
DWORD dwControlFlags,
LPDWORD lpdwBufferLength,
LPWSAQUERYSET lpqsResults
);
int WSAAPI WSALookupServiceEnd(
HANDLE hLookup
);
Параметры функции WSALookupServiceNext():
• hLookup — дескриптор, возвращенный функцией WSALookupServiceBegin().
• dwControlFlags — флаги, такие же, как в функции WSALookupServiceBegin().
• lpdwBufferLength — длина буфера lpqsResults в байтах.
• lpqsResults — буфер результатов.
Функция WSALookupServiceEnd() «закрывает базу», то есть завершает выполнение запроса. В случае ошибки функции возвращают SOCKET_ERROR.
Хотя указание большего или меньшего количества флагов не является ошибкой, флаги при вызове WSALookupServiceNext() не могут расширять фильтр и запрашивать больше возвращаемых структур, чем было запрошено в WSALookupServiceBegin().
Флаги комбинируются с флагами WSALookupServiceBegin(). Но флаги, указанные в WSALookupServiceNext(), применяются только к текущему вызову и не сохраняются для последующих.
В базе можно зарегистрировать новую службу либо удалить существующую. За это отвечает следующая функция:
#include <winsock2.h>
int WSAAPI WSASetService(
LPWSAQUERYSET lpqsRegInfo,
WSAESETSERVICEOP essoperation,
DWORD dwControlFlags
);
Параметры функции WSASetService():
• lpqsRegInfo — указатель на структуру WSAQUERYSET для регистрации или дерегистрации службы.
• essoperation — операция, выполняемая функцией:
• RNRSERVICE_REGISTER — зарегистрировать службу. Если такая служба уже зарегистрирована, при установленном флаге SERVICE_MULTIPLE — добавить новый адрес службы, иначе — переписать запись о ней полностью.
• RNRSERVICE_DEREGISTER — удалить службу из реестра. При установленном флаге SERVICE_MULTIPLE — удалить все адреса, не удаляя службу.
• RNRSERVICE_DELETE — удалить службу из динамического и постоянного пространства имен. При установленном флаге SERVICE_MULTIPLE — удалить заданные адреса.
• dwControlFlags — дополнительные флаги:
• SERVICE_MULTIPLE — флаг управляет адресами, позволяя добавлять или удалять адреса, по которым доступна служба.
В зависимости от провайдера эта операция может отправлять широковещательные сообщения или менять содержимое постоянного хранилища.
В nspapi.h есть также функции SetService() и GetService(), позволяющие зарегистрировать и получить службу. Эти функции требуются для совместимости с WinSock 1.1 и уже устарели.
Функция WSASetService() принимает набор адресов службы из массива структур CSADDR_INFO в поле lpcsaBuffer. Функции перечисления служб также заполняют этот массив, если был указан флаг LUP_RETURN_ADDR.
Службы могут принадлежать некоторому классу, описывающему пространства имен, в которых будет зарегистрирована служба. Класс задает ее некоторые характеристики. Например, требует ли служба логического соединения для взаимодействия с ней или ориентирована на обмен без соединения.
Если требуется использовать классы служб, их можно установить при регистрации службы; если классы больше не нужны, их можно удалить. Для этих операций применяются следующие функции:
// Установить класс службы.
int WSAAPI WSAInstallServiceClass(
LPWSASERVICECLASSINFO lpServiceClassInfo
);
// Удалить класс.
int WSAAPI WSARemoveServiceClass(
int LPGUID lpServiceClassId
);
Устанавливаемый класс описывается структурой WSASERVICECLASSINFO:
#include <winsock2.h>
typedef struct _WSANSClassInfo
{
// Название параметра, например TCPPORT.
LPTSTR lpszName;
// Идентификатор пространства имен.
DWORD dwNameSpace;
// Тип параметра, например REG_DWORD.
DWORD dwValueType;
// Размер параметра в байтах.
DWORD dwValueSize;
// Указатель на значение параметра.
LPVOID lpValue;
} WSANSCLASSINFO, *PWSANSCLASSINFO, *LPWSANSCLASSINFO;
typedef struct _WSAServiceClassInfo
{
// Идентификатор класса.
LPGUID lpServiceClassId;
// Имя класса.
LPTSTR lpszServiceClassName;
// Количество элементов в массиве lpClassInfos.
DWORD dwCount;
// Массив параметров.
LPWSANSCLASSINFO lpClassInfos;
} WSASERVICECLASSINFO, *PWSASERVICECLASSINFO, *LPWSASERVICECLASSINFO;
Как показано на рис. 15.4, класс — это просто группа, имеющая следующие параметры:
• идентификатор;
• имя;
• параметры, специфичные для пространства имен.
Рис. 15.4. Структура WSASERVICECLASSINFO
Параметры зарегистрированного класса можно получить, используя функцию WSAGetServiceClassInfo():
int WSAAPI WSAGetServiceClassInfo(
LPGUID lpProviderId,
LPGUID lpServiceClassId,
LPDWORD lpdwBufSize,
LPWSASERVICECLASSINFOA lpServiceClassInfo
);
Параметры функции WSAGetServiceClassInfo():
• lpProviderId — указатель на GUID провайдера пространства имен.
• lpServiceClassId — указатель на GUID, идентифицирующий класс.
• lpdwBufSize — размер указателя lpServiceClassInfo в байтах, как входного, так и выходного. Если функция завершается с ошибкой WSAEFAULT, в этом параметре она вернет минимальный размер буфера, необходимый для получения записи.
• lpServiceClassInfo — указатель на структуру WSASERVICECLASSINFO, содержащую информацию о классе обслуживания.
В случае успеха функция вернет 0.
В качестве примера реализуем программу, которая выводит данные о службе ECHO. Сначала инициализируем сетевую подсистему. Затем вызовем WSALookupServiceBegin():
// Определение GUID служб.
extern "C"
{
#include <svcguid.h>
}
...
int main(int argc, char* argv[])
{
uint32_t result = 0;
HANDLE h_lookup = 0;
WSAQUERYSET first_query = {};
// Идентификатор службы: SVCID_UDP = 7.
GUID guid = SVCID_ECHO_UDP;
try
{
socket_wrapper::SocketWrapper sw;
first_query.dwSize = sizeof(WSAQUERYSET);
first_query.lpServiceClassId = &guid;
// "Открыть базу" служб. Для проверки на корректность можно
// использовать макросы.
if (FAILED(WSALookupServiceBegin(&first_query,
LUP_RETURN_NAME |
LUP_RETURN_COMMENT |
LUP_RETURN_ADDR, &h_lookup)))
{
throw std::system_error(sw.get_last_error_code(),
std::system_category(),
"Error on WSALookupServiceBegin()");
}
GUID различных служб описан в svcguid.h, который мы включили.
Далее в цикле вызываем WSALookupServiceNext(), чтобы получить другие описания при их наличии.
Первый вызов необходим, чтобы выделить буфер подходящего размера:
uint32_t result = 0;
// Цикл получения служб.
while (true)
{
WSAQUERYSET test_query = {};
DWORD length = sizeof(test_query);
// Первый вызов функции должен завершиться с ошибкой
// из-за того, что ему передается запрос с нулевыми полями.
// Но вызов запишет в length требуемую длину буфера под ответ.
if (SUCCEEDED(WSALookupServiceNext(h_lookup, 0, &length,
&test_query)))
{
std::cerr << "Impossible" << std::endl;
break;
}
auto err_code = GetLastError();
if (WSAEFAULT == err_code)
{
// Выделить буфер нужной длины.
auto pdata_shared = std::make_shared<char[]>(length);
WSAQUERYSET* p_data =
reinterpret_cast<WSAQUERYSET*>(pdata_shared.get());
// А это уже реальный вызов, который возвращает ответ.
if (FAILED(WSALookupServiceNext(h_lookup, 0, &length,
p_data)))
{
result = WSAGetLastError();
// Windows может вернуть два разных кода ошибки, если
// результатов больше нет.
if ((WSA_E_NO_MORE == result) || (WSAENOMORE == result))
{
std::cout << "No more records found!" << std::endl;
break;
}
// Реальная ошибка.
else throw std::system_error(sw.get_last_error_code(),
std::system_category(),
"Error on WSALookupServiceNext()");
}
Покажем, как вывести адреса службы. Существуют службы вообще без адресов, например SVCID_HOSTNAME, возвращающая имя узла.
В данном случае адрес будет только один:
// Вывести имя службы, номер пространства имен, а также
// количество адресов.
std::wcout
<< "Service instance name: "
<< p_data->lpszServiceInstanceName << "\n"
<< "Name space num: " << p_data->dwNameSpace << "\n"
<< "Address count: " << p_data->dwNumberOfCsAddrs
<< std::endl;
// Вывести адреса службы.
for (size_t i = 0; i < p_data->dwNumberOfCsAddrs; ++i)
{
if (IPPROTO_UDP == p_data->lpcsaBuffer[i].iProtocol)
{
std::array<char, INET_ADDRSTRLEN> addr;
const auto& sa = reinterpret_cast<const sockaddr_in*>(
p_data->lpcsaBuffer[i].RemoteAddr.lpSockaddr);
// Для примера выводим только IPv4-адреса.
assert(AF_INET == sa->sin_family);
std::cout
<< inet_ntop(AF_INET, &sa->sin_addr, addr.data(),
addr.size())
<< std::endl;
}
}
}
Завершим работу с базой служб, вызвав функцию WSALookupServiceEnd() на дескрипторе, который был получен при открытии базы:
else if (result != WSA_E_NO_MORE && result != WSAENOMORE)
{
break;
}
else
throw std::system_error(sw.get_last_error_code(),
std::system_category(),
"Error on WSALookupServiceNext()");
};
// Завершить работу с базой служб.
if (WSALookupServiceEnd(h_lookup))
throw std::system_error(
sw.get_last_error_code(), std::system_category(),
"WSALookupServiceEnd(hlookup) failed");
}
...
После запуска примера будет выведено имя службы, номер пространства имен и адрес службы:
D:\build\bin> b01-ch15-service_base.exe
Service instance name: win-ws
Namespace num: 12
Address count: 1
192.168.3.254
Так же как в Linux, в ОС Windows существует API для работы с DNS-резолвером. Его функции и структуры объявлены в заголовочном файле windns.h. Обычно для работы достаточно вызова функции getaddrinfo() и подобных. Необходимость напрямую работать с резолвером возникает редко, но в некоторых сценариях этот API бывает востребован. Например, показательной является установка DNS-серверов для каждого приложения с возможностью использовать как серверы для приложения, так и системные.
Мы не будем подробно рассматривать API полностью, а лишь систематизируем его, чтобы было проще обращаться к MSDN.
Некоторые функции резолвера существуют не только для ANSI и широких символов, но и для UTF-8. Большинство этих функций отличаются суффиксами _A, _W и _UTF8.
Например, функция DnsQuery() существует в трех экземплярах: DnsQuery_A(), DnsQuery_W() и DnsQuery_UTF8().
Единственная функция, имеющая «классический» суффикс без подчеркивания, — DnsReplaceRecordSet(), которая раскрывается в DnsReplaceRecordSetA(), DnsReplaceRecordSetW() и DnsReplaceRecordSetUTF8().
«Обычные» ANSI и широкосимвольные функции можно использовать как обычно, а версию UTF-8 придется вызывать явно. Причем эти версии функции принимают строку из однобайтовых символов.
Многие функции из этого раздела выделяют память, которую требуется освободить, для чего используются отдельные функции.
Часть функций, в основном те, которые выполняют запросы, — асинхронные, поэтому существуют функции для их отмены.
Зачастую перед работой с DNS или непосредственно в процессе работы требуется выполнять различные «простые» операции. Например, сравнить имена без учета регистра, нормализовав их исключением лишних строк:
#include <windns.h>
// Сравнить два имени DNS.
bool DnsNameCompare(PCTSTR pName1, PCTSTR pName2);
Или проверить имя на корректность:
#include <windns.h>
// Проверить состояние указанного DNS-имени.
DNS_STATUS DnsValidateName(PCTSTR pszName, DNS_NAME_FORMAT Format);
// Проверить заданный IP-адрес на корректность
// DNS сервера, способного выполнять запросы.
DNS_STATUS DnsValidateServerStatus(PSOCKADDR server, PCWSTR queryName,
PDWORD serverStatus);
Первая функция выполняет проверки на формальную корректность: длину имени, допустимые символы, правильность расположения точек и т.д. — и в зависимости от этого возвращает разные ошибки.
Возвращаемый тип DNS_STATUS — обычный unsigned long:
typedef _Return_type_success_(return == 0) LONG DNS_STATUS;
typedef DNS_STATUS *PDNS_STATUS;
Возвращаемое значение EXIT_SUCCESS говорит о том, что функции завершились успешно.
Существуют функции для работы с учетными данными:
// Создать контекст для набора учетных данных и вернуть его хэндл.
// В этом контексте будут храниться учетные данные некоторого аккаунта.
DNS_STATUS DnsAcquireContextHandle(DWORD CredentialFlags, PVOID Credentials,
PHANDLE pContext);
// Освободить контекст, хранящий учетные данные.
void DnsReleaseContextHandle(PHANDLE pContext);
Созданный этими функциями контекст, содержащий учетные данные, может использоваться далее, например, в функциях работы с ресурсными записями DNS.
Запросы — основа работы с DNS. Для их выполнения существует набор функций в составе API резолвера.
Создать сообщение с DNS-запросом в буфере типа DNS_MESSAGE_BUFFER:
bool DnsWriteQuestionToBuffer_W(
PDNS_MESSAGE_BUFFER pDnsBuffer,
PDWORD pdwBufferSize, PCWSTR pszName, WORD wType,
WORD Xid, bool fRecursionDesired);
Существует также аналогичная функция DnsWriteQuestionToBuffer_UTF8().
Основной интерфейс для запросов к DNS, который предоставляет функциональность для выполнения разрешения имен:
DNS_STATUS DnsQuery(PCSTR pszName, WORD wType, DWORD Options, PVOID pExtra,
PDNS_RECORD *ppQueryResults, PVOID *pReserved);
Выполнить асинхронный DNS-запрос:
DNS_STATUS DnsQueryEx(PDNS_QUERY_REQUEST pQueryRequest,
PDNS_QUERY_RESULT pQueryResults,
PDNS_QUERY_CANCEL pCancelHandle);
Запросить конфигурацию локального хоста или конкретного сетевого интерфейса:
DNS_STATUS DnsQueryConfig(DNS_CONFIG_TYPE Config, DWORD Flag,
PCWSTR pwsAdapterName, PVOID pReserved,
PVOID pBuffer, PDWORD pBufLen);
Дает возможность получить доменное имя, список DNS-серверов, информацию о сетевом адаптере и т.п.
Отправить произвольный DNS-запрос из «сырых» данных, например уже сформированный пакет запроса:
DNS_STATUS DnsQueryRaw(DNS_QUERY_RAW_REQUEST *queryRequest,
DNS_QUERY_RAW_CANCEL *cancelHandle)
Выполнить запрос, используя mDNS. Система mDNS не требует наличия сервера имен. Она выполняет поиск заданных служб, рассылая широковещательные UDP-запросы:
DNS_STATUS DnsStartMulticastQuery(PMDNS_QUERY_REQUEST pQueryRequest,
PMDNS_QUERY_HANDLE pHandle)
Асинхронные запросы могут быть отменены следующими функциями:
// Отменить выполняемый запрос к DNS, инициированный вызовом DnsQuery().
DNS_STATUS DnsCancelQuery(PDNS_QUERY_CANCEL pCancelHandle);
// Отменить запрос, инициированный вызовом DnsQueryRaw().
DNS_STATUS DnsCancelQueryRaw(DNS_QUERY_RAW_CANCEL *cancelHandle);
// Остановить запрос DNS-SD.
DNS_STATUS DnsServiceResolveCancel(PDNS_SERVICE_CANCEL pCancelHandle);
// Отменить запрос к mDNS.
DNS_STATUS DnsStopMulticastQuery(PMDNS_QUERY_HANDLE pHandle);
Для работы с ресурсными записями DNS существует отдельный набор функций.
Сравнение записей:
// Сравнить две ресурсные DNS-записи.
bool DnsRecordCompare(PDNS_RECORD pRecord1, PDNS_RECORD pRecord2);
// Сравнить два набора ресурсных записей.
bool DnsRecordSetCompare(PDNS_RECORD pRR1, PDNS_RECORD pRR2,
PDNS_RECORD *ppDiff1, PDNS_RECORD *ppDiff2);
Копирование записей:
// Создать копию соответствующей ресурсной записи.
PDNS_RECORD DnsRecordCopyEx(PDNS_RECORD pRecord, DNS_CHARSET CharSetIn,
DNS_CHARSET CharSetOut);
// Создать копию набора ресурсных записей.
PDNS_RECORD DnsRecordSetCopyEx(PDNS_RECORD pRecordSet, DNS_CHARSET CharSetIn,
DNS_CHARSET CharSetOut);
В процессе копирования функция может выполнять конвертацию в другую кодировку.
Функции для замены и изменения записей в наборе:
// Заменить существующий набор ресурсных записей.
DNS_STATUS DnsReplaceRecordSet(PDNS_RECORD pReplaceSet, DWORD Options,
HANDLE hContext, PVOID pExtraInfo,
PVOID pReserved);
// Добавить, изменить либо удалить ресурсную запись в наборе.
DNS_STATUS DnsModifyRecordsInSet(PDNS_RECORD pAddRecords,
PDNS_RECORD pDeleteRecords,
DWORD Options, HANDLE hCredentials,
PVOID pExtraList, PVOID pReserved);
Чтобы извлечь записи из набора, можно использовать следующие функции:
// Извлечь записи ресурсов из DNS-сообщения в виде экземпляров
// структур DNS_RECORD.
DNS_STATUS DnsExtractRecordsFromMessage_W(PDNS_MESSAGE_BUFFER pDnsBuffer,
WORD wMessageLength,
PDNS_RECORD *ppRecord);
// Отделить первую запись из набора.
PDNS_RECORD DnsRecordSetDetach(PDNS_RECORD pRecordList);
Ниже описан набор функций для работы со службами, которые предоставляет DNS-сервер, а также со службами, которые опубликованы по mDNS.
// Инициировать запрос DNS-SD для обнаружения служб в локальной сети.
DNS_STATUS DnsServiceBrowse(
PDNS_SERVICE_BROWSE_REQUEST pRequest,
PDNS_SERVICE_CANCEL pCancel
);
// Отменить запрос DNS-SD обнаружения служб.
DNS_STATUS DnsServiceBrowseCancel(PDNS_SERVICE_CANCEL pCancelHandle);
Получить информацию о конкретной службе, объявленной в локальной сети:
DNS_STATUS DnsServiceResolve(PDNS_SERVICE_RESOLVE_REQUEST pRequest,
PDNS_SERVICE_CANCEL pCancel);
Можно не только получать сервисы, но и добавлять свои. Для этого служат функции описания и регистрации сервисов, а также вспомогательные функции.
Функция DnsServiceConstructInstance() позволяет описать параметры регистрируемой службы и получить корректную структуру описания:
// Создать структуру DNS_SERVICE_INSTANCE из описывающих ее данных.
PDNS_SERVICE_INSTANCE DnsServiceConstructInstance(
PCWSTR pServiceName,
PCWSTR pHostName,
PIP4_ADDRESS pIp4,
PIP6_ADDRESS pIp6,
WORD wPort,
WORD wPriority,
WORD wWeight,
DWORD dwPropertiesCount,
PCWSTR *keys,
PCWSTR *values
);
// Копировать объект структуры DNS_SERVICE_INSTANCE.
PDNS_SERVICE_INSTANCE DnsServiceCopyInstance(PDNS_SERVICE_INSTANCE pOrig);
// Освободить ресурсы, ассоциированные со структурой DNS_SERVICE_INSTANCE.
void DnsServiceFreeInstance(PDNS_SERVICE_INSTANCE pInstance);
Зарегистрировать описанную службу или удалить регистрацию можно, используя следующие функции:
// Зарегистрировать службу, информация о которой может быть получена
// с помощью DNS.
DWORD DnsServiceRegister(PDNS_SERVICE_REGISTER_REQUEST pRequest,
PDNS_SERVICE_CANCEL pCancel);
// Отменить регистрацию службы, когда она больше не требуется.
DnsServiceDeRegister(PDNS_SERVICE_REGISTER_REQUEST pRequest,
PDNS_SERVICE_CANCEL pCancel);
// Отменить выполняемую операцию регистрации или отмены регистрации.
DWORD DnsServiceRegisterCancel(PDNS_SERVICE_CANCEL pCancelHandle);
На DNS-сервере описания служб обычно хранятся в записях расположения службы, имеющих тип SRV. Каждая такая запись содержит хост, порт и протокол службы.
Выполнять запросы к DNS-серверам можно через прокси. Следующая функция позволяет получить настройки прокси:
#include <windns.h>
DWORD DnsGetProxyInformation(
PCWSTR hostName, DNS_PROXY_INFORMATION *proxyInformation,
DNS_PROXY_INFORMATION *defaultProxyInformation,
DNS_PROXY_COMPLETION_ROUTINE completionRoutine,
void *completionContext);
Функции для установки настроек прокси отсутствуют, скорее всего потому, что настройки являются глобальными, или из-за того, что вызов работает через RPC и функциональности установки, технически способной работать удаленно, не подразумевалось.
Следующие функции позволяют установить и получить настройки DNS для приложений без изменения системных настроек:
#include <windns.h>
// Настроить DNS для конкретного приложения.
DWORD DnsSetApplicationSettings(DWORD cServers,
const DNS_CUSTOM_SERVER *pServers,
const DNS_APPLICATION_SETTINGS *pSettings);
// Получить настройки DNS, специфичные для приложения.
DWORD DnsGetApplicationSettings(DWORD *pcServers,
DNS_CUSTOM_SERVER **ppDefaultServers,
DNS_APPLICATION_SETTINGS *pSettings);
Эти функции работают со следующей структурой:
// Заголовочные файлы лучше включать именно в таком порядке,
// чтобы использовать DNS_CUSTOM_SERVER вместе с ServerAddr.
#include <ws2ipdef.h>
#include <windns.h>
typedef struct _DNS_APPLICATION_SETTINGS
{
// Должен быть установлен DNS_APP_SETTINGS_VERSION1.
ULONG Version;
// Опции.
ULONG64 Flags;
} DNS_APPLICATION_SETTINGS;
// Описание сервера для приложения.
typedef struct _DNS_CUSTOM_SERVER
{
// Тип сервера, определяющий транспорт запроса.
DWORD dwServerType;
// Флаги.
ULONG64 ullFlags;
union
{
// Шаблон для DoH.
PWSTR pwszTemplate;
// Имя узла.
PWSTR pwszHostname;
};
// Хранилище SOCKADDR_INET.
char MaxSa[DNS_ADDR_MAX_SOCKADDR_LENGTH];
} DNS_CUSTOM_SERVER;
В структуре DNS_APPLICATION_SETTINGS поле опций может принимать одно значение — DNS_APP_SETTINGS_EXCLUSIVE_SERVERS, которое говорит о том, что необходимо использовать только DNS-серверы приложения, а не системные.
Тип сервера может быть следующим:
• DNS_CUSTOM_SERVER_TYPE_UDP — «обычный» DNS с выполнением запросов по UDP, то есть с использованием небезопасного канала передачи запроса.
• DNS_CUSTOM_SERVER_TYPE_DOH — сервер типа DoH. В этом случае запросы выполняются по HTTPS, что повышает безопасность.
В поле ullFlags структуры DNS_CUSTOM_SERVER определен единственный флаг — DNS_CUSTOM_SERVER_UDP_FALLBACK. Он говорит о том, что сервер может вернуться к небезопасному каналу в случае отказа DoH.
Функции ниже используются, чтобы освободить память, выделенную предыдущими вызовами:
#include <windns.h>
typedef enum
{
// Освобождаемые данные являются плоской структурой.
DnsFreeFlat = 0,
// Освобождаемые данные — список записей ресурсов,
// который включает подполя DNS_RECORD.
DnsFreeRecordList,
// Освобождаемые данные — разобранное поле сообщения.
DnsFreeParsedMessageFields
} DNS_FREE_TYPE;
// Освободить память, выделенную функцией DnsQuery() для полученных
// записей DNS.
void DnsFree(PVOID pData, DNS_FREE_TYPE FreeType);
// Освободить память, выделенную для структуры DNS_QUERY_RAW_RESULT,
// которая используется при вызове DnsQueryRaw().
void DnsQueryRawResultFree(DNS_QUERY_RAW_RESULT *queryResults);
// Освободить память, выделенную функцией DnsQuery() для записей DNS.
void DnsRecordListFree(PDNS_RECORD pRecordList, DNS_FREE_TYPE FreeType);
DnsRecordListFree() в новых версиях ОС Windows представляет собой не функцию, а макрос, вызывающий DnsFree():
#if (_WIN32_WINNT >= 0x0501)
#define DnsRecordListFree(p,t) DnsFree(p, DnsFreeRecordList)
...
Память, которая была выделена функциями для работы с прокси, освобождается с помощью отдельного набора функций:
// Освободить память, выделенную для поля proxyName в экземпляре структуры
// DNS_PROXY_INFORMATION, выделенной функцией DnsGetProxyInformation().
void DnsFreeProxyName(PWSTR proxyName);
// Освободить массив пользовательских серверов,
// возвращаемый DnsGetApplicationSettings().
void DnsFreeCustomServers(DWORD *pcServers, DNS_CUSTOM_SERVER **ppServers);
Параметры последней функции:
• pcServers — указатель на количество серверов в массиве ppServers. После вызова функции будет равен 0.
• ppServers — указатель на массив DNS_CUSTOM_SERVER, содержащий элементы pcServers. После вызова функции сюда тоже будет записан 0.
Более подробно работу с интерфейсами мы изучим позже, а сейчас разберем функции IP Helper API для настройки DNS-интерфейсов:
#include <netioapi.h>
// Получить настройки DNS.
NETIO_STATUS GetInterfaceDnsSettings(GUID Interface,
DNS_INTERFACE_SETTINGS *Settings);
// Установить настройки DNS.
NETIO_STATUS SetInterfaceDnsSettings(GUID Interface,
const DNS_INTERFACE_SETTINGS *Settings);
Параметры функций GetInterfaceDnsSettings() и SetInterfaceDnsSettings():
• Interface — GUID интерфейса, для которого требуется получить или установить настройки.
• Settings — структура, которая содержит настройки или в которую они будут записаны.
Возвращаемые значения — NO_ERROR в случае успеха или код ошибки.
Функции работают со структурой DNS_INTERFACE_SETTINGS, причем функция GetInterfaceDnsSettings() сама выделяет память:
#include <netioapi.h>
typedef struct _DNS_INTERFACE_SETTINGS
{
// Версия структуры. Должна быть DNS_INTERFACE_SETTINGS_VERSION1.
ULONG Version;
// Битовые флаги.
ULONG64 Flags;
// Доменное имя адаптера.
PWSTR Domain;
// Адреса серверов имен, разделенные запятыми или пробелами.
PWSTR NameServer;
// Имена поиска, разделенные так же, как адреса DNS-серверов.
PWSTR SearchList;
// Истина, если динамическая регистрация адаптера включена.
ULONG RegistrationEnabled;
// Истина, если регистрация адаптера включена.
ULONG RegisterAdapterName;
// Включение mDNS и LLMNR — Link-Local Multicast Name Resolution.
ULONG EnableLLMNR;
// Истина, если имя адаптера должно использоваться как суффикс поиска.
ULONG QueryAdapterName;
// Список DNS-серверов.
PWSTR ProfileNameServer;
} DNS_INTERFACE_SETTINGS;
Поле версии при вызове функции определяет тип передаваемой структуры и может иметь следующие значения:
• DNS_INTERFACE_SETTINGS_VERSION1 — для структуры выше.
• DNS_INTERFACE_SETTINGS_VERSION2 — должна быть передана расширенная структура DNS_INTERFACE_SETTINGS_EX, которая включает DNS_INTERFACE_SETTINGS.
• DNS_INTERFACE_SETTINGS_VERSION3 — передается структура DNS_INTERFACE_SETTINGS3, аналогичная DNS_INTERFACE_SETTINGS, но содержащая набор свойств DNS-сервера, список имен для поиска и т.п.
Структура для расширенных параметров:
typedef struct _DNS_INTERFACE_SETTINGS_EX
{
DNS_INTERFACE_SETTINGS SettingsV1;
// Зарезервированное поле.
ULONG DisableUnconstrainedQueries;
// Имена поиска, разделенные запятыми или пробелами.
PWSTR SupplementalSearchList;
} DNS_INTERFACE_SETTINGS_EX;
Значения флагов следующие:
• DNS_SETTING_IPV6 — настроить параметры интерфейса только для сетевого стека IPv6. Если этот параметр задан, все IP-адреса в атрибутах NameServer и ProfileNameServer должны быть IPv6-адресами.
• DNS_SETTING_NAMESERVER — настроить DNS-серверы статического адаптера на указанном интерфейсе через атрибут NameServer.
• DNS_SETTING_SEARCHLIST — настроить список поиска DNS-суффиксов для конкретного подключения с помощью атрибута SearchList.
• DNS_SETTING_REGISTRATION_ENABLED — включить динамическую регистрацию DNS для заданного адаптера. По умолчанию в системе включена.
• DNS_SETTING_DOMAIN — установить DNS-суффикс подключения для заданного адаптера из атрибута Domain.
• DNS_SETTINGS_ENABLE_LLMNR — включить разрешение имен с помощью LLMNR и mDNS на указанном адаптере. По умолчанию в системе включено.
• DNS_SETTINGS_QUERY_ADAPTER_NAME — включить использование имени адаптера в качестве суффикса для запросов DNS. По умолчанию в системе включено.
• DNS_SETTING_PROFILE_NAMESERVER — настроить DNS-серверы статического профиля на интерфейсе, то есть «обычные» DNS-серверы, с помощью атрибута ProfileNameServer.
Выделенную память необходимо удалить, используя следующую функцию:
#include <netioapi.h>
void FreeInterfaceDnsSettings(DNS_INTERFACE_SETTINGS *Settings);
Информацию о том, как использовать API, а также что они позволяют изменить, можно понять из назначения полей структуры DNS_INTERFACE_SETTINGS.
Для получения имени узла WinSock предоставляет функции gethostname() и GetHostNameW():
#include <winsock2.h>
int WSAAPI gethostname(char FAR *name, int namelen);
int WSAAPI GetHostNameW(PWSTR name, int namelen);
Функции для установки имени узла, а также получения его идентификатора в WinSock отсутствуют. Но имя узла можно установить, вызвав функцию SetComputerName() из группы API системной информации:
#include <sysinfoapi.h>
// Установить имя узла.
bool SetComputerName(LPCTSTR lpComputerName);
// Получить имя узла.
bool GetComputerName(LPTSTR lpBuffer, LPDWORD nSize);
Эти функции устанавливают и получают NetBIOS-имя.
Для работы с DNS-именами локального узла существует еще один набор функций:
#include <sysinfoapi.h>
// Тип имени для установки или получения.
typedef enum _COMPUTER_NAME_FORMAT
{
ComputerNameNetBIOS,
ComputerNameDnsHostname,
ComputerNameDnsDomain,
ComputerNameDnsFullyQualified,
ComputerNamePhysicalNetBIOS,
ComputerNamePhysicalDnsHostname,
ComputerNamePhysicalDnsDomain,
ComputerNamePhysicalDnsFullyQualified,
ComputerNameMax
} COMPUTER_NAME_FORMAT;
// Установить имя.
bool SetComputerNameEx(
COMPUTER_NAME_FORMAT NameType,
LPCTSTR lpBuffer
);
// Получить имя.
bool GetComputerNameEx(
COMPUTER_NAME_FORMAT NameType,
LPSTR lpBuffer,
LPDWORD nSize
);
Параметры функций SetComputerNameEx() и GetComputerNameEx():
• NameType — входной параметр, задающий тип устанавливаемого или получаемого имени, указанный в перечислении COMPUTER_NAME_FORMAT.
• lpBuffer — входной или выходной буфер.
• nSize — размер в символах буфера, в который должно быть записано имя. Если размер недостаточен, в этот параметр будет записан необходимый размер.
Функции возвращают истинное значение в случае успеха.
DNS-имя можно преобразовать в NetBIOS-совместимое имя, вызвав следующую функцию:
#include <winbase.h>
bool DnsHostnameToComputerName(
LPCTSTR Hostname,
LPTSTR ComputerName,
LPDWORD nSize
);
Эта функция почти не используется, поэтому мы не будем разбирать ее подробно. При желании это можно сделать, обратившись к MSDN.
Для преобразования имен объектов службы каталогов существует функция TranslateName(), которая может работать и с DNS-именами. Но службу каталогов мы в книге не рассматриваем и этой функции касаться тоже не будем.
Для получения структур описания протоколов существуют POSIX-совместимые функции:
struct protoent
{
// Официальное имя протокола.
char *p_name;
// Список псевдонимов. Завершается нулем.
char **p_aliases;
// Номер протокола в хостовом порядке байтов.
short p_proto;
} PROTOENT, *PPROTOENT, *LPPROTOENT;
protoent *WSAAPI getprotobynumber(int number);
protoent *WSAAPI getprotobyname(const char *name);
Напомним, что данные функции являются устаревшими и лучше использовать getaddrinfo().
Функции для получения описания служб:
struct servent
{
// Имя службы.
char FAR * s_name;
// Альтернативные имена, массив завершается нулем.
char FAR * FAR * s_aliases;
#ifdef _WIN64
// Имя протокола.
char FAR * s_proto;
// Порт.
short s_port;
#else
short s_port;
char FAR * s_proto;
#endif
} SERVENT, *PSERVENT, *LPSERVENT;
servent *WSAAPI getservbyport(int port, const char *proto);
servent *WSAAPI getservbyname(const char *name, const char *proto);
Их описание приведено в главе 3. Видим, что структура servent несколько отличается от POSIX-варианта.
Для получения информации о точках подключения — адреса локального и удаленного абонентов — WinSock предоставляет POSIX-совместимые функции:
int WSAAPI getsockname(SOCKET s, sockaddr *name, int *namelen);
int WSAAPI getpeername(SOCKET s, sockaddr *name, int *namelen);
Они были подробно описаны в главе 5.
В этой главе мы рассмотрели основные функции и структуры, предоставляемые WinSock для управления DNS, а также функции для работы с адресами IPv4 и IPv6.
Структуры адресов IPv4 и IPv6 аналогичны тем, что используются в POSIX-сокетах. WinSock предоставляет совместимые с POSIX функции для работы с ними. Например, получение списка адресов с помощью DNS осуществляется при помощи функции getaddrinfo(), а для обратного преобразования используется функция getnameinfo(), — их описание приведено в главе 2.
Для описанных операций существуют также Windows-реализации. Пример — WSAStringToAddress() и WSAAddressToString(). Таких функций достаточно много. Rtl-функции, помимо работы с IP-адресами, позволяют выполнять преобразования MAC-адресов.
В ОС Windows также существуют базы узлов и служб. Для навигации по базе служб используются функции WSALookupServiceBegin(), WSALookupServiceNext(), WSALookupServiceEnd().
Зарегистрировать новую службу в базе либо удалить из нее существующую возможно с помощью функции WSASetService().
Службы могут принадлежать некоторому классу, описывающему пространства имен, в которых будет зарегистрирована служба. Этот класс может задавать некоторые характеристики службы. Для работы с классами используются функции WSAGetServiceClassInfo(), WSAInstallServiceClass() и WSARemoveServiceClass().
Для работы с DNS существует Windows-специфичный набор функций, включая функции в IP Helper API для настройки DNS на интерфейсах. DNS можно настроить на конкретных интерфейсах либо даже специально для отдельных приложений. Использование этих функций позволит реализовать достаточно сложные сценарии адресации.
1. Существуют ли отличия структур адресов от POSIX-совместимых аналогов? Если да, то какие?
2. Зачем нужна структура CSADDR_INFO?
3. Можно ли использовать POSIX-совместимые функции для работы с IPv6-адресами в WinSock? Если да, какие примеры таких функций можно привести?
4. В чем чаще всего проявляются отличия функций с суффиксом Ex от функций с теми же именами, но без суффикса? Объясните на примере GetAddrInfoEx() и GetAddrInfo().
5. Какие семейства адресов поддерживает функция inet_ntop() в WinSock?
6. В чем отличия функции WSAAddressToString() от inet_ntop()?
7. В чем преимущества и недостатки Rtl-функций, таких как RtlIpv4StringToAddress()? Какие проблемы могут возникнуть при их использовании?
8. Как сконвертировать маску подсети в длину префикса?
9. Как преобразовать IDN в ANSI-строку?
10. В чем отличие функции getaddrinfo() в WinSock от ее POSIX-аналога?
11. Для чего нужна база служб в ОС Windows?
12. Как найти службу в базе по имени?
13. Какие особенности имеются у API DNS-резолвера?
14. Каким образом можно определить TTL полученной записи DNS?
15. Как настроить прокси-сервер для конкретного приложения?
16. Как получить и установить имя узла?
17. В чем отличие структуры servent в WinSock от ее POSIX-версии?
18. Как получить информацию о локальной точке подключения с помощью WinSock API?
19. Напишите программу, которая в качестве входного параметра будет принимать IP-адрес и выводить имена узлов, зарегистрированных в DNS для этого адреса.