WCF服务二:创建一个简单的WCF服务程序
在本例中,我们将实现一个简单的计算服务,提供基本的加、减、乘、除运算,通过客户端和服务端运行在同一台机器上的不同进程实现。
一、新建WCF服务
1、新建一个空白解决方案,解决方案名称为“WCFSolution”。
2、解决方案右键->添加->类库项目,类库名称为CalculateWcfService。
3、创建服务契约
WCF采用基于契约的交互方式实现了服务的自制。服务契约:是相关操作的集合。契约就是双方或多方就某个关注点达成的一种共识,是一方向另一方的一种承诺。签署了某个契约就意味着自己有义务履行契约中的各项规定,一旦违约势必影响契约双方的正常交互。我们主张通过抽象将接口和实现相互分离,鼓励接口的依赖,避免基于实现的依赖。接口是稳定的,而实现则是易变的,基于接口的服务调用能够更有效地应对实现的变化带来的影响。接口从本质上讲就是一种契约,当某个类实现了某个接口,就相对于签署了一份契约。所以契约关心的是“我能做到”,不在于“我如何做到”。所以,服务契约是以接口的形式进行定义的。
下面的代码中,定义了一个接口,通过在接口上应用System.ServiceModel命名空间下的ServiceContractAttribute特性将ICalculateService接口定义成服务契约。在应用ServiceContractAttribute特性的同时,还可以指定服务契约的名称和命名空间。每个服务契约都有一个确定的名称,当在一个接口上应用了该属性以后,默认的名称就是接口的名称。我们可以通过Name属性显示地指定需要的名称。
NameSpace:服务契约的命名空间,其作用是解决命名冲突的问题,提倡将你所在的公司名称或项目名称的URN作为命名空间。WCF默认的命名空间是:http://tempuri.org/。
服务契约是一组相关服务操作的集合,当我们在一个接口上应用了ServiceContractAttribute,便赋予了服务契约的属性。但是,对于这样一个类型,它的成语并不会自动成为契约的服务操作,只有应用了OperationContractAttribute特性后,相应的方法成员才能成为能够通过服务调用方式访问的服务操作。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.ServiceModel; 7 8 namespace CalculateWcfService 9 { 10 /// <summary> 11 /// 定义服务契约:通过接口实现契约 12 /// </summary> 13 [ServiceContract(Name = "CalculateService")] 14 public interface ICalculateService 15 { 16 /// <summary> 17 /// 定义操作契约 18 /// </summary> 19 /// <param name="num1"></param> 20 /// <param name="num2"></param> 21 /// <returns></returns> 22 [OperationContract(Name = "AddOperation")] 23 double AddOperation(double num1, double num2); 24 25 /// <summary> 26 /// 定义操作契约 27 /// </summary> 28 /// <param name="num1"></param> 29 /// <param name="num2"></param> 30 /// <returns></returns> 31 [OperationContract(Name = "SubOperation")] 32 double SubOperation(double num1, double num2); 33 34 /// <summary> 35 /// 定义操作契约 36 /// </summary> 37 /// <param name="num1"></param> 38 /// <param name="num2"></param> 39 /// <returns></returns> 40 [OperationContract(Name = "MulOperation")] 41 double MulOperation(double num1, double num2); 42 43 /// <summary> 44 /// 定义操作契约 45 /// </summary> 46 /// <param name="num1"></param> 47 /// <param name="num2"></param> 48 /// <returns></returns> 49 [OperationContract(Name = "DivOperation")] 50 double DivOperation(double num1, double num2); 51 } 52 }
4、创建服务:新建一个类,实现第3步中创建的接口
当服务契约成功创建时,我们需要通过实现服务契约来创建具体的WCF服务。WCF服务CalculateService实现了服务契约接口ICalculateService,实现了接口里面定义的所有服务操作。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace CalculateWcfService 8 { 9 public class CalculateService:ICalculateService 10 { 11 /// <summary> 12 /// 加 13 /// </summary> 14 /// <param name="num1"></param> 15 /// <param name="num2"></param> 16 /// <returns></returns> 17 public double AddOperation(double num1, double num2) 18 { 19 return num1 + num2; 20 } 21 22 /// <summary> 23 /// 减 24 /// </summary> 25 /// <param name="num1"></param> 26 /// <param name="num2"></param> 27 /// <returns></returns> 28 public double SubOperation(double num1, double num2) 29 { 30 return num1 - num2; 31 } 32 33 /// <summary> 34 /// 乘 35 /// </summary> 36 /// <param name="num1"></param> 37 /// <param name="num2"></param> 38 /// <returns></returns> 39 public double MulOperation(double num1, double num2) 40 { 41 return num1 * num2; 42 } 43 44 /// <summary> 45 /// 除 46 /// </summary> 47 /// <param name="num1"></param> 48 /// <param name="num2"></param> 49 /// <returns></returns> 50 public double DivOperation(double num1, double num2) 51 { 52 return num1 / num2; 53 } 54 } 55 }
二、定义宿主寄宿WCF服务
WCF服务不能孤立地存在,必须要寄宿于一个运行着的进程中,我们把承载WCF服务的进程称为宿主,为服务指定宿主的过程称为服务寄宿(Service Hosting)。服务寄宿的目的就是开启一个进程,为WCF服务提供一个运行的环境。WCF服务典型的宿主包括以下四种:
"Self-Hosting" in a Managed Application(自托管宿主)
Managed Windows Services(Windows Services宿主)
Internet Information Services(IIS宿主)
Windows Process Activation Service(WAS宿主)
1、以自托管宿主的方式寄宿。
1.1 通过代码的方式配置WCF服务
利用WCF提供的ServiceHost<T>提供的Open()和Close()方法,可以便于开发者在控制台、Windows应用程序乃自于ASP.NET应用程序中托管服务,不管自宿主的环境是何种应用程序,实质上托管服务的方式都是一致的。例如在控制台应用程序中:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.ServiceModel; 7 using System.ServiceModel.Description; 8 using CalculateWcfService; 9 10 namespace ConsoleHosting 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 17 using (ServiceHost host = new ServiceHost(typeof(CalculateService), new Uri("http://127.0.0.1:9099/CalculateWcfService"))) 18 { 19 BasicHttpBinding bind = new BasicHttpBinding 20 { 21 Name = "basichttpbinding" 22 }; 23 host.AddServiceEndpoint(typeof(ICalculateService), bind, "CalculateWcfService"); 24 if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) 25 { 26 ServiceMetadataBehavior behavior = new ServiceMetadataBehavior() { 27 HttpGetEnabled=true 28 }; 29 host.Description.Behaviors.Add(behavior); 30 } 31 host.AddServiceEndpoint(typeof(IMetadataExchange), bind, "mex"); 32 if (host.State != CommunicationState.Opened) 33 { 34 host.Open(); 35 } 36 37 Console.WriteLine("服务已启动"); 38 Console.Read(); 39 } 40 } 41 } 42 }
WCF服务寄宿通过一个特殊的对象完成:ServiceHost。在上面的例子中,基于WCF服务的类型(typeof(CalculateService))创建了ServiceHost对象,并添加了一个终结点,终结点地址为http://127.0.0.1:9099/CalculateWcfService,采用了BasicHttpBinding的绑定方式,并指定了服务契约的类型ICalculateService。
松耦合是SOA的一个基本的特征,WCF应用中客户端和服务端的松耦合体现在客户端只须要了解WCF服务基本的描述,而无须知道具体的实现细节,就可以实现正常的服务调用。WCF服务的描述通过元数据(Metadata)的形式发布出来。WCF中元数据的发布通过一个特殊的服务行为ServiceMetadataBehavior实现。在上面提供的服务寄宿代码中,我们为创建的ServiceHost添加了ServiceMetadataBehavior,并采用了基于HTTP-GET的元数据获取方式。在调用ServiceHost的Open方法对服务成功寄宿后,我们可以通过该地址获取服务相关的元数据。在IE地址栏上键入http://127.0.0.1:9099/CalculateWcfService?wsdl,你将会得到以WSDL形式体现的服务元数据,如下图所示。
由于ServiceHost实例是被创建在应用程序域中,因此我们必须保证宿主进程在调用服务期间不会被关闭,因此我们利用Console.Read()来阻塞进程,以使得控制台应用程序能够一直运行,直到认为地关闭应用程序。如果是Windows应用程序,则可以将创建ServiceHost实例的代码放在主窗体的相关代码中,保证服务宿主不会被关闭。
在通常的企业应用中,我们很少会采用自宿主方式托管服务,这是因为这种方式必须要在应用程序运行下,客户端才能够调用服务,且并不便于随时启动和停止服务。除了不具有易用性与易管理性之外,在可靠性、性能等诸多方面受到很多限制。但由于它简单、易于实现,因而往往用于开发期间的调试或演示环境。
注意:自托管宿主支持所有的绑定。
1.2 通过配置文件的方式配置WCF服务
在进行真正的WCF应用开发时,一般不会直接通过编码的方式进行终结点的添加和服务行为的定义,而是通过配置文件的方式进行。
配置文件基本结构如下图所示:
WCF的配置文件共分为两部分:服务端配置与客户端配置。两者由于功能的不同,在配置文件的使用上也略有不同。
1.2.1 WCF的服务端配置
服务端的配置文件主要包括endpoint、binding、behavior的配置。一个标准的服务端配置文件所包含的主要xml配置节如下所示:
<system.ServiceModel>
<services>
<service>
<endpoint/>
</service>
</services>
<bindings>
<!—定义一个或多个系统提供的binding元素,例如<basicHttpBinding> -->
<!—也可以是自定义的binding元素,如<customBinding>. -->
<binding>
<!—例如<BasicHttpBinding>元素. -->
</binding>
</bindings>
<behaviors>
<!—一个或多个系统提供的behavior元素. -->
<behavior>
<!—例如<throttling>元素. -->
</behavior>
</behaviors>
</system.ServiceModel>
1.2.1.1 <services>配置节点
在<services>配置节中可以定义多个服务,每一个服务都被放到<service>配置节中,WCF的宿主程序可以通过配置文件找到这些定义的服务并发布这些服务。
<service>配置节包含name和behaviorConfiguration属性。其中,name配置了实现Service Contract的类型名。类型名必须是完整地包含了命名空间和类型名。而behaviorConfiguration的配置值则与其后的<behaviors>配置节的内容有关。<endpoint>是<service>配置节的主体,其中,<endpoint>配置节包含了endpoint的三个组成部分:Address、Binding和Contract。由于具体的binding配置是在<bindings>配置节中完成,因而,在<endpoint>中配置了bindingConfiguration属性,指向具体的binding配置。如下所示:
<services>
<service name="CalculateWcfService.CalculateService" behaviorConfiguration="MyBehavior">
<endpoint address=""
binding="netTcpBinding"
bindingConfiguration="DuplexBinding"
contract="CalculateWcfService.ICalculateService" />
</service>
</services>
1.2.1.2 <Endpoint>配置节点
终结点由地址(Address)、绑定(Binding)和契约(Contract)三要素组成。由于三要素应为首字母分别为ABC,所以终结点一般简称为ABC。
address:指定服务的统一资源标识符(URI),它可以是一个绝对地址或者是一个相对于服务基址给定的地址。如果设置为空字符串,则表示在创建服务的ServiceHost时,终结点在指定的基址上可用。
binding:通常,指定一个类似WsHttpBinding的系统提供的绑定,但也可以指定一个用户定义的绑定。指定的绑定确定传输协议类型、安全和使用的编码,以及是否支持或启用可靠会话、事务或流。
bindingConfiguration:如果必须修改绑定的默认值,则可通过在bindings元素中配置相应的binding元素来执行此操作 此属性应赋予与用于更改默认值的binding 元素的name 属性相同的值。
contract:指定定义协定的接口。 这是在由service 元素的name 属性指定的公共语言运行库(CLR) 类型中实现的接口。
我们也可以在一个<service>节点中定义多个endpoint,例如:
<services>
<service
name="Microsoft.ServiceModel.Samples.CalculatorService"
behaviorConfiguration="CalculatorServiceBehavior">
<endpoint address=""
binding="wsHttpBinding"
contract="Microsoft.ServiceModel.Samples.ICalculator" />
<endpoint address="mex"
binding="mexHttpBinding"
contract=" Microsoft.ServiceModel.Samples.IMetadataExchange" />
</service>
</services>
如果address值为空,那么endpoint的地址就是默认的基地址(Base Address)。例如ICalculator服务的地址就是http://localhost/servicemodelsamples/service.svc,而IMetadataExchange服务的地址则为http://localhost/servicemodelsamples/service.svc/mex。这里所谓的基地址可以在<service>中通过配置<host>来定义:
<service
name="Microsoft.ServiceModel.Samples.CalculatorService"
behaviorConfiguration="CalculatorServiceBehavior">
<host>
<baseAddresses>
<add baseAddress=
"http://localhost/ServiceModelSamples/service.svc"/>
</baseAddresses>
</host>
<endpoint … />
</service>
1.2.1.3 <behaviors>配置节
当我们在定义一个实现了Service Contract的类时, binding和address信息是客户端必须知道的,否则无法调用该服务。然而,如果需要指定服务在执行方面的相关特性时,就必须定义服务的behavior。在WCF中,定义behavior就可以设置服务的运行时属性,甚至于通过自定义behavior插入一些自定义类型。例如通过指定ServiceMetadataBehavior,可以使WCF服务对外公布Metadata。配置如下:
<behaviors>
<serviceBehaviors>
<behavior name="metadataSupport">
<serviceMetadata httpGetEnabled="true" httpGetUrl=""/>
</behavior>
<serviceBehaviors>
<behaviors>
在WCF中,behavior被定义为Attribute,其中,System.ServiceModel.ServiceBehaviorAttribute和System.ServiceModel.OperationBehaviorAttribute是最常用的behavior。虽然,behavior作为Attribute可以通过编程的方式直接施加到服务上,但出于灵活性的考虑,将behavior定义到配置文件中才是最好的设计方式。
利用ServiceBehavior与OperationBehavior可以控制服务的如下属性:
1、 对象实例的生命周期;
2、 并发与异步处理;
3、 配置行为;
4、 事务行为;
5、 序列化行为;
6、 元数据转换;
7、 会话的生命周期;
8、 地址过滤以及消息头的处理;
9、 模拟(Impersonation);
例如,通过ServiceBehavior设置对象实例的生命周期:
<behaviors>
<serviceBehaviors>
<behavior name="metadataSupport">
<instanceContextMode httpGetEnabled="true" httpGetUrl=""/>
</behavior>
<serviceBehaviors>
<behaviors>
除了直接手动修改配置文件以外,还可以直接使用VS提供的配置工具。可以通过VS的工具(Tools)菜单,选择“WCF 服务配置编辑”子项,开启这样的一个配置编辑器,如下图所示:
或者在配置文件上直接点右键,选择“编辑WCF配置”打开WCF配置编辑器,如下图所示:
如果采用了配置文件的方式,服务寄宿代码将会得到极大的精简,只需包含下面几行代码:
1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Text; 4 using System.Threading.Tasks; 5 using System.ServiceModel; 6 using System.ServiceModel.Description; 7 using CalculateWcfService; 8 9 namespace ConsoleHosting 10 { 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 using (ServiceHost host = new ServiceHost(typeof(CalculateService))) 16 { 17 host.Open(); 18 Console.WriteLine("服务已启动"); 19 Console.Read(); 20 } 21 } 22 } 23 }
2、以Windows Services宿主的方式寄宿。
Windows Services宿主则完全克服了自托管宿主的缺点,它便于管理者方便地启动或停止服务,且在服务出现故障之后,能够重新启动服务。我们还可以通过Service Control Manager(服务控制管理器),将服务设置为自动启动方式,省去了服务的管理工作。此外,Windows Services自身还提供了一定的安全性以及检测机制和日志机制。
Windows Services宿主的实现也非常简单。我们可以在Visual Studio中创建Windows Services项目。在创建项目之后,就可以创建一个继承了System.ServiceProcess.ServiceBase类的Windows服务类。Windows服务类继承了ServiceBase类的OnStart()和OnStop()方法,完成Windows服务的启动与停止。我们可以重写这两个方法,将ServiceHost的启动与关闭对应地放入这两个方法的实现中。
1 using CalculateWcfService; 2 using System; 3 using System.Collections.Generic; 4 using System.ComponentModel; 5 using System.Data; 6 using System.Diagnostics; 7 using System.Linq; 8 using System.ServiceModel; 9 using System.ServiceModel.Description; 10 using System.ServiceProcess; 11 using System.Text; 12 using System.Threading.Tasks; 13 14 namespace WindowsServiceHosting 15 { 16 public partial class CalculateService : ServiceBase 17 { 18 public CalculateService() 19 { 20 InitializeComponent(); 21 } 22 23 protected override void OnStart(string[] args) 24 { 25 using (ServiceHost host = new ServiceHost(typeof(CalculateService), new Uri("http://127.0.0.1:9099/CalculateWcfService"))) 26 { 27 BasicHttpBinding bind = new BasicHttpBinding 28 { 29 Name = "basichttpbinding" 30 }; 31 host.AddServiceEndpoint(typeof(ICalculateService), bind, "CalculateWcfService"); 32 if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) 33 { 34 ServiceMetadataBehavior behavior = new ServiceMetadataBehavior() 35 { 36 HttpGetEnabled = true 37 }; 38 host.Description.Behaviors.Add(behavior); 39 } 40 host.AddServiceEndpoint(typeof(IMetadataExchange), bind, "mex"); 41 if (host.State != CommunicationState.Opened) 42 { 43 host.Open(); 44 } 45 } 46 } 47 48 protected override void OnStop() 49 { 50 51 } 52 } 53 }
如果在企业应用中要使用WCF技术,最佳的宿主方式我认为就是Windows Services,尤其是服务器的操作系统不是Vista的情况之下。它便于服务的管理,能够维持服务长时期的运行,同时它还支持所有的绑定,因而受到的限制最小。然而,这种方式唯一的缺点却是对宿主的部署相对比较复杂,必须通过.NET提供的Installutil.exe工具完成对服务宿主的安装(也可以通过安装包的自定义操作完成)。
将WCF服务寄宿到Windows服务中,也可以采用配置文件的方式实现,配置方式和自托管寄宿中的配置方式一样。
3、以IIS宿主的方式寄宿WCF服务
我们知道,每一个ASP.NET Web服务都具有一个.asmx文本文件,客户端通过访问.asmx文件实现对相应web服务的调用。与之类似,每个WCF服务也具有一个对应的文本文件,其文件扩展名为.svc。基于IIS的服务寄宿要求相应的WCF服务具有相应的.svc文件,.svc文件部署于IIS站点中,对WCF服务的调用体现在对.svc文件的访问上。
第一步、新建WCF服务站点
在解决方案上右击,选择“添加”->“新建网站”,打开新建网站对话框。在“添加新网站”对话框中,我们选择“WCF服务”,并把网站的名子命名为“CalculateIISHost”
建立起来的新的WCF服务站点的App_Code文件中自动为我们生成两个类文件:IService.cs和Service.cs。这两个文件对我们来说没有用,我们删掉。
第二步、添加引用
在刚刚创建的WCF服务站点上添加对WCF服务库项目--CalculateWcfService项目的引用。
第三步、配置Service.svc文件
双击Service.svc文件,我们可以看到它的声明指示如下:
<%@ ServiceHost Language="C#" Debug="true"
Service="Service" CodeBehind="~/App_Code/Service.cs" %>
由于在第二步中我们已经把IService.cs和Service.cs两个文件已经删除了,所以这里的声明指示内容修改一下,让这个Service.svc文件的后台代码指向我们上次创建的WCF服务库项目--CalculateWcfService项目中的类,改后的代码如下:
<%@ ServiceHost Language="C#" Debug="true" Service="CalculateWcfService.CalculateService" %>
我们把其中的Service属性指定为CalculateWcfService命名空间下的CalculateService类,并把CodeBehind属性删去了。
第四步、配置此WCF服务站点与WCF服务库项目之间的类的对应。
虽然在第二步中我们添加了对Services项目的引用,并且在第三步中修改了Service.svc的类的对应,但此时我们的WCF服务站点并不能把WCF服务库中的服务和终结点发布出来,还需要我们对web.config进行一系列的配置工作。web.config配置文件的配置和上面讲过的配置方式一样,在此不再重复。
第五步、测试运行WCF服务站点
在Service.svc上右击,选择“在浏览器中查看”,在IE中运行此服务。
由此我们看到我们可以在ASP.NET Development Server中发布我们的WCF服务了。
第六步、在IIS布署此WCF服务站点。
在IIS建立Web应用程,指向我们的WCF服务站点所在的目录。然后在IIS运行我们发布的WCF服务 。
到此为至我们在IIS中发布WCF服务成功
4、创建客户端调用服务
服务被成功寄宿后,服务端便开始了服务调用请求的监听工作。此外,服务寄宿将服务描述通过元数据的形式发布出来,相应的客户端就可以获取这些元数据创建客户端程序进行服务的消费。在VS下,当我们添加服务引用的时候,VS在内部帮我们实现元数据的获取,并借助这些元数据通过代码生成工具(SvcUtil.exe)自动生成用于服务调用的服务代理相关的代码和相应的配置。
第一步、创建客户端
在解决方案上面右键,选择“添加”->"新建项目",打开新建项目对话框,在“添加新项目”对话框中,选择“控制台应用程序”,名称为“ConsoleClientTest”。
第二步、添加服务引用
在服务寄宿程序运行的情况下,为客户端添加服务引用。在客户端程序上面,右键“引用”,选择“添加服务引用”,打开添加服务引用对话框,在地址里面输入地址“http://127.0.0.1:9099/CalculateService”,并指定一个命名空间,点击确定。
第三步、编写客户端测试代码
1 namespace ConsoleClientTest 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 //引用服务 8 ServiceReference1.CalculateServiceClient serviceClient = new ServiceReference1.CalculateServiceClient(); 9 Console.WriteLine(string.Format("x + y = {2} when x = {0} and y = {1}", 1, 2, serviceClient.AddOperation(1,2))); 10 Console.WriteLine(string.Format("x - y = {2} when x = {0} and y = {1}", 1, 2, serviceClient.SubOperation(1, 2))); 11 Console.WriteLine(string.Format("x * y = {2} when x = {0} and y = {1}", 1, 2, serviceClient.MulOperation(1, 2))); 12 Console.WriteLine(string.Format("x / y = {2} when x = {0} and y = {1}", 1, 2, serviceClient.DivOperation(1, 2))); 13 14 Console.ReadKey(); 15 } 16 } 17 }
运行后输出结果如下: