ASP 组件指南

网络整理 - 09-12
作者:J.D. Meier
Microsoft Corporation

2000 年 1 月 24 日

如果您符合以下几种情况,这篇文章正适合您:

从 Active Server Pages (ASP) 代码调用组件
设计将从 ASP 代码调用的组件
希望利用 ASP 代码中的组件
目录
简介
为什么使用组件?
状态管理
范围
分割服务
线程模型
安全性
Server.CreateObject 与 CreateObject
传递参数
事件
OnStartPage/OnEndPage 与 ObjectContext
错误处理
全局变量
分布组件
结论

简介
组件。有人喜欢它们,有人则害怕。害怕组件的人通常都能给您讲一个骇人的经历。让我们面对它:当开始在 ASP 下使用组件时,并不知道什么能伤害您。如果您摔倒了,那么站起来,自己拍干净,然后接着来。在这篇文章中,我将提供从实践中获得的一般指南,帮助您建立更好的基于组件的 ASP 解决方案。

为什么使用组件?
在我开始讨论组件指南之前,值得考虑将组件添加到 ASP 应用程序的价值。许多对组件不熟悉的开发人员总觉得一切都那么新鲜。组件可以为 ASP 应用程序带来以下种种益处:

封装功能和隐藏实现细节
可重用性(包括被不同客户机应用程序重复使用)
知识产权保护
可伸缩性(体现在允许将应用程序分布到多台计算机上)
配置和部署灵活性
性能(尤其在早期绑定是重要因素时)
访问系统,例如 Win32 API 调用或编程语言的任何其他底层功能
键入功能较强(“Visual Basic® 脚本编辑器 [VBscript]”的键入功能较弱,而且 Jscript® 也不太好)
业务逻辑与用户界面分离,或者 Web 设计人员与 Web 开发人员分离
利益与付出同在。就增加开发过程的复杂性而论,创建组件解决方案可能更加昂贵。部署和疑难解答也可能变得更困难并成为现实因素。但是,不要让眼前的困难阻碍了长期的利益。如何知道付出的成本是否值得呢?请考虑下列方面:

现有的代码库是如何?
开发队伍的水平和经验怎样?
您对主服务器的控制范围如何?
为特定任务选择了什么样的工具和语言?
存在什么协同问题?
存在性能和可伸缩性的因素吗?
项目时间框架是什么?
谁会继续维护和支持该应用程序?例如,开发小组能介入和接管吗?
审查以上考虑的问题后,请考察您的假设。原型能迅速从设想中得出实际情况。

现在,对组件可能带来的益处有了一定的理解,让我们继续讨论。下面的指南将帮助您获得最大的益处。这些指南可能成为指引您顺利建立更稳定的、可升级的、性能更优的 ASP 组件应用程序的向导。

状态管理
建议
一般来说,在可能的场合尽量使用无状态的组件和无状态的 ASP 页面。组件不应需要状态从一个方法调用到下一个状态。将复杂的状态存储在数据库中。对于简单的数据、沿用 cookies、QueryString 或在页面之间传递数据的隐藏的表单字段。

为什么
服务器资源是有限的。维护组件中的状态意味着应用程序在资源冲突和并发问题时将消耗宝贵的资源。无状态组件将帮助您避免这些问题。无状态组件还提供更多的部署选项,并增强在多个客户机上共享资源的能力。

常见的陷阱
开发人员常犯的错误是设计或使用需要维护状态的组件。请注意防止这种常用于桌面开发的思想。通常,具有桌面开发背景的开发人员会设计出依赖状态的组件。

详细信息
避免使用“ASP 会话”将提高服务器的性能,因为它简化了代码路径并减少了服务器资源的消耗。如果不使用“ASP 会话”,请通过“Internet 服务管理器” (请参阅“Internet 信息服务 [IIS]”文档)禁用“会话”状态。也可以在不需要“会话”的 ASP 页面中使用下面的标记禁用基于页的“会话”:

<%@ENABLESESSIONSTATE=False %>
部署灵活性是另一个重要方面,尤其在 Web 区域中运行应用程序时。如果依赖“ASP 会话”, 则给定用户的请求绑定在指定的 Web 服务器上,因为“会话”状态是服务器专用的。在中间层和 Web 服务器中避免状态,并使用数据库,将使 ASP 请求可由区域中任何有效的 Web 服务器处理。因此, 您将减少竞争,提供更好的冗余,并允许更多的分布选项。

