Реализация ISAPI в ATL Server

Internet Server Application Programming Interface (ISAPI) — это API для IIS (Internet Information Services), коллекции сетевых сервисов Microsoft Windows. Наиболее известный вид применения IIS и ISAPI — это веб-сервер от Microsoft (как, например, последняя их операционная система — MS Windows Server 2003, которая эффективно управляет всеми серверными операциями). ISAPI был разработан для реализации N-уровневой архитектуры.


ATL Server предоставляет развитую реализацию расширений ISAPI (Internet Service API). Он обеспечивает управление потоками и ресурсами IIS, избавляя вас от необходимости возиться с ними. Разберемся же, как работают его внутренние элементы.

“Сердцем” реализации интерфейсов ISAPI в ATL является класс CIsapiExtension


template ,

  class CRequestStatClass=CNoRequestStats,

  class HttpUserErrorTextProvider=CDefaultErrorProvider,

  class WorkerThreadTraits=DefaultThreadTraits,

  class CPageCacheStats=CNoStatClass,

  class CStencilCacheStats=CNoStatClass>

class CIsapiExtension :

  public IServiceProvider,

  public IIsapiExtension,

  public IRequestStats {

protected:

  CIsapiExtension();




  DWORD HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) ;

  BOOL GetExtensionVersion(__out HSE_VERSION_INFO* pVer) ;

  BOOL TerminateExtension(DWORD /*dwFlags*/) ;




  // ...

};

Как видите, этот класс основывается на шаблоне. Три параметра шаблона (CRequestStatClass, CPageCacheStats и CStencilCacheStats) используются для оценок производительности и ведения журналов. Использование предлагаемых по умолчанию параметров шаблона приводит к отсутствию механизмов оценки производительности и ведения журналов; в ATL Server есть другая реализация, которая будет собирать статистику, но, поскольку на это тратится часть производительности сервера, по умолчанию сбор статистики отключен.

Три метода CIsapiExtension предоставляют реализации собственно функций ISAPI. Метод GetExtensionVersion довольно длинный, но в нем нет ничего особенно сложного. Поскольку этот метод вызывается при загрузке расширения ISAPI, в нем выполняется солидная часть инициализации класса.


BOOL GetExtensionVersion( HSE_VERSION_INFO* pVer) {

  // выделяем ячейку Tls для хранения поточных данных

  m_dwTlsIndex = TlsAlloc();




  // создаем отдельную кучу для данных запроса

  // эта куча должна быть поточно-ориентированной, чтобы

  // обеспечивать возможность асинхронной обработки запросов

  m_hRequestHeap = HeapCreate(0, 0, 0);

  if (!m_hRequestHeap) {

    m_hRequestHeap = GetProcessHeap();

    if (!m_hRequestHeap) {

      return SetCriticalIsapiError(IDS_ATLSRV_CRITICAL_HEAPCREATEFAILED);

    }

  }




  // создаем отдельную кучу (синхронизированную) для

  // выделения памяти. Это снизит фрагментацию памяти

  // по сравнению с использованием кучи процесса

  HANDLE hHeap = HeapCreate(0, 0, 0);

  if (!hHeap) {

    hHeap = GetProcessHeap();

    m_heap.Attach(hHeap, false);

  } else {

    m_heap.Attach(hHeap, true);

  }

  hHeap = NULL;




  if (S_OK != m_WorkerThread.Initialize()) {

    return SetCriticalIsapiError(IDS_ATLSRV_CRITICAL_WORKERINITFAILED);

  }




  if (m_critSec.Init() != S_OK) {

    HRESULT hrIgnore=m_WorkerThread.Shutdown();

    return SetCriticalIsapiError(IDS_ATLSRV_CRITICAL_CRITSECINITFAILED);

  }




  if (S_OK != m_ThreadPool.Initialize(

    static_cast(this), GetNumPoolThreads(),

    GetPoolStackSize(), GetIOCompletionHandle())) {

    HRESULT hrIgnore=m_WorkerThread.Shutdown();

    m_critSec.Term();

    return SetCriticalIsapiError(

      IDS_ATLSRV_CRITICAL_THREADPOOLFAILED);

  }




  if (FAILED(m_DllCache.Initialize(&m_WorkerThread,

    GetDllCacheTimeout()))) {

    HRESULT hrIgnore=m_WorkerThread.Shutdown();

    m_ThreadPool.Shutdown();

    m_critSec.Term();

    return SetCriticalIsapiError(

      IDS_ATLSRV_CRITICAL_DLLCACHEFAILED);

  }




  if (FAILED(m_PageCache.Initialize(&m_WorkerThread))) {

    HRESULT hrIgnore=m_WorkerThread.Shutdown();

    m_ThreadPool.Shutdown();

    m_DllCache.Uninitialize();

    m_critSec.Term();

    return SetCriticalIsapiError(

      IDS_ATLSRV_CRITICAL_PAGECACHEFAILED);

  }




  if (S_OK != m_StencilCache.Initialize(

    static_cast(this),

    &m_WorkerThread,

    GetStencilCacheTimeout(),

    GetStencilLifespan())) {

    HRESULT hrIgnore=m_WorkerThread.Shutdown();

    m_ThreadPool.Shutdown();

    m_DllCache.Uninitialize();

    m_PageCache.Uninitialize();

    m_critSec.Term();

    return SetCriticalIsapiError(IDS_ATLSRV_CRITICAL_STENCILCACHEFAILED);

  }




  pVer->dwExtensionVersion = HSE_VERSION;

  Checked::strncpy_s(pVer->lpszExtensionDesc,

    HSE_MAX_EXT_DLL_NAME_LEN, GetExtensionDesc(), _TRUNCATE);

  pVer->lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN - 1] = '\0';




  return TRUE;

}

