可扩展的 WebService 及其 IE 托管客户端






4.97/5 (30投票s)
2003年11月3日
13分钟阅读

60001

617
本文介绍了 WebService 的设计,该 WebService 由通用部分和用于处理用户不同类型请求的插件组成。这种架构简化了专用插件的设计,使其能够共享通用部分的功能。
引言
本文及示例介绍了可扩展的、可配置的 WebService 及其对应的基于 UserControl
的客户端的设计。
WebService 由以下几部分组成:
- 不变部分(后文称为“服务器”)负责处理通用任务(如安全性、扩展性、对象池、配置、数据加密和压缩等),以及
- 插件程序集,其中包含用于执行特定任务的对象(后文称为“处理器”)(例如,数据库查询、文本解析或图像处理)。
服务器预处理客户端的查询,然后将其分派给合适的处理器进行最终处理。服务器还负责在响应数据从特定处理器返回到客户端的过程中进行后处理。插件必须实现由服务器定义的知名接口。这种方法可以大大简化和标准化插件的设计,使其免于编写通用任务代码,并为其开发设定规则。
客户端是 Internet Explorer (IE) 中的 .NET 控件。其设计解决了客户端安全性(“沙箱”)和控件-宿主协作的问题。WebService 客户端代理仅依赖于服务器的 Web 方法,并且与处理器无关。
简要工作流程描述
管理员通过浏览器访问 Admin.html 文件,该文件包含一个引用了基于 UserControl
的控件的 <object>
标签。该控件连接到服务器并向其发送初始化数据。利用这些数据,服务器启动其“安全性”机制(或者说其占位符,因为此示例中的安全性机制不应过于认真 :)),加载特定的任务程序集,并为每个程序集实例化所需数量的处理器对象。还会设置 WebService 进程中同时运行的最大线程数。WebService 初始化成功完成后,客户端可以访问 Client.html 文件,并借助它查询 WebService。在被成功识别后,客户端构建查询,将其作为合适 Web 方法的参数发送给 WebService,并获得响应。所有进出 WebService 的数据都打包在一个定义明确的对象中,该对象被序列化为 XML,以便在客户端和服务器端统一处理。
测试示例
本文的示例以演示项目和源代码的形式提供。本章的“组成和编译”部分涉及源代码,而“安装”部分涉及演示项目和源代码。
安装
不幸的是,手动安装 WebService 需要一些繁琐的工作。因此,请耐心等待,如果首次尝试无法运行示例,请原谅我 :)。应执行以下步骤来安装演示。
在服务器计算机上
- 将 ExtensibleWebService_demo.zip 文件解压缩到 ..\ExtensibleWebService 根文件夹,并保留其内部目录结构。
- 启动 IIS 管理控制台,并在 默认网站 下创建新的虚拟目录
- ExtensibleWebService(别名:ExtensibleWebService,目录:..\ExtensibleWebService),以及
- ExtensibleWebService/ExtensibleWS(别名:ExtensibleWebService/ExtensibleWS,目录:..\ExtensibleWebService\ExtensibleWS)。
- 服务器计算机的名称应从“Igor”更改为您自己的服务器计算机名称。在 ExtensibleWebService 虚拟目录中的 Admin.html 和 Client.html 文件中,必须在以下片段中进行此更改
classid="http://Igor/ExtensibleWebService/...
- 在 ..\ExtensibleWebService\AdminCtrl 文件夹的 ConfigAdmin.xml 文件中,更改
<RootDir>
节点的Path
属性为您服务器上 ExtensibleWebService 文件夹的实际路径。 - 在 ..\ExtensibleWebService\ProcessAssembly1 文件夹的 ConfigProcessor1.xml 文件中,更改 SQL Server 和 Access 数据库连接字符串中的“Data Source”参数。Data Source 应指向 Northwind 和 Hotel 数据库的实际位置(它们是 Microsoft SQL Server、Access 和 Crystal Reports 安装的一部分)。
在客户端计算机上
- 将 DotNetPermissions.exe 应用程序(位于演示的 ..\ExtensibleWebService\DotNetPermissions 文件夹中;对于源代码示例,需要编译)复制到所有管理员和客户端计算机上,并在每台计算机上运行它。在出现的对话框中,将 Permitted Site 从“Igor”更改为您服务器计算机的名称,不要更改其他字段。(通过将权限集设置为“FullTrust”,我们赋予了
AdminCtrl
和ClientCtrl
控件在客户端几乎可以做任何事情的权限。在此示例中,这显然是过度的,因为AdminCtrl
所需的唯一权限是用于读取 ConfigAdmin.xml 配置文件,“File IO”,以及ClientCtrl
的“Security->Allow calls to unmanaged assemblies”。但关于“沙箱”客户端安全的详细讨论超出了本文的范围。有关以编程方式更改安全设置的更多信息,请参见例如 Rajiv Sharma 的笔记和代码 此处)。 - 将 ..\ExtensibleWebService\AdminCtrl 文件夹中的 ConfigAdmin.xml 文件从示例复制到您的管理员计算机(最好放在桌面,因为这是管理员控件的默认目录,从而节省了一些输入时间)。
组成
该示例由服务器(ExtensibleWS、Framework 和 InHouseServerSecurity 程序集)、处理器插件(ProcessorAssembly1 和 ProcessorAssembly2 程序集)、管理客户端(称为 Administrator,AdminCtrl 程序集)和普通客户端(称为 Client,ClientCtrl 程序集)组成。ProcessorData 程序集负责客户端和 WebService 之间的数据传输。服务器、处理器和客户端都使用该程序集。最后,服务器端的 XmlWalker 程序集实现了 XML 解析器。
编译
- 将 ExtensibleWebService.sln(..\ExtensibleWebService 文件夹)加载到 Visual Studio .NET 中。您很可能使用的是 Microsoft Development Environment (MDE) 2003。在这种情况下,您可能会被要求将解决方案从我用于此项目的 MDE 2002 转换。
- 构建 ExtensibleWS 和 DotNetPermissions 项目。
- 确保 ExtensibleWebService 和 ExtensibleWebService/ExtensibleWS IIS 虚拟目录已创建(参见“安装”部分)。运行 WSProxyGenerator.bat 文件(在 ..\ExtensibleWebService\ExtensibleWS 文件夹中)生成 ExtensibleWSProxy.dll 代理,并将其移至根文件夹。(有关完整的代理生成过程,请参阅 WSProxyGenerator.bat 文件中的注释)。在构建 AdminCtrl 和 ClientCtrl 项目之前,必须将 ..\ExtensibleWebService\ExtensibleWSProxy.dll 程序集的引用添加到它们中。
- 构建 AdminCtrl、ClientCtrl、ProcessorAssembly1 和 ProcessorAssembly2 项目。
- 运行 CopyDlls_Release.bat(或 CopyDlls_Debug.bat,..\ExtensibleWebService 文件夹)文件,将编译后的程序集的 Release(或 Debug)版本复制到相应文件夹。
- 完成“安装”部分中列出的步骤。
现在我们可以运行示例了。
工作原理
下面的用户操作运行演示项目的说明,辅以对其背后源代码的解释。
管理员控件
首先,在 IE 中浏览 Admin.html
http://Igor/ExtensibleWebService/Admin.html
(像往常一样,将“Igor”替换为服务器计算机的名称)。管理员控件出现在 IE 中。更改服务器计算机名称,检查并根据需要更正其他编辑框(Name 和 Password 字段在此示例中不进行分析,所以将其留空),然后按 Submit 按钮。
Submit 按钮处理程序 AdminControl.btnSubmit_Click()
(在 AdminCtrl 项目中)创建一个 WebService 代理实例,将 ConfigAdmin.xml 配置文件内容打包到一个 ProcessorData.Data
类型的对象中,并调用 WebService 的 WebMethod ProcessAdmin()
。
服务器
ExtensibleWS.WS
WebService 有两个 WebMethods(在 Service.asmx.cs 文件中),即 ProcessAdmin()
和 ProcessClient()
。它们接受序列化的 ProcessorData.Data
类型变量(在实际应用中,此变量应进行加密和压缩)。ProcessorData.Data
是一个通用类型,用于表示客户端请求和 WebService 响应的数据。数据包含发送给 WebService 的特定命令、该命令的参数以及应该处理该命令的 Processor 程序集的 GUID。它们还包含用于服务器响应和可能的服务器错误消息的字符串成员。
ProcessAdmin()
方法使用管理员接收到的数据初始化 WebService。这是通过 InHouseServerSecurity.SecurityManager
单例类完成的。管理员控件将 ConfigAdmin.xml 文件内容发送到 WebService。服务器从中提取 ConfigServer.xml 文件的路径并实际初始化自身。在 ConfigServer.xml 文件中,MaxProcessorsrNum
表示同时运行的最大处理器数量,每个处理器都在自己的线程中运行。然后列出所有要加载的处理器程序集。ID(0 基础的顺序编号)定义了相应处理器程序的特定实例。在该示例中,在 WebService 初始化期间实例化了两个 ProcessorAssembly2 程序集的处理器对象(ID = 0, 1),以及三个 ProcessorAssembly1 程序集的处理器对象(ID = 2, 3, 4)。
服务器的主要功能实现在 Framework
命名空间中,主要在 ProcessorManager
和 ProcessorFactory
类中。ProcessorFactory.CreateProcessor()
方法加载处理器程序集并实例化处理器对象。单例 ProcessorManager
执行大部分的查询处理。由 WebMethod ProcessClient()
调用的 ProcessorManager.Run()
方法选择合适的处理器对象并使其处理客户端数据。Run()
方法还确保了对同时运行的处理器线程数量的限制。
WebService 管理员可以更改同时运行的线程数量以及每种类型实例化处理器对象的数量(通过在初始化 WebService 之前在 ConfigServer.xml 文件中进行相应更改)来实现特定系统配置的最大生产力。
ConfigServer.xml 文件其余内容定义了安全设置。服务器配备了一个简单的基于角色的安全机制(或者说是其说明)。它假定“受保护”资源的粒度是源->部分(例如,MDB 文件(源)->表(部分))。要访问特定的源->部分片段,其源名称应位于查询的 ProcessorData.Data.ServerObjectName
中,而 ProcessorData.Data.CommandText
应包含其部分名称。代码位于 InHouseServerSecurity
命名空间中,并涵盖所有处理器类型。请注意,这种“安全性”完全不安全,因此不能用于商业应用程序,仅作为占位符和说明。
WebService 初始化结果打包到同一个 ProcessorData.Data
对象中,并发送回管理员。如果 WebService 已正确初始化,则它已准备好处理客户端的请求。
客户端控件
接下来,在 IE 中浏览 Client.html(像往常一样,将“Igor”替换为服务器计算机的名称)。
http://Igor/ExtensibleWebService/Client.html
客户端控件出现在 IE 中。在编辑框中插入与管理员控件相同的数据,指定一些客户端的名称和密码(参见 ..\ExtensibleWebService 文件夹的 ConfigServer.xml 文件中的 <Users>
标签下的内容),或者使用默认值,然后按 Submit 按钮。如果操作成功,客户端控件的外观在 IE 中会发生变化;顶部会出现一个蓝色的客户端名称。
客户端控件的新外观便于构建 SELECT
SQL 查询。在实际应用程序中,将客户端的 SQL 查询直接发送到服务是不可接受的,特别是出于安全原因。但为了简单起见,ProcessorAssembly1 的处理器被设计成面向 SQL 查询的。查询参数的默认值对应于 Northwind 数据库(SQL Server 和 Access 均适用)。用户需要选择 SOURCE,填写查询字段,然后按 Submit 按钮(保持 Load Test 复选框未选中)。来自服务的响应将以原始 XML 的形式显示在文本框控件中,并以用户友好的形式显示在下方的网格中。
通过尝试不同的用户和数据库,您将注意到“安全限制”(例如,“Friends”组的用户不允许访问 Northwind 数据库中的 Customers 表,请参见 ConfigServer.xml 文件)。
管理员和客户端都是基于 UserControl
的控件。在该示例中,它们是从 IE 中使用的(但也可以从独立的 WinForm 应用程序中使用)。重要的是嵌入式控件与其 IE 主机之间的协作。单击客户端控件会导致激活 Client.html 文件中的 JScript ClientControl::ClickEvent()
函数(有关从 UserControl
调用脚本函数的更多信息,请参见 此处)。该脚本函数反过来调用客户端控件的方法。对于这两种调用,都会显示相应的消息框给用户。
处理器程序集
每个处理器程序集都旨在处理特定类型的客户端查询。为了能够被服务器加载和激活,处理器程序集应实现 Framework.IFactory
接口、Framework.Processor
类,并拥有其唯一的 GUID 和名称。处理器程序集可以通过其自己的 XML 配置文件进行配置。该示例提供了两个处理器程序集,即 ProcessorAssembly1 和 ProcessorAssembly2。
ProcessorAssembly1 的 Processor1
对象使用 ADO.NET 处理简单的 SQL 查询。它从其配置文件 ConfigProcessor1.xml(..\ExtensibleWebService\ProcessorAssembly1 文件夹)中读取数据库资源的连接字符串。每个 Processor1
对象在其构造函数中建立与配置文件中列出的所有数据库的连接,并将其连接对象保存在 htConnectionObject
哈希表中。这使得每个 Processor1
对象能够快速地为用户服务到任何数据库的查询。
ProcessorAssembly2 没有特定的功能,只能作为开发更实用处理器对象的模板。
负载测试
如果从 VS .NET 以 Debug 模式运行,则提供了一个简单的 WebService 负载测试。可以通过在 Client 控件中选中 Load Test 按钮来激活它。在此模式下,Client 每隔 [Time] 间隔(默认为 200 毫秒)查询 WebService 100 次。测试结果显示在 VS .NET Debug 窗口中。您可以调整时间间隔,更改单个查询的处理时长(在 ConfigProcessor1.xml 文件中更改 <Debug>
节点的属性),并在 ConfigServer.xml 文件中更改同时运行的线程数以及特定处理器实例的数量。
进一步开发
当然,最直接的方法是为服务器添加功能安全、加密和 zip 功能。还值得考虑对频繁请求的数据进行缓存。然后就会想到更多花哨的东西。
STA 处理器中的 COM 对象
例如,开发人员经常希望使用现有的 Win32 代码构建 WebService。如果我们想将现有的单线程单元 (STA) COM 对象集成到我们的处理器中(使用 COM Interop),则需要进行一些额外的工作。我们可能希望在处理器构造函数中创建一次 COM 对象(因为这是一个昂贵的操作),然后使用它来处理用户请求。问题在于我们的服务器设计方式使得处理器对象的代码可能在不同的线程中执行。因此,如果 COM 对象只是在 Process 构造函数中创建,那么它就会被不同线程直接访问。显然,对于 STA COM 对象来说,这是不可接受的。
为了避免这种情况,建议采用以下设计。处理器构造函数创建一个额外的线程(称为线程 B)。线程 B 函数创建 COM 对象,启动内部循环,并在同步自动重置事件上等待。当线程池的线程(称为线程 A)调用 Processor.Process()
方法时,它为 COM 对象准备 ProcessorData.Data
结构,并设置同步事件以释放线程 B。同步事件被重置,线程 A 开始等待事件,而 COM 对象在线程 B 中执行工作。完成工作后,线程 B 准备输出数据结构,并设置事件以释放线程 A。线程 B 再次等待事件,而线程 A 完成 Processor.Process()
方法并将输出 ProcessorData.Data
结构返回给调用者。这种设计允许处理器始终在创建 COM 对象的线程中执行 COM 对象代码。线程 A 和 B 永远不会同时运行。实现此方法所需资源是每个处理器对象的额外线程和一个同步事件。
结论
本文介绍了 WebService 提供者和使用者。提供者的设计允许通过相对简单的插件来扩展它,以处理特定的用户查询。提供者的不变部分负责处理重要的通用任务,如安全性、扩展性、数据加密和压缩等,让插件可以专注于其特定功能。这种方法简化了新 WebService 的开发,将其过程简化为设计相对小型的专用插件组件。基于 UserControl
的 WebService 消费者与提供者及其 Internet Explorer 主机进行协作。演示项目和源代码展示了这些功能。
参考文献
- Tom Archer. Inside C#.
- Jeffrey Richter. Applied Microsoft .NET Framework Programming.
- Allan L. Dennis. .NET Multithreading.
- Jesse Liberty & Dan Hurwitz. Programming ASP.NET.
- Adrian Turtshci et al. C# .NET. Web Developer's Guide.
- David Sceppa. Microsoft ADO.NET.
- Xiangyang Liu. Web Service Dispatcher。
- Rajiv Sharma 的笔记和代码.
- HOW TO: Sink Managed C# Events in Internet Explorer Script.
致谢
我谨向我的朋友和同事 Timur Igamberdiev、Vitaly Shelest、Victor Katz 和 Marina Kogan 表示衷心的感谢,他们以各种方式帮助我完成了这篇文章。