不使用“ASP 会话”而在页间传递数据的其他方法,请参阅下面的“知识库 (KB)”文章:

Q175167 HOWTO: Persisting Values Without Sessions(英文)  
Q157906 HOWTO: Maintain State Across Pages with VBscript(英文)  
Don Box 在 ActiveX® Q&A(英文) 一文中 还提出有关 MTS 状态管理的更多见解。

范围
建议
通常,请在页面范围内使用组件。页面范围的含义就是在同一页上创建对象、并使用它和释放它 — 所有这些均在同一页上操作。

标记为“双重”或“单元”的组件在页面范围内都能正常工作。仅在页面范围内使用“单元”模型组件,例如 VisualBasic 组件。如果需要在“应用程序”或“会话”中存储组件,则建议使用“双重”。可以在“会话”或“应用程序”范围内存储标记为“双重”的组件,但组件需要保证线程安全。

为什么
在页面范围内使用组件使服务器资源得以回收。释放资源将使并发问题减到最小程度,并允许可汇集的资源在客户机上共享。另外,页面范围组件避免了影响“会话” 或“应用程序”范围的对象的线程问题。在下面的“线程模型分类”中将详细讨论线程问题。

常见的陷阱
最常见的问题之一就是在“应用程序”范围内存储 Visual Basic 或其他“单元”模型对象。如果您尝试在“应用程序”范围内存储一个用 Server.CreateObject 创建的“单元”模型对象,可以看见下面的错误:

应用程序对象错误 'ASP 0197: 80004005'

不允许的对象使用

/VirDir/global.asa, line 7

不能将带有单元模型行为的对象添加到应用程序的内部对象。

但是,如果使用 <OBJECT> 标记在“应用程序”范围内存储“单元”模型对象,就不会出现运行错误。相反,对象将创建在指定的“单线程单元 (STA)”线程上,并且所有调用都汇集到那个线程 — 而且是连续地。原因是没有复选该组件的线程模型。很遗憾,在运行时出现了问题。

另一个常见问题是在“会话”范围存储“单元”模型对象,该“会话”范围将用户会话绑定到指定的线程。这个行为严重影响服务器的性能。由于所有调用将连续地汇集到创建该对象的线程,因此从根本上影响了线程缓冲池的目的。

详细信息
有关详细内容,请参阅下面的 KB 文章:

Q243543 INFO: Do Not Store STA Objects in Session or Application(英文)  
Q243548 INFO: Design Guidelines for VB Components Under ASP(英文)  
分割服务
建议
将表达、业务和数据服务分离。业务组件应该实施业务规则。业务组件不应包含数据访问技术。那是数据层组件的任务。业务组件不应包含对 ASP 对象的引用。

ASP 提供表达服务。引用 ASP 的对象应该呈现为 HTML。这些对象能够依次调用对 MTS/COM+ 注册的业务对象。

为什么
将应用程序分割为单独的和截然不同的服务,有以下好处:

更便于组件的重用
支持 Windows DNA model(英文)  
更好地孤立疑难问题
更灵活的部署选项(去掉服务的耦合允许在多台计算机上分布应用程序)
常见的陷阱
有一种我们称为“瑞士军刀”组件的常见问题。 该“瑞士军”组件将所有服务合成一体 (就像有螺丝锥、牙签等 17 种工具的小瑞士军刀)。 把不相关的服务组合到一个组件中, 使该组件很难使用、理解和维护。

容易掉入的另一个陷阱是从业务组件中引用 ASP。使 ASP 和业务逻辑耦合(通过使用 请求或响应对象,或在其内部构建 HTML),不仅限制不同的客户机重用您的组件,而且限制了横向的可伸缩性。引用 ASP 内置对象的对象应该与 Web 服务器在同一框围中。理想情况下,由于横向可伸缩性,业务组件可以分布在不同的框围中。可以直接在 ASP 脚本中提供表达服务,也可以建立呈现引用 ASP 内置对象的组件的 HTML,并将这些组件保持在 IIS 框围中。

