1. SOA的基本概念和设计思想
1.1 SOA就是采用Web服务的架构吗?
不是,Web服务是实现SOA的一种手段,并不能对等,可以这样说,实现SOA有很多种技术手段,Web服务是其中一种。
SOA,面向服务架构,其中面向服务与面向对象、面向组件都是一种设计理念,体现的是我们对所关注的点进行分解的思想。因此,SO(面向服务)是一种设计理念,与技术无关。
Web服务(这里指的是广义的Web服务,既包括微软平台下的ASP.NET .asmxWeb服务和WCF,也包括其他平台的Web服务)是一种实现SOA理想的技术手段。
1.2 SOA的基本特性
1.2.1 服务是自治的
服务的自治原则要求单个服务在底层逻辑控制方面尽可能是独立和自包含的,服务尽可能不依赖于访问它的客户端和其他服务。服务可以独立地进行部署及实施版本策略和安全策略。
1.2.2 SOA依赖于开放的标准
SOA的一个目标是让不同厂商开发的服务能够进行互操作。
SOA采用基于消息的通信方式,从消息交换的角度来讲,就是要求消息自身标准化。在此方面,SOAP消息的采用对消息承载的内容提供了一致性的表示。
客户端进行服务调用的前提是对服务描述的理解,所以服务描述也需要一种标准化的表示。在此方面,SOA采用XML、XSD及WSDL作为服务描述的“语言”。
当SOA真正用于企业级应用时,还需要考虑一些额外的因素,比如传输安全、可靠消息传输、事务的支持等。要实现真正意义上的跨平台互操作,实现这些特性的互操作方式同样需要通过一种开放的标准确定下来。
1.2.3 SOA支持跨平台
能够让不同的平台进行通信是SOA产生的主因。跨平台性最大的好处就是促进了异质系统的集成。
1.2.4 SOA鼓励创建可组合的服务
按照所提供功能大小的差异,不同的服务具有不同的粒度。我们可以把提供具有最小粒度功能实现的服务称为原子服务。多个原子服务可以通过合理的组合、编排构成一个新的聚合型服务。
1.2.5 SOA鼓励服务的复用
基于类型系统交互方式面向组件的不同,SOA通过“契约”实现客户端对服务的调用,双方只需要采用能够匹配的契约就能保证正常的交互。基于契约的服务交互,又进一步地促进了服务的自治,只要契约不发生改变,服务本身的实现就可以自由地变化。
2. WCF是对现有分布式通信技术的整合
WCF的动机是将现有的所有分布式技术统一起来,提供一个统一的应用编程接口(API)
2.1 WCF和.NET Remoting
1)两者都是分布式通信架构
2)两者都采用了基于信道栈的“管道式”消息处理与传输机制,信道栈是通过一系列相关信道进行有序组合构成的管道,其中的每一“节”信道独立地完成基于某种功能的消息处理。这是一种极具扩展性的设计,信道栈的组合性使我们能够根据具体的消息处理需求对构成管道的信道进行合理的组合。
3).NET Remoting不能提供跨平台的支持,仅仅提供服务端和客户端均处于.NET平台下的通信
2.2 WCF是各种分布式技术的集大成者
3. 构建一个简单的WCF应用
3.1 需求描述
实现一个简单的计算服务来提供基本的加、减、乘、除运算。客户端和服务通过运行在同一台机器上的不同程序来模拟。图体现了客户端和服务端进程互相调用的关系。
WCF的服务不能孤立地存在,需要寄宿于一个运行着的进程中,我们把承载WCF服务的进程称为宿主,为服务指定宿主的过程称为服务寄宿(Service Hosting)。我们采用自我寄宿(Self-Hosting)和IIS寄宿两种服务寄宿方式。对于前者,我们通过一个控制台应用作为服务的宿主;对于后者,则将服务寄宿于IIS的工作进程(W3WP.exe)。客户端通过另一个控制台应用模拟(进程为Client.exe)
3.2 构建整个解决方案
3.2.1 创建一个名为WcfServices的解决方案并新建4个项目
1)Service.Interface:用于定义服务契约(Service Contract)的类库项目,引用WCF的核心程序集System.ServiceModel.dll。
using System.ServiceModel;
namespace Artech.WcfServices.Service.Interface
{
/// <summary>
/// 定义了服务契约的名称和命名空间
/// </summary>
[ServiceContract(Name = "CalculatorService", Namespace ="http://www.artech.com/")]
public interface ICalculator
{
/// <summary>
/// 通过应用ServiceContractAttribute特性将接口定义成服务契约之后,接口的方法并不能自动成为服务操作。
/// WCF采用的是显式选择(Explicit Opt-in)的策略,意味着我们需要在相应的操作方法上面显式地应用OperationContractAttribute特性。
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
[OperationContract]
double Add(double x, double y);
[OperationContract]
double Subtract(double x, double y);
[OperationContract]
double Multiply(double x, double y);
[OperationContract]
double Divide(double x, double y);
}
}
2)Service:用于定义服务类型的类库项目。由于服务类型需要实现定义在Service.Interface中相应的契约接口,因此该项目具有对Service.Interface项目的引用。
注意:从功能上讲,服务契约抽象了服务提供的所有操作;而站在消息交换的角度来看,服务契约则定义了基于服务调用的消息交换过程中请求消息和回复消息的结构,以及采用的消息交换模式,我们一般将服务契约定义成接口。
当契约接口ICalculator成功创建后,我们在Service项目中创建实现它的服务CalculatorService。
using Artech.WcfServices.Service.Interface;
namespace Artech.WcfServices.Service
{
public class CalculatorService : ICalculator
{
public double Add(double x, double y)
{
return x + y;
}
public double Subtract(double x, double y)
{
return x - y;
}
public double Multiply(double x, double y)
{
return x * y;
}
public double Divide(double x, double y)
{
return x / y;
}
}
}
3)Hosting:作为服务宿主的控制台应用。该项目同时引用Service.Interface、Service项目和System.ServiceMode.dll程序集。
注意:WCF服务需要依存一个运行着的宿主进程,服务寄宿就是为服务指定一个宿主的过程。WCF采用基于终结点(Endpoint)的通信手段。终结点由地址(Address)、绑定(Binding)和契约(Contract)三要素组成。
地址(Address):地址决定了服务的位置,解决了服务寻址的问题
绑定(Binding):绑定实现了通信的所有细节,包括网络传输、消息编码,以及其他为实现某种功能(比如传输安全、可靠消息传输、事务等)对消息进行的相应处理。WCF中具有一系列的系统定义绑定,比如BasicHttpBinding、WSHttpBinding和NetTcpBinding等
契约(Contract):契约是对服务操作的抽象,也是对消息交换模式及消息结构的定义
服务寄宿的目的就是开启一个进程,为WCF服务提供一个运行的环境,并为服务添加一个或多个终结点,使之暴露给潜在的服务消费者。
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
using Artech.WcfServices.Service;
using Artech.WcfServices.Service.Interface;
namespace Artech.WcfServices.Hosting
{
class Program
{
static void Main(string[] args)
{
//基于服务类型(typeof(CalculatorService))创建了ServiceHost对象
using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
{
//添加了一个基于WSHttpBinding绑定的终结点
//该终结点的地址为http://127.0.0.1:3721/calculatorservice
//服务契约的类型ICalculator
host.AddServiceEndpoint(typeof(ICalculator), new WSHttpBinding(),"http://127.0.0.1:3721/calculatorservice");
if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null)
{
//WCF中元数据的发布通过一个特殊的服务行为ServiceMetadataBehavior来实现
//我们为ServiceHost添加了ServiceMetadataBehavior这样一个服务行为,
//并采用了基于HTTP-GET的元数据获取方式,
//并且通过ServiceMetadataBehavior的HttpGetUrl属性指定元数据发布地址(http://127.0.0.1:3721/ calculatorservice/metadata)。
ServiceMetadataBehavior behavior = new ServiceMetadataBehavior();
behavior.HttpGetEnabled = true;
behavior.HttpGetUrl = new Uri("http://127.0.0.1:3721/calculatorservice/metadata");
host.Description.Behaviors.Add(behavior);
}
host.Opened += delegate
{
Console.WriteLine("CalculaorService已经启动,按任意键终止服务!");
};
host.Open();
Console.Read();
}
}
}
}
在进行真正的WCF应用开发时,一般采用配置而不是编程的方式进行终结点的添加和服务行为的定义。
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="metadataBehavior">
<serviceMetadata httpGetEnabled="true"
httpGetUrl="http://127.0.0.1:3721/calculatorservice/metadata" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="Artech.WcfServices.Service.CalculatorService"
behaviorConfiguration="metadataBehavior" >
<endpoint address="http://127.0.0.1:3721/calculatorservice"
binding="wsHttpBinding"
contract="Artech.WcfServices.Service.Interface.ICalculator" />
</service>
</services>
</system.serviceModel>
</configuration>
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
using Artech.WcfServices.Service.Interface;
using Artech.WcfServices.Service;
namespace Artech.WcfServices.Hosting
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
{
host.Opened += delegate
{
Console.WriteLine("CalculaorService已经启动,按任意键终止服务!");
};
host.Open();
Console.Read();
}
}
}
}
4)Client:一个控制台应用模拟服务的客户端,该项目引用System.ServiceModel程序集。
在Client项目中可以直接利用通过添加服务引用生成的服务代理类来调用我们寄宿的CalculatorService服务。
3.2.2 创建客户端调用服务
在运行服务寄宿程序(Hosting.exe)的情况下,右键单击Client项目并在弹出的快捷菜单中选择“添加服务引用”,会弹出如图1-7所示的添加服务引用对话框。在地址栏输入服务元数据发布的源地址(http://127.0.0.1:3721/ calculatorservice/metadata),并指定一个命名空间,单击“OK”按钮,Visual Studio会生成一系列用于服务调用的代码和配置。
当服务引用被成功添加后,被客户端用于进行服务调用的服务契约接口CalculatorService会被生成出来。客户端的契约接口之所以会命名为CalculatorService,而不是ICalculator,是因为我们在定义契约接口的时候将ServiceContractAttribute特性的Name属性设置成了CalculatorService。CalculatorService是与定义在Service.Interface项目中的ICalculator接口等效的契约接口。
真正被客户端用于服务调用的则是一个叫做CalculatorServiceClient的类。
客户端通过服务代理对象进行服务的调用,上面的例子通过创建自动生成的、继承自ClientBase<TChannel>的类型对象进行服务调用。实际上还有另外一种创建服务代理的方法,那就是通过System.ServiceModel.ChannelFactory<TChannel>直接创建服务代理对象。
3.2.3 让服务端和客户端引用相同的契约
WCF采用基于契约的服务调用方法。Visual Studio在进行服务引用添加的过程中,会在客户端创建一个与服务端等效的服务契约接口。由于服务端和客户端都在同一个解决方案中,因此完全可以让服务端和客户端引用相同的契约。
1)Client项目添加对Service.Interface项目的引用
2)在客户端程序中按照如下的方式基于地址和绑定对象创建一个ChannelFactory<ICalculator>,然后调用它的CreateChannel方法创建的服务代理对象完成服务调用
using System;
using System.ServiceModel;
using Artech.WcfServices.Service.Interface;
namespace Artech.WcfServices.Client
{
class Program
{
static void Main(string[] args)
{
using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(new WSHttpBinding(), "http://127.0.0.1:3721/calculatorservice"))
{
ICalculator proxy = channelFactory.CreateChannel();
Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, proxy.Add(1, 2));
Console.WriteLine("x - y = {2} when x = {0} and y = {1}", 1, 2, proxy.Subtract(1, 2));
Console.WriteLine("x * y = {2} when x = {0} and y = {1}", 1, 2, proxy.Multiply(1, 2));
Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 1, 2, proxy.Divide(1, 2));
}
Console.Read();
}
}
}
终结点是WCF进行通信的唯一手段,ChannelFactory<TChannel>本质上是通过指定的终结点创建用于进行服务调用的服务代理。在上面的代码中,在创建ChannelFactory<TChannel>的时候在构造函数中指定终结点的ABC三要素,其中地址和绑定则通过参数指定,而契约体现在ChannelFactory<TChannel>的泛型参数上。真正的WCF应用大都采用配置的方式进行终结点的定义。可以通过下面的配置指定终结点的地址、绑定和契约,并为相应的终结点指定一个终结点配置名称calculatorservice。
<configuration>
<system.serviceModel>
<client>
<endpoint name="calculatorservice"
address="http://127.0.0.1:3721/calculatorservice"
binding="wsHttpBinding"
contract="Artech.WcfServices.Service.Interface.ICalculator" />
</client>
</system.serviceModel>
</configuration>
using System;
using System.ServiceModel;
using Artech.WcfServices.Service.Interface;
namespace Artech.WcfServices.Client
{
class Program
{
static void Main(string[] args)
{
using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
{
ICalculator proxy = channelFactory.CreateChannel();
Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, proxy.Add(1, 2));
Console.WriteLine("x - y = {2} when x = {0} and y = {1}", 1, 2, proxy.Subtract(1, 2));
Console.WriteLine("x * y = {2} when x = {0} and y = {1}", 1, 2, proxy.Multiply(1, 2));
Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 1, 2, proxy.Divide(1, 2));
}
Console.Read();
}
}
}
3.3 通过IIS寄宿服务
3.3.1 创建.svc文件
每个WCF服务具有一个对应的文本文件,其文件扩展名为.svc。基于IIS的服务寄宿要求相应的WCF服务具有相应的.svc文件,.svc文件部署于IIS站点中,对WCF服务的调用体现在对.svc文件的访问上。
.svc文件仅仅包含一个%@ServiceHost%指令(Directive),该指令具有一个必需的Service属性和一系列可选的属性。
3.3.2 创建Web应用
寄宿在IIS下的WCF服务实际上就是一个Web应用,所以需要通过IIS管理器为寄宿的服务创建一个Web应用。
下面通过配置的方式来定义寄宿服务的终结点和用于元数据发布的ServiceMetadataBehavior服务行为。由于寄宿在IIS中的服务本质上是一个Web应用,承载Web应用的配置自然是定义在Web.config文件中的。
我们在Service项目的根目录下创建一个Web.config,并将之前定义在Hosting项目中的App.config配置复制过来。由于服务调用是通过访问服务对应的.svc文件来实现的,这个.svc文件所在的地址对于客户端来说就是服务(终结点)的地址,因此需要将配置的终结点的address属性删除。
除了终结点不具有地址之外,在为服务应用服务行为ServiceMetadataBehavior用于元数据发布时也没有指定元素的发布地址。在这种情况下,.svc文件的地址加上?wsdl查询字符串就是元数据发布地址。对于本例来说,当服务被成功寄宿到本地名称为WcfServices的Web应用后,就可以通过地址http://127.0.0.1/wcfservices/calculatorservice.svc?wsdl得到表示服务元数据的WSDL文件。
由于在创建Service项目的时候,我们并不曾引用System.ServiceMode.dll程序集,因此必须加上对该程序集的引用。因为Web应用在运行的时候只会从位于根目录下的bin子目录中加载程序集,而在默认的情况下,编译后的程序集会自动保存到bin\Debug|Release目录下,因此需要通过Visual Studio修改Service项目属性,将编译输出目录设置成\bin。
客户端仅仅需要修改终结点的地址,从而转向对寄宿于IIS下的CalculatorService的访问,该地址即为.svc文件的网络地址:http://localhost:8081/CalculatorService.svc。