65.9K
CodeProject 正在变化。 阅读更多。
Home

将 WCF 服务集成到基于 UDDI 的企业 SOA 中

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (14投票s)

2009年1月13日

CPOL

18分钟阅读

viewsIcon

72583

downloadIcon

765

演示如何将 WCF 服务集成到基于 UDDI 的 SOA 中。这包括在运行时发现 WCF 服务以及客户端的运行时配置。

引言

本文档旨在深入探讨如何将 WCF 服务集成到企业 SOA 环境中。该 SOA 基于 Microsoft UDDI V2 注册表,该注册表随 Windows Server 2003 和 2008 一起提供。本文档涵盖了 WCF UDDI 集成方面以及动态 WCF 代理的创建。

背景

开箱即用的 WCF 是面向服务的架构吗?

如果您认为 WCF 是 SOA,那么让我们回顾一下“面向服务的架构”定义的一部分。OASIS(结构化信息标准促进组织)将 SOA 定义为:“一种组织和利用可能由不同所有权域控制的分布式能力(capabilities)的范式。它提供了一种统一的方式来公开、发现、交互和使用能力,以产生与可衡量的先决条件和期望相一致的期望效果。”现在,让我们用“服务”来翻译“能力”。所以,我们得到了稍微不同的表述:“SOA 由分布式服务、某种提供和查找它们的方法、与服务交互的方法以及最后,调用服务的方法组成。”如果您尝试将其分解为 Web 服务技术术语,您现在将其翻译为:

  • 服务(这就是 WCF 的全部意义所在)
  • 查找和提供(这可能是一个 UDDI 注册表(企业版),或 WS-Discovery(类似 UPnP))
  • 交互(这可能是 WSDL)
  • 调用(这可能是 SOAP)

那么,WCF 涵盖了 SOA 的哪个部分?

服务、交互和服务调用。运行时发现不是 WCF 的一部分。

结论

WCF 没有提供恰当的机制来在运行时提供和查找服务,因此它是一种经典的 3 层架构技术,我们实际上已经使用了 15-20 年了。我们之前称之为 DCOM 或其他名称,现在相同的技术又被重新贴上了最新的流行语“服务”的标签。

仅仅提供服务并不是 SOA。

WCF 遵循 Web 服务

WCF 基本上将 .NET 2.0 的所有不同通信技术(Remoting、Queues 等)整合到一个统一的、通用的编程模型下。模型本身遵循 .NET 2.0 引入的 Web 服务模型。Web 服务最大的缺点之一是 Web 服务的通信层几乎固定为 WS-I 标准(在 WCF 中为基本 HTTP 绑定)。微软现在为 WCF 所做的基本上是解决这个问题,并引入了一个完全灵活的通信层。因此,我们不再预先分配 WS-I 协议,而是可以从各种通信协议中选择,几乎适用于任何情况。但是,从程序员的角度来看,WCF 服务/客户端的实现方式与我们之前编写 Web 服务的方式几乎相同。在客户端,您现在只是将“服务引用”添加到项目中,而不是“Web 服务引用”,但感觉和以前一样。在服务端,您现在需要定义一个服务接口,并使用其他一些属性,但同样,感觉就像在使用 Web 服务一样。但是,这种灵活性有一些影响。

  • 对于每个服务引用,您现在都会添加一个包含“ServiceModel”部分的配置文件,其中保留了所有特定于协议的配置选项。
  • Web 服务的 WSDL 路径几乎总是 http://ServiceAddress?WSDL。现在,对于非 HTTP 协议,必须有另一种方法来访问服务元数据。因此,在 WCF 中,引入了“Mex”或“MetadataExchange”终结点,它应该位于“ServiceURL/mex”地址上。

目标企业架构

请看以下场景:我有 10 种不同的服务正在运行,每种服务有 100 个客户端。工作日期间一个小时的非计划服务停机可能会给我造成高达 1 亿美元的损失。

那么我的要求是什么?

  • 我必须能够在不影响任何客户端的情况下迁移我的服务。
  • 服务必须是冗余的,客户端必须支持故障转移。
  • 我必须能够动态添加更多相同类型的服务以进行负载均衡和容错。
  • 我希望快速了解我的服务在哪里运行。
  • 我必须能够更改通信参数(例如,在服务上启用传输加密),而不会影响任何客户端。

标准的 WCF 实现是否满足我们的要求?