详细信息
成功的设计模型可用作处理公共业务问题的模型。例如,处理“创建读更新删除(CRUD)” 操作的模型,可帮助您将应用程序分为几个截然不同的逻辑服务,即表达、业务规则和数据访问。

请参阅下文以获得更多的设计模型具体示例,可以在您自己的应用程序中模仿它:

Scalable Design Patterns(英文)
Simplify MTS Apps with a Design Pattern(英文)
FMStocks Application: Start Here(英文)
线程模型
建议
选择组件的范围还是选择组件的线程模型,哪种方法优先?两种方法都要考虑线程分支,除非决定在页面范围内使用“单元”或“双重”模型组件。(如果 Visual Basic 程序员不知道组件是哪种线程模型,则总是“单元”。)

如果需要在“应用程序”或“会话”中存储对象,则需要使用标记为“双重”的组件并聚集“自由线程编组程序 (FTM)”。

不要使用“单线程”组件并避免使用来自 ASP 的“自由线程”组件。

注意: 如果不小心, Visual Basic 可产生“单线程”组件。请确保在项目属性页的常规选项卡上将线程模型设置为单元线程。还要注意在相同选项卡上选定无人值守执行和保留在内存中选项。

为什么
如果您使用的是 Visual Basic,它是一种“傻瓜”开发环境。 Visual Basic 仅限于使用“单元”模型。假如 Visual Basic“单元”模型对象执行得非常良好,我不想对页面范围上的限制考虑太多。 Fitch 和 Mathers Stock 2000 破坏了对性能的任何预先想法。另外,由 ASP、SQL 和 Visual Basic 构建的许多现有网站,无时不刻都在证明页面范围的“单元”模型组件是可伸缩和执行的。

如果在标记为“双重”的组件上聚集 FTM,则可以不用任何编组或线程切换,便能在线程之间调用。如果标记为“双重”的组件没有聚集 FTM,ASP 将其视为“单元”线程对象 — 就像 Visual Basic 组件一样。请记住,如果计划利用“COM+ 对象池”,则不要聚集 FTM。 有关“对象池”的规则,请参阅“平台 SDK”文档。

“单线程”和“自由线程”组件运行在“系统”安全环境下。更糟的是,“单线程”组件会导致死锁。

常见的陷阱
也许最常见的陷阱就是使用了没有被设计为在 ASP 下运行的组件,如“单线程”组件。大多数开发人员陷入其中,是因为将桌面应用程序移向 ASP,或者使用了第三方的控件时。如果您不能确定组件的线程模型,可以检查组件的注册表项(但不能总依赖它)。

详细信息
有关线程模型及其对 ASP 的影响,请参阅下面的文章:

Don Box's Active Server Pages and COM Apartments(英文)  
Agility in Server Components
另外,下面的 KB 文章提供了有关线程问题的详细内容:

Q243543 Single-Threaded Apartment Objects in Session or Application(英文)  
Q243544 INFO: Component Threading Model Summary Under Active Server Page(英文)  
Q150777 INFO: Descriptions and Workings of OLE Threading Models(英文)  
安全性
建议
组件不应对它运行的用户环境做任何假设。不要访问用户专用信息,如 HKEY_CURRENT_USER,或桌面计算机的专用资源,因为这些对组件来讲是不可用的。应用程序也不要使用 SendKeys 或调用依赖用户界面的组件,执行通常需要桌面交互的操作,如打开对话框。

为什么
组件将运行在不同安全性的桌面上。首先,这表示应用程序不能打开对话框,并不能与其他 GUI 实用程序交互(例如,使用 SendKeys)。默认情况下,不允许 Inetinfo.exe 与桌面交互。不同的用户环境也会限制组件访问某些资源 — 主要是注册表的 HKEY_CURRENT_USER 部分。

常见的陷阱
常见的失误是引用 HKEY_CURRENT_USER 下的表项。例如,Visual Basic 的 GetSetting 和 SaveSetting 函数不能在 ASP 下使用,因为它们引用了 HKEY_CURRENT_USER 配置单元下的表项。下面的 KB 将讨论这个问题:

Q248348 PRB: SaveSetting and GetSetting Not Available in Visual Basic 6.0 Webclass (IIS Application)(英文)  
当从 ASP 而不是从桌面客户机调用组件时,打印机、MAPI 信息和网络共享通常“失效”。

