ИП_Л_6 Лекция № 6. Асинхронные сокеты Windows 1. 2. Операционная система Windows ориентирована на объект под названием «окно». В большинстве случаев «окно» — это просто кадр или поле, в котором программа может отображать данные. Windows посылает сообщения объекту «окно», если в системе происходят определенные события или если появляется сообщение от другого работающего приложения. Каждое окно, открываемое программой, имеет собственный тип класса window. Класс window позволяет задавать параметры, такие как размер, цвет или расположение меню пользователя. Эти характеристики являются общими для всех окон, принадлежащих данному классу. Для каждого типа класса window существует процедура (функция), обрабатывающая сообщения для окон этого типа. Класс window и процедура-обработчик сообщений соотносятся между собой просто. Каждый раз, когда Windows генерирует сообщение для определенного объекта, оно посылается процедуре класса window, связанной с этим объектом. Программа может открыть несколько окон одного и того же класса. Каждое окно, принадлежащее данному классу, называется его образцом (instance). Другими словами, в системе одновременно могут существовать несколько образцов одного и того же класса. Если в системе несколько образцов, в процедуре-обработчике сообщений должен быть механизм распознавания, какому из образцов адресовано то или иное сообщение Windows. Для этого каждому образцу данного класса присваивается уникальный номер-дескриптор. Когда посылается сообщение, в нем, в качестве одной из составляющих, всегда передается дескриптор того образца, кому оно адресовано. Для того чтобы успешно программировать в оболочке Windows, необходимо четко сознавать взаимоотношения между классом window, процедурами и дескрипторами окон. Асинхронный поиск в DNS Вместо gethostbyname используется WSAAsyncGetHostByName, a вместо gethostbyaddr — WSAAsyncGetHostByAddr. Асинхронные функции Winsock Ниже приведен прототип функции WSAAsyncGetHostByAddr. На первый взгляд, она кажется сложнее своего аналога из интерфейса Беркли: HANDLE PASCAL FAR WSAAsyncGetHostByAddr(HWND hWnd, unsigned int wMsg, const char FAR * addr, int len, int type, char FAR * buf, int buflen); У функции WSAAsyncGetHostByAddr семь параметров, на четыре больше, чем у gethostbyaddr. Три из параметров WSAAsyncGetHostByAddr в 1 ИП_Л_6 точности соответствуют параметрам gethostbyaddr. Вот прототип функции gethostbyaddr: struct hostent FAR * PASCAL FAR gethostbyaddr(const char FAR * addr, int len, int type); У обеих функций есть одинаковые параметры: const char FAR * addr; // Указатель на IP-адрес с сетевым // порядком байтов, int len; // Длина адреса. В случае Интернет // всегда равна 4 байтам, int type; // Тип адреса. Для Интернет всегда AF_INET. Главное отличие между WSAAsyncGetHostByAddr и gethostbyaddr в типе возвращаемого результата, gethostbyaddr возвращает указатель на структуру данных с информацией о сетевом компьютере (host-entry), a WSAAsyncGetHostByAddr — дескриптор задания Windows. Ключевой момент в понимании работы W'SAAsyncGetHostByAddr — знать, что делать дальше с этим дескриптором. Что такое дескриптор асинхронного задания Windows? Дополнительно к обыкновенным параметрам функций Winsock в стиле Беркли, их асинхронным аналогам требуются два специальных параметра. Первый параметр — дескриптор окна Windows, которому посылается сообщение об окончании работы функции. Второй — само посылаемое сообщение. Как только асинхронная функция отработает, указанное сообщение отсылается заданному окну. Например, функция LookupHostAsync вызывает функцию WSAAsyncGet-HostByName следующим образом: hTask = WSAAsyncGetHostByName(hwndSockman, szUserEntry, szHostEntryBuffer, MAXGETHOSTSTRUCT); WM_ASYNC_LOOKUP_DONE, Функция WSAAsyncGetHostByName выполняет задачу по преобразованию имени в IP-адрес, используя информацию из параметра szUserEntry. Функция WSAAsyncGetHostByName возвращает управление вызывавшему модулю сразу, как только задача запущена на выполнение. Она не ждет, пока операция поиска в DNS закончится. Поскольку на момент возврата функции WSAAsyncGetHostByName результат еще не получен, она не может вернуть указатель на структуру с информацией о сетевом компьютере, как это делает gethostbyname. Вместо этого WSAAsyncGetHostByName возвращает дескриптор задачи Windows, идентифицирующий процесс преобразования имени в IP-адрес. Дескриптор заносится в переменную hTask. Как только сетевая операция закончится, Windows пошлет сообщение окну с дескриптором hwndSockman. Идентификатором этого сообщения будет WM_ASYNC_LOOKUP_DONE. Другими словами, первые два параметра функции WSAAsyncGetHostByName определяют, кому и что послать по завершении асинхронной задачи. Асинхронные функции Winsock начинают операции и немедленно возвращают управление вызывавшему модулю. Результатом большинства асинхронных функций является дескриптор задачи Windows. Дескриптор задачи однозначно идентифицирует 2 ИП_Л_6 задачу, запущенную асинхронной функцией, и в дальнейшем используется для идентификации сообщений, генерируемых асинхронной задачей. Параметры асинхронного сообщения Windows Все сообщения Windows имеют определенный формат. Каждое сообщение состоит из четырех переменных: дескриптора окна-получателя сообщения, идентификатора сообщения, 16-битного и 32-битного параметров сообщения. Первый параметр сообщения часто называется wParam (word parameter). Второй параметр часто называется lParam (long parameter). Многие Windows-сообщения не используют оба параметра для хранения сколь-нибудь полезной информации. Однако это не относится к сообщениям функций Winsock. Параметр wParam сообщения Winsock содержит дескриптор асинхронного задания, который вернула функция, запустившая это задание. Если старшие 16 бит переменной lParam не равны нулю, то в них содержится код ошибки. В следующем разделе разъясняется смысл младших 16 бит переменной lParam. Ошибки асинхронных функций Winsock При вызове большинства асинхронных функций Winsock требуется задавать указатель на буфер для хранения данных, равно как и его длину. Как правило, два этих аргумента — последние в списке параметров. Разработчики Winsock предусмотрели возможность возникновения ошибки, связанной с нехваткой места в буфере для принятых сетевых данных. Младшие 16 бит переменной lParam служат именно для этих целей. Предположим, что количество принятых сетевых данных превысило отведенный для них размер буфера, который вы задали при вызове функции. Вместо того чтобы превысить допустимый объем буфера или неудачно закончиться, функция посылает сообщение с кодом ошибки WSANOBUFS. Код ошибки WSANOBUFS попадает в старшие 16 бит переменной IParam. Ошибка означает, что либо вы не отвели места для буфера, принимающего данные, либо этого места недостаточно. При возникновении ошибки WSANOBUFS устанавливаются значения младших 16 бит параметра IParam. Если буфер-приемник данных слишком мал, в них заносится требуемый размер буфера. Другими словами, асинхронная функция Winsock не только сообщает о превышении размеров буфера, но и говорит, сколько памяти нужно для него отвести. Если размер отведенного вашей программой буфера слишком мал, чтобы вместить все данные, функция возвращает значение ошибки WSANOBUFS в двух старших байтах переменной lParam. В двух младших байтах lParam находится требуемый размер буфера. Рассмотрим эту ситуацию на практике. Если прикладная программа работает в условиях нехватки памяти, память для буфера можно отводить динамически. Например, в вызове функции можно указать размер буфера, равный нулю, получить сообщение об ошибке WSANOBUFS и выделить необходимую память динамически. Для этого программа должна проанализировать два младших байта 3 ИП_Л_6 переменной IParam сообщения Windows. Определив размер буфера, программа динамически отводит память и вызывает асинхронную функцию снова, но уже с правильным размером буфера. Поступая таким образом, программа никогда не отнимет лишней памяти у системы. Конечно, такому подходу свойственны и определенные недостатки. Прежде всего страдает производительность — ведь одну и ту же функцию приходится вызывать два раза вместо одного. Разработчик должен выбрать, что важнее — потребление памяти или скорость работы системы. Для получения кода ошибки и требуемого размера буфера из параметра IParam сообщения Winsock в сокетах Windows предусмотрены два макроса. Макрос WSAGETASYNCERROR возвращает код ошибки — содержимое двух старших байтов переменной lParam, а макрос WSAGETASYNCBUFLEN — требуемую длину буфера данных — содержимое двух младших байтов переменной lParam. Макросы используются следующим образом: UINT nErrorNumber = WSAGETASYNCERROR(IParam); UINT nRequiredBuf = WSAGETASYNCBUFLEN(IParam); Возвращаясь к функции DoWinsockProgram Функция LookupHostAsync, как вы помните, возвращает дескриптор задачи Windows (функции WSAAsyncGetHostByName или функции WSAAsyncGet-HostByAddr). Как показано ниже, функция DoWinsockProgram заносит значение этого дескриптора в глобальную переменную hAsyncLookupTask: if (wParam == IDM_LOOKUP_ASYNC) hAsyncLookupTask = LookupHostAsync(szLookupText, szLookupBuffer, (LPDWORD)&wLookupAddr); С этого момента ответственность за происходящее переходит к Windows. Другими словами, в ответ на выбор пользователем подпункта меню Async Lookup Sockman произвел все необходимые действия для запуска асинхронной задачи поиска в системе DNS. Эти действия таковы: 1. Sockman вывел на экран диалоговое окно и позволил пользователю ввести имя или адрес (в формате «десятичное с точкой») компьютера. 2. Sockman передал введенные пользователем данные функции LookupHostAsync. 3. Функция LookupHostAsync запустила задачу по асинхронному поиску в DNS. 4. Sockman записал значение дескриптора задачи в глобальную переменную hAsyncLookupTask. (Глобальной эта переменная сделана потому, что это облегчает доступ остальных приложений к сообщениям задачи асинхронного поиска в DNS.) Как только задача асинхронного поиска в DNS закончится, Windows пошлет сообщение. Вот как осуществляется вызов асинхронных функций Winsock в функции LookupHostAsync: hTask = WSAAsyncGetHostByName(hwndSockman, WM_ASYNC_LOOKUP_DONE, szUserEntry, szHostEntryBuffer, MAXGETHOSTSTRUCT); hTask = WSAAsyncGetHostByAddr(hwndSockman, WM_ASYNC_LOOKUP_DONE, (LPCSTR)&lpdwAddr, AF_INET_LENGTH, 4 ИП_Л_6 AF_INET, szHostEntryBuffer, MAXGETHOSTSTRUCT); Обратите внимание, что обе функции (WSAAsyncGetHostByName и WSAAsyncGetHostByAddr) имеют одинаковые параметры — дескриптор окна, равный hwndSockman и идентификатор сообщения, равный WM_ASYNC_LOOKUP_DONE. Дескриптор окна hwndSockman идентифицирует окно Sockman. Сообщение WM_ASYNC_LOOKUP_DONE высылается окну Sockman по окончании работы функции. Другими словами, его получит обработчик сообщений окна Sockman — функция WndProc. Модифицируем функцию WndProc Обе функции, LookupHostAsync и LookupHostBlocking, запускают задачу поиска в DNS и заставляют Windows послать сообщение по окончании этой задачи. Сообщение посылается процедуре WndProc, являющейся обработчиком всех сообщений, адресованных главному окну Sockman. Как только задача поиска в DNS, запущенная функцией LookupHostAsync, заканчивается, Windows посылает сообщение WM_ASYNC_LOOKUP_DONE функции WndProc. Как только функция LookupHostBlocking заканчивается, Windows посылает сообщение WM_BLOCK_LOOKUP_DONE функции WndProc. Эти сообщения обрабатываются операторами case, как показано во фрагменте исходного текста функции WndProc из файла SOCKMAN2.CPP: long FAR PASCAL _export WndProc(HWND hwnd, UINT iMessage, UINT wParam, LONG lParam) { switch (iMessage) { case WM_ASYNC_LOOKUP_DONE: case WM_BLOCK_LOOKUP_DONE: DisplayHostEntry(hwnd, iMessage, wParam, lParam); return(0); Закончили // Как видим, оба сообщения вызывают функцию DisplayHostEntry. Если результаты работы функций поиска в DNS требуются другим модулям программы, эти операторы следует изменить. В данном случае на экран выводится окно-сообщение с этими результатами. Функция DisplayHostEntry Функция DisplayHostEntry, как следует из названия, выводит результаты поиска в DNS на экран компьютера. При этом она проверяет наличие сообщений об ошибках от асинхронных функций при помощи макроса WSAGETASYNCERROR. Примечание: Обратите внимание на то, что функция LookupHostBlocking эмулирует асинхронное сообщение об ошибке, вызывая функцию SendMessage с установленной переменной lParam. Получается, что DisplayHostEntry проверяет ошибки как асинхронной, так и блокирующей функции. VOID DisplayHostEntry(HWND hwnd, UINT iMessage, UINT wParam, LONG lParam) { int nErrCode; // Код ошибки от преобразователя адресов if (nErrCode = WSAGETASYNCERROR(lParam)) { wsprintf(szScratchBuffer, "%s LOOKUP caused Winsock ERROR No. %d", szLookupText, nErrCode); MessageBeep(0); 5 ИП_Л_6 MessageBox(NULL, szScratchBuffer, szLookupText, MB_OK|MB_ICONSTOP) ; // Если произошла ошибка, буфер обнуляется szLookupBuffer[0] = '\0'; } else { PHOSTENT pHostEntry; pHostEntry = (PHOSTENT) szLookupBuffer; lstrcpy(szHostName, pHostEntry->h_name); lstrcpy(szIPAddress, inet_ntoa(*(PIN_ADDR)*(pHostEntry->h_addr_list))); wsprintf(szScratchBuffer, "%s\tLOOKUP RESULTS\nHost Name:\t%s\nIP Address:\t%s", szLookupText,szHostName, szIPAddress); } PaintWindow(szScratchBuffer); return; } Если содержимое lParam, как показывает вызов WSAGETASYNCERROR, свидетельствует об ошибке, функция DisplayHostEntry выводит сообщение об ошибке и устанавливает нулевую длину буферу szLookupBuffer. Если ошибки не было, выводится сообщение с результатами, взятыми из структуры данных host-entry (с информацией о сетевом компьютере), записанной в szLookupBuffer, приведенной предварительно к типу PHOSTENT (указатель на структуру host-entry). Поле h_name структуры host-entry копируется в глобальную переменную szHostName. Имя сетевого компьютера, записанное в szHostName, может использоваться остальными программными модулями Sockman, как имя компьютера по умолчанию. Для преобразования элемента h_addr_list из формата структуры hostentry в простой ASCII-текст, используется функция inet_ntoa. Далее, эта строка копируется в глобальную переменную szIPAddress. Опять-таки, последний извлеченный из DNS адрес может использоваться другими приложениями Sockman по умолчанию. После того как адрес и имя компьютера сохранены, DiplayHost-Entry при помощи функции wsprintf формирует строку результата в буфере общего назначения szScratchBuffer. Указатель на этот буфер передается функции PaintWindow, которая и выводит результаты поиска в DNS в главное окно Sockman. Подводя итоги В этой главе вы узнали, как отдельная утилита, например поиска в DNS, интегрируется в приложение Windows. Вы узнали, как создаются асинхронные эквиваленты сетевых функций в стиле Беркли. Вы узнали, что большинство асинхронных функций, в отличие от синхронных (возвращающих данные или указатели на данные), возвращают дескрипторы заданий Windows. Другими словами, асинхронные функции начинают сетевую операцию и немедленно возвращают дескриптор задания вызывавшему модулю. Дескриптор задания Windows однозначно идентифицирует асинхронную процедуру, запущенную асинхронной 6 ИП_Л_6 функцией. В дальнейшем дескриптор задания используется для идентификации сообщений от асинхронной процедуры. В следующей главе вы узнаете, как простая утилита Finger, рассмотренная в десятой главе, интегрируется в настоящее Windowsприложение. Также вы узнаете, как выглядит асинхронная версия той же самой процедуры. До того как перейти к двенадцатой главе, проверьте, хорошо ли вы усвоили следующие важные понятия: * При вызове асинхронной функции сетевого ввода-вывода Winsock, как правило, задается указатель на буфер данных и размер этого буфера. * Асинхронные функции ввода-вывода Winsock, как правило, возвращают дескриптор задания Windows. *Сообщение от асинхронной функции Windows содержит 16-битный параметр wParam, в котором находится дескриптор задания. * Сообщение об ошибке асинхронной функции хранится в старших шестнадцати битах 32-битного параметра lParam. * Если размеров буфера при вызове асинхронной функции не хватает, чтобы вместить все данные, генерируется сообщение об ошибке WSANOBUFS (размер буфера слишком мал), а необходимый размер буфера извлекается программой при помощи макроса WSAGETASYNCBUFLEN из 16 младших бит 32-битного параметра сообщения lParam. 7