我所说的“标准”是指创建服务和客户端的方式。也就是说,设置服务,然后创建客户端,将服务引用添加到服务,然后调用感兴趣的服务函数。如果我们

  • 将服务移动到另一台服务器?
  • 客户端会失败。

  • 服务失败?
  • 客户端会失败。

  • 更改服务的通信参数(例如绑定或加密)?
  • 客户端会失败。

  • 想引入另一种同类服务以进行负载均衡?
  • 已安装的客户端将忽略它。

  • 我想快速了解我的服务在哪里运行?
  • 我不能。

结果是,对于以上任何更改,我都需要更新 100 个客户端配置文件,但我不知道这些客户端位于哪些工作站上。因此,上述任何更改都会破坏我的生产流程,这在企业环境中是不可接受的。

结论

WCF 的标准实现不适用于企业环境。

在我们的架构中引入中央服务注册表

作为在企业环境中查找和提供 Web 服务的一种机制,这让人想起 UDDI,它自 Windows Server 2003 起就包含在内,但似乎至今仍未引起足够重视。如果我说文档和支持稀少,这已经是一种轻描淡写了。

UDDI 环境中的 Web 服务

在 UDDI 注册表环境中处理 Web 服务相当直接。您只需从 WSDL 中剥离除 TypesMessageOperations 部分之外的所有内容,将其作为模型发布,然后将服务终结点添加到注册表中(每个服务只有一个)。作为客户端,您将 Web 服务引用添加到已剥离的 WSDL 中,然后在运行时,您只需输入 UDDI 中的地址,一切都应该正常工作,因为通信层固定为 WS-I 并且只有一个终结点。

UDDI 环境中的 WCF 服务

有了 WCF,我们的服务可以通过多种方式进行通信。这包括服务可以同时提供不同通信协议的选项。如果我们有多个服务实例,我们不能再假设它们都将使用相同的通信协议。但是,如果我们尝试像处理 Web 服务一样处理 UDDI 中的 WCF 服务,会发生什么?

首先,我们从 WSDL 中剥离除 TypesMessageOperations 部分之外的所有内容。这就是您第一次停止的地方……查看 WSDL,您会发现“Types”部分不是内联声明的,而是每个命名空间都有一个 import 引用。您现在有三个选择。

  • 您在浏览器中打开所有导入 URL,然后用结果替换导入(在第十次之后会非常痛苦),或者
  • 您可以使用例如 FlatWSDL 修改服务的 WSDL 行为。但这不像听起来那么好,因为 Visual Studio 中存在一个错误,在某些情况下不允许您选择数组类型。因此,如果您的服务返回 T 的数组,您可以配置它以在您的代码中作为 List<t>,但在某些情况下,Visual Studio 现在不允许您将数组类型更改为除数组以外的任何类型。这非常糟糕,因为 WSDL 是完全有效的,并且该类型被完美识别为 T[],但无论您选择什么作为 ArrayType,您都无法再将其作为 List<T> 获得。
  • 您跳过此步骤,并针对正在运行的服务实例进行开发。此选项的缺点是您无法强制开发人员使用 UDDI。

但是,在我们解决了第一个问题之后,如果您简单地用从 UDDI 注册表中获得的 URL 替换客户端的服务终结点地址,会发生什么?

如果我们

  • 将服务移动到另一台服务器?
  • 如果客户端不使用加密,则客户端工作,否则会失败。

  • 服务失败?
  • 如果客户端不使用加密,则客户端工作,否则会失败。

  • 更改服务的通信参数(例如绑定或加密)?
  • 客户端会失败。

  • 想引入另一种同类服务以进行负载均衡?
  • 如果客户端不使用加密,则客户端工作,否则会失败。

  • 我想快速了解我的服务在哪里运行?
  • 我随时都能从我的注册表中获得一个很好的概览。

因此,如果我们使用任何形式的加密,我们用于 Web 服务的概念会使我们所有的架构要求都失败(除了一项)。

WCF 服务不能集成到 UDDI 环境中吗?

问题主要在于客户端配置文件中的“ServiceModel”部分。它会冻结服务状态,直到我们添加服务引用为止。如果您使用带有加密的绑定,而加密默认基于 Windows 用户,您会发现,例如,有一个名为“Identity”的标签,它是 Windows 身份验证,通常是“Hostname\Username”之类的,用于客户端对服务的加密。但是,一旦我们将服务移动到另一台服务器,这当然会因身份不匹配而失败。

将 WCF 服务集成到 UDDI 环境中