有关详细内容,请参阅下面的 KB 文章:

Q184291 PRB: COM Objects Fail to Print When Called From ASP(英文)
Q217144 INFO: Difficulties Using Net APIs in ISAPI and ASP COM Objects(英文)  
Q207671 HOWTO: Accessing Network Files from IIS Applications(英文)  
详细信息
有关安全性的几点考虑:

启用哪种 IIS 身份验证方法?
您的 Web 应用程序是进程内的还是进程外的?
如果组件以 MTS 或 COM+ 注册,它是在“服务器”上还是在库软件包中?
您正在调用本地 DLL、远程 DLL、本地 EXE、远程 EXE 吗?
有关安全性的详细说明超出了本文的范围。但是,由于这个主题的复杂性,下面的文章对从 ASP 组件角度理解问题有很大帮助:

Securing a Web-based Microsoft Transaction Server Application(英文)
Q172925 INFO: Security Issues with Objects in ASP and ISAPI Extensions(英文)  
Q217202 PRB: CGI Applications and IIS OOP Applications May Fail(英文)  
下文很好地概述了 IIS 如何处理安全性:

Authentication and Security for Internet Developers(英文)
Server.CreateObject 与 CreateObject
建议
使用 Server.CreateObject。如果正在使用 MTS/COM+ 库软件包,请使用 Server.CreateObject 来避免线程阻塞。

为什么
CreateObject 相当于通过脚本引擎调用 CoCreateInstance。如果使用 CreateObject 而不是 Server.CreateObject,将发生下面情况:

ASP 不能识别该对象。
OnStartPage/OnEndPage 页面方法没有调用。
ASP 不知道对象的线程模型。
Server.CreateObject 相当于 GetObjectContext.CreateInstance。这表示 ASP 清楚该对象并知道它的线程模型。另外,如果 ASP 页面是事务性的,则通过调用 Server.CreateObject 可使组件与 ASP 页面在同一事务中。(请注意,事务性的页面可能意味着可避免的业务规则与表达层的耦合。)

常见的陷阱
如果对象处于防火墙后面,可能需要调用 CreateObject。请参阅 Q193230 PRB: Server.CreateObject Fails when Object is Behind Firewall(英文)  以获得详细信息。

详细信息
虽然在 IIS 4.0 下面 CreateObject比 Server.CreateObject 快,但在 IIS 5.0 下性能是相同的。同样,如果正在使用 MTS/COM+ 库软件包/应用程序, Server.CreateObject 可防止线程阻塞。

传递参数
建议
声明 Out 参数为 Variant。在 Visual Basic 术语中,这表示按引用 参数应该为 Variant。按值传递的参数(In 参数)不限于 Variant,但必须与 Variant 兼容。

为什么
脚本客户机使用 Variant。 COM 服务器可使用指定的数据类型。当您将指定的数据类型按值传递给 COM 服务器时, COM 服务器可以毫无问题地接收。但除 Variant 外,其他按引用参数无法“回送”给 ASP 脚本。

常见的陷阱
最常见的错误之一是“类型不匹配”。这通常是因为按引用 传递到 COM 对象的变量不是 Variant。通常的解决方法是按值传递参数或者将参数变为 Variant。

详细信息
如果要在多台计算机上分布组件或在进程外运行它们,可能看到按值 传递参数获得的显著性能。按引用传递将在进程或计算机间造成更多的编组开销,因为数据必须往复发送。如果实际上并不需要按引用传递参数时, 按值传递参数的正确性和有效性也是一个问题。注意,在默认情况下 Visual Basic 按引用传递参数。

下面的 KB 文章讨论将参数从 ASP 传递到 COM 对象:

Q197956 PRB: Passing Parameters By Reference to a VB COM Object(英文)  
Q197957 PRB: Passing Parameters By Reference to a VC COM Object(英文)  
事件
建议
避免调用等待其他组件返回事件的组件。

组件方法应尽快返回对 ASP 的执行。请考虑使用“MSMQ”或“COM+ 排队组件” 来提供异步调用 — 或当要做的工作正长时间运行并且不必联机运行时。

请异步地分派工作项目,而不要让 ASP 等待长时间运行的进程结束。然后您将从 ASP 给客户机返回一个响应。一旦工作项目完成,您可以用电子邮件或其他方法通知客户机(请参阅下面内容)。