Этот метод выделяет две кучи Win32 для использования при обработке запроса, подготавливает пул потоков и инициализирует различные кеши.

Собственно обработкой запроса занимается метод HttpExtensionProc. Этот метод вызывается для каждого HTTP-запроса, который IIS направляет нашей DLL расширения. Прежде чем мы рассмотрим реализацию этого метода, нам нужно разобраться, за счет чего можно добиться от сервера хорошей производительности.

Производительность и многопоточность

Любая работа Web-сервера связана с обслуживанием множества сетевых запросов, часто поступающих одновременно. У первой платформы для обслуживания таких запросов, CGI (Common Gateway Interface — Стандартный шлюзовой интерфейс), порождался отдельный процесс для обслуживания каждого запроса. Такой подход был приемлемым для небольших сайтов, работавших под управлением UNIX, но вскоре накладные расходы на порождение процессов стали ограничивающим фактором для производительности серверов.

В Windows модель с созданием процессов работала еще хуже, поскольку создание процессов в Windows требует гораздо больших накладных расход

В Windows модель с созданием процессов работала еще хуже, поскольку создание процессов в Windows требует гораздо больших накладных расходов, чем в Unix. Однако у платформы Win32 есть вполне очевидная альтернатива: вместо отдельного процесса использовать для обслуживания запроса поток. Потоки создавать гораздо проще, чем процессы. К сожалению, у такого подхода есть определенные недостатки для больших систем. Потоки могут обходиться нам дешево, но они не бесплатны. По мере роста количества потоков процессор тратит все больше времени на управление этими потоками и все меньше — на собственно обработку запросов к Web-сайту.

Решение этой проблемы заключается в использовании особенностей HTTP. Поскольку этот протокол не поддерживает состояний, и каждый запрос является независимым, то не важно, какой именно поток обрабатывает каждый из запросов. Соответственно, по завершении обработки запроса поток может начать обрабатывать другой запрос, а не завершаться. Именно такой подход и называется пулом потоков (thread pool).

В IIS для обработки входящего трафика используется внутренний пул потоков. Каждый запрос передается одному из потоков этого пула. Поток обрабатывает запрос (возвращая статические данные с диска или выполняя функцию HttpExtensionProc из соответствующей DLL расширения ISAPI). В общем случае такой подход работает хорошо, но поток должен обрабатывать запрос быстро. Если все потоки в пуле будут заняты, сервер начнет отбрасывать запросы, для обработки которых он не может выделять потоки. На выдачу в качестве ответов на запросы статических данных уходит немного времени. Но если для обработки запроса нужно выполнять длинный код (например, для генерации динамического HTML), предсказать время обработки запроса будет сложно, и поток может оказаться занятым надолго.

В общем, нам нужно возвращать потоки в пул IIS как можно быстрее. Но для выполнения запросов в любом случае нужно выполнять какой-то код. Вместо того чтобы заставлять обработчиков оптимизировать каждый оператор в коде расширения ISAPI, ATL Server предоставляет отдельный пул потоков. По запросу функция HttpExtensionProc (работающая в потоке IIS) помещает запрос в отдельный пул потоков ATL Server. После этого поток IIS возвращается в пул и становится доступным для обработки следующего запроса. Вот код функции HttpExtensionProc.

DWORD HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) {

  AtlServerRequest *pRequestInfo = NULL;

  _ATLTRY {

    pRequestInfo = CreateRequest();

    if (pRequestInfo == NULL)

      return HSE_STATUS_ERROR;




    CServerContext *pServerContext = NULL;

    ATLTRY(pServerContext = CreateServerContext(m_hRequestHeap));

    if (pServerContext == NULL) {

      FreeRequest(pRequestInfo);

      return HSE_STATUS_ERROR;

    }




    pServerContext->Initialize(lpECB);

    pServerContext->AddRef();




    pRequestInfo->pServerContext = pServerContext;

    pRequestInfo->dwRequestType = ATLSRV_REQUEST_UNKNOWN;

    pRequestInfo->dwRequestState = ATLSRV_STATE_BEGIN;

    pRequestInfo->pExtension =

      static_cast(this);

    pRequestInfo->pDllCache =

      static_cast(&m_DllCache);

#ifndef ATL_NO_MMSYS

    pRequestInfo->dwStartTicks = timeGetTime();

#else

    pRequestInfo->dwStartTicks = GetTickCount();

#endif

    pRequestInfo->pECB = lpECB;




    m_reqStats.OnRequestReceived();




    if (m_ThreadPool.QueueRequest(pRequestInfo))

      return HSE_STATUS_PENDING;




    if (pRequestInfo != NULL) {

      FreeRequest(pRequestInfo);

    }

  }

  _ATLCATCHALL() { }

  return HSE_STATUS_ERROR;

}

Метод CreateRequest просто выделяет блок памяти из кучи запроса, чтобы сохранить информацию об этом запросе.

struct AtlServerRequest {

  // Для обеспечения совместимости

  DWORD cbSize;




  // Необходимо, поскольку внутри содержится ECB

  IHttpServerContext *pServerContext;




  // Указывает, поступил ли вызов через файл  .srf или

  // через файл .dll

  ATLSRV_REQUESTTYPE dwRequestType;

  // Указывает степень завершенности обработки запроса

  ATLSRV_STATE dwRequestState;

  // Необходимо, поскольку обратный вызов (при асинхронных вызовах)

  // должен знать, куда направлять запрос

  IRequestHandler *pHandler;

  // Необходимо для корректного освобождения dll

  // (при асинхронных вызовах)

  HINSTANCE hInstDll;

  // Необходимо для помещения запроса в очередь (при асинхронных вызовах)

  IIsapiExtension *pExtension;

  // Необходимо для освобождения dll в асинхронном обратном вызове

  IDllCache* pDllCache;




  HANDLE hFile;

  HCACHEITEM hEntry;

  IFileCache* pFileCache;




  // необходимо для синхронизации вызовов HandleRequest,

  // если HandleRequest может сделать асинхронный вызов до

  // завершения. Используется только

  // при обозначении ATLSRV_INIT_USEASYNC_EX

  HANDLE m_hMutex;

  // Счетчик времени от получения запроса

  DWORD dwStartTicks;

  EXTENSION_CONTROL_BLOCK *pECB;

  PFnHandleRequest pfnHandleRequest;

  PFnAsyncComplete pfnAsyncComplete;

  // буфер для асинхронной очистки

  LPCSTR pszBuffer;

  // длина данных в pszBuffer

  DWORD dwBufferLen;

  // значение, с помощью которого можно передавать

  // пользовательские данные между родительскими и дочерними обработчиками

  void* pUserData;

};




AtlServerRequest *CreateRequest() {

  // Выделяем блок фиксированного размера во избежание фрагментации

  AtlServerRequest *pRequest = (AtlServerRequest *) HeapAlloc(

    m_hRequestHeap, HEAP_ZERO_MEMORY,

    __max(sizeof(AtlServerRequest),

      sizeof(_CComObjectHeapNoLock)));

  if (!pRequest) return NULL;




  pRequest->cbSize = sizeof(AtlServerRequest);

  return pRequest;

}

Как видите, в структуре содержится вся информация о запросе, которую нам передает IIS (указатель на ECB) и еще много чего.

Пул потоков ATL Server

ATL Server предоставляет реализацию пула потоков в классе CThreadPool.

template 

class CThreadPool : public IThreadPoolConfig {

    // ...

};

Параметры шаблона позволяют задавать способы создания потоков и их использования. Параметр шаблона Worker позволяет указать класс, который будет непосредственно обрабатывать запросы. Класс ThreadTraits управляет созданием потоков. В зависимости от символа ATL_MIN_CRT определение типа DefaultThreadTraits может обозначать один из двух других классов.