如何在运行时查询服务元数据?

我记得我有一个小型 Web 服务项目,我们动态查询服务的 WSDL,创建了一个 DOM 对象,然后在运行时编译客户端。该项目基于 Eshans Golkar 的文章“动态发现和调用 Web 服务”。所以,我的第一个 WCF 方法是类似的,而且有效。但后来,我基本上想要做的与 WCFTestClient.exe 所做的基本相同。您只需输入服务的 URL,WCFTestClients 就会发现它,然后创建一个客户端。但是,通过 Lutz Roeder 出色的 Reflector 工具进行一点点逆向工程,我们发现……

WCFTestClient.exe 只是在命令行调用 svcutil。:-)))

谁敢交付一个如此笨拙的解决方案?但当我用“svcutil”做同样的事情时,我在 System.ServiceModel.Description 命名空间中偶然发现了两个类。

MetadataExchangeClient 和 MetaDataResolver

它们基本上都是读取服务的 WSDL,然后返回一个 ServiceEndpointCollection。现在,您只需选择要在运行时与服务通信的终结点。但在这里,第一个问题开始出现。我需要 MetaDataResolver 来做什么,它需要一个 MetadataExchangeClient 并返回一个 ServiceEndpointCollection,当我可以使用 MetadataExchangeClientImportAllEndpoints 函数时?(更新:我的实现完全基于 MetadataexchangeClient。我试图通过使用 MetaDataResolver 来节省一些代码行,但它从未奏效。所以,我只能不鼓励使用 MetaDataResolver。)

如果您试图弄清情况,您很快就会发现,关于 WCF,您现在正走在一条不寻常的道路上。文档和参考资料从这一点开始变得非常稀少。但无论如何。这基本上是我们想要将 WCF 服务集成到 UDDI 环境中所需要做的。

  1. 我们查询 UDDI
  2. 我们从 UDDI 注册表中获取主要的 ServiceURL
  3. 我们在 ServiceURL/mex 或 ServiceURL?WSDL 上消耗服务元数据
  4. 我们选择一个终结点并配置绑定
  5. 我们返回一个客户端
  6. 我们使用客户端调用所需的服务函数

所以,我们基本上所做的是,而不是将服务的状态冻结到我们向客户端项目添加服务引用的那一刻,我们在代码中第一次想与我们的服务通信时,就执行“添加服务引用”功能!这基本上意味着我们不再需要配置文件中的 <ServiceModel>。这能工作一段时间,但之后……您会发现……

为什么我们需要 <ServiceModel> 部分

第一次崩溃发生在您收到一个大于 64K 的服务响应时。您的客户端将报告 MaxReceivedMessageSize 配额已超出。坦率地说,我问自己以下问题:在这个拥有 Tera 和 Peta 字节的时代,谁决定 64KB 是接收服务响应的好默认限制?我的电动牙刷可以处理大于 64KB 的响应!下一个多余的默认参数修改是 maxItemsInObjectGraph,它具有相同的任意 64K 限制。

如何修改未知的绑定?

问题的核心是我们不知道服务将提供什么绑定。

MaxReceivedMessageSize 和 MaxBufferSize

我们来看看绑定的继承。所有绑定都派生自 Binding 类。但是,除了某些超时参数之外,我们在这里无能为力。我们来看看绑定元素堆栈。在绑定元素的堆栈中,此参数属于传输绑定元素,它是任何绑定的组成部分。但是,我花了两天时间试图找到一种访问绑定元素的方法,但只有对于 CustomBinding 才能找到一些线索。对于所有其他绑定,这是一个死胡同。CreateBindingElements() 函数只会创建一个实际绑定元素的副本,而官方文档中未包含的 GetBindingProperty<t>() 函数也没有任何帮助。由于我们不知道服务将提供什么绑定,我们现在有三个选择:

  1. IT 耻辱榜候选
  2. 编写一个函数,尝试将发现的绑定强制转换为任何已知绑定,然后设置参数。

  3. 我们使用反射
  4. 我们使用一个函数,该函数首先尝试将对象强制转换为自定义绑定。如果转换成功,它会尝试为每个绑定元素的每个属性设置一个名为“Name”值为“Value”的属性,如果转换失败,则只尝试查找一个名为“Name”的属性,然后将其设置为“Value”。

  5. 我们返回一个 CustomBinding
  6. 我们使用发现的绑定的 CreateBindingElement() 函数,在 TransportBindingElement 上修改参数,然后返回一个 CustomBinding 而不是原始绑定。

