一个典型的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个主要的表:
理想状态下,服务注册中心应该能够在绑定级别实现可靠/安全数据的重用和在服务级别实现绑定的重用。我们目前还没有实现。目前服务和绑定、绑定与可靠/安全之间都是一对一的关系。虽然数据有重复,但大大简化了实现和维护。
除了这些表,我们使用一个视图(图5)简化了获取一个完整服务定义的数据库访问。
图5 完全服务联接
我们决定使用Web Services Software Factory (WSSF) Model Edition设计服务。下面是使用这个工厂定义服务所需的数据类型(图6)。
图6 服务注册中心数据类型
使用WSSF设计的服务注册中心契约,如图7:
图7 服务注册中心契约
服务的实现非常简单和直接。它的基础是用微软模式与实践企业库(数据访问模块 data access block)和仓储模式(repository pattern)【2】实现的一个持久层,它提供了一个无类型依赖的数据访问。包括以下的类(图8):
图8 持久层
Service的实现类直接使用数据访问层(ServiceRegistryRepository 类)来执行服务。当收到一个特定请求时,服务的实现调用 ServiceRegistryRepository 的一个对应方法来进行数据库操作并返回数据集。
服务注册中心的主要使用者是访问业务服务的服务消费者。服务注册中心的引入强化了在服务消费者实现中的依赖注入模式【3,4】——服务消费者依赖于一个特定的接口(这种方法完全对消费者隐藏了服务的WSDL/XSD。尽管它们在服务设计时非常重要,但消费者的实现只依赖于服务接口,因此完全隐藏了所有的分布和访问机制),而具体的实现(和绑定)在运行时通过对注册中心的访问而被动态注入,这些逻辑作为整体实现的一部分被封装在了一个代理构建器的类中。
为了提高性能,服务消费者缓存了端点的信息。目前我们使用的是基于时间(时间长度可以配置)的缓存策略;一个更高级的发布–订阅缓存策略将会在以后的某个版本中引入。一个客户实现可以通过以下的一个方法为一个给定的接口动态创建一个代理:
public static Interface getProxy<Interface>
(string sName, string sVersion, string sConsumer, string callBackCl)
这个方法接收服务名称,版本,消费类型(一般的QoS参数)和回调接口名称(可选的参数),并为一个给定的接口类型创建一个服务代理。这个方法首先尝试在本地缓存中解析所需的代理(见上),如果找到就立刻将之返还给调用者。否则,就会调用注册中心服务根据提供的参数查找服务信息。使用一个全限定接口名称作为一个额外的查找参数,这样可以用来保证一个指向的接口服务实现的引用满足客户要求1。
如前面讲到的,我们通过使用.NET 3.0 提供的 ChannelFactory<Interface> 实现代理的创建。这个类可以通过服务注册中心提供的接口,端点地址和绑定信息创建一个服务代理。一旦代理创建完成,它就会被存储在缓存中并返还给使用者2。
除了使用 WCF 连接服务,这个实现还能够支持“本地”绑定——直接从客户可以访问的程序集(assembly)中定位服务的实现。这个程序集或者已被客户程序加载或在GAC中。
我们可以通过将服务参数从代码移到一个配置文件中(见表1的例子)进一步简化ProxyBuilder的使用。这个方法参考了SCA的配置【5】,可以进一步简化服务消费者的维护。
<configSections>
<section name="composite" type="ProxyBuilder.CompositeSectionHandler,
ProxyBuilder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</configSections>
<composite>
<reference Name = "Calculator"> <Service Name = "TestService" Version = "02" ConsumerType = ""></Service>
<Callback Name = "Test.test"></Callback>
</reference>
</composite>
表1 引用配置
另外,注册中心本身的位置也可以移到配置文件中(表2)。
<registry>
<runtime Location ="http://localhost:58934/WCFServiceRegistry.Host/ServiceRegistry.svc"/>
</registry>
表2 注册中心位置的配置
这是一个独立的应用,用来维护服务注册中心的信息。它支持以下主要的功能:
这个应用采用一个Master-Details模式,其中master是基本的服务信息和绑定类型(在一个service表中),details包括服务使用的绑定类型的详细信息(图9)。这些详细信息和接下去的界面都基于绑定类型。
主界面的实现基于一个DataGridView控件,它本身提供了大部分所需的功能,包括显示,修改现有的记录和添加删除记录。绑定的详细信息被实现为一个自定义用户控件。因为不同的绑定类型有不同的详细信息,我们为各种绑定类型实现了不同的窗体。我们使用工厂模式(见附注)为某个绑定类型创建窗体并产生绑定详细信息。
图9 服务维护界面
附注:使用C#泛型实现工厂模式
工厂模式是根据不同类型创建多态对象的一个常见方法。使用C#泛型则可以大大简化这个实现(表3)。
namespace RegistryMaintanence.controls
{ public interface IControlsFactoryInterface
{ UserControl createControl(Service service, ServiceMaintanence parent, bool update);
}
public class ControlsFactory<T> : IControlsFactoryInterface
where T : UserControl, InitializableControl, new()
{
#region IControlsFactoryInterface Members
public UserControl createControl(Service service, ServiceMaintanence parent, bool update)
{
T t = new T();
t.initialize(service, parent, update);
return t;
}
#endregion
}
public class ControlsFactory{
private static Dictionary<BindingTypeEnum, IControlsFactoryInterface> factories = null;
private ControlsFactory(){}
private static Dictionary<BindingTypeEnum, IControlsFactoryInterface> getFactories(){
if(factories == null){
factories = new Dictionary<BindingTypeEnum,IControlsFactoryInterface>();
factories.Add(BindingTypeEnum.basicHTTPBinding,
new ControlsFactory<BasicHTTPBinding>());
factories.Add(BindingTypeEnum.netMsmqBinding,
new ControlsFactory<netMsmqBinding>());
factories.Add(BindingTypeEnum.netNamedPipeBinding,
new ControlsFactory<netNamedPipeBinding>());
factories.Add(BindingTypeEnum.netPeerTcpBinding,
new ControlsFactory<netPeerTcpBinding>());
factories.Add(BindingTypeEnum.netTcpBinding,
new ControlsFactory<NetTcpBinding>());
factories.Add(BindingTypeEnum.wsDualHttpBinding,
new ControlsFactory<WSDualHttpBinding>());
factories.Add(BindingTypeEnum.wsFederationHttpBinding,
new ControlsFactory<WSHTTPBinding>());
factories.Add(BindingTypeEnum.wsHTTPBinding,
new ControlsFactory<WSHTTPBinding>());
}
return factories;
}
public static UserControl createControl(BindingTypeEnum type ,Service service,
ServiceMaintanence parent, bool update)
{
Dictionary<BindingTypeEnum, IControlsFactoryInterface> builders = getFactories();
IControlsFactoryInterface builder = builders[type];
return builder.createControl(service, parent, update);
}
}
}表3 使用泛型实现工厂模式
在这个实现中,IControlsFactoryInterface 接口类定义了被具体工厂支持的接口。所有的具体工厂都由泛型类 —— ControlsFactory<T> 实现。这个类依赖C#泛型对约束的支持[7],这种支持允许定义一种客户类型必须遵守的约束。这样可以使一个具体的工厂调用定义在基类的方法。
最后,泛型工厂的实现基于一个dictionary,这样可以从一个绑定类型找到对应工厂的。
我们正在计划在未来的实现中增强一些主要的功能。
目前的实现支持服务端点信息在一个某个预定时间后过期。这意味着需要定期要从服务注册中心获取信息以检查缓存中的服务信息是否有效。这个策略:
因此,服务代理的配置需要在通过加大缓存更新间隔来优化性能(减少网络流量)和减低缓存更新间隔来减少服务信息的不匹配之间进行平衡。
一个对此问题更好的解决方案是实现一个pub/sub(见例子【6】)。这种情况下,在获取某个服务信息的同时,代理创建器订阅服务信息的更新,而这些信息由注册中心服务发布。
如同以上所讲,维护程序是用来简化服务信息的创建和修改。因此支持人员将是这个程序的目标客户。支持人员经常遇到的一个问题是如何知道在某个时刻哪个服务是可操作的。引入服务注册中心作为所有服务位置信息的集中位置可以很容易回答这个问题 —— 基于所有已存在的服务和其地址信息,维护程序可以“探寻(ping)”服务并显示其当前的状态。这个实现不仅需要对维护程序添加新的功能,也需要所有服务实现设计支持“探寻(ping)”。最简单的方法就是使所有的服务接口都从一个标准的“探寻(ping)”接口继承,这样就强制每一个服务都实现相应的功能。
namespace Service.Instrumentation.Management
{
// Define a service contract.
[ServiceContract(Namespace = "http://Service.Instrumentation.Management")]
public interface IPing
{
[OperationContract]
bool Ping();
}
}
表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.