Сообщений 2 Оценка 100 [+1/-0] Оценить |
ISAPI или Internet Server Application Programming Interface - это набор интерфейсов, предоставляемых веб-сервером для написания приложений, взаимодействующих с IIS и расширяющих его возможности. Такие приложения могут принадлежать к двум типам - ISAPI-расширение и ISAPI-фильтр.
Фильтр непосредственно участвует в обработке пользовательского запроса с момента его получения сервером и до момента отправки ответа. Он может модифицировать запрос или ответ, изменить адресата, ответственного за обработку запроса, но он сам не является конечным получателем запроса.
Расширение, наоборот, является адресатом запроса и не может влиять на его параметры и путь обработки. ISAPI-расширения IIS - это альтернатива CGI-приложениям. Расширения (и фильтры) реализуются в виде dll. IIS загружает dll при первом запросе к расширению и выгружает ее либо при выгрузке веб-приложения (если включено кэширование ISAPI), либо после окончания обработки запроса (если кэширование выключено). ISAPI-расширение может быть вызвано как явно (путем запроса вида http://.../myisapi.dll?params), так и неявно (через карту расширений или при отображении на него запроса с помощью фильтра).
Зачем нужны ISAPI-расширения, если есть CGI, ASP и т.д.? Ответ - они быстрее и требуют меньших ресурсов. В отличии от CGI, ISAPI-раширения многопоточны, т.е. для обработки еще одного запроса не требуется загрузки еще одной копии приложения. По сравнению с ASP, они имеют гораздо больше возможностей (использование множества функций Win32 API без необходимости писать для этого COM-объекты) и существенно выигрывают по скорости, т.к. код уже откомпилирован и оптимизирован. Правда, наряду с преимуществами имеются и недостатки, такие как большая сложность программирования и отсутствие поддержки сессий. Тем не менее, ISAPI-расширения наиболее хорошо подходят для написания критичных по времени приложений для IIS.
Любое ISAPI-расширение должно экспортировать три обязательные функции:
Прототипы этих трех функций и все используемые в ISAPI структуры описаны в файле httpext.h, который мы будем подключать к своему проекту. Чтобы экспортировать эти функции, мы используем файл myisapi.def, добавив его в ресурсы проекта.
Эта функция должна установить версию и описание ISAPI-расширения и вернуть TRUE. Если функция возвратит FALSE, расширение не будет загружено. Внутри GetExtensionVersion может быть проделана необходимая инициализация. Т.к. мы пишем прототип "серьезного" расширения, в нашем случае здесь будет происходить считывание параметров из реестра, подключение к логу сообщений Windows NT, инициализация семафора, создание порта завершения ввода/вывода (I/O completion port) и порождение нитей для пула.
В первом варианте (версии 1.0), я совершил тактическую промашку, создавая рабочие нити непосредственно перед началом обработки и завершая их в конце. Поскольку на создание нити тоже тратится какое-то время, при большом количестве запросов и коротком цикле обработки скорость работы приложения существенно снижается. В частности, новый вариант по моим тестам получился примерно на 25 процентов быстрее исходного (420 запросов в секунду против 340 в исходном варианте).
На этом проблемы, увы, не закончились. Ярослав Говорунов, в процессе нашей совместной работы над ISAPI_Rewrite 2.0, обнаружил грубое несоответствие реального процесса инициализации ISAPI-расширения процессу описанному в MSDN при отключенном кешировании ISAPI-расширений. MSDN утверждает, что функции GetExtensionVersion и TerminateExtension вызываются IIS'ом по одному разу (первая в самом начале, вторая - перед выгрузкой). На деле же оказалось, что в вышеупомянутых условиях вызов GetExtensionVersion и TerminateExtension выполняется для каждого потока IIS. Поэтому в версии 1.2 была добавлена синхронизация вызовов GetExtensionVersion и TerminateExtension с помощью глобальной критической секции.
Итак, из реестра будут считываться три параметра - количество нитей в пуле dwMaxThreads, время ожидания освобождения очереди dwQueueTimeout и максимальное количество запросов в очереди dwQueueSize. Семафор понадобится для отслеживания ограничения на количество запросов в очереди (в принципе, если не ожидается большого наплыва запросов, то от него можно спокойно избавиться). Здесь же будет создаваться основа пула нитей - порт завершения ввода/вывода, и сами нити, обслуживающие запросы. Вот как будет выглядеть код GetExtensionVersion:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) { //устанавливает версию pVer->dwExtensionVersion=MAKELONG(VERSION_MINOR, VERSION_MAJOR); //копируем описание memcpy(pVer->lpszExtensionDesc, szDescription, sizeof(szDescription)); CComCritSecLock<CComAutoCriticalSection> lock(g_cs); if (g_lInitCount++ > 0) return g_bInitResult; HKEY hkey; //регистрируемся в логе if((hEvt = ::RegisterEventSource(NULL, EventSource)) == NULL) { //выводим отладочное сообщение ::OutputDebugString(TEXT("MyISAPI: Failed to register event source.\n")); return FALSE; } //открываем ключ реестра if(::RegOpenKeyEx(HKEY_LOCAL_MACHINE, REG_PATH, 0, KEY_READ, &hkey) == ERROR_SUCCESS) { DWORD dwSize = 4; DWORD dwType; //запрашиваем значение размера пула if(::RegQueryValueEx(hkey, REG_ISAPI_THREADS_POOL, NULL, &dwType, (unsigned char *)&dwMaxThreads, &dwSize)!=ERROR_SUCCESS || dwType!=REG_DWORD || !dwMaxThreads) { //инициализируем значениями по умолчанию dwMaxThreads = DefaultPool; dwSize = 4; } //запрашиваем значение таймаута if(::RegQueryValueEx(hkey, REG_ISAPI_QUEUE_TIMEOUT, NULL, &dwType, (unsigned char *)&dwQueueTimeout, &dwSize)!=ERROR_SUCCESS || dwType!=REG_DWORD || !dwQueueTimeout) { dwQueueTimeout = DefaultTimeout; dwSize = 4; } if(::RegQueryValueEx(hkey, REG_ISAPI_QUEUE_SIZE, NULL, &dwType, (unsigned char *)&dwQueueSize, &dwSize)!=ERROR_SUCCESS || dwType!=REG_DWORD || !dwQueueSize) dwQueueSize = DefaultSize; ::RegCloseKey(hkey); } //создаем IO Completion port if(hIOPort = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) { //создаем семафор if(hSemaphore = ::CreateSemaphore(NULL, dwQueueSize, dwQueueSize, NULL)) { for(DWORD i=0; i < dwMaxThreads; i++) if(_beginthread(ExtensionThreadProc,0,NULL) == -1) ::ReportEvent(hEvt, EVENTLOG_ERROR_TYPE, 0, MSG_ISAPI_THREAD_FAILED, NULL, 0, 0, NULL, NULL); //не смогли создать нитку //запишем сообщение об успешной инициализации ::ReportEvent(hEvt, EVENTLOG_INFORMATION_TYPE, 0, MSG_ISAPI_INITIALIZED, NULL, 0, 0, NULL, NULL); g_bInitResult = TRUE; return TRUE; } } //запишем сообщение о сбое ::ReportEvent(hEvt, EVENTLOG_ERROR_TYPE, 0, MSG_ISAPI_INITIALIZATION_FAILED, NULL, 0, 0, NULL, NULL); return FALSE; } |
Обратите внимание, что создание новой нити осуществляется с помощью функции CRT _beginthread. Это необходимо, если внутри ExtensionThreadProc вы собираетесь использовать функции CRT. Если же функции CRT вам не нужны, вы можете заменить вызов _beginthread на CreateThread, только не забудьте затем освободить дескриптор нити (функция _endthread делает это автоматически, поэтому в этом примере не нужно беспокоиться об освобождении дескриптора). |
Функция TerminateExtension вызывается перед выгрузкой ISAPI-расширения из памяти. Этот вызов позволяет расширению освободить все используемые ресурсы. В нашем примере эта функция будет иметь следующий вид:
BOOL WINAPI TerminateExtension(DWORD dwFlags) { CComCritSecLock<CComAutoCriticalSection> lock(g_cs); if (--g_lInitCount > 0) return TRUE; if(hIOPort) ::CloseHandle(hIOPort); //освобождаем семафор if(hSemaphore) ::CloseHandle(hSemaphore); //записываем сообщение об успешном завершении ::ReportEvent(hEvt,EVENTLOG_INFORMATION_TYPE,0,MSG_ISAPI_UNINITIALIZED,NULL,0,0,NULL,NULL); //отключаемся от лога ::DeregisterEventSource(hEvt); return TRUE; } |
Итак, мы подошли к одной из основных частей ISAPI-расширения. Именно эту функцию вызывает IIS для обработки запроса, передавая ей в качестве параметра указатель на структуру EXTENSION_CONTROL_BLOCK, описывающую контекст запроса. Если ваше расширение не занимается длительной обработкой запроса, то вы можете реализовать всю логику непосредственно внутри этой функции. Однако, если расширение производит длительные вычисления, работает с базой данных и т.п., то рекомендуется производить обработку запроса в отдельной нити. Это связано с тем, что пул нитей IIS, принимающих запросы, ограничен (по умолчанию 20 нитей). И если вы исчерпаете этот пул, ваш сервер будет неспособен начать обслуживание вновь поступающих запросов. Чтобы этого избежать, "большое" ISAPI-расширение создает свой собственный пул нитей, в которых обрабатываются запросы. В такой схеме, HTTPExtensionProc возвращает управление сразу после начала обработки запроса, сообщая IIS, что запрос продолжает обрабатываться, с помощью кода возврата HSE_STATUS_PENDING. Вот как выглядит возможная реализация HTTPExtensionProc с пулом ниток:
DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) { //ждем освобождения нити или наступления таймаута if(::WaitForSingleObject(hSemaphore,dwQueueTimeout) == WAIT_TIMEOUT) { //истек таймаут ::ReportEvent(hEvt, EVENTLOG_ERROR_TYPE, 0, MSG_ISAPI_QUEUE_TIMEOUT, NULL, 0, 0, NULL, NULL); return HSE_STATUS_ERROR; } //передаем указатель на Extension Control Block вместо указателя на overlapped if(!PostQueuedCompletionStatus(hIOPort,-1,NULL,(LPOVERLAPPED)lpECB)) { ::ReleaseSemaphore(hSemaphore,1,NULL); return HSE_STATUS_ERROR; } //возвращаем статус отложенной обработки return HSE_STATUS_PENDING; } |
Пул нитей работает следующим образом: в GetExtensionVersion создается порт завершения ввода/вывода, управляющий потоками обработки. Каждый новый запрос ставится в очередь обработки (если не достигнут лимит размера очереди, отслеживаемый с помощью семафора) с помощью вызова PostQueuedCompletionStatus, при этом указатель на EXTENSION_CONTROL_BLOCK передается вместо указателя на структуру OVERLAPPED. Нити обработки ждут на порте завершения ввода/вывода посредством вызова GetQueuedCompletionStatus. I/O completion port регулирует выполнение рабочих потоков следующим образом: он позволяет параллельно выполняться не более чем заданному числу потоков (это число определяется последним параметром вызова CreateIoCompletionPort. В данном примере это число равно чилу процессоров в системе). Если в очереди порта есть сообщения, и число работающих (не ожидающих в GetQueuedCompletionStatus) потоков меньше максимального, то один из ждущих потоков освобождается для обработки сообщения.
ПРИМЕЧАНИЕ Можно подумать, что в таком случае нет смысла создавать пул с количеством нитей большим, чем ограничение порта завершения ввода/вывода. Однако это не совсем так. Если связанная с портом нитка будет блокирована на каком-либо другом объекте (семафоре, мьютексе и т.п.), то вместо нее может быть освобождена другая нитка. Таким образом реальное число выполняющихся потоков может временно превышать установленное ограничение. |
Реализовав пул нитей, мы переложили всю обработку запроса на функцию ExtensionThreadProc, которой мы и займемся далее.
Ну вот, в прошлом разделе мне удалось отвертеться от реализации функциональной части расширения. Теперь придется наконец за нее взяться. В качестве примера мы будем либо вычислять произведение параметров a и b запроса (если они присутствуют), либо возвращать все заголовки HTTP-запроса в том виде, в котором они дошли до расширения.
Сначала мы проанализируем строку запроса, которая содержится в поле lpszQueryString структуры EXTENSION_CONTROL_BLOCK на предмет наличия в ней параметров a и b. Если оба параметра присутствуют, то мы используем их произведение для формирования ответа. Если же хотя бы один параметр отсутствует, то мы используем функцию GetServerVariable, указатель на которую содержится в структуре EXTENSION_CONTROL_BLOCK для получения заголовков HTTP.
Затем мы сформируем свой заголовок ответа и отправим его, используя функцию ServerSupportFunction. После чего, использую функцию WriteClient, отправим тело ответа. В любом случае, в конце мы сообщим IIS об окончании обработки с помощью еще одного вызова ServerSupportFunction. Вот как будет выглядеть наша реализации ExtensionThreadProc:
void ExtensionThreadProc(LPVOID) { DWORD dwFlag, dwNull; OVERLAPPED * pParam; while(::GetQueuedCompletionStatus(hIOPort, &dwFlag, &dwNull, &pParam, INFINITE) && dwFlag != NULL) { //преобразуем указатель LPEXTENSION_CONTROL_BLOCK lpECB = (LPEXTENSION_CONTROL_BLOCK)pParam; //параметры a и b __int64 ia, ib; //флаги наличия парметров int iParamFlg = 0; //текущая позиция в строке запроса char *ppos = lpECB->lpszQueryString; char *pend = strchr(ppos, 0); ppos--; while(ppos && ppos + 3 < pend) { ppos++; char cPar = *ppos++; //ищем строчку вида "X=" if(*ppos++ == '=') { switch(cPar) { case 'a': ia = atoi(ppos); iParamFlg |= 0x01; break; case 'b': ib = atoi(ppos); iParamFlg |= 0x02; } } //ищем следующий параметр. ppos = strchr(ppos, '&'); } //указатель на буфер char *pAll = NULL; //размер буфера DWORD dwAll; if(iParamFlg == 3) //оба параметра { pAll = new char[21]; __int64 res = ia*ib; sprintf(pAll, "%I64i", res); dwAll = strlen(pAll); } else { dwAll = 0; //определим требуемый размер буфера lpECB->GetServerVariable(lpECB->ConnID, "ALL_RAW", pAll, (unsigned long *)&dwAll); //get size pAll = new char[dwAll]; //получим переменную lpECB->GetServerVariable(lpECB->ConnID, "ALL_RAW", pAll, (unsigned long *)&dwAll); //get data } char pszHdr[200]; static char szHeader[] = "Content-Length: %lu\r\nContent-type: text/plain\r\nPragma: no-cache\r\nExpires: 0\r\nCache-Control: no-cache\r\n\r\n"; //формируем заголовок sprintf(pszHdr, szHeader, dwAll); //create header HSE_SEND_HEADER_EX_INFO hInfo; hInfo.pszStatus = szRespOK; hInfo.pszHeader = pszHdr; hInfo.cchStatus = sizeof(szRespOK) - 1; hInfo.cchHeader = strlen(pszHdr); //используем KeepAlive http/1.1 hInfo.fKeepConn = TRUE; //отправляем заголовок lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &hInfo, NULL, NULL); //отправляем данные lpECB->WriteClient(lpECB->ConnID, pAll, &dwAll, HSE_IO_SYNC); DWORD dwStatus = HSE_STATUS_SUCCESS_AND_KEEP_CONN; //уведомляем об успешном завершении обработки lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_DONE_WITH_SESSION, &dwStatus, NULL, NULL); delete [] pAll; //освобождаем семафор ::ReleaseSemaphore(hSemaphore, 1, NULL); } _endthread(); } |
Как вы можете убедиться, ничего сильно сложного в написании ISAPI-расширений нет. Однако, если эта задача все же показалось вам сложной, вы можете писать свои ISAPI-расширения, используя MFC. Мастер проектов VC включает возможность создания каркаса ISAPI-расширений, использующих MFC-классы CHTTPServer и CHTTPServerContext, которые позволяют существенно упростить такие операции, как получение параметров из строки запроса. Однако, при этом сильно увеличивается объем кода и частично снижается производительность. Я сам начинал писать ISAPI-приложения, иcпользуя классы MFC. Написав один фильтр и три расширения, я решил для себя, что от MFC лучше отказаться.
В рассматриваемом примере используется лог сообщений NT. Данный раздел содержит описание того, как с ним работать. Если вас это не интересует, вы можете спокойно пропустить этот раздел и перейти к отладке.
Перед началом записи в лог приложение должно зарегистрировать источник событий. Это делается при помощи функции RegisterEventSource. В качестве первого параметра ей передается UNC-имя машины, к логу которой мы желаем обращаться (в нашем случае это локальный лог). Второй параметр - имя источника, которое предварительно должно быть зарегистрировано в реестре NT. Для регистрации источника событий в реестре нужно будет один раз запустить прилагаемый к проекту файл MyISAPI.reg. Реализация саморегистрации в данном случае нежелательна, т.к. у вашего расширения скорее всего не будет прав на запись в ключ HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\EventLog\Application\ реестра.
После успешной регистрации источника событий наше расширение может начать записывать в лог сообщения. Сообщения могут быть как простыми строками текста, так и заранее подготовленными сообщениями с параметрами. В данном примере используются сообщения, хранящиеся в отдельном модуле MyISAPIMsg.dll (именно он регистрируется в реестре). Для создания этого модуля нам потребуется написать файл, определяющий сообщения, и придется воспользоваться компилятором сообщений mc.exe. Файл сообщений MyISAPIMsg.mc будет иметь следующую вид:
LanguageNames=(Russian=0x419:MSG0419)
LanguageNames=(English=0x409:MSG0409)
MessageID=0x01
Severity=Informational
Facility=Application
SymbolicName=MSG_ISAPI_INITIALIZED
Language=English
ISAPI Extension Initialized.
.
Language=Russian
ISAPI-расширение загружено.
.
MessageID=0x02
Severity=Informational
Facility=Application
SymbolicName=MSG_ISAPI_UNINITIALIZED
Language=English
ISAPI Extension Uninitialized.
.
Language=Russian
ISAPI-расширение выгружено.
.
.....Часть текста опущена
MessageID=0x203
Severity=Error
Facility=Application
SymbolicName=MSG_ISAPI_THREAD_FAILED
Language=English
Thread creation failed.
.
Language=Russian
Ошибка при попытке создания новой нитки.
.
|
Как вы можете видеть, в начале файла идет описание языков сообщений. Затем идут собственно описания сообщений, причем каждое сообщение может содержать текст на нескольких языках.
ПРИМЕЧАНИЕ Обратите внимание на точки после текстов сообщений - это не ошибка, это признак конца текста. |
ПРИМЕЧАНИЕ Интересной особенностью компилятора сообщений mc.exe является то, что текстовые строки должны задаваться не в ANSI-кодировке, а в OEM-кодировке (например, cp866, а не cp1251 для русского языка). |
Помимо файла сообщений нам потребуется файл-болванка для создания библиотеки MyISAPIMsg.c с таким содержимым:
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved); |
Далее, удалите debug-конфигурацию из подпроекта MyISAPIMsg и добавьте следующие команды в Pre-link step:
mc MyISAPIMsg.mc rc -r -fo Release\MyISAPIMsg.res MyISAPIMsg.rc |
Затем, добавьте следующую строку к командной строке компоновщика:
Release\MyISAPIMsg.res /noentry |
(флаг /noenty нужен для уменьшения размеров модуля, поскольку мы собираемся создать чисто ресурсную библиотеку).
Теперь соберите проект. Вы получите файл MyISAPIMsg.dll, содержащий нужные ресурсы. Перепишите этот файл в системный каталог NT (либо подправьте путь в reg-файле). Вместе с ресурсной библиотекой будет сгенерирован заголовочный файл MyISAPIMsg.h, который мы подключим к основному проекту.
Сейчас у нас есть все необходимое для записи сообщений в лог. Мы также можем собрать основной проект. И приступить к его отладке.
Отладка ISAPI - процесс несколько более сложный, чем отладка обычного Win32-приложения. Это связано с тем, что у нас нет возможности запустить ISAPI-расширение отдельно от сервиса IIS, поэтому нам придется отлаживать сам сервис. В зависимости от того, как сконфигурировано ваше веб-приложение (In-process, pooled, isolated), может быть несколько вариантов подключения к нужному процессу. Я считаю, что самым простым являетcя следующий:
Существуют также и другие мощные средства отладки, которые можно применить для отлаживания ISAPI-приложений. Мне наиболее симпатичны два из них - Numega Bounds Checker, позволяющий в автоматическом режиме отлавливать большинство различного рода утечек, и Numega SoftIce -мощнейший низкоуровневый отладчик, в первую очередь предназначенный для отладки драйверов устройств, который, однако, позволяет с успехом отлаживать и высокоуровневые приложения, написанные на C. Тем не менее, рассмотрение этих средств не входит в рамки этой статьи.
Хотя этот очерк и не претендует на полноту и широту описания программирования ISAPI-расширений, я надеюсь, что рассматриваемый пример может стать неплохим шаблоном для написания ISAPI-расширений. По крайней мере, я его использую именно таким образом.
Сообщений 2 Оценка 100 [+1/-0] Оценить |