我不喜欢这些解决方案中的任何一个,但最终,我们决定采用反射解决方案。

如何修改 MaxItemsInObjectGraph?

有趣的是,“MaxItemsInObjectGraph”是 DatacontractSerializer 的一个属性,它也用于所有绑定中。如果您使用配置文件,您现在必须添加一个类型为 DataContractSerializer 的终结点行为,但如果您尝试在 ServiceModel 命名空间中查找它,您会发现您无法在运行时实例化这个类。但进一步查看,有一个 DataContractSerializerOperationBehavior 可以在运行时实例化,但它只能应用于一个操作。所以,我所做的是创建一个实现 IEndPointBehavior 的类,然后简单地将这个 DataContractSerializerOperationBehavior 设置为一个有用的值到所有操作。现在,我可以摆脱我的 <ServiceModel> 配置部分,我所有的函数都按预期工作。

所以,我们做到了吗?

如果您认为 64K 的默认参数(是的,我这是反讽)已经够您折腾的了,那您很快就会失望。现在一切都正常了,我可以来回切换我的服务绑定,我的客户端一直运行良好,直到有一天……它们停止工作了。MetadataExchangeClient 报告“元数据包含无法解析的引用”。我花了两天时间才找出发生了什么。我们的数据模型越来越大,直到有一天……您猜怎么着。是的,WSDL 变大了,超过了 64K。这时,所有 MetadataExchangeClientMetadataResolver 的参考实现都会失败。再猜猜。要解决这个问题,您必须自己创建一个绑定,设置 MaxReceivedMessageSize 参数,然后使用一些不在任何示例中提及的构造函数。

终于!

这是最后一艘在我的目标架构航线上沉没的潜艇。我不会说这是项目结束前的最后一个问题,但仍然,我相信我们将毫无限制地在 WCF 中实现所有架构要求。那么,现在会发生什么?

  • 将服务移动到另一台服务器?
  • 我更改 UDDI 中的地址,我所有的客户端都会自动跟随并继续工作。

  • 服务失败?
  • 客户端会很好地进行故障转移并重新连接,无论如何。

  • 更改服务的通信参数(例如绑定或加密)?
  • 客户端会适应。

  • 想引入另一种同类服务以进行负载均衡?
  • 只需将其注册到 UDDI 即可。

  • 我想快速了解我的服务在哪里运行?
  • 我随时都能从我的注册表中获得一个很好的概览。

我们所有的目标架构目标都已实现。

万岁!太棒了。

WCF 实现缺陷

  • 64K 的默认配额,包括 MaxReceivedMessageSizeMaxBuffersizeMaxItemsInObjectGraph,可能还有其他地方。
  • 默认的 WSDL 架构在“Types”部分使用“Import”语句。
  • Visual Studio 中的错误,它在处理扁平化 WSDL 时不允许您选择数组类型(MSConnect ID:387245)。
  • 绑定的继承,其中特定绑定具有高度冗余的属性,以及/或对非“自定义绑定”的绑定元素缺乏访问权限。
  • 配置文件配置和运行时配置之间的一致性问题。
  • 文档不足。

结论

  • WCF 标准实现由于其“一切都是静态的”方法,尚未为企业做好准备。
  • WCF 服务可以集成到企业 SOA 架构中,但一些实现缺陷阻碍了无缝集成并增加了项目风险。

给微软下一版 WCF 的建议

  • 默认情况下应禁用默认绑定配额。
  • 您默认设置的任何配额都是任意的,并且永远不适合所有项目范围。如果我不信任服务,那么我可以设置这个配额是很好的,但请让我选择最适合的内容。特别是,不要使用以后以定时炸弹方式导致您的框架爆炸的配额。

  • 默认情况下,WSDL 中的“Imports”。
  • 这只会引起问题,而且完全没有用。摆脱它。

  • Binding 的继承。
  • 应使其更细粒度,而不是两级层次结构。例如,在 BasicHttpBinding 类中,应只有 BasicHttpBinding 所独有的配置属性。所有其他属性都应继承。轻松添加更多继承可以做到,而不会破坏任何东西。这将为任意绑定的复杂运行时配置铺平道路。我更喜欢这样做,而不是像 CustomBinding.Elements 那样访问绑定元素。

  • 运行时配置和配置文件的配置应保持一致。

使用代码

该解决方案使用了 Microsoft Enterprise Library 4.1 的 Logging Application Block。要不就是注释掉所有 Logger.Write(...) 行并从配置文件中删除条目,要不就是确保已安装它。我强烈建议无论如何都使用 Enterprise Library。

解决方案包含四个项目。

UDDIServiceFactory

管理到 UDDI 的连接,并提供客户端故障转移的模式。UDDIServiceFactory 使用 DynamicWCFFactory 创建客户端。它使用 Microsoft UDDI SDK 中的 Microsoft.UDDI.dll 连接到 UDDI,并在其 ManagedURL 类中保持恒定的连接。

配置参数

UDDIServiceFactory 在 app.config 中查找以下参数:

  • ReturnDefaultClient
  • 如果设置为“1”,UDDIServiceFactory 在放弃之前,将在 UDDI 连接或所有 UDDI 注册的服务失败的情况下返回一个标准的 WCF 客户端。该客户端将通过您 app.config 文件中的 <ServiceModel> 部分进行配置。这被认为是一种紧急故障安全机制。在生产环境中,请务必确保配置指向正确的服务,然后再将其设置为 1。

  • LoadBalaceUDDIServices
  • 如果设置为“1”,UDDIFactory 将从可用服务列表中随机选择一个服务,而不是选择第一个。这提供了良好的负载均衡。

DynamicWCFFactory

它用于通过在运行时发现其 WSDL 来创建动态 WCF 客户端。它还为 WCF 客户端提供运行时配置功能,以及用于双工通道中的回调实例。它支持大于 64K 的 WSDL。

配置参数

DynamicWCFFactory 在 app.config 中查找以下参数:

  • UseBindingDefaults
  • 如果设置为“1”,DynamicWCFFactory 将通用绑定属性 MaxReceivedMessageSizeMaxBufferSizeMaxItemsInObjectGraph 设置为 int.MaxValue。这是为了避免 64K 限制带来的不必要麻烦。

WcfService

一个开箱即用的 WCF 服务。用于单元测试。

DynamicWCFFactoryTests

这是一个单元测试项目,包含两个测试。

TestDynamicWCFFactory

使用 DynamicWCFFactory 创建一个 WCF 客户端,并调用我们测试服务的一个函数。

[TestMethod]
public void TestDynamicWCFFactory()
{ 
    var dynWCFFactory = 
        new DynamicWCFFactory.DynamicWCFFactory<ServiceReference.Service1Client,
        ServiceReference.IService1>();
    ServiceReference.Service1Client client = dynWCFFactory.CreateProxyFromWSDL(
        "https://:4873/Service1.svc");
    string someString = client.GetData(2);
}

TestUDDIFactory

使用 UDDIServiceFactory 创建一个 WCF 客户端,并调用我们测试服务的一个函数。

在使用 UDDIServiceFactory 之前,您必须设置好 UDDI 注册表,并且您的服务必须已正确注册到 UDDI。

[TestMethod]
public void TestUDDIFactory()
{
    try
    {
        UDDIServiceFactory<ServiceReference.Service1Client,
            ServiceReference.IService1> uDDIFactory = 
        new UDDIServiceFactory<ServiceReference.Service1Client,
            ServiceReference.IService1>("UDDIURL","serviceKey","bindingKey");
          
        ServiceReference.Service1Client client = uDDIFactory.GetFirstProxy();

        string someString;

        while (true)
        {
            try
            {
                someString = client.GetData(2);
                client.Close();
                break;
            }
            catch (Exception ex)
            {
                // Free Ressources as we are going to create a new proxy
                if (client.State == CommunicationState.Faulted)
                    client.Abort();
                else if (client.State == CommunicationState.Opened)
                    client.Close();
                Logger.Write(ex, new List<string> { "General" }, 1, ERROR_SERVICE_CALL, 
                   System.Diagnostics.TraceEventType.Warning, "Service Call failed.");
                client = uDDIFactory.GetNextProxy();
            }

        }

    }
    catch (Exception ex)
    {
        Logger.Write(ex, new List<string> { "General" }, 1, 
          ERROR_UDDI_FACTORY_CREATION, 
          System.Diagnostics.TraceEventType.Warning, 
          "Creation of UDDIServiceFactory failed.");
        throw;
    }

}

通过 Visual Studio 的测试工具栏调用单元测试。如果它不可见,请转到“视图 -> 工具栏 -> 测试工具”。

© . All rights reserved.