企业服务优化原则中有下面一条原则,最近我优化一个企业服务时,对此有了特别深刻的体验,特整理本篇博客:
使用 COM 可封送的参数
如果企业服务组件的方法接受调用方传递数据时所用的参数,强烈建议您尽量使用 COM 和 .NET 之间易于封送的类型,例如:
? Boolean
? Byte、SByte
? Char
? DateTime
? Decimal
? Single、Double
? Guid
? Int16、UInt16、Int32、UInt32、Int64、UInt64
? IntPtr、UIntPtr
? String
如果仅使用这些类型,且要避免传递其他复杂的类型(例如 structures 或 arrays),.NET 序列化程序可以优化调用处理堆栈,并将调用直接序列化到线缆上(对于 RPC)或到虚拟线缆上(对于 LRPC)。这样,调用的执行速度更快。不过,如果您的方法要求复杂的类型,代码将使用通常的 DCOM 调用堆栈调用,这将产生额外的处理。
以上文字来自: .NET 企业服务性能
我最近优化的这个企业服务是CSDN新积分制论坛帖子列表企业服务缓存。
这个帖子列表缓存工作机制如下:
1、启动一个 ActivationOption.Server 的企业服务(ServicedComponent),这个企业服务是工作在一个独立的服务程序进程。
2、这个企业服务中,通过单件模式启用了一个缓存,缓存了CSDN每个大小论坛的帖子列表。
3、当WEB应用启动时候,把每个论坛,每种帖子列表的帖子都从数据库中获得,然后记录到缓存中。方便以后使用。
4、在优化前,这个版本的新积分制论坛并不是所有CSDN的论坛,有100左右的大小论坛,每个大小论坛又有6种帖子列表,每个帖子列表最多显示满足条件的500条记录。这样,初始化数据大致就需要 100*6*500 大致20万左右的数据需要初始化到帖子列表企业服务缓存。
下面是我做测试的三种情况:
1、优化前我代码逻辑:每一个论坛做一次初始化操作,也就是100个论坛调用企业服务的一个方法100次。
这样的代码逻辑,本地测试环境,完成初始化大致需要130秒左右。
2、我把这个初始化的代码,即从数据库获得并写到缓存的代码搬到企业服务内部执行,外部只需要传递一个简单的论坛编号即可。这样优化后,这个初始化过程大致需要40秒。
3、另外一种方法,我把100个论坛所有帖子列表的数据库都在企业服务外部计算好,然后一次性提交给企业服务,即企业服务这个初始化方法只调用一次,完成这个初始化大致需要190秒左右。
对比我做的上面三个测试情况,我们可以看到,业务逻辑没有发生任何变化,变化的只是企业服务接口参数发生变化了,把一些工作从企业服务外搬到了企业服务内部执行。三种情况对企业服务来说,差别就在于企业服务的参数发生变化了。
上面情况2的企业服务参数最简单,用的是Guid 类型的参数。
情况1和情况3用的是自定义的可序列化的类。
情况1企业服务调用了100次,情况3企业服务只调用了1次。
结论:
1、企业服务的参数要尽量使用 COM 和 .NET 之间易于封送的类型,而不是自己定义的实体和传递其他复杂的类型。
2、企业服务使用自己封装的实体或者复杂的类型,带来的性能损耗,比多次调用企业带来的损耗更厉害(对比情况1和情况3)
参考资料:
.NET 企业服务性能
我们要在这篇文章中将实现的功能:
如上图荧光笔画的地方所示:
我们需要通过编写服务程序的安装程序,把企业服务上面 Run application as NT Service 选择框的进行选中操作。
阅读本文基础:
如果你对企业服务不是很了解,可以阅读我之前写的一系列跟企业服务有关的博客。我书写的跟企业服务有关的系列文章可以访问以下链接。
http://blog.joycode.com/ghj/category/1320.aspx
如果你对编程控制企业服务根本不了解,需要首先阅读我之前写的这篇博客:
编程控制企业服务的行为
本文是在上述文章基础上的加深。
正文
编程控制企业服务的行为 这篇博客中我讲到,我们可以通过遍历 COMAdminCatalogCollection 来寻找到我们需要操作企业服务的某个属性,然后更新这个属性。这种做法适用于大多数编程操作企业服务属性。但是我们上面这个需求就无法用这种方式来作了。
我们遍历 COMAdminCatalogCollection 可以更新的属性时候,我们在微软提供的可操作属性列表(如下链接可以看到)中并没有找到可以设置 Run application as NT Service 的选项。
http://msdn2.microsoft.com/en-us/library/ms686107.aspx
只能看到一个可能跟这个需求有关的属性:
ServiceName
The service name corresponding to the application configured to run as an NT service. If this value is NULL, the application is not configured to run as an NT service. Otherwise, the configuration information for the service can be found by using the service name.
Access: ReadOnly
Type: String
Default: ""
Platform Requirements: Windows XP, Windows Server 2003
这个属性是只读的,我们又没法设置。
如何解决这个问题呢?
很简单,这个需求不是通过属性来指定的,而是通过 COMAdminCatalog 类的 CreateServiceForApplication 方法。
简单来说,我们就是要安装代码中有以下代码:
COMAdmin.COMAdminCatalog ca = new COMAdmin.COMAdminCatalogClass();
ca.CreateServiceForApplication(ApplicationName, NTServerName, "SERVICE_DEMAND_START", "SERVICE_ERROR_NORMAL", "", null, null, false);
CreateServiceForApplication 函数的定义如下:
HRESULT CreateServiceForApplication(
BSTR bstrApplicationIDOrName,
BSTR bstrServiceName,
BSTR bstrStartType,
BSTR bstrErrorControl,
BSTR bstrDependencies,
BSTR bstrRunAs,
BSTR bstrPassword,
VARIANT_BOOL bDesktopOk);
每个参数介绍如下:
bstrApplicationIDOrName
应用ID或者应用名字,我们安装这个企业服务后,在服务程序列表中,这个值就是我们看到的 NT Service 的Name 就是这个值。
bstrServiceName
服务名字,我们安装这个企业服务后,在服务程序列表中,这个值就是我们看到的 NT Service 的 Description 就是这个值。
bstrStartType
服务开始的几种情况,这里可以是下面几个值
SERVICE_BOOT_START, SERVICE_SYSTEM_START, SERVICE_AUTO_START, SERVICE_DEMAND_START, and SERVICE_DISABLED.
bstrErrorControl
服务错误发生时的情况,可以是以下几个值
SERVICE_ERROR_IGNORE, SERVICE_ERROR_NORMAL, SERVICE_ERROR_SEVERE, and SERVICE_ERROR_CRITICAL.
其他几个参数一般都比较固定,我直接Copy MSDN的说明。
- bstrDependencies
- [in] A list of dependencies for the service. There are two possible formats for the string: a standard null-delimited, double-null-terminated string (exactly as documented for CreateService); or a script-friendly list of service names separated by "\" (an invalid character to have in a service name). The rpcss service is implicit in this parameter and does not need to be specified.
- bstrRunAs
- [in] The user name to run this service as. This may be NULL to indicate that it should run as Local Service.
- bstrPassword
- [in] The password for the system user account. This must be NULL if the service is configured to run as Local Service.
- bDesktopOk
- [in] Indicates whether or not the service should be allowed to interact with the desktop. This parameter is valid only when the service is marked as Local Service and must be FALSE otherwise.
参考资料:
Register your Enterprise Service App as NT Service
我们如果需要编程控制企业服务的行为,则可以利用 COMAdminCatalog 来进行操作。
使用 COMAdminCatalog 之前,需要增加一个 Com 引用 COM +1.0 Admin Type Library ,如下图:
下面就简单的实现一个功能,遍历每个COM+ 应用,并显示出他们的一些信息。
同时,如果找到你指定的 COM+ 应用,则设置它一直运行(Leave running when idle)。
using COMAdmin;
COMAdminCatalog ca = new COMAdminCatalogClass();
COMAdminCatalogCollection cacc =
(COMAdminCatalogCollection)ca.GetCollection("Applications");
cacc.Populate();
foreach (COMAdminCatalogObject cac in cacc)
{
Console.WriteLine("**************");
Console.WriteLine(cac.Name);
Console.WriteLine(cac.Key);
Console.WriteLine(cac.Valid);
string[] nameArr = new string[] {
"Identity",
"ID",
"Description",
"Activation",
"Changeable",
"Deleteable",
"Name",
"ApplicationAccessChecksEnabled",
"AccessChecksLevel",
"RunForever",
"ApplicationAccessChecksEnabled",
};
foreach (string name in nameArr)
{
bool b2 = cac.IsPropertyWriteOnly(name);
bool b1 = cac.IsPropertyReadOnly(name);
object o = string.Empty;
if (!b2) o = cac.get_Value(name);
Console.WriteLine(string.Format("{0} ** ReadOnly {1} WriteOnly {2} = {3}{4}",
name, b1, b2, o, Environment.NewLine));
}
if (cac.Name.ToString() == "您自己的企业服务的名字")
{
cac.set_Value("RunForever", true);
cacc.SaveChanges();
}
}
上述代码的设置效果,等同于你通过以下步骤的设置:
在 Component Services 的 Com+ 应用列表中 选择你需要处理的企业服务。右键属性窗口中,选择 Advanced 属性页。
然后在 Server Process Shutdown 中,单选 Leave running when idle。
当然,上面只是一个简简单单的一项设置的变化。其实你可以有很多属性都可以按照上述代码进行变更。
每个字符串对应企业服务的那个属性,你可以在以下地址查询到:
http://msdn2.microsoft.com/en-gb/library/ms686107.aspx
http://msdn2.microsoft.com/en-gb/library/ms687763.aspx
参考资料:
http://msdn.microsoft.com/library/default.asp?url=/msdnmag/issues/0900/instincts/toc.asp
Automating COM+ Administration
http://blog.robinzhong.com/index.php/archives/2006/09/28/124.html
在.NET中使用COMAdmin组件管理COM+组件
http://www.csharphelp.com/archives3/archive482.html
COM+ Automation using .NET C#
http://www.codeproject.com/com/complus_admin.asp
Programmatic administration of COM+ Applications with the COM+ Admin objects in VC++
企业服务组件类的接口被定义在另外一个项目中,变更接口时候,导致错误:Unable to cast COM object of type 'System.__ComObject'
to interface type 'I***' 的解决 方案
最近在完善兴趣小组的开发,企业服务这里,由于遵循谋个接口,而这个接口,会按照 需求,时不时发生变化。
但是一旦发生修改。经常会报类似如下的错误信息:
这个修改后,在 Component Services 中删除这个企业服务,在GAC中删除这个企业服 务组件以及这个组件所引用的组件,之后,重起电脑,你会发现,下述错误仍然存在。
An exception of type 'System.InvalidCastException' occurred in mscorlib.dll but
was not handled in user code Additional information: Unable to cast COM object of
type 'System.__ComObject' to interface type 'ICommunityUserDataManager'. This operation
failed because the QueryInterface call on the COM component for the interface with
IID '{F70232A8-7959-3AE5-8643-3E0C0869D507}' failed due to the following error:
Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY)).
解决方法:
你定义的接口附带以下几个Attribute。
[ComVisible(true)]
[Guid("470418D8-E53C-4890-BD88-F6AC660C3162")]
public interface ICommunityUserDataManager
{
}
如果你把某个接口修改后,顺便把这个接口的Guid的Attribute进行修改,就不会出现 上述错误了。
另外,注意,不要把接口所在项目的ComVisible属性打开
即,我们应该是:[assembly: ComVisible(false)]
更多信息请参考我之前整理的一篇博客:
企业服务组件类的接口被定义在另外一个项目中可能会出现的一种错误
http://blog.joycode.com/ghj/archive/2006/10/24/85602.aspx
?
企业服务组件类的接口被定义在另外一个项目中可能会出现的一种错误
最近在作的一个项目中,一个企业服务组件要遵循一个接口,类似如下:
public class EnterpriseUserDataManager :
ServicedComponent,ICommunityUserDataManager
我对 ICommunityUserDataManager 接口的定义在另外一个项目中。 这个接口有好几个函数,如下:
public interface ICommunityUserDataManager
{
CommunityUser GetCommunityUser(string userName);
void InsertCommunityUser(CommunityUser user);
void UpdateCommunityUserBaseInfo(string userName,string
nickName,string description,string communityEmail);
void RemoveCommunityUser(string userName);
}
最近出现了一个怪异问题:
使用这个企业服务时候,调用 UpdateCommunityUserBaseInfo 方法的时候,会报错误,而调用其他几个方法的时 候不报错,报错的错误信息如下:
Exception Details:
System.InvalidCastException:
Unable to cast COM object of type 'System.__ComObject' to interface type 'ICommunityUserDataManager'.
This operation failed because the QueryInterface call on the COM component for the
interface with IID '{F70232A8-7959-3AE5-8643-3E0C0869D507}' failed due to the following
error:
No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).
为了这个错误,昨天下午一直到现在,都在找这个问题。
由于其他几个调用没有问题,而就只有UpdateCommunityUserBaseInfo调用出上述错误, 我整个走弯路了。
今天在请求装配脑袋帮助后,发现错误竟然是因为接口定义在其他项目中,这个接口没 有被定义成 [ComVisible(true)] 所至。
这个问题的解决方法很简单:
1、你这个企业服务干脆就别依赖于接口了;
2、把这个接口定义成 ComVisible
代码如下:
[ComVisible(true)]
public interface ICommunityUserDataManager
{
}
不过这个问题怪异的情况在于。
以前我没有把这个接口定义成ComVisible的时候,其他几个功能已经跑了很久,也没有 报错,现在新增了一个功能 UpdateCommunityUserBaseInfo
函数,就报了一系列的上述 错误,真是怪异。
我写的一个项目,其中用到COM+(企业服务),今天早上的时候,突然发现项目编译不过去了。报错误: “Error code 80040154 - Class not
registered”
细查下去,发现Com+的部署出了问题。
当我打开“Component Services”想看个究竟的时候,发现无法打开了,报错误如下图:
“An error occurred while processing the last operation. Error code 80040154 - Class
not registered The event log may contain additional troubleshooting information”
当时我就头大了,难道我得重装电脑??
后来在网上查询了不少相关资料,通过“修复损坏的COM+目录”把 Com+ 目录恢复成了Window 安装后的默认情况。
这个修复步骤如下:
要修复损坏的COM+目录,需要按照下列的步骤进行操作:
1.查找到“\%WinDir%\System32\Clbcatq.dll”文件,并且把它重命名为“~Clbcatq.dll”(要注意那个符号“~”)。“%Windir%”是个环境变量,这个变量通常可以转译成“C:\Windows”。
2.以安全模式重新启动计算机(开机时候一直按F8 就可以进入选择开始界面)。
3.打开注册表,并删除下面的键值:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\COM3
4.在“\%WinDir%”目录中查找名为“Registration”子目录。把这个目录完全删除,包括其中所包含的一切文件。
5.以正常方式重新启动计算机。
6.在命令提示行模式下,键入“regsvr32 \%windir%\system32\ole32.dll”,然后在出现的接受提示上点击“确认”。
7.打开“控制面板”,选择“添加或删除程序”,然后再选择“添加/删除Windows组件”功能。
8.点击“下一步”来重新安装COM+。事实上,你并不需要选择添加或删除任何组件;COM+照例会自动进行重新安装。
在这一步操作之后你不需要重新启动计算机;重新注册的COM+服务将会立即生效。
参考资料:
修复损坏的COM+目录
http://searchwinsystem.techtarget.com.cn/tips/141/2163641.shtml
补充:
第一次出现这个错误的时候,不重起电脑,报的错误是:
进 Commponent Services 找到这个对应的 Com + ,点击让他启动,报错误
---------------------------
Catalog Error
---------------------------
An error occurred while processing the last operation. Error code 80080005 - Server
execution failed The event log may contain additional troubleshooting information.
---------------------------
OK
---------------------------
看事件日志如下:
Event Type: Error
Event Source: DCOM
Event Category: None
Event ID: 10010
Date: 2006-7-25
Time: 16:49:02
User: N/A
Computer: GHJ1976
Description:
The server {28C236AD-71CE-4492-BDDF-223284738FB2} did not register with DCOM within
the required timeout.
For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.
重新开机后,报的错误才是本文最初提到的错误:
Class not registered
今天在写一个企业服务组件,是在一个ClassLibrary项目基础上修改的.这个企业服务中包含很复杂的业务逻辑,其中用到了Singleton 模式来确保只有一个缓存。相关技术参看:http://sajadi.co.uk/dflat/archives/2005/11/creating_a_cach.html
编译时候没有任何问题,然后就是部署,结果部署的时候报下面错误:
出现下列安装错误:
1: 在程序集中找到无效的 ServicedComponent 派生类。
(类必须是公共的、具体的、有公共的默认构造函数并满足所有其他 ComVisibility 要求)
这个错误信息让我对是否可以在企业服务中使用单键模式产生怀疑。网上也有不少对企业服务使用单键模式的讨论。观点各不相同,结果一下午就迷失在这里了。
http://www.pcreview.co.uk/forums/thread-1382837.php
http://www.pcreview.co.uk/forums/thread-1382837-2.php
一直到快下班,才发现,根本不是上述使用单键模式的问题,而是ClassLibrary项目的AssemblyInfo.cs文件,默认设置是 [assembly: ComVisible(false)]
如果你要做EnterpriseServices,则需要把这个修改为true ,汗颜,最基本的地方给忘掉了。
在AssemblyInfo.cs文件ComVisible上有明确的提示,自己竟然没留意,提示如下:
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.? If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(true)]
使用自定义的类,作为函数的参数,可以解决传递参数过多的问题。在Com+ 中使用自定义的类来传递参数,需要注意以下几点:
编码需要注意的:
你的自定义的类必须是可以序列化的,当然也可以继承自:? ServicedComponent 类。
因为 从 servicedcomponent 继承 就直接有序列化标志了。
比如下面的代码就是正确的:
??? using System;
??? namespace ComPlusLib
??? {
??????? public class WriteFileTest:System.EnterpriseServices.ServicedComponent
??????? {
??????????? public string SayHello(String s)
??????????? {
??????????????? System.IO.FileStream fs=new System.IO.FileStream("c:\\t.txt",System.IO.FileMode.Append,System.IO.FileAccess.Write);
??????????????? System.IO.StreamWriter sw=new System.IO.StreamWriter(fs);
??????????????? sw.WriteLine(s);
??????????????? sw.Close();
??????????????? fs.Close();
??????????????? return System.Diagnostics.Process.GetCurrentProcess().ProcessName + s ;
??????????? }
??????????? public string TestObject(Fac1 f)
??????????? {
??????????????? return f.FullName;
??????????? }
??????? }
???????
??????? public class Fac1:System.EnterpriseServices.ServicedComponent
??????? {
??????????? public string ShowMyProcess()
??????????? {
??????????????? return System.Diagnostics.Process.GetCurrentProcess().ProcessName;
??????????? }
??????????? public string FullName;
??????? }
??? }
部署需要注意的:
在部署这个Com+ 的时候,注意,一定要把编译后的Dll也部署到GAC中。
如果你没把这个Dll 部署到GAC,你会获得以下错误信息:
Insufficient state to deserialize the object. More information is needed.
满足以上两个条件,你就可以在Com+ 中使用自定义的类作为函数的参数了。
在我的一个程序中,就是没有注意第一点,编写的一个Com+ 没有错误。但是其实他的执行不是走得Com+。 而是直接调用GAC中的函数了。一直到作分布式部署的时候,才发现程序的问题。
分布式应用如果用 Com+ 来开发的话,一个好处就是,你不用修改任何代码,只需要修改Com+的部署,既可以实现分布式。
下面是就是部署分布式Com+需要注意的一些东西和步骤:
部署要求:有A、B、C 三台机子。A机子是实际 Com+ 应用运行的机子。B、C机子调用A机子的Com+应用。
首先:在A机子部署Com+应用。如果这个应用是基于.net 开发的,只需要简单的执行下面两步:(TopicManage.dll 为假设要部署的Com+应用)
gacutil -i TopicManage.dll? (部署到GAC,有些简单的Com+不需要部署到GAC中,后面一个Blog会具体谈到这个问题)
RegSvcs TopicManage.dll
在确保本地部署Com+成功后。
要进入 Component Services ,去修改已经部署的 Com+ 应用的默认设置。
在 Com+ 应用的属性中,首先需要在它的 Activation 属性中修改 Activation
type 为 Server application
(Run application as NT Service 的选项无所谓选不选,具体自己决定)
如下图:
?
如果只修改这一项,并保存,这时候启动这个 Com+? Application 会报下面错误:
---------------------------
Catalog Error
---------------------------
You do not have permission to perform the requested action.? If
security is enabled on the System Application of the target computer?
make sure you are included in the appropriate roles.
---------------------------
OK??
---------------------------
你需要修改这个 Com+ Application 的安全设置,
在 Security 属性的 Authorization 中,确认 Enforce access checks for this application 这个选项没有被打勾。然后保存设置。启动这个Com+ 应用。
这样A 机器的设置就完成了。
如下图:
?
下面我们需要到 B、C 机子安装 对 A 机器Com+ 的引用。 Com+ 应用可以方便的导出为一个安装文件。然后在其他机子上执行这个安装文件,实现其他机子的部署。
选择 A 机器的Com+? 应用,执行导出命令,在导出设置的时候,注意选择
Application proxy - Install on other machines to enable access to this machine 这个选项,如下图。
?
完成导出后,你可以获得一个 msi 文件,在 B、C 机子上执行这个msi文件,就可以安装完成。
最后一个问题,就是访问权限的问题,把B、C机子上调用Com+ 应用程序的帐号,对A机的Com+ 调用有权限既可。
剩下的就是在 B、C 机子上起一个测试应用,看应用是不是正常而已。
Com+ 类的部分代码:
namespace TransactUserManage
{
?[Transaction(TransactionOption.Required)]
?public class?TransactUserClass : ServicedComponent
?{
??public TransactUserClass()
??{
??}
??private string? strDeployName = null;
??public string DeployName
??{
???get
???{
????return strDeployName;
???}
???set
???{
????strDeployName = value;
???}
??}
??private Hashtable objDataBaseConnStrArr = null ;
??public Hashtable DataBaseConnStrArr
??{
???get
???{
????return objDataBaseConnStrArr;
???}
???set
???{
????objDataBaseConnStrArr = value;
???}
??}
??private string strCurrUserDataBaseConn = null;
??public string CurrUserDataBaseConn
??{
???get
???{
????return strCurrUserDataBaseConn;
???}
???set
???{
????strCurrUserDataBaseConn = value;
???}
??}
....? 其他代码。
??public bool ModifyPenName(int iUserID,string strPenName)
??{
???if (!ContextUtil.IsInTransaction)
????throw new Exception("ActivationUser 不支持事务处理!!");
???string WhichSolution = strDeployName;
????string strUserConn = strCurrUserDataBaseConn;
????string strCSDNExpertConn = objDataBaseConnStrArr["CSDN_BBS_ConnStr"].ToString();
????string strCSDNLtConn = objDataBaseConnStrArr["CSDN_Main_ConnStr"].ToString();
.... 其他代码
}
}
}
}
调用部分
????TransactUserManage.TransactUserClass obj = new TransactUserClass();
????obj.DeployName = ConfigInfoClass.DeployName;
????obj.CurrUserDataBaseConn = ConfigInfoClass.CurrUserDataBaseConn;
????obj.DataBaseConnStrArr = ConfigInfoClass.DataBaseConnStrCache;
????bool result = obj.ModifyPenName(iUserID,strPenName);
这里我确认? ConfigInfoClass.DeployName?? ConfigInfoClass.CurrUserDataBaseConn?? ConfigInfoClass.DataBaseConnStrCache 都是正确的值。
但是进入到???
string strUserConn = strCurrUserDataBaseConn;
这里后,
单步调试 strCurrUserDataBaseConn 的值竟然是 null ,
我确定 obj.CurrUserDataBaseConn = ConfigInfoClass.CurrUserDataBaseConn; 这里的 ConfigInfoClass.CurrUserDataBaseConn 是有值的呀。
?