oldi

Понимание многопоточности в VCL для Web-серверных ISAPI-расширений

Андрей Качанов

В среде Delphi можно создавать высокоэффективные Web-серверные ISAPI-расширения с помощью мастера (New -> Web Server Application — ISAPI DLL). Прилагаемая справочная документация, а также демонстрационный пример «$(DELPHI)\Demos\Webserv» позволяют достаточно быстро освоиться в приемах написания Web-серверных ISAPI-расширений. На выходе у вас получится обычная DLL (далее по тексту — библиотека).

Сложность заключается в том, что Web-сервер (для ускорения обработки поступающих запросов) вызывает нашу библиотеку в многопоточном режиме. В результате на разработчика ложится ответственность за написание поточно-безопасного кода. Не беспокойтесь, ребята из Borland постарались упростить вам жизнь, насколько это возможно. Поняв смысл «обертки» TWebApplication и наследника TISAPIApplication, я был восхищен и теперь готов поделиться этими знаниями с вами!

Согласно спецификации ISAPI-расширений, созданная библиотека имеет всего три экспортируемые функции: GetExtensionVersion, HttpExtensionProc, TerminateExtension. Нас интересует только HttpExtensionProc, через которую выполняется вся работа: получение запросов с Web-сервера (Request), обработка и обратная отправка результата (Response).

Итак, рассмотрим весь путь прохождения данных. Запрос Web-сервера поступает через экспортируемую библиотекой функцию HttpExtensionProc в TISAPIApplication через инкапсулированный метод с одноименным названием (объект Application, как и в любом VCL-приложении другого вида, присутствует всегда: создается при инициализации и разрушается при завершении приложения, однако в данном случае имеет тип TISAPIApplication):

function TISAPIApplication.HttpExtensionProc(var ECB:
TEXTENSION_CONTROL_BLOCK): DWORD;
var
   HTTPRequest: TISAPIRequest;
   HTTPResponse: TISAPIResponse;
{ ^ локально объявленные переменные запроса и ответа }
begin
try
HTTPRequest := NewRequest(ECB);
   { ^ инициализация переменной запроса по структуре ECB,
   полученной от Web-сервера }
   try
      HTTPResponse := NewResponse(HTTPRequest);
      { ^ инициализация переменной ответа }
      try
         if HandleRequest(HTTPRequest, HTTPResponse) then
         { ^ обработка переходит к TWebApplication.HandleRequest }
          Result := HSE_STATUS_SUCCESS
         else Result := HSE_STATUS_ERROR;
      finally
         HTTPResponse.Free;
      end;
   finally
      HTTPRequest.Free;
   end;
except
   HandleServerException(Exception(ExceptObject), ECB);
      Result := HSE_STATUS_ERROR;
   end;
end;

Из приведенного кода видно, что переменные HTTPRequest и HTTPResponse объявлены локально и объекты соответствующих типов создаются для каждого поступающего запроса Web-сервера. После инициализации этих переменных обработка переходит к TWebApplication.HandleRequest:

function TWebApplication.HandleRequest(Request: TWebRequest;
   Response: TWebResponse): Boolean;
var
   DataModule: TDataModule;
   Dispatcher: TCustomWebDispatcher;
   I: Integer;
begin
   Result := False;
   DataModule := ActivateWebModule;
   { ^ назначает объект, который не используется другими потоками }
   if DataModule <> nil then
   try
      if DataModule is TCustomWebDispatcher then
         Dispatcher := TCustomWebDispatcher(DataModule)
      else with DataModule do
      begin
         Dispatcher := nil;
         for I := 0 to ComponentCount - 1 do
         begin
            if Components[I] is TCustomWebDispatcher then
            begin
               Dispatcher := TCustomWebDispatcher(Components[I]);
               Break;
            end;
         ;end;
end;
if Dispatcher <> nil then
   begin
      Result := TWebDispatcherAccess(Dispatcher).DispatchAction(Request, Response);
      { ^ обработка переходит к TWebDispatcher.DispatchAction }
      if Result and not Response.Sent then
         Response.SendResponse;
         { ^ отправка ответа Web-серверу }
   end else raise Exception.CreateRes(@sNoDispatcherComponent);
finally
   DeactivateWebModule(DataModule);
   { ^ переводит в список неиспользуемых объектов — FInactiveWebModules }
end;
end;

Тут следующая хитрость: локально объявленная переменная DataModule получает ссылку на объект от метода TWebApplication.ActivateWebModule. Для каждого потока предоставляется неиспользуемый в настоящее время другими потоками объект типа TDataModule, для чего выполняется перемещение этих объектов между списками FInactiveWebModules и FActiveWebModules. Если список FInactiveWebModules исчерпан, то создается новый экземпляр объекта типа TDataModule. В результате этих манипуляций для каждого потока используется собственный экземпляр объекта типа TDataModule, и разработчик может быть уверен в поточно-безопасном объявлении полей данных своего объекта TWebModule! Но это еще не все.

Локально объявленные в TISAPIApplication.HttpExtensionProc переменные HTTPRequest и HTTPResponse, о которых говорилось выше, переданы в качестве параметров Request и Response методу TWebApplication.HandleRequest, который, в свою очередь, передает их методу TCustomWebDispatcher.DispatchAction:

function TCustomWebDispatcher.DispatchAction(Request: TWebRequest;
   Response: TWebResponse): Boolean;
var
   I: Integer;
   Action, Default: TWebActionItem;
   Dispatch: IWebDispatch;
begin
   FRequest := Request;
   FResponse := Response;
   {...}
end;

Здесь выполняется присваивание переменных Request и Response полям объекта TWebModule (как наследнику TCustomWebDispatcher). А нам уже известно, что экземпляр объекта TWebModule у каждого потока — собственный. Теперь посмотрим правде в глаза: у каждого запроса Web-сервера есть собственные экземпляры объектов TRequest и TResponse в полях TWebModule.Request и TWebModule.Response и они являются поточно-безопасными.

Далее путь лежит через метод TWebActionItem.DispatchAction, который вызывается в TCustomWebDispatcher.DispatchAction. Тут может вступать в действие ваш код обработки запроса, после чего подготовленному ответу предстоит обратная дорога.

 

Как видно из приведенного выше фрагмента кода TWebApplication.HandleRequest — DataModule передается в качестве параметра методу TWebApplication.DeactivateWebModule, в котором он может быть переведен в список FInactiveWebModules или вовсе разрушен (если выключено свойство CacheConnections — этим не стоит пользоваться без необходимости, так как существенно снижается производительность обработки запросов). После этого обработка возвращается к TISAPIApplication.HttpExtensionProc и ответ передается Web-серверу вызовом Response.SendResponse.

Что хотелось бы отдельно отметить. Мне несколько раз попадались на глаза рекомендации устанавливать глобальную переменную IsMultiThread к True в dpr-файл проекта — этого делать не нужно, так как в конструкторе TWebApplication эта работа уже выполняется!

Если вы используете доступ к BDE посредством наследников TBDEDataSet (TTable, TQuery, TStoredProc), то все, что вам нужно сделать для обеспечения поточной безопасности, — это присвоить в конструкторе TWebModule: Session.AutoSessionName := True (подробнее смотри в справочной документации: «Managing multiple sessions»).

Реализация инкапсуляции WinSock в компонентах TClientSocket и TServerSocket, которые вам могут потребоваться, также поточно-безопасна.

Конечно, при использовании файлового ввода-вывода, а также прямых вызовов WinSock все же нужно выполнять многопоточную защиту самостоятельно — для этого придется прочитать раздел документации «Programming with Delphi — Using threads».

КомпьютерПресс 11'2000