为什么
ASP 并不是为处理事件设计的。为了优化服务器性能,请尽快返回对 HTTP 请求的响应。

常见的陷阱
循环检查服务器上的状态标识并不是一种提供浏览器通知的“服务器友好”方法。

详细信息
通常开发人员关注事件的原因是为了给浏览器提供关于在服务器上处理的工作的通知。虽然可以开发出精致的浏览器通知系统,如通过套接字在服务器上打开另一个端口,但许多开发人员通过下面的技术实现他们的需要:

使用电子邮件通知
在页面中添加 Meta-Refresh 标记以轮询服务器。
发送到浏览器的连接,并让客户机手动检查未决请求的状态。
下面的 KB 文章讨论这些问题:

Q243547 PRB: ASP Does Not Provide Progress Notifications to Client Browsers(英文)  
Q243546 PRB: ASP Does Not Support Events(英文)  
OnStartPage/OnEndPage 与 ObjectContext
建议
在 IIS 4.0 及更高版本中使用 ObjectContext 访问 ASP 内置对象(如响应、请求、服务器等等)。无论何时请尽量避免使用 scriptingContext 对象、 OnStartPage 和 OnEndPage。

为什么
OnStartPage、OnEndPage 和 scriptingContext 对象是用于遗留支持的。

常见的陷阱
如果插入 ASP 对象,ATL 向导将使用 OnStartPage 和 OnEndPage。

详细信息
用“双重”或“单元”模型组件获取 ObjectContext 而无须以 MTS/COM+ 进行注册。对于本地的 ActiveX EXE 不能使用 ObjectContext,所以需要使用 OnStartPage/OnEndPage。若要使用“自由线程”和“单线程”组件环境,需要以 MTS/COM+ 注册这些组件。否则需要使用 OnStartPage。

错误处理
建议
错误处理器将期待着意外情况。捕获应用程序每一部分中的错误,并尽可能完整地记录下来。好的日志对于跟踪、隔离和疑难解答有重大意义。这些日志可以实现为文本文件或写入 NT 事件日志。在多数情况下,边添加信息边“冒出”错误,是通知调用者已经出错的有效途径。冒出错误使调用者可自由地与处理具体问题的具体方法交互。

当记录错误时,提供尽可能多的有用信息至关重要。考虑包括以下几点:

当前用户环境(调用 Win32 API — GetUserName)
当前线程 ID(调用 Win32 API — GetCurrentThreadId 或 Visual Basic 中的 App.ThreadId)
当前时间(使用 Win32 GetTickCount,得到的是毫秒数据)
传递至方法的参数
错误源,包括方法名
为什么
根据我们的经验,好的错误处理和记录是隔离和诊断运行时问题的最有效途径。

常见的陷阱
还记得 ASP 0115 错误吗? 但愿您不用和它苦苦斗争了。 如果还在为其苦恼, 建议您参阅 Troubleshooting with the IIS Exception Monitor(英文)。

ASP 0115 错误不是总出现在开发人员的控制下 — 但多数时候是这样,错误处理可能已经避免了很多这种情况的发生,还可能在其发生时帮助解决了它们。

总之,最大的问题为跳过错误处理或没有包含有用的诊断信息。

在 COM 中,罕有跨越组件的界限传播异常的情况。捕获异常 — 但返回 HResults,以向调用者传送失败信息。

详细信息
下面的文章提供了有关有效错误处理的应用示例:

Fitch & Mather Stocks: Web Application Design(英文)
全局变量
建议
避免在组件中使用全局变量。在 Visual Basic 术语中,这表示在标准的 .BAS 模块中没有 Public 或 Global 变量。

为什么
Global 变量并不是真正意义上的全局。每个线程都有自己的副本。如果几种方法恰好在同一线程中执行,它们将看到相同的变量;否则它们访问的是这些变量的不同副本。这意味着您可能给一个全局变量赋了值(在线程 A 中),但其另一个用户(在线程 B 中执行)看不到新值。

其原因是 Visual Basic 内部使用“线程本地存储 (TLS)”来引用全局变量。这意味着每个线程都有自己的 Public 变量的副本,并且因为它存在多个副本,全局数据并不是真正“全局的”。也就是说,恰好在同一线程中运行的用户才会访问到同一个变量,不论他们是否期望如此。

