Как "работает" multi-threaded COM компонента под MS MTS/Component Services? |
> Создал в VFP 6.0 (SP5/VS6) single-threaded COM server(dll) работает нормально.
> Однако, при повисание одного из клиентов (Win98)
> работа возобновляется только после ручного shutdown пакета.
> Имеется ли вариант избежать это?
Как решить проблему оставаясь в рамках single-threaded не знаю... разве что уменьшить idle timeout пакета/приложения. А вот кардинально проблему можно решить перейдя на multi-threaded COM server(dll) (VFP 6.0 SP3VS6 и выше). Нужно также иметь ввиду, что компоненты в модели multi-threaded COM могут быть как stateful так и stateless. Используя stateless компоненты, Вы можете значительно увеличить производительность Вашей системы.
Для того чтобы понять: за счёт чего может быть повышена производительность, попробуем коротко обозначить различия между stateful и stateless компонентами (в моём вольном переводе из книги Roger Jennings' Database workshop: Microsoft® Transaction Server 2.0, Sams publishing).
Stateful компоненты максимально совпадают с традиционным взглядом на объектно-ориентированное программирование, однако их реализация приводит к ощутимым проблемам. Рассмотрим это в свете таких понятий как: время жизни (livetime), владение компонентом (ownership) и производительности системы (performance).
Время жизни. Любой экземпляр компонента, будучи созданный продолжает "жить" так долго, сколько в нём существует необходимость пользователя, и уничтожается только после полного завершения работы с ним. Проблема в том, что каждый из таких экземпляров захватывает ресурсы, в частности оперативною память сервера. Всё это может привести к быстрому истощению его ресурсов.
Владение компонентом. Поскольку каждый экземпляр компонента имеет уникальную информацию о его данных (как значения его свойств), он не может быть использован совместно несколькими пользователями. Фактически только клиент, создавший подобный экземпляр компонента использует эту информацию. В результате между каждым клиентом и такими экземплярами компонентов устанавливается устойчивая связь на всё время работы клиента. Это может и не вызывать особых проблемы, если количество используемых пользователем экземпляров объектов относительно не велико. Однако, обратите внимание, что во-первых, это ресурсы на сервере, и во-вторых, эти ресурсы расходуются пропорционально как количеству компонент, используемых одним пользователем, так и количеству клиентов. К тому же, для доступа к базе данных каждый пользователь получает соединение. Установка одного соединения, например для MS SQL Server, может занять до двух секунд реального времени клиента, в то время как обходится серверу в 50К, и это серверный ресурс, которым должен владеть каждый клиент для выполнения своей работы.
Производительности системы. Даже краткое изложение проблем stateful компонент выше, приводит к мысли, что это отнюдь не будет способствовать высокой производительности, а скорее наоборот: производительность будет резко убывать с ростом количества клиентов и увеличением объёма информации необходимой им. Проблему также усугубляет работа со свойствами таких компонент. Чтение/Запись каждого свойства экземпляра компоненты приводит к обращению к межсетевому протоколу (RPC Call). Обратите внимание: пропорционально как количеству используемых экземпляров объектов, так и количеству их свойств. Всё это приведёт к большим затратам сетевых ресурсов даже по сравнению с системами, основанными на традиционной двух-слойной архитектуре.
Stateless компоненты обладают целым рядом преимуществ перед stateful компонентами при создании больших и масштабируемых информационных систем... И если их также рассмотреть с точки зрения понятий, обозначенных выше, то мы имеем следующее.
Время жизни. Поскольку каждый экземпляр компоненты на сервере не содержит никакой уникальной информации для клиента (т.е. отсутствуют свойства, хранящие данные между обращениями к различным методам экземпляра компонента), то он может быть использован сразу несколькими клиентами. Экземпляр компоненты появляется только тогда, когда какой-нибудь метод требует своего выполнения... и может быть уничтожен, как только никто больше не нуждается в исполнении его методов. Конечно же, такое поведение способствует удержанию лишь минимальное количество экземпляров объектов на сервере для обслуживания большего количества клиентов.
Владение компонентом. Поскольку экземпляры stateless компонент не удерживают в себе значений каких-либо информационных свойств, то они могут быть использованы повторно, обеспечивая так называемую концепцию пула экземпляров объектов (object pulling). Это когда имея n экземпляров сервер может обслужить m клиентом (n <= m), причём владея ограниченным количеством ресурсов, он может обеспечить работу произвольного количества клиентов. При этом производительность системы не находится в прямой зависимости ни от количества экземпляров объектов ни от количества работающих клиентов. Такой способ работы позволяет применить концепцию пула и к соединениям с базой данных. Поскольку компоненты не содержат значений каких-либо свойств, они не требуют удержания открытыми соединений с базой данных. Другими словами, соединения с базой данных используются только тогда, когда в них возникает необходимость.
Производительности системы. Компоненты "живут" на сервере, а отсутствие атрибутов (свойств) приводит к простому способу их вызова (Хм, если в сказанном ранее я не сомневался, и это было почти самоочевидно, то вот здесь позволю себе не согласиться, ибо отсутствие свойств автоматично ведёт к необходимости использования большого числа параметров в методах... Другое дело, что запрет на передачу ссылок приводит к простейшим методам сетевой маршализации. Кроме того, хранение всех данных непосредственно на клиенте и передачу их через параметры методов серверных компонент, скорее всего приведёт к необходимости написать wrap-покрытий для stateless компонент, чтобы без мучений их использовать на стороне клиента. Другими словами, возникнет необходимость создания ещё одного слоя, но "живущего" уже на стороне клиента только :-) Т.к. stateless компоненты являются очень нетребовательными к сетевым ресурсам, то могут быть легко перераспределены по серверам вместо того, чтобы быть сосредоточенными только на одной машине.
Мы знаем, что достоинств не бывает без недостатков, а посему рассмотрим кратко особенности использования stateless компонент. :-)
Как это работает попытаюсь пояснить на простом примере. Допустим на клиенте Вы пытаетесь создать экземпляр серверного объекта:
LOCAL loRefToServerObject loRefToServerObject = CreateObject("MyLib.MyClass")
на этом этапе на клиенте создалась Proxy-заглушка, в то время как на сервере был создан/активизирован конкретный экземпляр объекта... Обратите внимание, что это совсем не означает, что именно только что созданный/активизированный серверный экземпляр объекта будет "участником" дальнейших, обсуждаемых ниже, операций. Как понимаю, использование реального серверного объекта обусловлена лишь необходимостью контроля полномочий доступа для клиента к объекту. Здесь необходимо подчёркнуть, что клиент имеет ссылку, но не на реальный объект сервера, а только на экземпляр Proxy-заглушки, способной обратиться к какому-нибудь реальному экземпляру на сервере. Пусть далее имея ссылку, Вы обращаетесь к одному из методов экземпляра объекта:
loRefToServerObject.MyAnyMethod1(...)
вот только сейчас сервер, найдя у себя подходящий экземпляр реального объекта (или создав новый), как реакцию на запрос через Proxy-заглушку, начал выполнение Вашего метода, но как только метод завершился (внутри метода был выполнен IObjectContext::SetComplete или IObjectContext::SetAbort), сервер тут же отсоединил свою Stub-заглушку от только что используемого экземпляра объекта у себя на сервере... Предположим, что Вы тут же делаете новое обращение к компоненте:
loRefToServerObject.MyAnyMethod2(...)
Теперь опять, как реакцию на запрос через Proxy-заглушку, он должен это дело удовлетворить через какой-то экземпляр реального объекта на сервере... возможно нового, возможно какого-нибудь ранее существующего, но совсем не обязательно, что это будет в точности экземпляр объекта, для которого был выполнен метод MyAnyMethod1 выше... у клиента реально только ссылка на одну и туже Proxy-заглушку, но не ссылка на реальный экземпляр объекта на сервере...
Как надеюсь, это простое объяснение может раскрыть глаза на загадочное поведение Вашего серверного multi-threaded COM объекта в режиме stateless. Я не уверен, что в деталях это работает именно так как написано мной выше, однако как схема работы связи клиент-сервер, думаю написанное достаточно правдоподобно.
Кроме того, используя метод IObjectContext::CreateInstance Вы можете создавать экземпляры требуемых Вам объектов "на лету" прямо на сервере, т.е. по существу минуя накладных расходов сетевого маршалинга.
Чтобы дать тривиальный пример для Ваших экспериментов, ниже показан код простейшего компонента, использующий серверные возможности MTS/COM+
* //////////////////////////////////////////////////////// *// Class: longrun *// Description: The simple test for vfp-com-stateless object *// Author: Michael Drozdov *// Home page: http://vfpdev.narod.ru/ *// e-mail: Mailto:Drozdov@ics.perm.su *// Date: 01/11/2003 *//////////////////////////////////////////////////////// *// #DEFINE MTXAPPSRV "MTxAS.AppServer.1" #DEFINE STATELESS .T. DEFINE CLASS longrun AS SESSION OLEPUBLIC PROTECTED cObjID && unique ObjID *////////////////////////////////////// *// Init on create new object PROCEDURE Init =SYS(2339,0) && see Q258736 in MSDN * *-- Generate unique ObjID for instance object this.cObjID = SYS(2015) ENDPROC *////////////////////////////////////// *// Get unique ObjID for object FUNCTION GetObjID #IF STATELESS * *-- Create COMSVCSLib.IMTxAS object (see Q193295 in MSDN) LOCAL loMtx && As COMSVCSLib.IMTxAS loMtx = CreateObject(MTXAPPSRV) * *-- Get ObjectContext LOCAL loCtx && as COMSVCSLib.ObjectContext IF VARTYPE(loMtx) = 'O' loCtx = loMtx.GetObjectContext() ENDIF #ENDIF * *-- Set result LOCAL lcRetVal lcRetVal = this.cObjID #IF STATELESS * *-- SetComplete IF VARTYPE(loCtx) = 'O' loCtx.SetComplete() lcRetVal = lcRetVal+" Ctx" ENDIF #ENDIF RETURN lcRetVal ENDFUNC *////////////////////////////////////// *// Long-run method FUNCTION RunTest(tnSecTimeOut) IF VARTYPE(tnSecTimeOut) # 'N' tnSecTimeOut = 60 ENDIF LOCAL lcStatus lcStatus = "" #IF STATELESS * *-- Create COMSVCSLib.IMTxAS object (see Q193295 in MSDN) LOCAL loMtx && As COMSVCSLib.IMTxAS loMtx = CreateObject(MTXAPPSRV) * *-- GetObjectContext LOCAL loCtx && as COMSVCSLib.ObjectContext IF VARTYPE(loMtx) = 'O' loCtx = loMtx.GetObjectContext() lcStatus = " Mtx" && state is MTxAS.AppServer object ENDIF #ENDIF * *-- Do long-run actions LOCAL lnStart lnStart = SECOND() * *-- Wait tnSecTimeOut DO WHILE (SECOND()-lnStart) < tnSecTimeOut ENDDO #IF STATELESS IF VARTYPE(loCtx) = 'O' * *-- SetComplete lcStatus = lcStatus+"/Ctx" && state is ObjectContext IF loCtx.IsSecurityEnabled() lcStatus = lcStatus+"/Sec" && state is IsSecurityEnabled ENDIF loCtx.SetComplete() ENDIF #ENDIF RETURN 'Successful completed interval: '+LTRIM(STR(tnSecTimeOut))+lcStatus ENDFUNC ENDDEFINE *////////////////////////////////////////////////////////
Как видно из приведённого листинга, класс longrun имеет, как пользовательские: одно свойство и два метода. Закрытое свойство cObjID в событии Init() инициализируется уникальным значением используя VFP-функцию SYS(2015), а метод GetObjID возвращает значение этого свойства. Поскольку событие Init() для каждого экземпляра класса наступает лишь однажды, будем считать, что различным значениям возвращаемым методом GetObjID() соответствуют различные экземпляры классов. Обратите внимание, что при завершении метода GetObjID() производится попытка выполнения метода ObjectContext.SetComplete(). Наконец, метод longrun.RunTest(tnSecTimeOut) имитирует долго длящийся процесс, выполняя "пустой цикл" в течении времени, указанного в качестве параметра. По завершению цикла производится попытка обращения к ObjectContext.SetComplete() также. Попутно формируется строка, позволяющая определить: как именно "работал" ObjectContext (если работал) у экземпляра Вашего объекта на сервере, которая возвращается методом как результат.
Может показаться, что если мы добавим к классу longrun ещё одно закрытое свойство (скажем oMtx), в котором будем хранить ссылку на экземпляр объекта "MTxAS.AppServer.1" и тогда, добавив ещё один метод инициализации этого свойства, например:
PROCEDURE CreateMtx #IF STATELESS IF VARTYPE(this.oMtx) # 'O' this.oMtx = CreateObject(MTXAPPSRV) ENDIF #ENDIF RETURN (VARTYPE(this.oMtx) = 'O') ENDPROC
(обращение к которому будем выполнять в начале любого метода longrun класса в режиме stateless [как бы стремление иметь аналог непосредственного наличия функции GetObjectContext() из COM+ Services Type Library DLL (comsvcs.dll)]), то тем самым сможем обеспечить некоторую "устойчивость" значения экземпляра нашего longrun класса. Однако, это впечатление обманчиво, ибо после любого ObjectContext.SetComplete() значение свойства oMtx будет сброшено в несуществующий объект... Другими словами, эти действия всё равно не приведут к тому, что в режиме stateless нам удастся получить одинаковые значения при последовательном обращении к методу longrun.GetObjID().
Для вызова этого компонента мной написан простейший клиент на VFP/VB 6.0 SP5VS6, позволяющий проконтролировать работу этого компонента на сервере. На рисунке ниже показан его оконный интерфейс.
здесь назначение кнопок следующее:
Команда | Назначение |
Edit SOAP options | - Доступна, если включен флажок Use SOAP-protocol, и вызывает диалог редактирования параметров SOAP-протокола. |
Create object | - Создаёт экземпляр объекта, если он ещё не создан. |
Start longRun | - Создаёт экземпляр объекта, если он ещё не был создан, и выполняет его метод RunTest(tnSecTimeOut), где в качестве параметра используется время (в секундах) введённое в поле TimeOut, при этом в ListBox отображается происходящие события. По завершению выполнения метода возвращённый результат отображается в поле Return value. |
Get objectID | - Создаёт экземпляр объекта, если он ещё не был создан, и выполняет его метод GetObjID() отображая результат в ListBox. |
Clear log | - Очищает содержимое ListBox |
Release object | - Уничтожает ссылку на экземпляр созданного объекта. |
Cancel | - Закрывает окно диалога. |
Используя MS SOAP-протокол для любого COM компонента, опубликованного на Web сервере в качестве Web Service, Вы можете создать/использовать его экземпляр без какой либо предварительной регистрации (т.е. без того, как это обычно делается при использовании COM/DCOM/COM+) на стороне Internet-клиента. На следующей картинке представлен диалог редактирования параметров SOAP-протокола, вызываемого по кнопке Edit SOAP options из клиентского приложения testclient.exe:
Для демонстрации обращения к VFP-COM+ компоненте со стороны Internet-клиента через MS SOAP-протокол в примере я воспользовался возможностями MS SOAP Toolkit 3.0. Файлы, полученные применением утилиты WSDL Generator к тестовой VFP-COM компоненте (vfpcomtest.dll) прямо так, как это было получено мной на WinNT 5.0 Server, лежат в каталоге Soap/wsdl/. В диалоге выше, значения параметров таковы, как если бы при генерации был указан в качестве принимающего каталог C:\Inetpub\WebServices\vfpcomtest\ на сервере (безусловно с установленным MS IIS 5). Причём подкаталог WebServices, должен быть зарегистрирован из-под Computer Management в Services and Applications/Internet Information Servecies/Default Web Site/ как виртуальный с включенным флажком Run script (such as ASP), если при генерации файлов Вы использовали установку ASP. В своём же примере, при генерации WSDL-файла, я использовал установку по умолчанию (ISAPI), поэтому для виртуального каталога необходимо отметить флажок Execute (such as ISAPI applications or CGI) также.
Если у Вас всё будет сделано также как написано мной выше, то для обращения через тестовое VFP-клиентской приложение (testclient.exe), запущенное на Вашем Web сервере, потребности в редактировании параметров SOAP-протокола не возникнет, в то время как для обращения с удалённого Internet-клиента к Вашему Web серверу через SOAP-протокол, необходимо будет:
Код создания экземпляра VFP-COM компонента в клиентском приложении следующий:
#DEFINE VFPOBJPROGID "vfpcomtest.longrun" #DEFINE SOAPCLNTPROGID "MSSOAP.SoapClient30" WITH ThisForm IF VARTYPE(.oVfpComObj) # 'O' IF .chkUseSoap.Value * *--SOAP .oVfpComObj = CreateObject(SOAPCLNTPROGID) ELSE * *--DCOM .oVfpComObj = CreateObject(VFPOBJPROGID) ENDIF IF VARTYPE(.oVfpComObj) = 'O' IF .chkUseSoap.Value * *-- SoapInit .oVfpComObj.MSSoapInit(.cSoap_wsdlfile ; ,.cSoap_name, .cSoap_port, .cSoap_wsmlfile) ENDIF ENDIF ENDIF ENDWITH
здесь свойства формы: .cSoap_wsdlfile, .cSoap_name, .cSoap_port, .cSoap_wsmlfile именно те, которые получены из диалога редактирования параметров SOAP-протокола, показанного выше.
Замечание относительно версий. Я ориентировался на использование именно версии 3.0 и если у Вас это не так (т.е. у Вас установлена версия 2.0 и нет желания устанавливать именно 3.0), то Вам следует исправить "MSSOAP.SoapClient30" на "MSSOAP.SoapClient" (или "MSSOAP.SoapClient.1") Т.е. в
с последующей перекомпиляцией. К сожалению Microsoft не стал поддерживать VersionIndependentProgID.
В подкаталоге Soap/wsdl/ помимо полученных применением утилиты WSDL Generator, содержится vfpcomtestSoap2.js файл, для пробного обращения к компоненте с Вашего Web сервере, а в подкаталоге Soap/JsClient vfpcomtestSoap.js файл для обращения с любого Internet-клиента, для которого Ваш Web сервер доступен. Отредактируйте код в этих файлах на Ваши названия/месторасположения перед попыткой их выполнения, учитывая замечание относительно версий MS SOAP Toolkit выше.
Внимание. Для того, чтобы это работало, необходима установка MS SOAP Toolkit 3.0 (или 2.0) как на Вашем Web сервере, так и на Internet-клиентах (на клиентах лишнее можно будет убрать в последствии :-). Вот адрес для свободной загрузки MS SOAP Toolkit 3.0 http://msdn.microsoft.com/downloads/sample.asp?url=/msdn-files/027/001/948/msdncompositedoc.xml&frame=true Также, я обратил внимание, что с Web сервера, на котором был установлен MS IE 5.?, мне не удалось получить экземпляр компоненты через SOAP-протокол. Поэтому подозреваю, что на Web сервере требуется установка именно MS IE 6.0 (или выше) также.
Наконец, на картинке ниже показан результат обращения клиента из-под MS IE к asp-страничке. При выполнении asp - страницы VFP-COM-компонента используется на стороне Web сервера.
При щелчке на одну из двух кнопок, серверу отправляется submit методом POST. Однако, чтобы передать на сервер название метода, к которому требуется обратиться, на странице использовано hidden-поле, в значение которого прописывается затребованный клиентом метод. Исходный код как клиентской, так и серверной частей написан на Jave Script, и я попытался прокомментировать его настолько, насколько это только возможно :-)
Если понаблюдать за поведением приведённого выше класса longrun, то можно обнаружить, и это достаточно неожиданно, что события Init()/Destroy() наступают на каждом обращении к методу GetID() компонента! Т.е. событие Init() может быть использовано для получения ссылки на IContextObject, в то время как в событии Destroy() она должна быть уничтожена. Другими словами, события Init() / Destroy() мапируют события ObjectControl_Activate() / ObjectControl_Deactivate() для ObjectControl из comsvcs.dll. Вы можете проверить это на таком примере кода:
*///////////////////////////////////////////////////////////// #DEFINE LOGFILE "c:\Temp\Vfpcom.log" #DEFINE METHODLEN 13 #DEFINE MTXAPPSRV "MTxAS.AppServer.1" DEFINE CLASS SrvCls AS Session OLEPUBLIC PROTECTED oCtx PROTECTED cUniqueObjID oCtx = NULL cUniqueObjID = "" FUNCTION Init() DECLARE INTEGER GetCurrentThreadId IN win32api IF SYS(23390) # '0' =SYS(2339, 0) ENDIF This.WriteToLog("Init()") IF !This.GetObjectContext() RETURN .F. ENDIF This.cUniqueObjID = SYS(2015) ENDFUNC FUNCTION Destroy() This.WriteToLog("Destroy()") This.oCtx = NULL ENDFUNC FUNCTION GetID() This.WriteToLog("GetID") LOCAL lcID lcID = This.cUniqueObjID This.WriteToLog("GetID:..." + SUBSTR(lcID, 7)) This.SetComplete(.T.) RETURN lcID ENDFUNC PROTECTED FUNCTION GetThreadID() RETURN GetCurrentThreadId() ENDFUNC PROTECTED FUNCTION GetObjectContext() IF VARTYPE(This.oCtx) # 'O' LOCAL loMtx loMtx = CREATEOBJECT(MTXAPPSRV) IF VARTYPE(loMtx) = 'O' This.oCtx = loMtx.GetObjectContext() ENDIF ENDIF This.WriteToLog("GetObjectContext()") RETURN (VARTYPE(This.oCtx) = 'O') ENDFUNC PROTECTED FUNCTION SetComplete(tbResult) IF VARTYPE(This.oCtx) = 'O' IF tbResult This.WriteToLog("SetComplete()") This.oCtx.SetComplete() ELSE This.WriteToLog("SetAbort()") This.oCtx.SetAbort() ENDIF RETURN .T. ELSE This.WriteToLog("***UnknownResult***") RETURN .F. ENDIF ENDFUNC PROTECTED FUNCTION WriteToLog(tcMethod) IF VARTYPE(tcMethod) # 'C' tcMethod = '(Unknown)' ENDIF tcMethod = PADR(tcMethod, METHODLEN) LOCAL lcObjectContextInfo IF VARTYPE(This.oCtx) = 'O' lcObjectContextInfo = "Yes " ; + TRANSFORM(This.oCtx.ContextInfo.GetContextId()) ELSE lcObjectContextInfo = "No" ENDIF STRTOFILE(TTOC(DATETIME()) ; + " ThreadID: " + TRANSFORM(This.GetThreadID(), '9999') ; + " " + tcMethod + " fired! ObjectContext: " ; + lcObjectContextInfo ; + CHR(13) + CHR(10) , LOGFILE, .T.) ENDFUNC ENDDEFINE */////////////////////////////////////////////////////////////
Код обращения к компоненту может быть таким:
*/////////////////////////////////////////////////////// #DEFINE THISPROGID "smpsrv.SrvCls" LOCAL loT1, loT2 loT1 = CreateObject(THISPROGID) IF Vartype(loT1) # 'O' RETURN .F. ENDIF loT2 = CreateObject(THISPROGID) IF Vartype(loT2) # 'O' RETURN .F. ENDIF ACTIVATE SCREEN CLEAR ?loT1.GetID() ?loT1.GetID() ?loT2.GetID() ?loT2.GetID() RELEASE loT2 RELEASE loT1 *///////////////////////////////////////////////////////
вот такой файл c:\Temp\Vfpcom.log получился у меня в результате работы:
03/06/06 06:31:44 PM ThreadID: 3788 Init() fired! ObjectContext: No 03/06/06 06:31:44 PM ThreadID: 3788 GetObjectCont fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 1572 Init() fired! ObjectContext: No 03/06/06 06:31:44 PM ThreadID: 1572 GetObjectCont fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 3788 GetID fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 GetID:...PPUC fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 SetComplete() fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 Destroy() fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 Init() fired! ObjectContext: No 03/06/06 06:31:44 PM ThreadID: 3788 GetObjectCont fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 GetID fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 GetID:...PPV8 fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 SetComplete() fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 3788 Destroy() fired! ObjectContext: Yes {5EA82F8C-58D3-4244-945E-8CC191F7C6D5} 03/06/06 06:31:44 PM ThreadID: 1572 GetID fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 GetID:...PPUS fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 SetComplete() fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 Destroy() fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 Init() fired! ObjectContext: No 03/06/06 06:31:44 PM ThreadID: 1572 GetObjectCont fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 GetID fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 GetID:...PPV9 fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 SetComplete() fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA} 03/06/06 06:31:44 PM ThreadID: 1572 Destroy() fired! ObjectContext: Yes {4AA29188-099A-4F83-8000-F28CC473E0BA}
Здесь можно видеть, что:
Можно провести эксперименты над VFP-COM-компонентами двух типов: multi-threaded COM и single-threaded COM server (dll) (см. режим создания компоненты в диалоге Build... VFP-проекта [VFP 6.0 SP3VS6 и выше]). Создав компоненту одного из указанных типов, зарегистрируйте её на MTS/Component Services. Если машина клиента не является одновременно и сервером, осуществите экспорт созданного Вами пакета/приложения на машину клиента. (Относительно подробностей регистрации/экспорта см. статьи "Installing and Managing COM+ Applications: Part 1-2" by Scott Mauvais, "Understanding COM+ with VFP, Part 1-3" by Craig Berntson в MSDN, а также ссылку http://http://www.craigberntson.com/Articles/kb011.htm (eng) и/или статью "Microsoft Transaction Server for Visual FoxPro Developers" by Randy Brown в MSDN. При регистрации компоненты на удалённом сервере обратите внимание, что требуется явно указать сетевой пароль (возможно групповой) пользователя на закладке Identity/Account This user ... для пакета/приложения. Другой вариант: завести на сервере локального пользователя с административными правами и ввести его в качестве 'This user' (если сервер совмещён с клиентом, что бывает во время создания/отладки компонент, последний вариант самый предпочтительный) ... во всяком случае в противном варианте мне не удалось получить доступ к компоненте с удалённого клиента).
Попробуйте обратиться к серверной компоненте, используя приведённое выше клиентское приложение. При этом создайте несколько экземпляров этого приложения и поэкспериментируйте с командами: Create object, Release object, Get objectID при выполняющейся команде Start longRun для другого экземпляра приложения, и наблюдая (возможно через Terminal Server Client [MSTSC.EXE]) за "активностью" компоненты на сервере. Попробуйте также "неожиданно прекратить работу" клиентской машины во время работы нескольких запущенных клиентов, выполняющих команду Start longRun на ней (например через кнопку выключения питания :-). Что будет происходить с компонентой на сервере? Будет ли компонента способна обрабатывать клиентов сразу после подобного действия? :-)
Было обнаружено, что в VFP 6.0/7.0 для multi-theaded dll имеется проблема с CODEPAGE, в том смысле что:
Ниже приведён код простого класса, чтобы убедиться в этом.
DEFINE CLASS MyClsSes As Session OLEPUBLIC FUNCTION GetCpCurrent RETURN CPCURRENT() ENDFUNC ENDDEFINE
Если поместив этот класс в проект (например cpcomtest.pjx), то после создания cpcomtest.dll как multi-theaded dll и регистрации её в пакете/приложении под MS MTS/Component Services, из VFP окна Command для VFP 6.0 имеем:
ob=CreateObject("cpcomtest.MyClsSes") ?ob.GetCpCurrent() 1251 ob1=CreateObject("cpcomtest.MyClsSes") ?ob1.GetCpCurrent() 1252
Обратите внимание, что:
Решение нашёл Serg Suprun. Вот его эмоциональное сообщение из fido7.visual.foxpro.ru news :-)
Урррраааааа!!!!!!!
Уел я таки этот мелкософт, да икнется Биллу спросонья! :-)))
Дефолтная страница лежит в vfp6t.dll по адресу 000F2E93. Просто меняем по
этому адресу E4 на
E3 и получаем 1251 навеки! Это конечно не то решение,
которое хотелось бы получить, но проблема этим решается.
>Конечьно :-) ... речь идёт о запушенном процессе самого VFP...
>... Что касается dll, то как я понял, она плевать хотела и на CONFIG.FPW
>вставленный в проект (хотя вроде как должна :-(). В VB-пример (из письмя)
>независимо от подсунутого в проект CONFIG.FPW тупо и всегда, т.е. независимо
>от числа созданных экземпляров, -> выдаёт 1251 :-)
В доке к третьему сервиспаку это прямо написано. Я только не понял вот
чего - там же указано: если вы хотите иметь сервера с разными codepage -
разместите dll-ки в разных каталогах и положите в каждый из них свой
vfp6t.dll. Дословно:
Users who need to have servers, each with a unique codepage, can place these
servers, along with a copy of the Vfp6t.dll run-time library in a unique
folder. A server will always default to using a run-time library stored in
its folder.
С бодуна они это писали, что ли? :)))
By, Serg.
Вот адреса, любезно предоставленные by Serg Suprun в fido7.visual.foxpro.ru news и я, как и обещал, дублирую эту информацию здесь :-)
Файл | Адрес | Что на что менять |
vfp7t.dll(9262) | 000D38E8 | E4->E3 |
vfp7t.dll(9282) | 000D4DD1 | E4->E3 |
vfp7t.dll(sp1) | 000D5B42 | E4->E3 |
vfp8t.dll | 00003CD8 | E4->E3 |
vfp8t.dll(sp1) | 00003CE3 | E4->E3 |
vfp9t.dll(beta) | 00112ED7 | E4->E3 |
Для vfp9t.dll версии 9.0.0.2412 мной никаких проблем не обнаружено... и хотелось бы надеяться, что приведённый выше список закрыт!
На вопрос: а как побороть, если не делать исправлений в vfpXt.dll? Ответ может быть таким: отменить неявные преобразования на сервере, используя установку SET NOCPTRANS TO &lcListNamesAllLields, а у временных курсоров использовать утилиту .../Tools/Cpzero/Cpzero.prg для явной переустановки кодовой страницы перед использованием.
См. также адреса для исправлений аналогичной проблемы в vfpoledb.dll в статье Как в VFP 7.0 и выше обратиться к Хранимой Процедуре VFP-базы данных через OLE DB.
Может оказаться так, что шаманство над VFP6T.DLL/VFP7T.DLL не приведут к желаемому результату. Т.е. возвращаемое значение приведённым выше методом GetCpCurrent(), всегда будет 1252, несмотря на все Ваши усилия. Подозрение таково, что на Вашем сервере (на котором Вы установили свои VFP COM+ компоненты), не была установлена поддержка Русского языка во время установки самой OS. Для проверки этого факта (как я сейчас думаю :-) достаточно взглянуть на содержимое следующих ключей в системном реестре сервера (используя regedit например):
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage\ ACP = 1251 MACCP = 10007 OEMCP = 866 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Language\ Default = 0419
Выше это так, как должно быть, чтобы вопросов не было! :-)
(Обратите внимание не какой-нибудь
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet00<N>\...
а именно так как написано мной выше :-)
Ниже значения ключей, если установка OS была произведена без поддержки Русского языка:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage\ ACP = 1252 OEMCP = 437 MACCP = 10000 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Language\ Default = 0409
В последнем случае все Ваши усилия заставить систему возвращать CPCURRENT() = 1251 бесполезны ... и следует обратиться к системному администратору, отвечающего за работу этого сервера (только не пробуйте при этом намекнуть что Вы о нём думаете :-)
Предупреждение: Не пытайтесь изменить эти значения "ручками"...
См. также темы в MSDN: