一个典型的 SOA 实现通常依赖于多个服务。调用这些服务需要知道位置(即服务端点的地址)和绑定(到达端点的传输机制)信息。最简单的方法就是在实现中将端点地址硬编码。但这造成了方案实现和服务位置的紧密耦合(位置耦合)。将端点地址放到配置文件中可以改善这种情况。因为这样地址的改变就不会引起代码的改动。但是,随着服务或服务消费者(或者说是配置文件)增多,这种方法会带来扩展性问题。
通过一个将服务请求动态解析成端点地址和调用策略的专门组件——一个服务注册中心——为这一问题提供了一个更灵活并可维护的解决方案。在这里,注册中心包含所有关于服务的部署、位置和在每一位置的调用关联策略。
注册中心这个词最初作为 Web Services 愿景的一部分被引入,定义了 UDDI(Universal Description, Discovery and Integration,统一描述发现集成),它代表服务消费者和服务提供者之间的一个“撮合者”(代理者)。类似于黄页,UDDI 被用来根据消费者的功能需求动态提供一个服务生产者。尽管有多个提供商和标准团体的推广,用于服务撮合的 UDDI 一直没有得到发展。目前 UDDI 主要用途局限于引用在设计服务消费者时用到的服务 WSDL 文件。
服务注册中心的一个更实际的用法是根据服务名称(和策略)动态查找服务端点 / 绑定,其类似于广泛在 j2ee 中使用的服务定位器(service locator)模式【1】。这里服务的定义(接口)在开发的时候就已知了,注册中心的使用仅限于在运行时解析服务端点地址和动态绑定。
这篇文章讲述了一个能够简化 SOA 实现的服务注册中心的.NET 实现。
整体架构
解决方案的整体架构如图 1:
图 1 服务注册中心的整体架构
这个实现的基础是一个服务数据库,其包含系统中所有服务的信息和一个注册中心服务,注册中心服务封装了这个数据库并提供了一套访问这些信息的“标准”APIs。尽管理论上服务消费者可以直接从数据库中获取信息,但通过一个服务来做可以达到以下目的:
- 抽象了数据库模式的细节,这使得改变使用的数据库、数据库设计和访问方法都对服务消费者是透明的。
- 减少数据库连接的数量,提高了数据库的整体性能和吞吐量。
- 它可以将服务相关的信息改变立刻发布给客户(目前未实现),避免了客户需要自己从数据库中发现变化。
附注:Windows Communications Foundation
Windows Communication Foundation (WCF) 是一个将所有分布计算方法(现有的和新的)结合到一个统一的编程模型中的框架,简化了服务的创建和使用。它通过一个分层结构来实现,参见图 2
图 2 WCF 架构
最上层——契约——支持灵活的服务定义(通常由服务消费者使用)。这包括:
- 数据或消息契约,定义消息(数据)语义
- 服务契约,定义服务调用语义
- 策略和绑定,定义技术服务调用语义,包括通讯传输等。
服务运行层允许自定义服务本身的实现,包括:
- 流量控制,控制一个服务同时处理的消息数量
- 错误处理,控制在内部出错时提供给服务消费者的信息
- 元数据支持,控制服务的可查询信息
- 实例支持,控制所请求服务实现的实例化处理
- 事务处理,控制服务调用的事务语义
消息层通过消息分发管道(channel)达到自定义消息分发的目的。管道有两种:传输管道和协议管道:
- 传输管道支持物理消息分发传输,例如 HTTP,命名管道,TCP 和 MSMQ.
- 协议管道支持多种 web services 标准的实现,例如建立在基础传输管道上的 WS-Security 和 WS-Reliability。
最后,激活和宿主层负责支持在不同环境下服务的实现,包括独立执行程序,Window 服务,IIS,Windows Activation Service (WAS)等。
典型的服务注册中心实现只存储端点地址和绑定类型,我们在此基础上又添加了额外的配置信息,例如消息发送 / 接收的超时信息,消息大小等。这不仅可以让我们改变端点地址和绑定类型(很多注册中心的实现都有这些功能)外,也可以通过统一集中的注册中心对绑定参数进行细微调整。
使用注册中心后,需要对服务消费者实现稍微做一些相应的改变。通常服务消费者使用从现有服务中产生的服务代理。但这些代理必须单独维护,在每次服务接口改变的时候都要修改。
在我们的实现中,服务消费者通过使用.NET 的 ChannelFactory 类来根据服务接口,端点和绑定动态生成一个服务代理。这个方法不仅让我们无需单独生成服务代理,而且服务和服务消费者的实现都可以使用同样的服务接口。摒除了服务代理,可以减少需要维护的代码并消除因为接口改变要重新发布服务代理的必要。服务消费者和提供者共享一个服务接口定义工程,其保证了两者的同步。
最后负责注册中心维护的应用支持对服务定义数据库的查看和修改。它提供对服务的列表查看,对某个服务详细信息的查看,和添加修改服务定义的能力。目前可以通过这个应用输入已有服务的绑定 / 端点信息,这样服务消费者就可以使用这些服务了。
数据库设计
图 3 展示了 WCF 服务定义和支持的绑定及相关参数信息
图 3 服务定义
除了 WCF 支持的 Web Services 绑定,这个服务注册中心还支持本地(语言)绑定。
每一种绑定类型都有自己的 URL 格式。另外我们为本地绑定增加了一个 URL 格式。表 1 是对所支持的绑定 URL 格式的总结列表:
Bindings URL local “local://assembly/class” basicHTTPBinding “ http://localhost/servicemodelsamples/service.svc ” wsHTTPBinding “ http://localhost/servicemodelsamples/service ” wsDualHttpBinding “ http://localhost/servicemodelsamples/service ” wsFederationHttpBinding “ http://localhost/servicemodelsamples/service ” netTcpBinding “net.tcp://localhost:9000/servicemodelsamples/service” netPeerTcpBinding “net.p2p://broadcastMesh/servicemodelsamples/announcements” netNamedPipeBinding “net.pipe://localhost/servicemodelsamples/service” netMsmqBinding "net.msmq://localhost/private/ServiceModelSamplesTransactedBatching"表 1 绑定和对应的 URLs
对应的数据库设计如图 4:
图 4 数据库设计
整个数据库设计包括 4 个主要的表:
- 服务(Service)表代表了一个服务的基本信息。除了服务名称外,还包括服务类别(服务名称对应的一个命名空间,它能帮助防止命名冲突和更好的组织服务)和服务版本,服务版本允许同时部署一个服务的不同版本并允许消费者进行选择。接口是服务接口的一个全限定名称,它保证消费者需要的接口确实是这个服务提供的。最后,客户类型这个参数允许我们针对不同的消费者提供不同的服务端点和绑定。例如为白金 / 黄金等不同等级的客户提供不同的 SLA(服务水平协议)(端点 / 绑定)。
- 绑定(Binding)表提供绑定的大部分信息,包括类型和对应的参数。这个表是一个关于所有支持的绑定类型的所有参数的超集。可靠(reliability)和安全(security)作为仅有的例外被放到两个单独的表中。
- 可靠(Reliability)表为可靠消息传输提供所需的参数。
- 安全(Security)表(目前还没有完全实现)为安全服务通讯提供所需的一组参数。
理想状态下,服务注册中心应该能够在绑定级别实现可靠 / 安全数据的重用和在服务级别实现绑定的重用。我们目前还没有实现。目前服务和绑定、绑定与可靠 / 安全之间都是一对一的关系。虽然数据有重复,但大大简化了实现和维护。
除了这些表,我们使用一个视图(图 5)简化了获取一个完整服务定义的数据库访问。
图 5 完全服务联接
服务实现
我们决定使用 Web Services Software Factory (WSSF) Model Edition 设计服务。下面是使用这个工厂定义服务所需的数据类型(图 6)。
图 6 服务注册中心数据类型
使用 WSSF 设计的服务注册中心契约,如图 7:
服务的实现非常简单和直接。它的基础是用微软模式与实践企业库(数据访问模块 data access block)和仓储模式(repository pattern)【2】实现的一个持久层,它提供了一个无类型依赖的数据访问。包括以下的类(图 8):
- Repository(泛型)。这个 repository 泛型基类通过使用不同的工厂类发送和接收特定领域的对象。工厂类使用 ADO.NET DbCommand 对象并通过 Enterprise Library Data Access Application Block 将之执行。
- ServiceRegistryRepository。这个类是服务注册中心Repository 类的一个具体实现,它为数据访问层提供了基础的接口。
- IDomainObjectFactory(泛型)。这个接口为一个工厂定义了使用 DataReader 类型创建一个域(domain)对象的签名。
- ServiceObjectFactory(实体特定)。IDomainObjectFactory 接口的实现,把一个返回的结果集转变为一个服务对象。
- IdentityObject (概念)。定义了一个查询是针对哪个或哪些实体。因为查询决定了具体的 IdentityObject 类型,这里并没有一个具体的 IdentityObject 类或接口。
- ISelectionFactory (泛型)。为从一个 IdentityObject 类型转换到一个用来查找对应结果集的 DbCommand 命令的类提供签名。
- ServiceRegistrySelectFactory。这个工厂针对一个服务注册中心,它接收一个 identity 对象(服务查询 Service Query)并返回一个对应的 DbCommand ,该 DbCommand 返回服务结果集。
- IInsertFactory(泛型)。这个工厂接口类产生一个用来插入一个新的域对象的 DbCommand。
- ServiceRegistryInsertFactory (操作特定)。针对服务注册中心的插入操作;它产生一个对应的 DbCommand 来插入一个新的服务对象。
- IUpdateFactory (泛型)。这个工厂接口类产生一个用来修改域对象的 DbCommand。
- OperationUpdateFactory(操作特定)。这个工厂只针对单个操作(例如,UpdateCustomer);它产生一个对应的 DbCommand 来完成数据库域对象的修改。
- IDeleteFactory (泛型)。这个工厂接口类产生一个用来删除域对象的 DbCommand。
- ServiceRegistryDeleteFactory (操作特定)。针对服务注册中心的删除操作;它产生一个对应的 DbCommand 来删除一个服务对象。
- IDataReader。定义在 System.Data 命名空间。这个接口通过在一个数据源执行一个 command 读一个或多个只向前(forward only)的结果集流。它由.NET 框架针对关系型数据库访问的数据提供者实现。
Service 的实现类直接使用数据访问层(ServiceRegistryRepository 类)来执行服务。当收到一个特定请求时,服务的实现调用 ServiceRegistryRepository 的一个对应方法来进行数据库操作并返回数据集。
对服务消费者的支持
服务注册中心的主要使用者是访问业务服务的服务消费者。服务注册中心的引入强化了在服务消费者实现中的依赖注入模式【3,4】——服务消费者依赖于一个特定的接口(这种方法完全对消费者隐藏了服务的 WSDL/XSD。尽管它们在服务设计时非常重要,但消费者的实现只依赖于服务接口,因此完全隐藏了所有的分布和访问机制),而具体的实现(和绑定)在运行时通过对注册中心的访问而被动态注入,这些逻辑作为整体实现的一部分被封装在了一个代理构建器的类中。
为了提高性能,服务消费者缓存了端点的信息。目前我们使用的是基于时间(时间长度可以配置)的缓存策略;一个更高级的发布–订阅缓存策略将会在以后的某个版本中引入。一个客户实现可以通过以下的一个方法为一个给定的接口动态创建一个代理:
public static Interface getProxy<Interface><br></br> (string sName, string sVersion, string sConsumer, string callBackCl)
这个方法接收服务名称,版本,消费类型(一般的 QoS 参数)和回调接口名称(可选的参数),并为一个给定的接口类型创建一个服务代理。这个方法首先尝试在本地缓存中解析所需的代理(见上),如果找到就立刻将之返还给调用者。否则,就会调用注册中心服务根据提供的参数查找服务信息。使用一个全限定接口名称作为一个额外的查找参数,这样可以用来保证一个指向的接口服务实现的引用满足客户要求 1 。
如前面讲到的,我们通过使用.NET 3.0 提供的 ChannelFactory
除了使用 WCF 连接服务,这个实现还能够支持“本地”绑定——直接从客户可以访问的程序集(assembly)中定位服务的实现。这个程序集或者已被客户程序加载或在 GAC 中。
ProxyBuilder 使用的简化
我们可以通过将服务参数从代码移到一个配置文件中(见表 1 的例子)进一步简化 ProxyBuilder 的使用。这个方法参考了 SCA 的配置【5】,可以进一步简化服务消费者的维护。
<configSections><br></br> <section <span color="#ff0000">name</span><span color="#0000ff">="composite"</span> <span color="#ff0000">type</span><span color="#0000ff">="ProxyBuilder.CompositeSectionHandler,<br></br> ProxyBuilder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" </span>/><br></br> </configSections><br></br> <composite><br></br> <reference <span color="#ff0000">Name</span> <span color="#0000ff">= "Calculator"</span>> <Service <span color="#ff0000">Name</span> <span color="#0000ff">= "TestService"</span> <span color="#ff0000">Version</span> <span color="#0000ff">= "02"</span> <span color="#ff0000">ConsumerType</span> = ""></Service><br></br> <Callback <span color="#ff0000">Name</span> <span color="#0000ff">= "Test.test"</span>></Callback><br></br> </reference><br></br> </composite>
表 1 引用配置
另外,注册中心本身的位置也可以移到配置文件中(表 2)。
<registry><br></br> <runtime <span color="#ff0000">Location</span> <span color="#0000ff">="http://localhost:58934/WCFServiceRegistry.Host/ServiceRegistry.svc"/></span><br></br> </registry>
表 2 注册中心位置的配置
维护服务注册中心的应用
这是一个独立的应用,用来维护服务注册中心的信息。它支持以下主要的功能:
- 服务信息的查找和查看
- 服务信息的更改
- 服务信息的添加
- 服务信息的删除
这个应用采用一个 Master-Details 模式,其中 master 是基本的服务信息和绑定类型(在一个 service 表中),details 包括服务使用的绑定类型的详细信息(图 9)。这些详细信息和接下去的界面都基于绑定类型。
主界面的实现基于一个 DataGridView 控件,它本身提供了大部分所需的功能,包括显示,修改现有的记录和添加删除记录。绑定的详细信息被实现为一个自定义用户控件。因为不同的绑定类型有不同的详细信息,我们为各种绑定类型实现了不同的窗体。我们使用工厂模式(见附注)为某个绑定类型创建窗体并产生绑定详细信息。
图 9 服务维护界面
附注:使用 C#泛型实现工厂模式
工厂模式是根据不同类型创建多态对象的一个常见方法。使用 C#泛型则可以大大简化这个实现(表 3)。
namespace RegistryMaintanence.controls<br></br> { public interface IControlsFactoryInterface<br></br> { UserControl createControl(Service service, ServiceMaintanence parent, bool update);<br></br> }<br></br> public class ControlsFactory<T> : IControlsFactoryInterface<br></br> where T : UserControl, InitializableControl, new()<br></br> {<br></br> #region IControlsFactoryInterface Members<p> public UserControl createControl(Service service, ServiceMaintanence parent, bool update)</p><br></br> {<br></br> T t = new T();<br></br> t.initialize(service, parent, update);<br></br> return t;<br></br> }<p> #endregion</p><br></br> }<p> public class ControlsFactory{</p><br></br> private static Dictionary<BindingTypeEnum, IControlsFactoryInterface> factories = null; <p> private ControlsFactory(){}</p><p> private static Dictionary<BindingTypeEnum, IControlsFactoryInterface> getFactories(){</p><p> if(factories == null){</p><br></br> factories = new Dictionary<BindingTypeEnum,IControlsFactoryInterface>();<br></br> factories.Add(BindingTypeEnum.basicHTTPBinding,<br></br> new ControlsFactory<BasicHTTPBinding>());<br></br> factories.Add(BindingTypeEnum.netMsmqBinding,<br></br> new ControlsFactory<netMsmqBinding>());<br></br> factories.Add(BindingTypeEnum.netNamedPipeBinding,<br></br> new ControlsFactory<netNamedPipeBinding>());<br></br> factories.Add(BindingTypeEnum.netPeerTcpBinding,<br></br> new ControlsFactory<netPeerTcpBinding>());<br></br> factories.Add(BindingTypeEnum.netTcpBinding,<br></br> new ControlsFactory<NetTcpBinding>());<br></br> factories.Add(BindingTypeEnum.wsDualHttpBinding,<br></br> new ControlsFactory<WSDualHttpBinding>());<br></br> factories.Add(BindingTypeEnum.wsFederationHttpBinding,<br></br> new ControlsFactory<WSHTTPBinding>());<br></br> factories.Add(BindingTypeEnum.wsHTTPBinding,<br></br> new ControlsFactory<WSHTTPBinding>());<br></br> }<br></br> return factories;<br></br> }<br></br> public static UserControl createControl(BindingTypeEnum type ,Service service,<br></br> ServiceMaintanence parent, bool update)<br></br> {<br></br> Dictionary<BindingTypeEnum, IControlsFactoryInterface> builders = getFactories();<br></br> IControlsFactoryInterface builder = builders[type];<br></br> return builder.createControl(service, parent, update);<br></br> }<br></br> }<br></br> }
表 3 使用泛型实现工厂模式在这个实现中,IControlsFactoryInterface 接口类定义了被具体工厂支持的接口。所有的具体工厂都由泛型类 —— ControlsFactory
实现。这个类依赖 C#泛型对约束的支持 [7],这种支持允许定义一种客户类型必须遵守的约束。这样可以使一个具体的工厂调用定义在基类的方法。 最后,泛型工厂的实现基于一个 dictionary,这样可以从一个绑定类型找到对应工厂的。
未来的增强
我们正在计划在未来的实现中增强一些主要的功能。
为服务的更新实现 Pub/Sub
目前的实现支持服务端点信息在一个某个预定时间后过期。这意味着需要定期要从服务注册中心获取信息以检查缓存中的服务信息是否有效。这个策略:
- 增添了额外的网络负担 —— 不管信息有没有改变都要检查服务中心。
- 在某一个时间段,缓存的服务信息有可能和实际的不匹配。
因此,服务代理的配置需要在通过加大缓存更新间隔来优化性能(减少网络流量)和减低缓存更新间隔来减少服务信息的不匹配之间进行平衡。
一个对此问题更好的解决方案是实现一个 pub/sub(见例子【6】)。这种情况下,在获取某个服务信息的同时,代理创建器订阅服务信息的更新,而这些信息由注册中心服务发布。
扩展维护程序
如同以上所讲,维护程序是用来简化服务信息的创建和修改。因此支持人员将是这个程序的目标客户。支持人员经常遇到的一个问题是如何知道在某个时刻哪个服务是可操作的。引入服务注册中心作为所有服务位置信息的集中位置可以很容易回答这个问题 —— 基于所有已存在的服务和其地址信息,维护程序可以“探寻(ping)”服务并显示其当前的状态。这个实现不仅需要对维护程序添加新的功能,也需要所有服务实现设计支持“探寻(ping)”。最简单的方法就是使所有的服务接口都从一个标准的“探寻(ping)”接口继承,这样就强制每一个服务都实现相应的功能。
<span color="#0000ff">namespace</span> Service.Instrumentation.Management<br></br> {<br></br><span color="#008000">// Define a service contract</span>.<br></br><span color="#008080">[ServiceContract</span>(Namespace = <span color="#ff0000">"http://Service.Instrumentation.Management"</span>)]<br></br><span color="#0000ff">public interface</span> <span color="#008080">IPing</span><br></br> {<p><span color="#008080">[OperationContract]</span><span color="#0000ff">bool</span> Ping();</p><br></br> }<br></br> }
表 4 Ping 接口
一旦完成了这个工作,把验证服务的功能加入到注册中心维护程序就很直接了。
结论
如同在【9】中的描述,服务注册中心和仓库是实现 SOA 的关键。商业实现(很少有例外)基本不被用做一个纯粹的运行时仓库。另外,商业产品通常对于小型和中型的项目过于昂贵(许可证,客户化,培训,部署的费用)。这篇文章展示了一个非常简单的注册中心的实现(从设计到最终实现不到三个星期),它提供了 SOA 运行时注册中心的大部分的功能。同时也提供了一些在商业产品中没有的功能。
感谢
作者向他的同事表示感谢,尤其是 Paul Rovkah 和 Robert Sheldon,他们在多次讨论中为项目的实现和这篇文章的提高起到了决定作用。
关于作者
Boris Lublinsky 在软件工程和技术架构上有超过 25 年的经验。在最近几年,他关注于企业架构,SOA 和过程管理。在整个职业生涯中,Lublinsky 博士一直是一个积极的技术宣讲者和作者。他在各种不同的杂志上发表了超过 50 篇技术文章,包括在 Avtomatika i telemechanica,IEEE Transactions on Automatic Control,Distributed Computing,Nuclear Instruments and Methods,Java Developer’s Journal,XML Journal,Web Services Journal,JavaPro Journal,Enterprise Architect Journal 和 EAI Journal。目前 Lublinsky 博士是 HerzumSoftware 的首席架构师,他的工作包括开发软件工厂并帮助客户实施。
引用
1. Core J2EE Patterns - Service Locator .
2. Edward Hieatt and Rob Mee. Repository .
3. Martin Fowler. Inversion of Control Containers and the Dependency Injection pattern. January 2004.
4. Griffin Caprio. Design Patterns: Dependency Injection. September 2005.
5. Service Component Architecture Specifications.
6. Juval Lowy. What You Need To Know About One-Way Calls, Callbacks, And Events .
7. Juval Lowy. An Introduction to C# Generics .
8. Windows Communication Foundation Architecture .
9. B. Lublinsky. Explore the role of service repositories and registries in Service-Oriented Architecture (SOA) IBMDeveloperWorks, May 2007.
查看英文原文: Implementing a Service Registry for .NET Web Services - - - - - -
译者简介:戴垚,2000 年计算机硕士毕业后一直从事软件开发管理工作,目前在一家大型外企担任开发部门经理。关心软件技术和相关工具的动态,深信技术的使用应以创造价值为根本。目前致力于 SOA 的研究,希望能对业已复杂的企业环境有所帮助。参与 InfoQ 中文站内容建设,请邮件至 china-editorial@infoq.com 。
评论