none
我的WCF之旅(1):创建一个简单的WCF程序 RRS feed

  • 常规讨论

  • 写在前面 在Microsoft提出.NET战略以来, 先后推出了一系列产品和技术, 这些产品和技术为我们在.NET平台下建立企业级的分布式应用提供了很大的 便利。这些技术和产品包括:.NET Remoting,XML WebSerivce,WSE(2.0,3.0),Enterprise Service, MSMQ ...... 我们知道,和一个相对独立的应用不同,我们开发一个分布式应用, 尤其是开发一个企业级的分布式应用, 我们需要考虑较多的东西。比如我们要考虑数据在不同的应用之间传递时采取什么样的机制, 这种数据传递是否是安全的,可靠的;如何在分布式的环境下进行异常处理;如何把分别在 不同应用中执行的操作纳入同一个事务…… 对于我们上面提到的这些 问题, 这些都是开发分布式应用考虑的典型的问题。值得庆幸的是,Microsoft开发的分布式的产品能够部分的解决这些问题。.NET Remoting 为我们在.NET平台下提供了非常好的解决方案(我个人认为,.NET Remoting是.NET平台下最为成熟的分布式技术。比如相较于另一个使用更为广泛的技术XML Web Service,它具有一些自己独特的特性:可以使用不同的传输层协议进行通信——Http & TCP;可以使用不同的消息编码方式——Bianry & Text (XML);可以寄宿到IIS和任何一种托管的应用下 ——Console Application 、WinForm Application、 Windows Service……;Server端可以通过双向通信回调(Callback)客户端的操作;……)XML Web Service为使我们实现跨平台的系统能够集成显得如此简单。随着技术的不断发展,相关的技术规范(WS-* Specification)不断完善, XML Web Service现在已经成为使用最为广泛的分布式技术了。XML Web Service能够得到如此广泛的推广,这得得益于Microsoft先后两次推出的Web Service Enhancement (WSE 2.0 、WSE 3.0)。如果没有WSE, 单纯的asmx下的如此的担保和不可靠。WSE为Web Service解决了几大难题:Security、Reliable Messaging、transaction Handling以及大数据的有效传输。 MSMQ作为一种简单而有效的机制为不同应用之间数据的传递提供了保障。 其实,通过合理利用上面这些分布式的技术完全可以为我们建立的一套适合不同层次需要的分布式构架。但这里面仍然存在一些问题,那就是上面这些技术和产品只能解决某一方面的问题; 比如.NET Remoting虽然在.NET平台下是一个很好的依靠, 但是考虑到他不能提供不同平台之间的互操作性。另外,这些技术适合用了完全不同的编程方式,使得我们很难从容地从其中一种转移到另一种上来。基于这些原因, 我们需要一套全新的技术整合以上都这些技术, 于是我们有了今天的WCF——Windows Communication Foundation。WCF建立一套框架,是我们通过一致的编程模式,使用不同的技术构建我们的分布式应用。 虽然很早开始接触WCF,但所学的总是零零碎碎。现在开始系统地研究WCF,希望与大家一同分享我的一些所得, 同时希望能通过这样的一个机会与大家一些探讨WCF,不对的地方希望大家指正。 一开始我们先建立一个简单程序看WCF如何工作: 1 建立整个应用的简单构架 整个构架如图所示,这个Solution由5个Project组成:Artech.WCFService.Contract;  Artech.WCFService.Service;Artech.WCFService.Hosting;Artech.WCFService.Client;http://localhost/WCFService。 Artech.WCFService.Contract: Class Library Project,用来保存Contract(Service Contact、Message Contract、Data Contract), 之所以把Contract独立出来的原因是考虑到他同时被Server端——Service本身和Service Hosting和Client端使用。(现在很多的参考书,包括MSDN都使用ServiceModel Metadata Utility Tool (Svcutil.exe)这样的一个工具来访问Service的Metadata Endpoint来生成我们的客户段代码,这些代码就包括Service Contract(一般是一个Interface),实现了这个Contract的Proxy Class(一个集成自System.ServiceModel.CientBase的一个Class)和相应的Configuration。 这个工具确实给我提供了很大的方便。但我不推荐使用这样的方法(我天生不倾向对于这些代码生成器),因为我觉得, 在Contract可得的情况下-比如Service和Client都是自己开发,让Service和Client实现的Contract是同一个Contract能够保证一致性。这个Project引用System.ServiceModel DLL。 Artech.WCFService.Service:Class Library Project,Service的业务逻辑, 这个Project引用Artech.WCFService.Contract Project和System.ServiceModel DLL。 Artech.WCFService.Hosting:Console Application, 用于以Self-Hosting的方式Host Service。这个Project引用Artech.WCFService.Contract和Artech. Project WCFService.Service。Project和System.ServiceModel DLL。 Artech.WCFService.Client:Console Application, 用以模拟现实中的调用Service的Clinet。这个Project引用Artech.WCFService.Contract Project 和System.ServiceModel DLL。 http://localhost/WCFService: Web Site Project, 用于模拟如何把Service Host到IIS中。这个Project引用Artech.WCFService.Contract、Artech.WCFService.Service和System.ServiceModel DLL。 2 创建Service Contract 在这个例子中我们建立一个简单的案例,做一个计算器, 假设我们只要求他做简单的加法运算就可以了。在Artech.WCFService.Contract添加一个interface,名称叫做ICalculator。 using System.Collections.Generic; using System.Text; using System.ServiceModel; namespace Artech.WCFService.Contract { [ServiceContract] public interface ICalculator { [OperationContract] double Add(double x, double y); } } 使一个Interface成为Service Contract的方法很简单,就是把ServiceContractAttribute应用到这个interface上,并在代表单个Operation的方法上应用OperationContractAttribute。这个使用Custom Attribute的编程模式被称为声明式的编程(Declarative)方式, 他在.NET Framework 1.1以前用到的地方还不是太多,在.NET Framework 2.0, 尤其是NET Framework 3.0中,这种方式已经变得随处可见了。 我们可以把Contract定义成一个Interface,也可以把它定义到一个Class中——这个Class中既包涵Service本身又作为一个Contract而存在。但我推荐使用第一种方法——Serive和Contract相分离。 在WCF中,Contract的功能实际上就定义一个Service包含哪些可用的Operation, 以及的每个Opertaion的方法签名。从消息交换(Message Exchange)的角度讲,Contract定义了调用相应的Serive采取的消息交换的模式(Message Exchange Pattern - MEP),我们经常使用的MEP包括三种:Oneway, Request/Response,和Duplex。因为调用Service的过程实际就是消息交换的过程, 以常见的Request/Response为例。Client调用某个方面远程访问Service,所有的输入参数被封装到Request Soap Message并被发送到Service端, Service端监听到这个Soap Request,创建相应的Service Object并调用相关的操作,最后将Result(这可以是Return Value,Reference Parameter和Output Parameter)封装成Soap Message发送回Client端。这里需要注意,如果采用的是Request/Response的模式,即使相应的操作没有Return Value,Reference Parameter和Output Parameter(它被标记为void),Service仍然会返回一个空的Soap Message给Client端。 3 创建Service 前面我们已经创建了我的Artech.WCFService.Contract。其实我们从Contract这个单词上讲, 它就是一种契约,一种承诺。 他表明在上面签了字你就的履行Contract上义务。Service就是这样一个需要履行Contract义务的人。在这个例子中, Contract以Interface的方式定义的一些Operation。作为Service, 在Contract上签字的方式就是实现这样的一个Interface。 下面的Service得到code, 很简单。 using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using Artech.WCFService.Contract; namespace Artech.WCFService.Service { public class CalculatorService:ICalculator { #region ICalculator Members public double Add(double x, double y) { return x + y; } #endregion } } 4.Hosting Service 就像Remoting一样,我们继承自System.MarshalByRefObject 的对象必须Host到某一个运行的进程中, 他才开始监听来自Client端的请求,当Client才能通过Proxy远程的调用,Remoting Infrastructure监听到来自Client端的请求,他会激活相应的remote Object(我们只考虑Server Activate Object——SAO)。实际上对于WCF Service也需要一个Host环境才有其发挥作用的舞台。就像Remoting一样,你可以使用任何一种Managed Application——Console Application、WinForm Application、ASP.NET Application——作为它的Host环境。 你甚至可以用它Host到Windows Service中和IIS中(后面我将会讲到如何做)。 我们知道WCF中,Client端和Service端是通过Endpoint来通信的,Endpoint有包含3个部分,经典地称为ABC. A代表Address,它包含一个URI,它指明Service存在于网络的某个地方,也就是说它为Client断指明在什么地方去找到这个Service。很多人认识Address仅仅只是一个具有Identity的URI,实际上不然, Address不止于此, 它还包含一些Header,这些信息在某些情况下对于如何寻址有很大的意义(比如在client的最终Service之间还有一些Intermediary节点的情况下)。 在.NET中, Address用System.ServiceModel.EndpointAddress 来表示。 B代表Binding,Binding封装了所有Client和Service段消息交换的通信细节。比如他定义了通信应该采用的Transport-比如我们是因该采用Http, TCP,Named Pipe或者是MSMQ;通信的内容应该采取怎样的编码——比如是Text/XML,Binary还是MTOM。以上这些都得一个Binding所必须定义的内容, 此外,Binding 还可以定义一些其他的有关通信的内容, 比如Security,Reliable Messaging, Session, Transaction等等。正因为Binding对于通信的重要性,只有Service端的Binding和Client的Binding相互匹配的时候,他们之间在可以相互通信。如何使Client Binding 匹配Service Binding呢?最简单也是最直接的方法就是使用相同的Binding。WCF为我们定义了一系列的System Defined Binding,这些Binding在Transport,Interoperability,Security,Session Support,以及Transaction Support方面各有侧重。基本上WCF为我们定义的这些Binding 已经够我们使用的了,如果,实在满足不了要求, 你还可以建立自己的Custom Binding。 C 代表Contract这在上面已经提及,这里不再累赘。 Host的本质就是把一个Service 置于一个运行中的进程中,并以Endpoint的形式暴露出来,并开始监听来自Client端的请求。这里值得注意的是,同一个Service可以注册若干不同的Endpoint,这样不同的Client就可以以不同的方式来访问同一个Service.比如,同一个Intranet的Client可以以TCP的方式访问Service,另一个存在已Internet中的Client则只能以Http的方式访问。 using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using Artech.WCFService.Contract; using Artech.WCFService.Service; using System.ServiceModel.Description; namespace Artech.WCFService.Hosting { class Program { static void Main(string[] args) { HostingServiceViaCode(); } static void HostingServiceViaCode() { //Specify the base Address Uri baseUri = new Uri("http://localhost:8080/calculatorService"); //create a new ServiceHost object and specify the corresponding Service and base Address //It is recommended to apply the using pattern to make sure the sevice host can be closed properly. using (ServiceHost calculatorServiceHost = new ServiceHost(typeof(CalculatorService), baseUri)) { //Create a Binding for Endpoint. BasicHttpBinding Binding = new BasicHttpBinding(); //Create a Service Endpoint by specify the Address(it is absolute or relative path based on the base Address, the empty string indicates the Address equals base Address), //Binding(the basicHttpBinding created) and Contrace(it is now the type info of the contract interface) calculatorServiceHost.AddServiceEndpoint(typeof(ICalculator), Binding, string.Empty); //Such a segment of code snip shows how to make the metadata exposed to the outer world by setting the Service metadata behavior //Find the Service metadata behavior if exists, otherwize return null. ServiceMetadataBehavior behavior = calculatorServiceHost.Description.Behaviors.Find(); //If the Service metadata behavior has not to added to the Service. we will create a new one and evaluate the HttpGetEnabled&HttpGetUrl to make outer world can retrieve to metadata. if (behavior == null) { behavior = new ServiceMetadataBehavior(); behavior.HttpGetEnabled = true; //HttpGetUrl is absolute or relative based on base Address behavior.HttpGetUrl = baseUri; //We must add the new behavior created to the behavior collection, otherwize it will never take effect. calculatorServiceHost.Description.Behaviors.Add(behavior); } //if the metadata behavior exists in the behavior collection, we just need to evaluate the HttpGetEnabled&HttpGetUrl else { behavior.HttpGetEnabled = true; behavior.HttpGetUrl = baseUri; } //Add the opened event handler to make a friendly message displayed after opening the Service host indicating the Service begin to listen to request from Clients. calculatorServiceHost.Opened += delegate { Console.WriteLine("Calculator Service begin to listen via the Address:{0}", calculatorServiceHost.BaseAddresses[0].ToString()); }; //Open the Service host make it begin to listen to the Clients. calculatorServiceHost.Open(); Console.Read(); } } } } 我们现在可以单独运行Hosting Projet,以下是运行后的截图。 当Hosting启动之后,由于我们为它开启了Http Enabled,Hosting为专门创建一个Metadata Endpoint,通过访问这个Endpoint,我们可获取Service相关的Metadata。像ServiceModel Metadata Utility Tool (Svcutil.exe) 这得工具就是通过获取Metadata来帮我们生成相应的客户端代码和配置文件。当Hosting被启动之后,我们可以在IE中输入Metadata Endpoint的Address来测试Service的可访问性。如下图。 以上我们完全以代码的方式Host一个已经创建的Service。在这个例子中,Endpoint的所有信息都在代码中指定。实际上真正的项目开发之中,我们基本上不采用这样的方法。这是因为,Service的Addresss以及Binding 信息在开发阶段和部署阶段往往是不一样的。所以我们通常的做法是把这些信息放在Config文件中,这样我们可以根据实际的需求改变存储于Config文件中的信息,比如我们把Service移植上另外的位置,我们只要改变Endpoint的Address配置信息就可以了; 再比如,我们把原来存在Intranet的Service放到Internet上,原来可能基于TCP的Binding必须改成基于Http的Binding,在这种情况下,我们依然可修改配置文件就可以了,这样的改动通常是很小的,并且修改了配置文件之后我们不需要对现有的代码进行重编译和重部署,它们可以其实生效。下面我们来看看如何创建基于Configuration的Hosting。 首先,在Artech.WCFService.Hosting中创建App.config,并编写如下结构的配置信息。 然后我们相应地改变我们的Hosting代码,现在由于我们的Endpoint的信息都放在我们的配置文件中,所以我们可以较大的简化我们的Hosting 代码。 using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using Artech.WCFService.Contract; using Artech.WCFService.Service; using System.ServiceModel.Description; namespace Artech.WCFService.Hosting { class Program { static void Main(string[] args) { using (ServiceHost calculatorServiceHost = new ServiceHost(typeof(CalculatorService))) { calculatorServiceHost.Opened += delegate { Console.WriteLine("Calculator Service begin to listen via the Address:{0}", calculatorServiceHost.BaseAddresses[0].ToString()); }; calculatorServiceHost.Open(); Console.Read(); } } } } 当calculatorServiceHost.Open();被调用之后,系统会查看calculatorServiceHost对应的Service的类型,结果发现它的类型是Artech.WCFService.Service. CalculatorService(在ServiceHost被创建时最为第一个传入参数);然后会在config文件中Services部分中找name属性和这个Service type相匹配,找到之后,把它host进来,然后添加它在配置文件中定义的所有的Endpoint,并设置在配置文件中定义的所有Binding 以及 Behavior。 现在我们运行Hosting,将会得到同上面一样的结果。同样,在IE中输入Metadata Endpoint的Address,也会看到上解图一样的显示。 5.创建Client 到现在为止,Service端的工作已经完成,当你启动Hosting的时候,一个可用的Service就已经存在了。现在所做的事情是如何创建我们的客户段程序去使用这个Service。几乎所有的WCF的书,其中包括MSDN都是叫你如何使用Service Utility这样的一个工具来帮你生成客户端代码和配置信息。为了让我们能够清晰的Client的整体内容, 我们现在选择手工的方式来编写这样的部分代码。 几乎所有分布式的调用都有这样的一个概念,调用的具体实现被封装在Server端,Client不可能也不需要了解这个具体实现,它所关心的就是我如何去调用,也就是说Cient需要的不是Service的实现,而是一个interface。WCF也是一样,Client不需要了解Service的具体实现,它只需要获得Service 的Contract,已经如何与Service通信就足够了。说到Contract和通信,我们很自然地会想到Endpoint,不错,Endpoint恰恰给我们提供这两个方面的内容。所以到现在我们可以这样说,这样在Client建立和Serivce端相匹配的Endpoint,Client就可以调用它所希望的Service。前面提到Endpoint包括三个部分, Address, Binding,Contract那我们现在来看看Client如何获得这3要素的信息。 在System。Service。Model 命名空间里,定义了一个类abstract class ClientBase,给我们调用Service提供极大的便利。我们只要是我们的Client继承这样一个类,并为它指定Endpoint的三要素就一切OK了,下面我们来看看我们可以以那些方式来指定这些内容 1. Conract:我们看到了ClientBase是一个Generic的类,我们在创建一个继承自这个类的时候必须给它指定特定的TChannel.我们可以把Contract对应的类型作为Client的generic类型。 2. Binding和Address:和Service端的Endpoint一样,我们可以把相关的信息放在我们的Client端代码里面,也可以放在Client的Config里面。那个这些数据如何应用要我们创建的派生自ClientBase的类的对象上呢。其实很简单,ClientBase给我们定义了若干重载的构造函数,我们只要定义我们相应的构造函数应简单地调用基类的构造函数。下面列出了ClientBase定义的全部的构造函数 protected ClientBase():这个构造函数没有任何的参数,它用于Endpoint的信息全部存放于Config protected ClientBase(InstanceContext callbackInstance):指定一个Callback instance用于Service回调Client代码,这用Deplex Communication。 protected ClientBase(string EndpointConfigurationName):指定一个ID,它标识configuration 文件中定义的某一个Endpoint。这个方法在使用不同的Endpoint调用同一个Service的情况下用到的比较多。 ClientBase(Binding Binding, EndpointAddress remoteAddress);显示的指定Binding 和Address ClientBase(InstanceContext callbackInstance, string EndpointConfigurationName) ClientBase(string EndpointConfigurationName, EndpointAddress remoteAddress) ClientBase(string EndpointConfigurationName, string remoteAddress) ClientBase(InstanceContext callbackInstance, Binding Binding, EndpointAddress remoteAddress) ClientBase(InstanceContext callbackInstance, string EndpointConfigurationName, EndpointAddress remoteAddress) ClientBase(InstanceContext callbackInstance, string EndpointConfigurationName, string remoteAddress) 介绍完ClientBase后, 我们来创建我们自己的CalculatorClient。下面的相应的Code using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using System.ServiceModel.Channels; using Artech.WCFService.Contract; namespace Artech.WCFService.Client { class CalculatorClient:ClientBase,ICalculator { internal CalculatorClient() : base() { } #region ICalculator Members public double Add(double x, double y) { return this.Channel.Add(x, y); } #endregion } } 上面的例子中我们仅仅定义了一个无参的构造函数,因为我们会把所有的Endpoint信息放在Config文件里面: 然后我们再Client Project中的Program class里面通过这样的方式调用CalculatorService; using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using System.ServiceModel.Channels; using Artech.WCFService.Contract; namespace Artech.WCFService.Client { class Program { static void Main(string[] args) { try { using (CalculatorClient caluclator = new CalculatorClient()) { Console.WriteLine("Begin to invocate the calculator Service"); Console.WriteLine("x + y = {2} where x = {0} and y = {1}", 1, 2, caluclator.Add(1, 2)); Console.Read(); } } } catch (Exception ex) { Console.WriteLine("StackTrace:{0}", ex.StackTrace); Console.WriteLine("Message:{0}", ex.Message); Console.Read(); } } } }
    2009年6月22日 3:49