class CRTThreadTraits {

public:

  static HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpsa,

      DWORD dwStackSize, LPTHREAD_START_ROUTINE pfnThreadProc,

      void *pvParam, DWORD dwCreationFlags, DWORD *pdwThreadId) {

    // _beginthreadex вызывает функцию CreateThread,

    // которая сообщит код последней ошибки

    // перед возвратом управления.

    return (HANDLE) _beginthreadex(lpsa, dwStackSize,

      (unsigned int (__stdcall *)(void *)) pfnThreadProc,

      pvParam, dwCreationFlags, (unsigned int *) pdwThreadId);

  }

};




class Win32ThreadTraits {

public:

  static HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpsa,

      DWORD dwStackSize, LPTHREAD_START_ROUTINE pfnThreadProc,

      void *pvParam, DWORD dwCreationFlags, DWORD *pdwThreadId) {

    return ::CreateThread(lpsa, dwStackSize, pfnThreadProc,

      pvParam, dwCreationFlags, pdwThreadId);

  }

};




#if !defined(_ATL_MIN_CRT) && defined(_MT)

     typedef CRTThreadTraits DefaultThreadTraits;

#else

     typedef Win32ThreadTraits DefaultThreadTraits;

#endif

Помимо всего прочего, в процессе инициализации класс CThreadPool пользуется классом ThreadTraits при создании начального набора потоков. Все потоки в пуле выполняют такую функцию.

DWORD ThreadProc() {

  DWORD dwBytesTransfered;

  ULONG_PTR dwCompletionKey;




  OVERLAPPED* pOverlapped;




  // этот блок должен гарантировать, что theWorker будет уничтожен

  // до закрытия дескриптора потока {

    // Мы создаем экземпляр класса Worker в стеке

    // на время жизни потока.

    Worker theWorker;

    if (theWorker.Initialize(m_pvWorkerParam) == FALSE) {

      return 1;

    }




    SetEvent(m_hThreadEvent);

    // Получаем запрос

    while (GetQueuedCompletionStatus(m_hRequestQueue,

      &dwBytesTransfered, &dwCompletionKey, &pOverlapped,

      INFINITE)) {

      if (pOverlapped == ATLS_POOL_SHUTDOWN) // Shut down {

        LONG bResult = InterlockedExchange(&m_bShutdown, FALSE);

        if (bResult) // Завершение отменено

          break;




      // иначе завершение отменено — продолжаем, как и прежде

      }

      else {

        // Выполняем работу

        Worker::RequestType request =

          (Worker::RequestType) dwCompletionKey;




        // Обрабатываем запрос. Обратите внимание:

        // (1) Объект класса Worker должен освободить память, связанную с

        // запросом, после завершения обработки этого запроса

        // (2) Если запрос требует дополнительной обработки,

        // объект класса worker должен поместить запрос в очередь

        // для отправки на обработку

        theWorker.Execute(request, m_pvWorkerParam, pOverlapped);

      }

    }




    theWorker.Terminate(m_pvWorkerParam);

  }




  m_dwThreadEventId = GetCurrentThreadId();

  SetEvent(m_hThreadEvent);




  return 0;

}

Базовая последовательность действий, выполняемых в пуле потоков, вполне понятна. Поток ожидает поступления запросов от порта ввода-вывода. Чтобы приказать потоку завершиться, нужно использовать специальное значение; если поток не завершается, запрос передается рабочему объекту для обработки.

Рабочие объекты могут относиться к любому классу с определением типа RequestType и соответствующим методом Execute.

Как видите, ATL Server сильно упрощает разработку расширений ISAPI. Он выполняет за нас всю черную работу по поддержанию производительности сервера; все, что требуется от нас — написать класс, который будет обрабатывать запросы, и реализовать логику их обработки в методе Execute этого класса. Однако задача генерации HTML-текста, который нужно отправить клиенту, все равно возлагается на нас. Генерировать HTML-текст в программе на C++ несложно, но это скучно и неинтересно. Кроме того, если мы будем генерировать HTML-текст в коде программы, нам придется перекомпилировать ее каждый раз, когда мы захотим поменять этот текст. Нам нужен какой-то способ генерации HTML-текста по шаблонам. ATL Server предоставляет такие шаблоны — это файлы ответа сервера (Server Response Files).



Опубликовал admin
26 Июн, Вторник 2007г.



Программирование для чайников.