通过面向消息的中间件安全地使用 Web 服务






4.96/5 (42投票s)
2003年1月24日
72分钟阅读

341972

832
描述了一种使用 ASP.NET Web 客户端服务序列化并通过 MSMQ 和 MQ 进行消息定向中间件传递 SOAP 消息的方法
引言
本文描述了利用 ASP.NET Web 服务提供的可插入传输基础结构。该基础结构允许开发人员以文档/字面格式构建的 SOAP 消息通过默认 HTTP 传输以外的机制传递到终结点。
本文介绍了一个支持添加替代物理传输机制的框架,并包含两种实现:MSMQ 和 Websphere MQ。这两种实现用于说明一种存储转发功能,以便更“有保证”地传递 Web 服务请求。将开发的框架集成了 Web Services Enhancements (WSE) 1.0,用于在非 HTTP 机制上安全地进行双向消息传输,从而让开发人员可以选择“受信任的”(明文消息)或“不受信任的”(安全消息)方式将消息传递到选定的终结点。
SOAP 被设计为一种与传输无关的协议,可与多种传输协议(如 HTTP(S)、SMTP、FTP 等)结合使用,以便在两个参与者之间传递结构化和类型化的信息。在使 ASP.NET Web 服务能够生成 SOAP 消息方面付出了大量努力,以促进异构环境(例如 .NET 消费 BEA Weblogic Server 托管的 Web 服务)内的互操作性,但到目前为止,在 Microsoft 领域内对 Web 服务的标准访问几乎完全是面向 HTTP 的。
HTTP 上的 SOAP
HTTP 传输当然有其优缺点——一个明显的优点是随着 Web 的普及而成熟的相关安全基础结构的可用性。相反,一个缺点是该协议缺乏任何形式的保证和可靠的(仅单条消息)传递,这催生了 IBM 的 HTTPR 等解决方案。再加上后端服务必须物理在线才能通过 HTTP 接收请求这一事实,就有了一些相当有说服力的理由来寻找替代方法来“传递消息”。
其他传输协议上的 SOAP
虽然 ASP.NET 在促进面向 HTTP 的 Web 服务绑定方面做了很多工作,但 HTTP 并不是传输 SOAP 的唯一协议。事实上,一个鲜为人知的领域是 .NET 框架内已有的 Web 服务基础结构,它允许在静态(设计时)或动态(运行时)地更改消息传递机制和终结点。该基础结构的真正强大之处——微软称之为“可插入协议”——在于传输方法可以更改在逻辑客户端请求之下,以便将 SOAP 通过开发人员选择实现的任何合理媒介进行路由。
本案例研究将演示能够在 ASP.NET 客户端与 HTTP、MSMQ 和 Websphere MQ 之间有效地切换传输。它将演示将 SOAP Web 服务请求的“目标”从标准的 IIS 托管变体更改为等待 MSMQ 或 MQ 队列的侦听器的简单性。我们将演示使用字符串、数组、二进制数据和自定义可序列化类型等标准类型调用 API。我们还将展示如何以异步方式实现这种支持。
SOAP 的安全
很可能排队式的传输将在企业内部使用,即在一个组织的受信任网络内,并具有相应的(通常较低的)安全要求。然而,通过允许非 HTTP 交付选项,我们可能会失去一层经过验证的安全性,而我们应该对其进行替换。幸运的是,在开发新一代 Web 服务协议(包括但不限于安全性、消息路由和消息附件)的联合标准方面,已经进行了大量工作。这项工作主要集中在以与传输无关的方式支持业务事务中多个处理节点之间的消息安全,而无需依赖 SSL 等仅适用于点对点的技术。这项工作最近以 Microsoft 对上述标准实现的发布为顶点,即 Web Services Enhancements (WSE) 1.0 包。因此,本案例研究还将展示如何通过 WSE 1.0 中 WS-Security 的实现来保护(在本例中是数字签名和对称加密)ASP.NET 客户端和服务器生成的 P2P 消息,而不是使用两个排队产品固有的本机安全机制。我们还将探讨 WSE 提供的 DIME 支持,作为发送二进制数据的另一种方式,它专为传输大型二进制数据流而开发。
文章目标
希望在案例研究结束时,以下方面将得到澄清
- 如何在 ASP.NET 可插入协议基础结构中编写新的传递协议
- 如何在此基础结构中从 C# 访问 MSMQ 和 Websphere MQ
- 如何在客户端和服务器端(直接使用 WSE 过滤器管道)集成 WSE 1.0 与 Web 服务以保护数据
- 如何在使用 WSE 传输二进制数据
- 如何从客户端驱动 SOAP 消息的排队异步传递
工作背景
这项工作的原始背景涉及一个概念验证项目,旨在确定 .NET 客户端可以轻松访问现有的面向 Java 的后端(使用 Web 服务进行互操作),但通过 IBM Websphere MQ 而不是 HTTP。本文提供了一个面向 .NET 的后端,因为这样做我们可以将文章涵盖的技能集限制在 .NET 范围内,并通过在两端都拥有 WSE 来更轻松地演示安全通信。但是,在文章的适当位置将识别出与 Java 服务实现互操作的特殊性。
系统要求
构建的解决方案将需要以下部署环境
1. 安装了 MSMQ 和 IIS 的 Windows 2000 SP2 或 Windows XP Pro
2. .NET Framework 1.0 (SP1 或更高版本)
3. WSE 1.0 SP1 运行时
4. (仅限 MQ 支持) Websphere MQ 5.3 试用版 [2]
5. (仅限 MQ 支持) VB6 运行时 [10]
为了重新构建解决方案二进制文件,您将需要以下附加工具
6. VS.NET (C#)
7. WSE 1.0 SP1 – 推荐完整安装
8. VS.NET WSE 设置工具 (可选) [8]
技能要求
从技能角度来看,读者应该具备- 熟悉中级水平的 C#。接触过编写和使用 .NET 中的基本 Web 服务,因为我们将详细研究客户端基础结构(包括 WSDL 生成的代理)。
- 对“第二代”Web 服务领域(如 WS-Security 和 WS-Attachments / DIME)的工作有所了解将是有益的。
- 如果想使用 Websphere MQ 作为传输(它可以在示例应用程序配置文件中禁用),强烈建议安装试用版 [2] 并配置标准设置。
- 对统一建模语言 (UML) 符号的基本理解。
文章目标
基本上,我们的目标是在以下领域取得进展
- 将表面上看起来紧密耦合的消息 SOAP 序列化与通过 HTTP 将消息传递到终结点的关系分开。
- 我们希望重用 .NET Web 服务的序列化部分,这样我们就永远不必手动编写 SOAP。
- 我们的目标是利用基础结构的消息传递部分,以便将 SOAP 请求传递到队列并从队列接收 SOAP 响应。我们特别希望支持 MSMQ 和 Websphere MQ。
- 为此,我们希望客户端能够以尽可能标准的方式“表达”它希望将请求传递到的队列,即与描述 URL 的方式相比,更改最少。
- 在实现这一点后,我们希望了解使用 WSE 来调整 SOAP 消息会对我们带来什么影响/好处——在保护消息流方面,以及允许在 SOAP 信封“外部”传输二进制附件。
- 理想情况下,我们希望将生成的代码打包,以便最大程度地减少希望在其解决方案中包含排队式传输的开发人员的工作量。
- 我们要确保基本框架在多用户并发使用环境中运行。
.NET 中的 WSDL 和 Web 服务代理生成
接口合同使用 WSDL(类似于 COM 的 IDL)进行描述。一旦这个 WSDL 可用(位于 URL 或文件中),就可以通过直接使用 SDK 附带的独立工具 WSDL.EXE 或在 Visual Studio .NET 的“添加 Web 引用”功能中隐式地生成 Web 服务代理类。
图 1:WSDL-到-代理过程
生成的代理文件(通常命名为 reference.cs
)将位于项目的一个子文件夹中,该子文件夹位于名为 \Web References
的子文件夹下。它包含一个通常派生自 SoapHttpClientProtocol
的类。生成的类非常有效地隐藏了与标准 XML Web 服务调用相关的序列化和传输细节的复杂性。在生成的代理文件中,通常会看到同步和异步形式的方法组合——例如,以下显示了一个基本的生成(同步)方法
[SoapDocumentMethodAttribute(...)] public string IsCollaboratorRegistered(string vstrEnvUUID) { object[] results = this.Invoke("IsCollaboratorRegistered", new object[] { vstrEnvUUID }); return ((string)(results[0])); }
在标准代理到位的情况下,客户端可以使用类似以下的代码访问相应 URL 处的 Web 服务
// Get an instance of the proxy localhost.RegService objWSReg = localhost.RegService(); // Endpoint can be varied at runtime etc. objWSReg.Url = "http://192.168.32.1/MyService.asmx"; // Make the call string strRetXML = objWSReg.IsCollaboratorRegistered("12345678901234567890123456789012");
能够更改 URL 并根据“协议方案”和“终结点”来描述此 URL 是该方法的一个关键方面。
在运行时,代理中的单行代码 this.Invoke( … )
隐藏了大量工作,这些工作被细分为以下基本部分
- 传输选择/支持确认
- API 调用 SOAP 序列化
- 传输协议驱动
我们将在本文稍后的部分进一步细分,展示可插入协议基础结构如何与 SOAP 序列化结合以构建和传递消息。特别是,有几种方法可以在代理中重写,以接入甚至替换消息传输协议过程。
为 Web 服务定义 Internet 和其他网络传输
虽然 SOAP 消息的实际传递协议是 HTTP,但 .NET 框架内存在一个基础结构,允许在静态(设计时)或动态(运行时)地更改消息传递机制。
.NET Framework 使用三个特定的类来提供访问 Internet/网络资源所需的信息,通过请求/响应模型
1. Uri
类,其中包含您正在查找的 Internet 资源的 URI(包括协议方案,例如 http://www.microsoft.com/webservices/stockcheck.asmx),
2. WebRequest
类,它封装了对网络资源的访问请求,
3. WebResponse
类,它提供了一个容器,用于接收来自网络资源的响应。
客户端应用程序通过将网络资源的 URI 传递给 WebRequest.Create()
方法来创建 WebRequest
实例。此静态方法为特定协议(如 HTTP)创建 WebRequest
实例。返回的 WebRequest
实例提供对控制服务器请求以及访问请求时发送的数据流的属性的访问。WebRequest
实例上的 GetResponse()
方法将请求从客户端应用程序发送到 URI 中标识的服务器。
[注意:统一资源标识符 (URI) 是 RFC2396 中定义的标识抽象或物理资源的紧凑字符串。 “方案”是 URI 的标准部分,可以细分为多个不同的段。所有网络标识符都可以映射到其中一个部分。方案通常是协议标识符,例如 http、ftp 等。]
开箱即用,支持三种特定的协议方案——“http:”、“https:”和“file:”。我们将创建 WebRequest 和 WebResponse 类的**新版本**以提供另外两种支持
- “msmq:” 支持请求响应模型,其中目标是本地或远程 MSMQ 队列
- “mq:” 支持请求响应模型,其中目标是 MQ 队列管理器/队列名称组合
可以想象,可以为其他方案添加额外的支持,例如用于 SMTP 和 FTP 的“mailto:”或“ftp:”等。
请注意,尝试为涉及不受支持的方案的 URI 创建 WebRequest
Uri uri = "mq://accountq1"; WebRequest newWebRequest = WebRequest.Create(uri);
将产生一个 NotSupportedException
形式的异常,表明请求 URI 中指定的请求方案未注册。这就是我们接下来必须解决的问题,才能成功引入新方案。现在应该清楚,我们的目标是允许客户端将队列标识为请求的 URI,并能够像对基于 http 的终结点一样对该 URI 执行方法调用。
定义自定义传输
WebRequest
和 WebResponse
类构成了“可插入协议”基础结构的基础。这意味着为了指定像 "xxx://"
这样的新协议,至少应该创建这些基类的派生类,加上一个实现 IWebRequestCreate
的类。下图描绘了生成新协议所涉及的基本模型
图 2:可插入协议的对象模型
已包含主要操作和属性,以显示任何实现的键方面。
通用方面
WebRequest
和 WebResponse
类的后代可以重写的几个属性和操作在实现非互联网特定传输时相当冗余。此外,两个队列实现之间存在很大的共性。这给了我们机会将这些共性和冗余抽象到一个中间类中。例如,可以想象 Method
属性(用作每个 Web 类型请求(GET、POST 等)的一部分)在我们提议的新方案实现中将不相关。
我们定义以下新的派生类型来扩展层次结构
类型 | 基类型 | 目的 |
MyBaseWebRequest |
WebRequest |
包含 MQ 和 MSMQ 实现的通用重写 |
MyBaseWebResponse |
WebResponse |
包含 MQ 和 MSMQ 实现的通用重写 |
MSMQWebRequest |
MyBaseWebRequest |
WebRequest 的 MSMQ 实现(密封) |
MSMQWebResponse |
MyBaseWebResponse |
WebResponse 的 MSMQ 实现(密封) |
MQWebRequest |
MyBaseWebRequest |
WebRequest 的 MQ 实现(密封) |
MQWebResponse |
MyBaseWebResponse |
WebResponse 的 MQ 实现(密封) |
为了使协议特定类可用作可插入协议,必须满足三个标准
1. 准备方案注册
实现必须包含 IWebRequestCreate
接口的一个版本,例如 MSMQ
public class MSMQRequestCreator : IWebRequestCreate { public WebRequest Create(Uri uri) { // Create a new MSMQ request object return new MSMQWebRequest(uri); } }
2. 方案注册和 URI 创建
为了允许管理对非标准网络资源(如队列)进行实际连接的详细信息,必须将 WebRequest 的后代类与 WebRequest 类进行注册,以便实现新的协议。
这通常通过类似以下方式实现
// Register the "mq:" prefix here for WebRequest MQRequestCreator objCreator = new MQRequestCreator(); WebRequest.RegisterPrefix("mq", objCreator);
这有效地允许了新方案实例的 Create 代码的后续连接,使得我们上面提到的代码
Uri uri = "mq://accountq1"; WebRequest newWebRequest = WebRequest.Create(uri);
将奏效,因为 .NET Web 服务基础结构已知新方案。
3. 关键重写成员的实现
派生类必须重写 WebRequest
和 WebResponse
的抽象方法和属性,以提供可插入接口。这些成员在 .NET Framework 1.0 中有充分文档记录——此处显示了相关子集
成员 | 目的 |
方法 |
通常与要在此请求中使用的协议方法形式相关,例如 PUT、GET 等。此处未使用。 |
Headers |
获取或设置与请求关联的头部名称/值对的集合。此处未使用。 |
ContentLength |
包含 WebRequest 实例发送到 Internet 资源的字节数。 |
ContentType |
当在派生类中重写时,获取或设置正在发送的请求数据的类型。在本例中始终为 text/xml。 |
Credentials |
获取或设置用于向 Internet 资源验证请求的网络凭据。在本例中未使用。 |
PreAuthenticate |
指示是否预先验证请求。在本例中未使用。 |
Proxy |
获取或设置用于访问此 Internet 资源的网络代理。在本例中未使用。 |
GetRequestStream |
通常创建并返回一个 Stream ,用于将数据(SOAP 消息)写入 Internet 资源。在我们的基类中实现。 |
BeginGetRequestStream |
上述方法的异步版本。在本例中不支持。 |
EndGetRequestStream |
上述方法的异步版本。在本例中不支持。 |
GetResponse |
返回对请求的响应。此重写构成了我们实现 MSMQ 和 MQ 的主力,因为这是访问特定协议代码的地方。 |
BeginGetResponse |
上述方法的异步版本。在本例中不支持。 |
EndGetResponse |
上述方法的异步版本。在本例中不支持。 |
|
取消使用 BeginGetResponse 方法启动的异步请求。在本例中不支持。 |
如上所述,我们将把 MQ 和 MSMQ 版本通用的任何重写实现到 MyBaseWebRequest
/ MyBaseWebResponse
中,并将具体实现放在相应的派生类中。
执行 WebRequest 派生实例
在这里,我们将尝试将通用的客户端序列化和传输过程映射到 .NET Web 服务基础结构在同步 Web 方法调用期间执行的离散调用。
该图描绘了“请求”执行路径上的以下事件序列
图 3:WebRequest 的交互图
操作的关键方面包括
- 在我们的生成 SOAP 代理(派生自
SoapHttpClientProtocol
)中重写GetWebRequest
允许我们挂接注册(用于 msmq / mq 方案)和创建适当的WebRequest
实例。如果方案是“msmq”,则通过MSMQRequestCreator
实例实现此创建。 - .NET Web 服务基础结构调用私有方法
BeforeSerialize()
,该方法触发获取和设置我们通用WebRequest
派生类实例内的各种成员数据,设置方法(对我们来说不相关,但我们必须支持此操作——返回NotSupported
异常会导致 .NET Web 服务基础结构拒绝整个请求!)并获取(空的)Web 头部集合。 - 接下来,在我们的
WebRequest
派生类上调用GetRequestStream()
方法,我们只需创建一个流(在本例中为MemoryStream
)并将其返回给基础结构。 - 此时,基础结构开始 SOAP 序列化过程,并将结果存入我们的流中。此时一个巨大的问题是,为了将序列化流推送到线路上传输,标准处理涉及关闭流!鉴于我们还没有到达要将流推送到队列的地步,这乍一看有点像一个拦路虎!!所以,暂时方便地忽略它 :-) “访问序列化的 SOAP 流”部分解释了问题和可能的解决方案。
- 一旦流准备好,基础结构就会调用我们重写的
GetResponse()
方法(在MSMQWebRequest
类实例中)。这里会使用适当的机制(MSMQ 为System.Messaging
,MQ 为 IBM 提供的 COM 库)处理队列的具体细节。这无疑将涉及写入 SOAP 消息并等待后端服务的响应。MSMQ 和 MQ 传输的具体内容将在下一节中介绍。我们又一次方便地忽略了此时对流的访问…… - 无论 MSMQ / MQ 的具体工作内容如何,结果都是返回适当类型的响应对象(例如
MSMQWebResponse
),或者在传输中发生故障时引发WebException
实例。这些类型的异常将在本文后面讨论。
如果您有机会,我强烈建议您调试一下,在代码中设置断点,因为这对于整个过程的事件顺序非常具有指导意义。
实现 MSMQ 和 Websphere MQ 传输
假设我们可以访问序列化的 SOAP 消息(我们将在下面处理),我们需要挂接一个基于 WebRequest
的协议处理程序。本节重点介绍了实现的键方面,并扩展了上面时序图中的信息。
杂项通用请求方面
几个重写的属性对于操作来说不是必需的,因此可以允许它们抛出NotSupported
异常,例如public override IWebProxy Proxy { /* override */ get { throw new NotSupportedException(); } /* override */ set { throw new NotSupportedException(); } }
其他属性,如 ContentLength
、ContentType
和 Timeout
等是必需的,并且已正确设置
public override int Timeout { /* override */ get { return m_intTimeout; } /* override */ set { m_intTimeout = value; } }
这些方面由 MSMQ 和 MQ 实现共享,并被抽象到 MyBaseWebRequest
类中。
为序列化提供流
MyBaseWebRequest
包含一个受保护的数据成员(在派生类中可访问),名为 m_RequestStream
,它在客户端 Web 服务基础结构调用时设置
public override Stream GetRequestStream() { if (m_RequestStream == null) m_RequestStream = new MemoryStream(); else throw new InvalidOperationException("Request stream already retrieved"); return m_RequestStream; }
由于我们不期望在单次 Web 服务方法调用过程中提供两次流,而且我们也不期望在多个代理之间共享 WebRequest
实例,因此我们提供了一些保护措施,以防止这种情况发生。
注意:我希望这很明显,但以防万一,我们自己不做序列化。我们只是提供一个容器(流)供 Web 服务基础结构使用。
杂项通用响应方面
此外,MyBaseWebResponse
被 MSMQ 和 MQ 实现无更改地使用。此类包含更类似于 Internet 资源世界的信息(状态代码、状态描述等),实际上,由于所有错误都作为异常抛到堆栈之上,此类中的唯一关键方法是 SetDownloadStream()
,它允许 WebRequest
调用者将响应 SOAP 流存入类实例中,以及 GetResponseStream()
,它将此响应流返回给调用的 .NET Web 服务响应处理基础结构。假设调用成功,一旦流被返回,就会进行标准的 SOAP 反序列化。
排队式传输的通用方面
我们希望将几个操作方面纳入我们的队列管理中。
“请求超时”
标准的生成代理包含一个超时数据成员,该成员具有默认值,并且可以在调用实际 Web 服务调用之前设置。我们希望排队实现能够遵守此超时,并在超时时像标准协议处理程序一样引发异常。
请求-响应匹配(在协议级别)
在多用户场景中,很可能同时有许多请求未完成,并且对这些请求的响应将并发接收。在这种情况下,需要能够将响应与关联的请求进行匹配。支持此功能主要有两种选择
1. 支持应用程序级别 API(即在 WSDL 中)的某种请求 ID 形式,服务可以对其进行操作。
2. 使用基础结构提供的任何本地支持来关联请求与响应。
虽然第一个选项将使我们能够支持一种与传输无关的关联消息的方式,但这将以牺牲队列协议处理程序为代价,它们需要有效地窥视队列中的所有消息以识别响应。选择选项 1 也会在一定程度上扭曲应用程序 API 的纯粹性,因为每个方法调用都需要支持一个额外的参数,仅仅是为了允许进行协调。最后,这两种排队形式都通过在其队列消息的版本中包含 CorrelationID
插槽来支持关联。因此,经过大量权衡,我们决定协议将处理关联(如果添加更多传输,可能需要重新审视此决定,并且通常如果决定反转,结果将是向 SOAP 信封添加自定义头部以支持关联方法)。这种关联处理需要在每个实现的 GetResponse()
方法中执行。
“响应 URI”
尽管传输基础结构是专门为支持非标准协议而开发的,但它主要围绕目标终结点,并假定在请求和响应的传递方面存在一个接触点。实际上,在排队方面,我们希望能够让客户端能够描述请求 URI(要命中的终结点)和响应 URI,后端服务可以调用该响应 URI 来获取调用结果。在我们看来,这意味着我们希望能够分别描述目标请求队列和目标响应队列。
图 4:MySoapClientProtocol 的对象模型
最佳方法似乎是从 SoapHttpClientProtocol
派生,并引入严格的接口(ISoapClientProtocol
)来管理名为 ResponseUrl
的新数据成员(命名是为了使请求-响应 URL 对保持对称)。
结果类称为 MySoapHttpClientProtocol
。引入此类需要每个生成的代理都从此类派生,以便能够访问新属性。这将需要手动重新指向关联的 reference.cs
文件。
值得牢记的一点是,在代理重新生成(例如通过 VS.NET 菜单项“更新 Web 引用”)或删除 Web 引用时,对该代理所做的任何更改都会丢失。一旦手动更改了代理,最好备份 reference.cs
文件,以便在需要扩展基础 Web 服务 API 定义并因此需要重新生成代理时,可以稍后重新应用它们。
关于响应队列信息如何使用,可以设想在处理程序的 GetResponse()
方法中实现,该方法等待相关响应队列中的一条消息,该消息与它发送的请求相匹配。这通常涉及使用 Queue 特定的 API,例如 ReceiveByCorrelationID()
。
队列的“URI 表示”
正如在“定义 Web 服务的 Internet 和其他网络传输”部分所建议的那样,Uri
类是我们通过 URI 语法标识终结点服务的手段。URI 语法依赖于协议方案。通常,绝对 URI 的编写方式如下
<scheme>:<scheme-specific-part>
URI 语法不要求 scheme-specific-part 具有任何通用结构或语义,而这些结构或语义在所有 URI 中都是通用的。然而,URI 的一个子集确实共享一种通用的语法,用于在命名空间内表示层次关系。这种“通用 URI”语法由四个主要组件序列组成
<scheme>://<authority><path>?<query>
广为人知的例子包括 http 格式,如
http://www.microsoft.com/webservices/testservice.asmx?wsdl
为了利用可插入协议基础结构,我们需要将 MSMQ 和 MQ 风格的队列名称映射到此语法。正如下一节所示,根据我们讨论的排队实现,这可以以不同程度的难易程度实现。
处理错误
作为 .NET Web 服务可插入协议基础结构的一部分,WebRequest
和 WebResponse
类的版本会引发系统异常(如 ArgumentException
)和特定于传输的异常(通常采用 WebException
的形式,由特定的重写 GetResponse()
方法引发)。我们希望将我们的队列相关故障映射到此异常结构,因为它很好地隐藏了 MQ 和 MSMQ 异常报告世界之间的差异。WebException
类派生自 System.InvalidOperationException
,并包含一些扩展以支持详细错误信息
图 5:WebException 对象模型
两种排队协议支持此方法,使用 Status 属性来反映以下 WebExceptionStatus
枚举返回值
状态 | 描述 |
ConnectFailure |
在传输级别无法联系远程服务。在我们的例子中,这意味着请求队列的打开因无法将队列名称解析为资源的原因而失败。 |
NameResolutionFailure |
名称服务无法解析主机名。在我们的例子中,这意味着 URI 中标识的请求队列(或 MQ 的情况下的队列管理器——见下文)无法映射到资源。 |
ProtocolError |
从服务器收到的响应是完整的,但指示了协议级别的错误。在我们的例子中,这意味着返回了一个有效的 WebResponse 派生对象,但该对象将包含与相关排队传输内部的失败有关的信息。该对象将作为 WebException 的一部分提供。 |
ReceiveFailure |
未能从远程服务器接收完整的响应。在我们的例子中,这意味着读取响应队列失败,原因不是超时或无法将队列名称解析为资源。 |
SendFailure |
未能将完整的请求发送到远程服务器。在我们的例子中,这意味着在成功打开(连接)队列后,将消息放入请求队列失败。 |
Timeout |
在为请求设置的超时时间内未收到响应。在我们的例子中,这意味着请求已成功放入请求队列,但未在超时时间内在响应队列中识别出相应的响应。 |
这些状态代码从下面的相应协议特定部分映射到特定的 MSMQ / MQ 失败代码。
MSMQ 特定实现
所有工作都在重写的 GetResponse()
方法中完成。
访问 MSMQ 函数
为了使用 MSMQ,必须将 System.Messaging
程序集包含在项目的引用中。对于初学者来说,它提供了一个易于使用的对象模型,用于与 MSMQ 交互,涵盖连接和管理本地和远程消息队列,以及通过这些队列发送和接收消息。
图 6:System.Messaging
队列名称格式
有几种方法可以识别 MSMQ 队列——通过路径、格式名称或队列标签。格式名称是队列的原生、唯一表示。路径是队列的友好表示,必须由 MSMQ 目录服务解析为格式名称。通常,MSMQ 理解的队列路径规范的格式采用以下形式之一
队列类型 | 路径语法 |
公共队列 | MachineName\QueueName |
专用队列 | MachineName\Private$\QueueName, .\Private$\QueueName |
日志队列 | MachineName\QueueName\Journal$ |
机器日志队列 | MachineName\Journal$ |
机器死信队列 | MachineName\Deadletter$ |
机器事务性死信队列 | MachineName\XactDeadletter$ |
我们需要能够以 URI 格式来表示这种路径级别的信息,以便与可插入协议基础结构配合使用。不幸的是,MSMQ 路径名在这种情况下似乎没有特别好的匹配。RFC 2396“统一资源标识符 (URI):通用语法”建议禁止 URI 规范中的几个字符,因此在被 .NET Uri
类等实现解析时会被删除。其中一个字符是“\”,这对于 MSMQ 队列名称的格式来说是个问题。事实上,这最终迫使我们使用替代方法来描述我们的 MSMQ 队列资源——在我们的例子中,使用“/”来分割队列名称层次结构的部分。这将在 URI 级别以有效形式保持
msmq://./private$/QueueName
虽然这与 URI 配合得很好,但需要两点
- 开发人员在调用使用此传输的 Web 服务函数之前,需要记住执行此修改
- WebRequest 实现必须将此 URI 友好格式转换回 MSMQ 可用的格式
然而,我们希望以标准方式描述队列——因为它可能在我们的应用程序的更广泛的上下文中被使用,可能从配置文件中读取等等。所以我们需要提供一些帮助。由于我们理想情况下不想区分调用代码请求 MSMQ 和进行请求的代码(或 HTTP 等),我们将向 ISoapClientProtocol
接口和 MySoapHttpClientProtocol
实现类添加一个名为 FormatCustomUri()
的新方法。这将允许代理的调用者继续使用对 MSMQ 友好的队列名称,但会在字符串馈送到 Uri 类之前包装一个简单的 Uri 友好修改。
// Get Queue name (from configuration file perhaps) string strMyQ = ".\\private$\\myq1"; // Create the Service proxy objHelloWorldService = new localhost.Service1(); // Set up the URI objHelloWorldService.Url = objHelloWorldService.FormatCustomUri("msmq://" + strMyQ); … // Make call string[] arrRes = objHelloWorldService.HelloWorldArr("Simon");
需要明确的是,FormatCustomUri()
调用对于 HTTP 和 MQ 规范来说只是一个传递参数——只影响 MSMQ 队列名称格式。
MSMQ 处理
本节主要描述 GetResponse() 方法的核心内容。
首先,我们需要将 SOAP 消息放在请求 URI 中描述的 MSMQ 队列上。如上所述,对于 MSMQ,队列名称的 URI 格式有点尴尬,需要重新格式化才能使其对 MSMQ 友好。
// Create a message System.Messaging.Message objMsg = new System.Messaging.Message(); // Simply hook in the stream as the Body of the message objMsg.BodyStream = m_RequestStream; // Here we need to interpret the MSMQ Uri a little string strQueueName = this.m_RequestUri.LocalPath.Replace("/", "\\"); if (strQueueName.ToUpper().IndexOf("PRIVATE$") > = 0) strQueueName = "." + strQueueName; // Open the Queue for writing too objQueue = GetQ(strQueueName); // Send the message under a single MSMQ internal transaction objQueue.Send(objMsg, MessageQueueTransactionType.Single); // Free resources on main Queue objQueue.Close();
利用 MSMQ 的“单次”事务类型确保一条消息(且仅一条)被发送到队列。为了获得更复杂的异构事务支持(例如,覆盖此消息发送到数据库等的审计),我们需要考虑将此核心工作与 .NET Enterprise Services 集成,以利用 COM+ 事务。请注意,由于我们确定将使用特定于传输的关联,因此在此点捕获并记住出站消息的 MSMQ 消息 ID。我们将依赖后端进程提供此 ID 作为“关联 ID”(MSMQ 和 MQ 都支持此功能),以便我们更容易地查找匹配的响应。
// Get the ID of the outbound message to use as the Correlation ID // to look for on any inbound messages string strCorrelationID = objMsg.Id;
一旦消息被放入队列,我们就等待响应队列中的消息响应,并匹配关联 ID。我们等待调用者定义的周期,调用者可以在创建代理时影响超时。
// Open the response MSMQ Queue and wait for a message try { … // Wait for a response with the Correct Correlation ID TimeSpan tWaitResp = new TimeSpan(0, 0, m_intTimeout / 1000); objQueue = GetQ(strQueueNameResp); msg = objQueue.ReceiveByCorrelationId(strCorrelationID, tWaitResp); } catch (Exception e) { … }
等待响应队列的结果将是以下两种情况之一
1. 发生错误(访问出站/入站队列时出错、等待后端消息响应时超时等)。在这种情况下,从托管 MSMQ 提供程序收到的错误将直接抛给调用者。参考“处理错误”部分提供的通用 WebException 状态代码表,此处列出了主要 MSMQ 失败场景到相应 WebException 状态代码的特定映射
WebException 状态 | 触发事件 |
ConnectFailure | 在 MSMQWebRequest::GetQ() 中打开请求队列失败 |
NameResolutionFailure | 在 MSMQWebRequest::GetQ() 中定位请求或响应队列失败 |
SendFailure | 在 MSMQWebRequest::GetResponse() 中将消息放入请求队列失败 |
Timeout | 在 MSMQWebRequest::GetResponse() 中,在超时时间内未能从响应队列接收到相应的消息 |
ReceiveFailure | 在 MSMQWebRequest::GetResponse() 中,由于非超时原因未能从响应队列接收到相应的消息 |
ProtocolError | MSMQWebRequest / MSMQWebResponse 中的任何其他故障。在这种情况下,将创建一个 MSMQWebResponse ,其中包含确切的失败详细信息。 |
注意,在后一种情况下,将创建一个 MSMQWebResponse
对象并将其作为参数传递给 WebException 类的构造函数——这也会包装实际的异常,该异常将成为 WebException
的“InnerException”。
catch( … ) { // Have we already processed this exception in some way? if ((e is System.Net.WebException) == false) { // No - Create an MSMQWebResponse - this is a protocol specific error objMSMQWebResponse = new MSMQWebResponse( (into)QueueTransportErrors.UnexpectedFailure, e.Message, e.StackTrace); // And throw the exception throw new WebException( "MSMQ Pluggable Protocol failure.", e, WebExceptionStatus.ProtocolError, objMSMQWebResponse); } }
2. 收到 SOAP 消息作为流,创建 MSMQWebResponse
实例并直接使用返回的流进行初始化,为 Web 服务客户端基础结构处理它做准备。
// Create a return, and stream results into it... objMSMQWebResponse = new MSMQWebResponse(); objMSMQWebResponse.SetDownloadStream(msg.BodyStream); … return objMSMQWebResponse;
Websphere MQ 特定实现
与 MSMQ 支持一样,所有工作都在重写的 GetResponse()
方法中完成。
图 7:IBM ActiveX 类用于 MQ
访问 MQ 函数
为了使用 MQ,在 Websphere MQ 5.3 中,我们唯一可行的选择是 MQ 客户端附带的 COM 包装器(MQ 是可选的。演示应用程序可以在任何 HTTP、MSMQ 和 MQ 传输组合上运行。这可在演示中配置)。COM 包装器必须作为 COM 互操作引用包含在我们的项目中。附加说明部分描述了一种可以提高使用 MQ 时的总体吞吐量的替代方案,但目前 COM 包装器就足够了。
请注意,生成的互操作程序集 Interop.MQAX200.dll
在以这种方式包含时未签名,因此任何使用它的程序集都无法放置在 GAC 中,除非能够创建已签名的互操作程序集版本。
可以通过执行以下步骤来实现此签名
- 找到 COM DLL
mqax200.dll
,可以在默认安装中找到,位于C:\Program Files\IBM\WebSphere MQ\bin
文件夹 - 如果开发团队中尚未使用强名称,则使用
sn –k <KEYFILENAME>
生成一个包含强名称密钥对的文件 - 使用
tlbimp
工具处理 COM DLL 文件,指定适当的密钥文件作为选项/keyfile<KEYFILENAME>
并使用/out<OUTFILENAME>
为输出的“已签名”结果程序集提供名称。
完成此操作并将生成的程序集作为项目引用包含后,宿主项目便可以根据需要进行签名并部署到 GAC。
队列名称访问和格式
访问 MQ 队列的模型与 MSMQ 不同。主要方面是,MSMQ 的对象层次结构本质上是“扁平”的,而 MQ 采用分层结构,涉及 3 级访问,包括
- 获取 MQ 会话
- 通过会话连接到命名的 MQ 队列管理器
- 通过队列管理器请求访问命名的 MQ 队列
此外,队列的名称由两部分组成
<queue manager name>/<queue name>
例如 QM_yoda/queue32
这里需要注意的一个主要问题是,与 MSMQ 不同,实体名称区分大小写。事实上,在使用 URI 类(如果您还记得它会将主机名转换为小写)的组合时,会出现一些“陷阱”。在使用框架时,命名问题的迹象是状态码为 NameResolutionFailure
的 WebException
,表示无法解析队列管理器或队列(请参阅下文的详细信息)。以下是一些队列访问示例。
如果连接到名为“fred”的队列(默认队列管理器),则应使用 URI 表示法,如
objHelloWorldService.Url = "mq://fred"
如果连接到名为“QM_john”的队列管理器上的名为“bill”的队列,则 URI 将是
objHelloWorldService.Url = "mq://QM_john/bill"
希望这能让您对 MQ 队列命名法有所了解。为了帮助将 URI 分解为队列管理器/队列名称组合,提供了一个名为 ResolveManagerAndQueueName()
的实用方法,传输代码像这样使用它
// What is the Queue Manager and Queue Name we need to direct the message at? ResolveManagerAndQueueName( this.m_RequestUri, out strQueueManagerName, out strQueueName);
MQ 处理
除了上一节描述的明显语义差异外,此实现与 MSMQ 版本的主要区别在于发送消息和等待响应队列的语法。此外,虽然 MSMQ 在发送和接收消息时为直接使用流提供了良好的支持,但 MQ(至少在使用 ActiveX 包装器时)没有这种支持,我们必须主要依赖发送和接收字节数组。
因此,由于 MQ 不支持将流自动用作消息正文,我们需要将流转换为字节数组,然后可以使用该字节数组来初始化出站消息。
// Access the Soap serialised stream as an array of bytes byte[] bytBody = new Byte[m_RequestStream.Length]; m_RequestStream.Read(bytBody, 0, bytBody.Length);
鉴于我们有 SOAP 正文,并已解析了请求和响应队列管理器/队列名称组合(如上所示),第一件事是获取 MQ 会话。在尝试任何排队工作之前都需要此操作。
// Access a Session objMQSession = new MQAX200.MQSessionClass();
接下来,我们尝试连接到队列管理器,并根据名称为其请求一个队列对象。
// Get the named Queue Manager objQueueManager = GetQManager(objMQSession, strQueueManagerName, true); // Get the named Queue from the Manager objQueue = GetQ(objQueueManager, strQueueName, true);
一旦成功,我们就构造一个消息,设置消息选项,并写入消息正文。
// Set up the message to send objMsg = (MQAX200.MQMessage)objMQSession.AccessMessage(); objPutMsgOpts = (MQAX200.MQPutMessageOptions)objMQSession.AccessPutMessageOptions(); // We need to send a byte array to cover off binary attachments etc.... objMsg.Write(bytBody);
将消息放入队列使用我们刚刚设置的(默认)消息选项。
// Finally, put the message to the queue objQueue.Put(objMsg, objPutMsgOpts);
[注意:如果与在 EBCDIC 中运行的 AS/400 上的 MQ 进行通信,则需要在发送之前明确指示消息是 ASCII 格式 - objMsg.Format = "MQSTR " ;]
请注意,由于我们确定将使用特定于传输的关联,因此在此点捕获并记住出站消息的 MSMQ 消息 ID。我们将依赖后端进程提供此 ID 作为“关联 ID”(MSMQ 和 MQ 都支持此功能),以便我们更容易地查找匹配的响应。
// Get the ID of the outbound message to use as the Correlation ID // to look for on any inbound messages string strCorrelationID = msg.MessageId;
一旦消息被放入队列,我们就等待响应队列中的消息响应,并匹配关联 ID。我们等待调用者定义的周期,调用者可以在创建代理时影响超时。
try { // Open the response MQ Queue and wait for a message objQueueResponse = GetQ(objQueueManager, strQueueNameResp, false); objMsgResp = (MQAX200.MQMessage)objMQSession.AccessMessage(); objMsgResp.CorrelationId = strCorrelationID; objGetMsgOpts = (MQAX200.MQGetMessageOptions) objMQSession.AccessGetMessageOptions(); objGetMsgOpts.Options = (int)MQAX200.MQ.MQGMO_SYNCPOINT + (int)MQAX200.MQ.MQGMO_WAIT; objGetMsgOpts.WaitInterval = this.Timeout; objQueueResponse.Get( objMsgResp, objGetMsgOpts, System.Reflection.Missing.Value); // Use the appropriate reader strMsgRecv = objMsgResp.MessageData.ToString(); } catch (Exception e) { ... }
等待响应队列的结果将是以下两种情况之一
1. 发生错误(访问出站/入站队列时出错、等待后端消息响应时超时等)。在这种情况下,从托管 MSMQ 提供程序收到的错误将直接抛给调用者。参考“处理错误”部分提供的通用 WebException 状态代码表,此处列出了主要 MSMQ 失败场景到 WebException 状态代码的特定映射
WebException 状态 | 触发事件 |
ConnectFailure | 在 MQWebRequest::GetQManager() 中打开请求队列管理器失败或在 MQWebRequest::GetQ() 中打开请求队列失败 |
NameResolutionFailure | 在 MQWebRequest::GetQ() 中定位请求或响应队列失败 |
SendFailure | 在 MQWebRequest::GetResponse() 中将消息放入请求队列失败 |
Timeout | 在 MQWebRequest::GetResponse() 中,在超时时间内未能从响应队列接收到相应的消息 |
ReceiveFailure | 在 MQWebRequest::GetResponse() 中,由于非超时原因未能从响应队列接收到相应的消息 |
ProtocolError | MQWebRequest / MQWebResponse 中的任何其他故障。在这种情况下,将创建一个 MQWebResponse ,其中包含确切的失败详细信息。 |
2. 收到 SOAP 消息作为字节数组,转换为流,然后创建 MQWebResponse
实例并直接用转换后的流进行初始化,为 Web 服务客户端基础结构处理它做准备。
// Convert the result into a byte array byte[] buf = new System.Text.UTF8Encoding().GetBytes(strMsgRecv); MemoryStream stResponse = new MemoryStream(); stResponse.Write(buf, 0, buf.Length); // Create a return, and stream results into it... objMQWebResponse = new MQWebResponse(); objMQWebResponse.SetDownloadStream(stResponse); … return objMQWebResponse;
访问序列化的 SOAP 流
如上所述,在实现 WebRequest
派生类时,我们需要为 SOAP 序列化提供一个流。然后,在我们通过新的传输之一将此 SOAP“请求流”发送到任何网络资源之前,我们必须首先访问此序列化流。
我们上面介绍的 WebRequest
派生类(我们将从中派生 MSMQ 和 MQ 变体)包含一个名为 m_RequestStream
的数据成员,其基本类型为 Stream
,专门用于承载出站 SOAP 流。基础结构将在适当的时候调用我们重写的 GetRequestStream()
方法,以在 Web 服务调用的出站传递阶段创建并提供此流。我们的初始尝试将使用 MemoryStream
类型的成员变量。
然而,(从对代码流程进行一些单步调试可以看出)在 WebRequest
派生类中,在调用我们的 GetResponse()
覆盖之前,无法直接访问请求流(此时请求流已被处置——例如,请参阅 m_RequestStream
的 Length
属性)。
图 8:访问 SOAP 请求流
观察前面案例研究中的 UML 时序图将证实,在我们需要它时,基础结构已经关闭了流。我们需要提出一种替代方法来访问流,然后再将其处置掉。事实证明,Microsoft 基础结构必须 Close()
您提供的流,以便数据被推送到网络——这是 WebRequests
的标准编程模型。
我们有两种选择
- 通过创建和使用
SoapExtension
及其关联的属性,在序列化之后立即截取流。我们可以挂接SoapMessageStage.AfterSerialize
处理阶段来存储序列化的 SOAP 流。这将增加额外的类,导致对(重新)生成的代理进行更多返工,并且在我们开发框架以包含 WSE 时可能会导致问题。 - 提供自定义流,并重写
Close()
来修改流的行为——有效地延迟流的实际关闭,直到我们有机会访问它。这是首选方法,因为它更符合框架。
图 9:MySoapStream
因此,我们提供一个名为 MySoapStream
的类,它抑制流的实际关闭,并提供一个名为 InternalClose()
的替代方法,该方法实际关闭底层流。
public override void Close() { // DON'T CLOSE! BUT REWIND! m_Stream.Position = 0; } internal void InternalClose() { m_Stream.Close(); }
我们还更改 GetRequestStream() 方法的实现以使用这种新类型的流。
public override Stream GetRequestStream() { if (m_RequestStream == null) m_RequestStream = new MySoapStream(new MemoryStream(), true, true, true); else throw new InvalidOperationException("Request stream already retrieved."); return m_RequestStream; }
因此,以 MSMQ WebRequest 处理程序为例,重写的 GetResponse()
略有不同(显示 MSMQ 版本,但两者均适用),如下所示
public override WebResponse GetResponse() { … // Create a message using the Stream objMsg.BodyStream = m_RequestStream; objMsg.Recoverable = true; … // Open the Queue for writing objQueue = GetQ(strQueueName); // Send the message under a single MSMQ internal transaction objQueue.Send(objMsg, MessageQueueTransactionType.Single); … // And close Soap Stream m_RequestStream.InternalClose(); … // Now wait on a response … }
在这种情况下,流可以直接用作 MSMQ 消息体,使用后者的 BodyStream
属性。虽然 MQ 中没有等效的直接流链接,但只需要多一个步骤将流读取到字节数组中,然后就可以直接写入 MQ 消息。
一旦消息发送完毕,我们就可以使用我们的 InternalClose()
方法自己关闭流。
将新协议挂接到生成的代理类
现在我们有了两个新的传输协议处理程序,让它们可从代理类使用。生成的代理现在看起来与其原始的“即时生成”形式略有不同。
public class Service1 : MySoapClientProtocol { ... // The individual methods of the API are unchanged from their generated form [System.Web.Services.Protocols.SoapDocumentMethodAttribute(...)] public string[] HelloWorldArr(string name) { object[] results = this.Invoke("HelloWorldArr", new object[] {name}); return ((string[])(results[0])); } // The individual methods of the API are unchanged from their generated form ... // Overridden methods for directing Soap messages protected override WebRequest GetWebRequest(Uri uri) { // Delegate to a common generator for new protocols return ProxyCommon.GetWebRequest(uri, this); } protected override WebResponse GetWebResponse(WebRequest webReq) { // Delegate to a common generator for new protocols return ProxyCommon.GetWebResponse(webReq); } }
代理现在派生自 MySoapClientProtocol
,包括 GetWebRequest()
和 GetWebResponse()
调用的重写。这是唯一需要的更改。请注意,如果 Web 服务代理稍后因任何原因被重新生成,这些更改将丢失,需要重新应用。
从客户端使用新代理
现在繁重的工作已经完成,代理的客户端可以按如下方式使用它
localhost.Service1 objHelloWorldService = null; string strRequestUrl = "msmq://.\\private$\\myreq"; string strResponseUrl = "msmq://.\\private$\\myresp"; try { // Create the Service proxy objHelloWorldService = new localhost.Service1(); // Set up the request Queue via the Uri objHelloWorldService.Url = objHelloWorldService.FormatCustomUri(strRequestUrl); // Set up an alternative response Queue other than "<QUEUENAME>_resp" objHelloWorldService.ResponseUrl = objHelloWorldService.FormatCustomUri(strResponseUrl); // And the timeout objHelloWorldService.Timeout = vintTimeoutInSecs * 1000; // Run the method and print out the return Console.WriteLine(objHelloWorldService.HelloWorld("Simon")); } catch(Exception e) { ... }
支持异步消息传递
有许多文章描述了如何利用 Web 服务的异步支持(详情请参见文章底部),因此我们不会过多地纠缠于此主题。足可以说,实现异步调用时有三种基本选项
- 轮询完成
- 使用 WaitHandles
- 使用回调
无论选择哪种方法,它们都依赖于在异步调用开始后返回给它们的一个令牌的访问。生成的代理类为每个方法调用提供了同步和异步版本——异步版本包含一对方法,形式为
public System.IAsyncResult BeginHelloWorldArr( string name, System.AsyncCallback callback, object asyncState) { return this.BeginInvoke("HelloWorldArr", new object[] {name}, callback, asyncState); } public string[] EndHelloWorldArr(System.IAsyncResult asyncResult) { object[] results = this.EndInvoke(asyncResult); return ((string[])(results[0])); }
BeginXXX
方法调用将立即返回一个 IAsyncResult
类型的令牌,该令牌可用于轮询或等待请求完成。
虽然不复杂,但异步请求支持被很好地封装在 AsyncUIHelper
这样的程序集中,就像 MSDN 文章 [3] 中提供的。使用此库意味着我们的队列传输不需要自己实现异步功能(至少目前是这样)。
支持的关键方面是一个名为 Asynchronizer
的类。
private Asynchronizer m_ssiReturnImage = null;
方法调用者将包含 Asynchronizer 的使用,如下所示,在本例中,我们首先挂接基于委托的回调,然后调用标准的 ReturnImage()
调用
// Create the Service proxy objHelloWorldService = new localhost.Service1(); // Set up the URI objHelloWorldService.Url = objHelloWorldService.FormatCustomUri(vstrURI); // And the timeout objHelloWorldService.Timeout = vintTimeoutInSecs * 1000; // Create a new Async token m_ssiReturnImage = new Asynchronizer( new AsyncCallback(this.ReturnImageCallback), objHelloWorldService); // Begin the method call IAsyncResult ar = m_ssiReturnImage.BeginInvoke( new ReturnImageEventHandler(objHelloWorldService.ReturnImage), null);
任何异步响应都将定向到一个基于委托的事件处理程序,该委托与我们正在“异步化”的标准同步版本方法的返回类型相同。
protected delegate byte[] ReturnImageEventHandler();
在这种情况下,对调用的响应(即此处的图像数据)可以通过异步令牌中的数据成员访问。
protected void ReturnImageCallback(IAsyncResult ar) { localhost.Service1 objHelloWorldService = (localhost.Service1)ar.AsyncState; ... AsynchronizerResult asr = (AsynchronizerResult) ar; byte[] bytImage = (byte[])asr.SynchronizeInvoke.EndInvoke(ar); ... }
假装 - “后端服务”
到目前为止,我们主要关注客户端如何以正确的方式与请求和响应队列进行通信。这几乎毫无意义,除非我们有一些服务来接收消息并处理它们。在实际的解决方案中,人们期望 Web 服务用于异构环境,其中客户端和服务器运行在不同的平台上,而 Web 服务是连接这两个世界的良好方式。
然而,由于希望将跟上本文所需的技能集保持在可管理的范围内,我们将使用 .NET 应用程序在后端来消费消息并产生(虚拟)响应,以便测试客户端的消息接收。应该注意的是,该服务纯粹是为了作为我们开发的客户端框架的对等点而存在,因此在使其健壮或可伸缩性方面付出的努力非常少——但有足够的内容来演示客户端的功能。
在我们目前描述的非 WSE 领域中,服务实际上可以相当基础。它的工作是执行以下步骤
- 读取包含 MSMQ / MQ 队列的配置文件,等待消息
- 在短时间内循环队列以等待消息
- 对于任何已确认的消息,将手动生成一个响应 SOAP 流并将其放入响应队列,以便客户端拾取。
逻辑包含在一个类 BEService
中。
它首先读取提供的 app.config 文件以获取所有应用程序级别的设置(即 <appsettings>
节点内的设置)。
// Read configuration settings NameValueCollection colSettings = ConfigurationSettings.AppSettings;
在配置文件中找到的每个队列都将被添加到相应的存储桶中。
string[] arrVals = colSettings.GetValues(strKeyName); if (arrVals[0].ToLower().IndexOf("msmq://") == 0) arrMSMQQueuesToMonitor[intNumMSMQQueues++] = arrVals[0].Replace("msmq://", string.Empty); else if (arrVals[0].ToLower().IndexOf("mq://") == 0) arrMQQueuesToMonitor[intNumMQQueues++] = arrVals[0].Replace("mq://", string.Empty);
MSMQ 和 MQ 队列以及轮询延迟(后端服务目前不使用线程池)将从集合中提取。然后进入以下简单循环
// Wait on some queues - some MSMQ, and some MQ... for (;;) { // Do MSMQ queues first foreach (string strName in arrMSMQQueuesToMonitor) { WaitOnMSMQQ(strName, intPollDelay); } // Now MQ queues foreach (string strName in arrMQQueuesToMonitor) { WaitOnMQQ(strName, intPollDelay); } }
WaitOnMSMQ 实现
此方法很简单。首先,我们将所有工作包装在一个 try-catch 块中,以捕获所有异常并过滤掉“健康”的异常,例如等待请求消息时超时。
try { … // Do the work here } catch(Exception e) { // Ignore timeouts as this simply means no queue message was there if (e.Message.IndexOf("Timeout for the requested operation has expired") < 0) TSLog("Exception caught in WSAltRouteBEFake[MSMQ]: " + e.Message + e.StackTrace); }
我们首先等待一段时间。
// We'll need to wait a little time TimeSpan tWaitResp = new TimeSpan(0, 0, 0, 0, vintPollTimerDelay); // Get hold of the request queue MessageQueue objQueue = GetQ(vstrQueueName); // Wait for a message Message objRequestMsg = objQueue.Receive( tWaitResp, MessageQueueTransactionType.Single); // Close the Queue objQueue.Close();
假设我们收到了一条消息,即没有抛出异常,我们就打开消息并将数据内容读入字节数组,同时提取一些显著的属性,例如潜在的关联 ID 和响应队列名称。
// Get the message content direct from the BodyStream string strResp = string.Empty; byte[] bufIn = new Byte[objRequestMsg.BodyStream.Length]; objRequestMsg.BodyStream.Position = 0; objRequestMsg.BodyStream.Read(bufIn, 0, (int)msg.BodyStream.Length); string strCorrelationId = objRequestMsg.Id; // Close stream objRequestMsg.BodyStream.Close(); // Get the response queue - we shall assume a default but // hope the client has identified a queue for us to call back on string strResponseQueue = vstrQueueName + "_resp"; if (objResponseMsg.ResponseQueue != null) strResponseQueue = objRequestMsg.ResponseQueue.QueueName;
然后我们检查这是否是已知消息,如果是,则为其构建一个响应消息。匹配方法或响应消息的生成中的智能非常少——它纯粹是为了服务客户端响应代码而存在。
// 假装响应消息 strResp = BuildFakeResponseFromBytes("MSMQ" , bufIn, strResponseQueue);
如果我们识别出请求并能够为其生成响应,则通过响应队列使用关联 ID 将其发送回客户端。
// If we have a valid response...use it if (strResp.Length > 0) { // Get the response queue objQueue = GetQ(strResponseQueue); // Send a message back MemoryStream stResp = new MemoryStream(); stResp.Write(new UTF8Encoding().GetBytes(strResp), 0, strResp.Length); Message objMsg = new Message(); objMsg.BodyStream = stResp; objMsg.Recoverable = true; objMsg.CorrelationId = strCorrelationId; // Send the message under a single MSMQ internal transaction objQueue.Send(objMsg, MessageQueueTransactionType.Single); // Free resources on response Queue objQueue.Close(); }
在这里,我们再次准备使用 MSMQ 消息的 BodyStream
属性,所以我们将响应字符串的内容写入流。我们再次作为单个 MSMQ 事务发送,然后清理响应队列。
这就是通过队列介质实现基本端到端 Web 服务处理所需要的一切。我们可以在此时停止,或者利用一项非常有趣的新技术,它可以帮助我们保护消息,而与我们运行它们的基础传输无关。
Web Services Enhancements (WSE) 如何改变客户端和服务器的图景
WSE 是 Microsoft 对一些第一代 GXA Web 服务规范的初始实现——WS-Security、WS-Attachments(通过 DIME)和 WS-Routing / WS-Referral。它于 2002 年 12 月发布,经过 4 个月的 Beta 测试。整个标准化推动是改进 Web 服务堆栈中交互/互操作能力倡议的延续,而这些特定标准的应用将真正构成基于 Internet 的入门级交易伙伴应用程序的基础。
虽然预计通过排队传输的 SOAP 将在单个组织的受信任网络内使用,但这并不排除在企业应用程序之间传输数据时需要保护数据的可能性(例如,敏感的薪资数据)。
因此,WSE 在本文中对我们来说最有趣的主要方面源于 WS-Security 规范的实现;即能够
- 控制 SOAP 消息的“有效生命周期”,
- 生成带有相关密码的“用户名令牌”,用于用户验证,
- 使用该令牌签署 SOAP 有效负载以允许篡改检查,
- 使用对称密钥(称为“共享密钥”,因为后端服务将访问相同的密钥)加密 SOAP 主体的有效负载,
- 保护 SOAP 请求和响应,使其成为端到端解决方案。
应该注意的是,此功能代表了 WSE 功能的子集。我们在此处未使用的一些有趣功能包括使用二进制安全令牌(例如,可以包含 Kerberos 票证)和使用 X509 证书进行非对称加密。
此外,由于我们正在使用 WSE,而 WSE 实现了 WS-Attachments 规范,因此我们可以扩展我们的演示应用程序以探索通过 DIME 支持添加二进制附件的能力。DIME 支持不遵循标准的 SOAP 处理模型——期望在未来 12 个月内看到其实现发生变化。
WSE 概述
WSE 是一个数据处理引擎,它本质上(在指令下)对 .NET Web 服务基础结构序列化后的出站 SOAP 消息应用一系列转换,反之亦然,在入站 SOAP 消息反序列化为对象/参数之前应用进一步的转换。这些转换涉及包含额外的 SOAP 头部,在某些情况下(如加密)还会修改 SOAP 消息的主体。
WSE 提供的 API 相对较低级别,这使得它非常适合影响客户端和服务器端的操作。一篇关于 WSE 内部工作的精彩文章可以在 [4] 中找到。我们将在以下各节中提炼出关键点,为我们将对客户端和服务器进行的更改提供一些背景。
WSE 1.0 的物理表现是一个名为 Microsoft.Web.Services.dll
的单个 GAC 安装程序集,它实现了上述三个规范的大部分内容。
WSE 过滤器管道架构
Beta 版软件(称为 WSDK,于 2002 年 8 月发布)和 WSE 1.0 之间的最大变化是,影响 SOAP 消息转换的功能已重新打包为一系列过滤器。这些过滤器有两种类型:输出过滤器专注于处理出站 SOAP 消息,而输入过滤器则处理入站 SOAP 消息。
图 10:过滤器管道架构
输入过滤器 | 输出筛选器 | 目的 |
TraceInputFilter |
TraceOutputFilter |
将消息写入日志文件以帮助调试 |
SecurityInputFilter |
SecurityOutputFilter |
身份验证、签名和加密支持 |
TimestampInputFilter |
TimestampOutputFilter |
时间戳支持 |
ReferralInputFilter |
ReferralOutputFilter |
路由路径的动态更新 |
RoutingInputFilter |
RoutingOutputFilter |
消息路由 |
每组筛选器都组织成一个输入筛选器或输出筛选器管道,这些管道在开箱即用时会按照以下顺序运行筛选器(仅适用于客户端到服务器流)
图 11:筛选器管道架构详解(客户端 -> 服务器流)
除其他事项外,这确保了加密和签名(安全)应用于 SOAP 载荷,该载荷已完成所有其他修改,并且安全操作发生在请求发送之前,而解密和签名验证则发生在接收请求之后立即进行,在任何其他数据敏感操作可以发生之前。
这里要记住的主要一点是,每个管道都对 SOAP 信封进行操作,以在该信封内生成不同的内容。
WSE 中的管道概念非常强大,因为管道可以以编程方式进行操作以移除或重新排序阶段,并且可以基于 WSE 提供的基类 SoapOutputFilter
和/或 SoapInputFilter
引入新的自定义管道阶段。
WSE 管道输入和输出筛选器执行与标准 SOAP 序列化和传递机制的集成需要以两种不同的方式进行管理
- 对于 ASP.NET Web 服务客户端,这是通过将生成的代理类派生自一个名为
Microsoft.Web.Services.WebServicesClientProtocol
的新代理基类来实现的。 - 对于 ASP.NET Web 服务,提供了一个新的服务器端 SOAP 扩展,形式为
Microsoft.Web.Services.WebServicesExtension
。
新的代理基类和服务器端扩展的目的是位于传输和 SOAP 序列化/反序列化模块之间,拦截消息并根据需要构建/分析 SOAP 标头。因此,当使用 Web 服务进行签名验证时,如果此验证检测到篡改并生成适当的 SOAP 故障,应用程序业务逻辑甚至一行代码都不会在生成故障之前被调用。这很好地将应用程序代码与 WSE 的基础结构代码隔离开来,这似乎是一个吸引人的好处。
WSE SoapWebRequest 和 SoapWebResponse
WSE 本身采用了我们之前讨论过的 .NET Web Services 可插拔协议基础结构,提供了两个新的通信类,名为 SoapWebRequest
和 SoapWebResponse
。与我们的 MSMQ 和 MQ 版本一样,它们分别派生自 System.Net.WebRequest
和 System.Net.WebResponse
。
SoapWebRequest
类将包含标准序列化 SOAP 消息的请求流解析为 WSE 提供的另一个名为 SoapEnvelope
的类的实例,该类派生自 System.Xml.XmlDocument
。然后,它驱动标准的输出筛选器管道,该管道按照上一节所述对信封内容进行条件处理。
我们在框架中的兴趣将围绕将我们的基于队列的类与这些新的 SOAP Web 请求/响应类集成,特别是获取生成的 SOAP 流以供我们的协议处理程序使用。
然而,对于我们的后端服务,SoapWebRequest
和 SoapWebResponse
对我们来说无关紧要,因为我们运行在客户端或 ASP.NET 服务器基础结构之外,并且必须自己“手工制作”WSE 处理。
用于通信的 WSE SoapContext
我们已经讨论了 WSE 的操作,但迄今为止还没有讨论这会受到我们可能编写的任何应用程序代码如何影响。SoapContext 类是应用程序代码和 WSE 格式化代码间接通信的载体。这取决于过程中的哪个点,有两种方式可以实现:
- 在出站处理期间构建 SOAP 标头时,应用程序首先对
SoapContext
的属性进行预置,这些属性将成为筛选器操作的指令,即在这种情况下,是SoapContext
属性决定了出站 SOAP 消息的内容。 - 在入站处理期间分析标头时,WSE 会用入站筛选的“结果”预置
SoapContext
,然后再将控制权交给服务器端应用程序,即在这种情况下,是传入 SOAP 消息的内容决定了SoapContext
的设置。
该类包含几个有趣的属性
图 12:WSE Soap Context
Envelope 包含 XML,标记为“正在进行的工作”,因为筛选器正在运行。Security 属性是附加加密 KeyInfo 和签名信息的位。Attachments 包含 DIME 附件——稍后会详细介绍。
您可能会毫不意外地发现,客户端和服务器端都支持通过客户端的 SoapWebRequest
类暴露 SoapContext
,以及在 ASP.NET Web 服务中的静态 HttpSoapContext.RequestContext
和 HttpSoapContext.ResponseContext
类。因此,这对我们来说部分是可以的,因为我们的客户端应该能够操作 SoapContext
,希望筛选器能相应地受到影响。然而,像往常一样,后端服务不是托管在 ASP.NET 中,所以我们必须希望在这种环境中还有其他方法可以检索 SoapContext
。
SecurityProvider 架构
如前所述,WSE 基础结构基于拦截,并且它将安全检查与任何应用程序业务逻辑完全分开。这就引出了一个有趣的问题,因为发送方将进行“用户令牌”的生成,该令牌包含用户名和关联的(在本例中是哈希的)密码,签名并对称加密 SOAP 主体等,但如果 SOAP 载荷要成功处理,就必须了解用户名/密码和相应的解密密钥,以实现目标端。
WSE 在其入站处理中提供了钩子来实现这一点。具体来说,它提供了两个接口,IPasswordProvider
和 IDecryptionKeyProvider
。前者将由应用程序代码实现,以提供给定用户的密码,通常来自用户数据库。后者将用于提供解密密钥。如果 SOAP 标头指示消息已被签名和/或加密,则两者都将在 WSE 入站消息处理期间被调用。
图 13:WSE 安全提供程序架构
WSE 允许开发人员为这两个钩子编写代码,然后通过相关应用程序配置文件中的信息使 WSE 运行时了解它们。根据环境,有三种可能性作为这些文件的来源:
- 对于 ASP.NET Web 服务,它是 web.config 文件
- 对于 ASP.NET 客户端,它是 app.config 文件
- 对于独立程序,它也是应用程序配置文件
两个提供程序的条目将如下所示:
<microsoft.web.services> <security> <passwordProvider type="WSCommon.WSESecurityProvider, WSAltRouteBEFake" /> <decryptionKeyProvider type="WSCommon.WSESecurityProvider, WSAltRouteBEFake" /> </security> </microsoft.web.services>
每个条目都指示了 Type(类)的名称及其所在的程序集。因此,对于双向安全(即 SOAP 请求和响应消息),人们期望在服务端(接收 SOAP 请求时使用)和客户端(接收 SOAP 响应时使用)都能找到类似的条目。
适用于 .NET 的 WSE 1.0 设置工具
这些提供程序的灵活性唯一的问题是,很容易出错——或者也许只是我!无论如何,为此(不仅仅是给我),Microsoft 还提供了一个不受支持的 VS.NET 插件 [8],它可以更轻松地添加这些设置。安装后,当右键单击解决方案资源管理器中项目级别的节点时,会出现一个新的上下文菜单项。这允许开发人员启用 WSE 安全,然后在对话框中指定提供程序信息,并将其注入到相关的配置文件中。在这种情况下,我们已经表明,我们的后端服务将包含一个名为 WSCommon.WSESecurityProvider
的类,该类将处理密码和解密密钥的解析。
请注意,作者建议该工具仅适用于 WSE 1.0,因此可能不适用于 WSE 的未来更新,但届时我们应该都对配置设置更加熟悉了。
更新客户端框架以使用 WSE
好了,理论就到这里。需要进行一系列特定的更改才能使我们的框架能够处理非 WSE 和 WSE 请求。
添加 MySoapClientProtocol 的补充类
由于 WSE 期望代理从 Microsoft.Web.Services.WebServicesClientProtocol
派生以调用筛选器管道处理,因此我们需要为我们的框架引入一个补充类,名为 MyWSESoapClientProtocol
,它还将支持 ResponseUrl
属性。
图 15:从客户端访问 WSE
对生成的代理的更改
对代理代码的唯一更改涉及重新基于 MyWSESoapClientProtocol
,并对重写的 GetWebRequest()
方法调用进行微小调整,以及移除重写的 GetWebResponse()
方法调用。
public class Service1 : MyWSESoapClientProtocol { // All API method calls remain unchanged … // Overridden portions protected override WebRequest GetWebRequest(Uri uri) { // Register our scheme if needs be ProxyCommon.RegisterRelevantScheme(uri, this); // And do the default thing using SoapWebRequest, which will integrate // nicely with our MSMQ / MQ WebRequest / WebResponse classes when it // needs to. return base.GetWebRequest(uri); } }
这得益于 SoapWebRequest
的强大功能,它能够无缝地挂接到 WSE 的筛选器处理代码,然后将生成的 SOAP 信封传递给我们的传输层进行进一步的传输。框架代码的其余部分(我们的 MSMQWebRequest
、MQWebRequest
等)完全不需要更改。
对调用代码的更改
当然,正如我们之前讨论过的,应用程序需要设置与要进行的调用关联的 SoapContext
对象。这是通过向客户端添加一个私有方法来实现的,该方法将基本信息添加到 SoapContext
。然后,调用者将按如下方式发起调用:
// Create the Service proxy objHelloWorldService = new localhostWSE.Service1(); // Set up the URI objHelloWorldService.Url = objHelloWorldService.FormatCustomUri(vstrURI); // Augment request context with WSE goodies via our common routine // This includes: // // * Time-to-live of 60s // * User name and password credentials // * Encryption and Signing // // To achieve this we shall be prodding attributes into an initially empty // SoapContext!! ProxyCommonWSE.SetupSOAPRequestContext(objHelloWorldService.RequestSoapContext); // Run the method and get the return string strResult = objHelloWorldService.HelloWorld("Simon"); // Verify that a SOAP request was received, with valid credentials // and a signature via our common routine – we will be doing this by // interrogating the generated SoapContext!! ProxyCommonWSE.ValidateCredentials(objHelloWorldService.ResponseSoapContext);
为了说明 SoapContext
对象是如何预置的,这里是私有方法 SetupSOAPRequestContext()
的一部分:
… // Get a symmetric encryption key to encrypt the Soap body with EncryptionKey encKey = GetEncryptionKey(); … // Create a User Token from a tick count and static User Name UsernameToken userToken = new UsernameToken( strUserName, CalculatePasswordForProxyUser(strUserName), PasswordOption.SendHashed); // Stick it in the Soap Context vobjSoapContext.Security.Tokens.Add(userToken); // Request a signature for the message (based on the User Token in our case) vobjSoapContext.Security.Elements.Add( new Microsoft.Web.Services.Security.Signature(userToken)); // Request the Soap body be encrypted (with a separate symmetric key in our // case) vobjSoapContext.Security.Elements.Add( new Microsoft.Web.Services.Security.EncryptedData(encKey)); // Set an expiry on this SOAP message too vobjSoapContext.Timestamp.Ttl = SOAP_MSG_EXPIRY_TIME; // 60s
要点包括能够发送 160 位 SHA-1(不可逆)密码哈希。其思想是,一旦远端调用了 SecurityProvider
钩子,这个哈希将与计算出的版本进行测试。签名和加密使用不同的密钥进行请求,并且消息将在 SOAP_MSG_EXPIRY_TIME
(60 秒)后过期。
在加密和/或签名 SOAP 消息时应谨慎。存在选项可以表达消息应被保护的粒度,从整个消息到单个 SOAP 消息元素。虽然我们在这里没有利用 WSE 中的路由功能,但读者应该意识到,在保护预期会流经中间节点以到达最终目的地的消息时应谨慎。主要是因为 SOAP 处理模型允许中间阶段移除 SOAP 标头。因此,例如,如果整个消息(包括标头)被签名,然后从处理节点 A 通过节点 B 传递到节点 C,则节点 B 可能能够通过移除导致最终目的地签名验证失败的标头来意外使消息无效。
这里应该特别说明一下,当在一个场景中运行一个派生自 WebServicesClientProtocol
(通过我们的 MyWSESoapClientProtocol
)的代理,但 SoapContext
对象在调用之前没有设置任何内容时,观察到的当前奇怪行为。在这种情况下,人们可能期望非 WSE 启用的接收应用程序能够愉快地处理 SOAP 消息。事实并非如此。到目前为止,已经观察到两个问题,都围绕着奇怪的标头行为。
- 在运行任何 Web 服务方法调用时,您将收到“路径不包含 <action> 元素”异常。这似乎只发生在自定义传输场景中——标准的 HTTP 传输似乎内部处理了这个问题。
- 通过将路由路径预置为类似
vobjSoapContext.Path.Action = string.Empty
的内容来显式设置此项,尽管人们可能期望不会向 SOAP 消息添加其他内容,因此对于未运行 WSE 兼容工具包的后端,消息应该可以被愉快地处理。事实并非如此。除了路由信息之外,还会添加时间戳标头信息,并且我们已经看到一些 Java 后端服务对这些信息以及此路由信息反应非常奇怪,主要是因为soap:mustUnderstand="1"
属性嵌入在代表后者的 SOAP 片段中——这会告诉接收端如果它无法理解具有此属性的特定标头,则终止处理。
这很遗憾,因为这意味着我们不能从单个类(MyWSESoapClientProtocol
)派生代理,并使用对我们 SetupSOAPRequestContext()
方法的调用是否存在来控制 SOAP 消息的安全性,而是需要两个代理版本,一个派生自 MyWSESoapClientProtocol
,另一个派生自 MySoapClientProtocol
。此外,我们必须知道何时使用它们(主要是在设计时)。据我所知,这些问题是已知的,但构成了一些需要随着标准成熟而被解决的进一步问题。
soap:mustUnderstand
标志可以由消息的发送者影响,因此可以解决部分问题。
更新后端服务以使用 WSE 进行安全通信
鉴于我们在本文前面描述的服务仅仅是一个从队列中提取消息的可执行文件,它明确地存在于 ASP.NET Web 服务提供的“茧”之外,因此,我们必须使用 WSE 的原始管道设施来驱动输入筛选器管道,以验证签名并解密内容以获取载荷。反之,由于我们支持双向安全,因此在加密和签名服务在返回 SOAP 响应之前启动它们,我们也必须驱动输出筛选器管道(基本上是在传统的 HTTP 托管 ASP.NET Web 服务中免费完成的所有工作)。
准备 SecurityProvider
需要在“SecurityProvider 架构”部分讨论的配置定义。这会在要求用户密码和解密对称密钥时将 WSE 指向正确的位置。
调用入站消息管道进行请求
假设我们收到了一条消息,即没有抛出异常,我们就打开消息并将数据内容读入字节数组,同时提取一些显著的属性,例如潜在的关联 ID 和响应队列名称。
// Get the message content direct from the BodyStream string strResp = string.Empty; byte[] bufIn = new Byte[objRequestMsg.BodyStream.Length]; objRequestMsg.BodyStream.Position = 0; objRequestMsg.BodyStream.Read(bufIn, 0, (int) objRequestMsg.BodyStream.Length); string strCorrelationId = objRequestMsg.Id; // Process through WSE - this may be a passthru operation // if WSE was not used to create the stream // Hmmm - we need to see if this is DIME based or not // To do this we shall simply see if we can read the stream // as a DIME stream - creating an Envelope in any case SoapEnvelope env = InputStreamToEnvelope(objRequestMsg.BodyStream); // Close stream objRequestMsg.BodyStream.Close(); // Create Pipeline - with default filter collection for input and output // (i.e. we are not overriding default behaviour in any way) Pipeline objWSEPipe = new Pipeline(); // Run the default input pipeline objWSEPipe.ProcessInputMessage(env); // Extract the resultant Envelope bufIn = new UTF8Encoding().GetBytes(env.OuterXml); // Get the response queue - we shall assume a default but // hope the client has passed us a queue to call back on string strResponseQueue = vstrQueueName + "_resp"; if (objResponseMsg.ResponseQueue != null) strResponseQueue = objRequestMsg.ResponseQueue.QueueName;
粗体文本显示了从入站消息流中反序列化 SoapEnvelope
对象。一旦我们有了这个信封(稍后我们会详细介绍如何获取它),我们就可以创建一个 WSE 管道并对信封运行默认的输入筛选器。我们不关心输入管道的内容(尽管,由于我们知道我们不使用路由,我们实际上可以以编程方式从输入管道中移除路由和推荐筛选器,这会略微提高管道运行速度)。
ProcessInputMessage()
调用几乎是一个原子操作,它将在 env
变量中留下一个明文版本的 SOAP 消息。作为运行的副作用,信封对象中的 Context
属性将已用对入站 SOAP 消息的解释填充。
一个非常酷的功能是,如果入站 SOAP 消息中存在加密或签名标头,我们的 SecurityProvider
支持将在管道运行期间自动挂接到此过程。
一旦管道处理完成,我们的输入缓冲区就会被管道运行的结果填充,准备好供我们应用程序代码的其余部分处理。
调用出站消息管道进行响应
一旦请求消息被匹配并且响应消息被生成,我们就需要决定是否要保护后者。可以通过合成的 env.Context
属性来判断 WSE 是否参与了请求消息的制定,它是一个 SoapContext
类的实例,我们在前面几节中讨论过。
// Try and ascertain if the WSE was involved... if (vobjSoapContext != null) { if (vobjSoapContext.Attachments.Count > 0 || vobjSoapContext.Security.Elements.Count > 0 || vobjSoapContext.Security.Tokens.Count > 0) strWSEIsInvolved = " {WSE}"; }
请记住,我们已决定支持双向安全消息。因此,上述逻辑是确定是否要保护响应消息所需的所有内容。
// Should we apply WSE on this message response before giving it // over to transportation? We only want to do this if we have a response // message to operate on AND the request was WSE'd if (strResp.Length > 0 && strWSEIsInvolved.Length > 0) { // Yes....Process through WSE // Create an Envelope SoapEnvelope env = new SoapEnvelope(); // Get the basic Soap information into it env.LoadXml(strResp); // Augment request context with WSE goodies // This includes: // // * Time-to-live of 60s // * User name and password credentials // * Encryption and Signing // Add the instance of EncryptedData to the SOAP response. WSCommon.SetupSOAPRequestContext(env.Context); // Create Pipeline - with default filter collection for input and output // (i.e. we are not overriding default behaviour in any way) Pipeline objWSEPipe = new Pipeline(); // Run the default output pipeline objWSEPipe.ProcessOutputMessage(env); // Extract the resultant Envelope strResp = env.OuterXml; }
可以看到,我们在运行输出管道之前对 env.Context
属性进行了预置。我们使用与客户端使用的“调用代码的更改”一节中描述的 SetupSOAPRequestContext()
例程的副本。
更新后端服务以使用 WSE 进行二进制附件
我们可以使用 WSE 发送和接收不需要在 SOAP 信封中编码为 base64Binary
的二进制附件。因此,当我们阅读 SOAP 输入消息时,我们将查找一个或多个传递给我们的 DIME 消息的指示。
DIME 是一个描述在 SOAP 信封外携带二进制内容并在其中引用的能力的规范。这有利有弊——二进制信息不会被编码,因此比等效的 API 参数(例如 byte[]
类型)的 Web 服务表示要小。然而,在 SOAP 信封外携带会导致两个主要问题:它不能作为参数表达,也不能真正地在 WSDL 中描述,更糟糕的是,没有办法将相同的 SOAP 处理模型(包括签名和加密措施)应用于 DIME 内容。目前我们只能接受这些限制,但请密切关注。
[注意:目前正在进行工作,以整合 DIME 的工作方式与拥有单一 SOAP 处理模型的愿望。预计二进制附件将回到 SOAP 信封内,在那里它们可以受到与其他元素相同的规则的约束。我们仍然预计它们会通过此更改(以某种方式)保留其高效的非 Base64 编码格式。]
最后,应该理解的是,DIME 处理是唯一完全独立于 WSE 筛选器处理实现的领域(实际上是因为它有一个与标准 SOAP 模型完全不同的处理模型,该模型旨在处理 SOAP 信封的内容),因此我们需要做一些工作来以正确的方式预置 SoapContext.Attachments
集合属性。这在 InputStreamToEnvelope()
方法中实现,我们在呈现“调用入站消息管道进行请求”一节时有所忽略。
// NOTE – some code omitted to keep size down SoapEnvelope env = new SoapEnvelope(); try { // Create reader for DIME message DimeReader dr = new DimeReader(vstInputMessage); // Try to read record containing SOAP message. If this fails with a DIME // version error, the stream was most probably not a DIME one DimeRecord rec = dr.ReadRecord(); // Read Soap message from DIME record env.Load(rec.BodyStream); // Now add any Attachments we find in this stream to the Soap Context while (rec != null) { // Read the next record rec = dr.ReadRecord(); // Did we? if (rec != null) { // Here is our item - pull out the binary data representing it BinaryReader stream = new BinaryReader(rec.BodyStream); … // And store in a new stream … stNew.Write(bytAttachmentItem, 0, bytAttachmentItem.Length); // ... which we will attach to the Soap Context DimeAttachment objBin = new DimeAttachment( rec.Id, rec.Type, rec.TypeFormat, stNew); env.Context.Attachments.Add(objBin); } } } catch(Exception) { // If we get an exception here, assume this stream was not DIMEd // Now just load the whole stream into an array of bytes byte[] bufIn = new Byte[vstInputMessage.Length]; vstInputMessage.Read(bufIn, 0, (int)vstInputMessage.Length); // ...and from there into the Soap envelope env.LoadXml(new UTF8Encoding().GetString(bufIn)); } return env;
完成上述加载后,后端代码对 SoapContext.Attachments
集合属性的内容做出反应如下:
// Look for the DIME attachment - there should only be one DimeAttachmentCollection colAttachments = vobjSoapContext.Attachments; // Any attachments found? if (colAttachments.Count > 0) { // Get the first attachment only – hey, it's just a demo! // Here is our item - pull out the binary data representing it BinaryReader stream = new BinaryReader(colAttachments[0].Stream); byte[] bytAttachmentItem = new byte[stream.BaseStream.Length]; stream.Read(bytAttachmentItem, 0, bytAttachmentItem.Length); stream.BaseStream.Close(); stream.Close(); // Write the binary data to a file – we know it's a gif, but could // check the type as described in this DimeAttachment object. FileStream fs = new FileStream(vstrLocOfImages + "SendDIMEImage" + vstrScheme + strWSEIsInvolved + ".gif", FileMode.Create); fs.Write(bytAttachmentItem, 0, bytAttachmentItem.Length); fs.Close(); }
客户端选项摘要——使用 WSE 还是不使用 WSE
此表试图阐明我们在上面详细描述的各种使用基于队列的传输的场景中对生成代理及其调用者提出的要求。
场景 | 对生成代理的要求 | 代理调用者的要求 |
无 WSE |
|
|
WSE |
|
|
队列世界中的并发
下面概述的示例应用程序已在“多客户端 - 单服务器”模式下进行了测试。在此模式下,客户端同时将消息放入“单个请求”队列,并同时在不同的“响应队列”上等待响应。目的是为了找出队列和消息识别与访问方面任何多用户问题。
首次测试时,MQ 传输表现完美,最多可处理 30 个客户端(此时我失去了兴趣!),但 MSMQ 传输经常抛出以下形式的异常:
“光标当前指向的消息已从队列中移除”
原因似乎是 ReceiveByCorrelationID()
API 是非事务性的,因此在活动线程识别消息(通过相关 ID)所需的时间内,标记消息的光标可能已失效。虽然这没有任何科学依据,但观察表明,当 20 个进程都在等待同一 MSMQ 队列上的 100 条相关消息时,上述异常平均每运行(2000 条消息读取)抛出 2.7 次——错误率为 0.135%。
如果发生这种情况,则必须重试接收。因此,代码中存在循环。
框架打包
本文所描述的构成框架的类被打包到一个名为 WSQTransports.dll
的程序集中。
图 16:WSQTransports 打包
最左边的类型构成了对附加功能的支持,例如 ResponseUrl
。其他类型构成了我们对基于队列的可插拔协议的实现。程序集已签名,使其有资格加入 GAC。
示例应用程序
由于工作的性质更侧重于一项技术而非其实际应用,因此演示很简单,它显示了 2 个控制台应用程序——客户端和服务器——之间使用我们上面开发的框架进行消息交换的交互。
演示显示了通过各种标准数据类型和 DIME API 组合,传递“明文”和“WSE 安全”的 SOAP 消息。虽然演示很基础,但它确实很好地说明了各种概念。
图 17:演示运行两个客户端和一个后端 MQ / MSMQ 服务
演示显示后端服务处理 MSMQ 和 MQ 队列上的传入 SOAP 消息并返回响应。还包括对非平凡数据类型(实际序列化的自定义对象)的处理。截图实际上显示了基于名为“Product”的任意类的复杂类型的处理。它显示了这些“Product”的数组被序列化并在服务器和客户端之间传递。在上图的截图中,可以看到两个客户端通过队列和 HTTP 运行 SOAP 调用。
为了评估可能的并发水平,启动后端服务的一个实例,然后启动任意数量的测试客户端实例进行测试。所有客户端实例都应该在没有任何异常的情况下完成其测试运行。由于使用相关 ID 将请求和响应联系起来,因此客户端保证它收到了正确的响应,并且在消息获取请求下不应该出现光标移动问题。
应用程序简介
下图介绍了示例应用程序的离散部分,并描绘了它们在客户端-服务器模型中的契合度。
图 18:文章交付物
有两个 ASP.NET Web 服务(一个启用了 WSE),它们支持一个基本 API,该 API 由以下内容组成:
- 返回字符串和字符串数组的 HelloWorld 变体
- 二进制数据操作——发送和接收图像
- 复杂数据类型序列化——一个 ProductService 提供 Product 实例
- [对于 WSE 版本的服务] 使用 WSE 中的 DIME 支持发送图像
这些服务的 IIS 虚拟目录称为 WSAltRoute
和 WSAltRouteWSE
。
虚拟后端(WSAltRouteBEFake.exe
)也位于服务器空间——它的功能与上述 ASP.NET Web 服务非常相似,但它响应来自队列的消息而不是通过 HTTP。
实现了可插拔队列协议的框架代码位于单个程序集(WSQTransports.dll
)中,以便可以在多个项目中重复使用。
框架由一个名为 WSAltRouteTest.exe
的示例控制台应用程序驱动,该应用程序测试各种 API 在各种传输上的各种版本。
异步操作(未显示)的支持由一个名为 AsyncHelper.dll
的程序集提供。
MSMQ 和 MQ 队列使用一个名为 WSQSetup.exe
的工具进行配置(未显示)。
应用程序设置
zip 文件包含下面章节中描述的几个项目。部署的关键操作是:
- 确保安装了所需的先决条件(尤其是 MQ 和 WSE)
- 解压文件并保留文件夹结构
- 找到
\Bin
文件夹 - 运行
WSQSetup.exe
可执行文件以创建相关队列。按照屏幕上的提示操作。 - 找到
\Client
子文件夹 - 编辑
WSAltRouteTest.exe.config
文件,审阅并进行任何更改。条目列表是:条目 描述 NoTrace
如果为 true,则在发生异常时不转储堆栈跟踪 EnableHTTPCalls
如果为 true,则通过 HTTP 传输进行 API 调用 EnableMSMQCalls
如果为 true,则通过 MSMQ 传输进行 API 调用 EnableMQCalls
如果为 true,则通过 MQ 传输进行 API 调用 HTTPUriNonWSE
WSAltRoute
Web 服务的终结点HTTPUriWSE
WSAltRouteWSE
Web 服务的终结点MSMQRequestQ
MSMQ 请求队列的终结点 MSMQResponseQ
MSMQ 响应队列的终结点 MQRequestQ
MQ 请求队列的终结点 MQResponseQ
MQ 响应队列的终结点 LocationOfImages
用于读取和写入图像的路径 例如,如果您不想通过 HTTP 运行,请将
EnableHTTPCalls
值设置为 false。 - 重新定位
\Bin
文件夹 - 找到
\Server
子文件夹 - 编辑
WSAltRouteBEFake.exe.config
文件,审阅并进行任何更改。条目列表是:条目 描述 NoTrace
如果为 true,则在发生异常时不转储堆栈跟踪 QueueToMonitor1
要监视的第一个 SOAP 消息队列的名称 QueueToMonitor2
要监视的第二个 SOAP 消息队列的名称 QueueToMonitorX
要监视的第 X 个 SOAP 消息队列的名称 PollDelayTime
接收延迟(毫秒)。 LocationOfImages
用于读取和写入图像的路径 例如,如果您想添加一个要监视的队列,请添加
QueueToMonitor3
并将关联值设置为队列的 URI 名称。 - 如果想探索 HTTP 传输
- 找到
\WebServices
文件夹 - 将两个子文件夹(
WSAltRoute
和WSAltRouteWSE
)复制到您的主网站文件夹(通常是c:\inetpub\wwwroot
)。 - 对于第一个文件夹,在默认网站(端口 80)下创建一个名为
WSAltRoute
的新 IIS 虚拟目录,指向WSAltRoute
目标文件夹。 - 对于第二个文件夹,在默认网站(端口 80)下创建一个名为
WSAltRouteWSE
的新 IIS 虚拟目录,指向WSAltRouteWSE
目标文件夹。
- 找到
- 如果想探索 MSMQ 传输
- 检查客户端应用程序的
WSAltRouteTest.exe.config
中定义的 MSMQ 请求和响应队列是否存在。使用 Windows 2000 或 Windows XP 的计算机管理功能进行检查。
- 检查客户端应用程序的
- 如果想探索 MQ 传输
- 检查客户端应用程序的
WSAltRouteTest.exe.config
中定义的 MQ 请求和响应队列是否存在。使用 Websphere MQ Explorer 工具进行检查。
- 检查客户端应用程序的
- 重新定位
\Bin
文件夹 - 找到
\Images
子文件夹 - 将两个
.gif
文件CD.gif
和Help.gif
复制到客户端的WSAltRouteTest.exe.config
和服务器的WSAltRouteBEFake.exe.config
中LocationOfImages
键名下的指定文件夹。
- 要运行演示,请先启动后端进程
WSAltRouteBEFake.exe
,观察启动消息是否正常显示。然后启动客户端进程WSAltRouteTest.exe
。接下来几分钟内,这两个控制台都会显示活动,因为各种 Web 服务 API 都将通过选定的传输进行访问。不应出现任何异常。 - 如果出现异常,可能只会显示异常的消息部分。如果出现这种情况,可以通过将相关(通常是客户端)应用程序配置文件中的
NoTrace
值更改为 false 来打开堆栈跟踪。
应用程序构建
解决方案由以下项目组成:
项目名称 | 目的 |
WSAltRouteTest |
客户端程序,用于访问新传输。 |
WSAltRouteBEFake |
虚拟服务器程序,响应请求。 |
WSQTransports |
支持可插拔传输框架和队列传输实现的代码。 |
AsyncHelper |
来自 MSDN 文章 [3] 的业务对象的异步支持。 |
WSAltRoute |
配置为不使用 WSE 的 ASP.NET Web 服务 |
WSAltRouteWSE |
配置为 WSE 操作的 ASP.NET Web 服务 |
WSQSetup |
队列的设置程序——在安装 MSMQ(以及可选的 MQ)之后,但在运行演示之前运行此程序。 |
WSSetupMQ |
创建 MQ 队列的支持——需要 VB6 运行时 [10]。 |
加载解决方案文件应该非常迅速。您可能需要重新指向两个面向 HTTP 的 Web 服务的 .webinfo
文件(默认假定为端口 80),但除此之外,项目应该可以直接编译到 \Bin\Client
和 \Bin\Server
子文件夹(假设其中的文件未被锁定)。
案例研究回顾
这是解决方案主要方面的回顾:
基本支持
- 乍一看,生成的 SOAP 代理似乎与 HTTP 传输紧密绑定。事实并非如此。
- ASP.NET Web 服务包含一个专门设计的底层结构,允许支持基于
System.Net.WebRequest
和System.Net.WebResponse
的替代传输协议。 - 为了支持新协议,底层结构提供了一个方案注册机制。一旦注册了新的“网络方案”,客户端在指定终结点时就可以使用它们。
- MSMQ 和 Websphere MQ 都能够轻松地被包装成传输协议,客户端可以轻松地指定基于方案的终结点。这会产生最小化客户端使用新传输所需代码更改的效果。
安全支持
- WSE 可以无缝集成到 ASP.NET 客户端、服务器和更底层的任意后端 .NET 服务中。在前者的情况下,
SoapWebRequest
完成了大部分工作,例如驱动 SOAP 消息的序列化,然后将其移交给特定的传输进行传递。 - 将代码(包括框架和客户端)从“明文”模式切换到 WSE 安全模式所需的更改非常少。
二进制支持
- 有了 WSE,通过
SoapWebRequest
支持二进制附件是一个简单的过程。
进一步工作
构建本节就像“易如反掌”!一些需要考虑的事情包括:
- 代码中有一些地方使用字符串连接而不是使用 StringBuilder 构建。因为我使用了这种“技术”,并不意味着我赞同它!
- 应该做更多工作来保护解决方案的各个方面。立即想到的两个:
- 使用
StrongNameIdentityPermissionAttribute
来明确控制谁调用敏感程序集(仅限使用特定强名称密钥签名的调用者) - 加密密钥应存储在代码外部,要么存储在(安全保护的)文件中,要么存储在注册表的安全区域。
- 使用
- 演示是围绕一种非常 RPC 风格的消息协议构建的,其中期望对请求有响应。研究单向 SOAP 消息会很有趣——这最接近于队列式发送操作,而无需相应的接收,并且将是该库的一个有用补充。
- 该框架已用于与使用 Axis Toolkit 1.1 RC1 和 J2SE 1.4.1 的 Java 后端进行互操作。不幸的是,在这里加入 Java 可能会使复制量增加一倍!根据对此文章的反应,可能会有后续文章更侧重于互操作性方面。
- 将压缩功能(如 SmartLib 压缩库 [9])添加到 DIME 部分以处理大型附件会很有趣。
- 其他传输可以实现到该框架中——SMTP、FTP、SQL2000 等都是不错的选择。例如,我最近在此框架中实现了 TCP/IP,事实证明这是一个微不足道的任务。
相关链接
[1] 如前所述,MQWebRequest
类使用 IBM MQSeries Automation Classes for ActiveX COM 类型库作为引用。有比此方法更高效的选择,例如 Neil Kolban 在 http://www.kolban.com/mq/DotNET/index.htm 上的托管“.NET Provider for MQSeries”。这可能会被纳入未来版本的 Websphere MQ 中。
[2] Websphere MQ 5.3 可从 IBM 试用下载:http://www-3.ibm.com/software/ts/mqseries/messaging/
[3] 异步支持由 MSDN 文章“为 .NET Windows 窗体客户端创建异步业务对象”中提供的代码稍作改编后提供:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwinforms/html/asyncui.asp
[4] 使用 Microsoft .NET 的 Web Services Enhancements 1.0 进行编程,网址为:http://msdn.microsoft.com/webservices/building/wse/default.aspx?pull=/library/en- us/dnwebsrv/html/progwse.asp
[5] Inside WSE pipeline 描述了 WSE 筛选器的管道模型,网址为:http://msdn.microsoft.com/webservices/building/wse/default.aspx?pull=/library/en- us/dnwebsrv/html/insidewsepipe.asp
[6] 使用 Microsoft .NET 的 Web Services Enhancements 1.0 进行编程,网址为:http://msdn.microsoft.com/webservices/building/wse/default.aspx?pull=/library/en- us/dnwebsrv/html/progwse.asp
[7] 使用 Web Services Enhancements (WSE) 进行用户名/密码身份验证,网址为:http://www.eggheadcafe.com/articles/20021227.asp
[8] WSE 设置工具可在以下网址找到:http://msdn.microsoft.com/downloads/default.asp?url=/downloads/sample.asp?url=/msdn- files/027/002/108/msdncompositedoc.xml
[9] SmartLib 压缩库是一个完全托管的 .NET 压缩库,网址为:http://www.icsharpcode.net/OpenSource/SharpZipLib/
[10] 如果您使用 WSQSetup.exe 设置我们演示应用程序的 MQ 队列,则需要 VB6 SP5 运行时——可以在以下网址找到:http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=BF9A24F9- B5C5-48F4-8EDD-CDF2D29A79D5。
历史
- 2003 年 4 月 8 日 - 更新以支持 WSE。
- 2003 年 1 月 18 日 - 初始发布