常见的陷阱
如果在标准 .BAS 模块中使用 Public 变量,当不同线程向还想使用同一个数据的不同用户请求提供服务时,这个数据可能已被破坏了。

详细信息
Visual Basic Programmer's Journal(英文)1999 年 6 月版中由 Matt Curland 所著的下列文章 是必读的:

Black Belt Programming - Create Worker Threads in DLLs
COMponent Builder - Create Efficient Multithreaded Apps
另外,下面 Daniel Appleman 所著的文章 很好地概述了 Visual Basic 中多线程的工作原理: A Thread to Visual Basic(英文)

分布组件
建议
组件的分布涉及性能、可伸缩性和安全性问题。相同组件的不同分布可能产生更高性能、更易伸缩和更易管理的配置。

下面的指南有助于提高在多台计算机上分布组件时的性能和可伸缩性:

在 IIS 的同一框围中运行引用 ASP 内置对象的组件。
在应用程序服务器上运行数据库组件。
在哪一台计算机上运行业务组件很重要。倘若您去掉业务组件与任何 ASP 的耦合,您就可以根据您的应用程序设计、计算机的可用性和测试,来自由选择。
当然还有例外。但这些是指南的好的开始。

为什么
跨计算机分布组件使应用程序可以满足伸缩性要求。其次,上面提到的指南有助于实现应用程序的性能和可伸缩性目标。

对象引用 ASP 内置对象,会与您的 Web 服务器进行大量通讯,并且由于它们是表达层的一部分,因此它们就在那里。

数据库或对数据极为敏感的逻辑可能在数据库的存储过程中。将数据访问组件置于应用程序服务器而非数据库上,避免了组件之间的昂贵调用。相反,数据访问组件则利用 SQL Server 通信(如 TCP/IP)与数据库更有效地通信。

常见的陷阱
您应当尝试避免下列问题:

当横向可伸缩性较为合适之后,继续追求从您的计算机开始的纵向可伸缩性。
忽视了防火墙的考虑(帮自己一个忙。如果计算机间的产品环境有防火墙,则在测试方案中添加防火墙。)
将引用 ASP 内置对象的组件置于与 IIS 服务器分离的计算机上(回调和编组 ASP 内置对象的成本很高。)
使用组件内部的后期绑定(这产生对 GetIdsOfNames 的额外调用,这在分布式应用程序中可能很昂贵。尽量使用早期绑定。)
按引用传递参数(这产生更多的编组开销。尽可能“按值”传递参数。)
成功地从 IIS 调用远程 MTS 组件也可能很棘手。一个简单有效、既提高性能又简化安全性问题的解决方案,是调用中间的 MTS/COM+ 软件包/应用程序。早期绑定可减少网络路程段,提高性能。如果您使用“服务器”软件包/应用程序,则可以设置软件包/应用程序的运行标识。这个技术将在 KB 文章 159311 Instantiating Remote Components in Microsoft Transaction Server and Internet Information Server 中讨论。

详细信息
如果已经解耦了服务,特别是已使 ASP 在业务组件之外,则分布将相当灵活。您就可以更多地考虑框围,并根据需要分散组件以解决随之而来的可伸缩性和性能问题。如何知道?进行测试。如何测试?请看下面的基本指南:

若要测试 Web 站点的可靠性,请剖析计算机并检查错误。
若要测试性能,请查看每秒可处理多少 ASP 请求。
若要测试可伸缩性,请设置每秒需要处理多少 ASP 请求的阀值。用重要的工具考验应用程序——添加用户直到性能坏到不能接受为止。
加强对应用程序的测试非常重要,因为需要暴露运转条件和单浏览器测试中不会出现的其他问题。
有关对应用程序加强测试的详细内容, 请参阅 I Can't Stress It Enough -- Load Test Your ASP Application(英文)。

结论
正如所见,有一些事情在整个开发中需要时刻注意。在此,应用程序指南所涉及的诸多因素已全部阐明,因为它们有助于彻底避免严重失误。在整个开发周期中遵循本文中略述的几个指南,不仅可以避免一些额外的工作,而且能够提交可收缩的、可靠的、高性能的基于 ASP 组件的解决方案。