Windows Communication Foundation之旅(Part Two)
Filed under: WCF — bruce zhang @ 8:39 am
《Windows Communication Foundation之旅》系列之二
三、WCF的技术要素
作为基于SOA(Service Oriented Architecture)的一个框架产品,WCF最重要的就是能够快捷的创建一个服务(Service)。如下图所示,一个WCF Service由下面三部分构成:
1、Service Class:一个标记了[ServiceContract]Attribute的类,在其中可能包含多个方法。除了标记了一些WCF特有的Attribute外,这个类与一般的类没有什么区别。
2、Host(宿主):可以是应用程序,进程如Windows Service等,它是WCF Service运行的环境。
3、Endpoints:可以是一个,也可以是一组,它是WCF实现通信的核心要素。
WCF Service由一个Endpoints集合组成,每个Endpoint就是用于通信的入口,客户端和服务端通过Endpoint交换信息,如下图所示:
从图中我们可以看到一个Endpoint由三部分组成:Address,Binding,Contract。便于记忆,我们往往将这三部分称为是Endpoint的ABCs。
Address是Endpoint的网络地址,它标记了消息发送的目的地。Binding描述的是如何发送消息,例如消息发送的传输协议(如TCP,HTTP),安全(如SSL,SOAP消息安全)。Contract则描述的是消息所包含的内容,以及消息的组织和操作方式,例如是one-way,duplex和request/reply。所以Endpoint中的ABCs分别代表的含义就是:where,how,what。当WCF发送消息时,通过address知道消息发送的地址,通过binding知道怎样来发送它,通过contract则知道发送的消息是什么。
在WCF中,类ServiceEndpoint代表了一个Endpoint,在类中包含的EndpointAddress,Binding,ContractDescription类型分别对应Endpoint的Address,Binding,Contract,如下图:
EndpointAddress类又包含URI,Identity和可选的headers集合组成,如下图:
Endpoint安全的唯一性识别通常是通过其URI的值,但为了避免一些特殊情况造成URI的重复,又引入了Identity附加到URI上,保证了Endpoint地址的唯一性。至于可选的AddressHeader则提供了一些附加的信息,尤其是当多个Endpoint在使用同样的URI地址信息时,AddressHeader就非常必要了。
Binding类包含Name,Namespace和BindingElement集合,如下图:
Binding的Name以及Namespace是服务元数据(service’s metadata)的唯一标识。BindingElement描述的是WCF通信时binding的方式。例如,SecurityBindingElement表示Endpoint使用SOAP消息安全方式,而ReliableSessionBindingElement表示Endpoint利用可信赖消息确保消息的传送。TcpTransportBindingElement则表示Endpoint利用TCP作为通信的传输协议。每种BindingElement还有相应的属性值,进一步详细的描述WCF通信的方式。
BindingElement的顺序也非常重要。BindingElement集合通常会创建一个用于通信的堆栈,其顺序与BindingElement集合中元素顺序一致。集合中最后一个binding element对应于通信堆栈的底部,而集合中的第一个binding element则对应于堆栈的顶端。入消息流的方向是从底部经过堆栈向上,而出消息流的方向则从顶端向下。因此,BindingElement集合中的binding element顺序直接影响了通信堆栈处理消息的顺序。幸运的是,WCF已经提供了一系列预定义的Binding,能够满足大多数情况,而不需要我们自定义Binding,殚精竭虑地考虑binding element的顺序。
下表是WCF中预定义的Binding:
Class Name
Element Name
Transport
Encoding
WS-* Protocols
BasicHttpBinding
basicHttpBinding
HTTP
XML 1.0
WS-I Basic Profile 1.1
WSHttpBinding
wsHttpBinding
HTTP
XML 1.0
Message security, reliable sessions, and transactions
WSDualHttpBinding
wsDualHttpBinding
HTTP
XML 1.0
Message security, reliable sessions, and transactions
NetTcpBinding
netTcpBinding
TCP
Binary
Transport security, reliable sessions, and transactions
NetNamedPipeBinding
netNamedPipeBinding
Named Pipes
Binary
Transport security, reliable sessions, and transactions
NetMsmqBinding
netMsmqBinding
MSMQ
Binary
Transport security and queue transactions
Contract是一组操作(Operations)的集合,该操作定义了Endpoint通信的内容,每个Operation都是一个简单的消息交换(message exchange),例如one-way或者request/reply消息交换。
类ContractDescription用于描述WCF的Contracts以及它们的操作operations。在ContractDescription类中,每个Contract的operation都有相对应的OperationDescription,用于描述operation的类型,例如是one-way,还是request/reply。在OperationDescription中还包含了MessageDecription集合用于描述message。
在WCF编程模型中,ContractDescription通常是在定义Contract的接口或类中创建。对于这个接口或类类型,标记以ServiceContractAttribute,而其Operation方法则标记以OperationContractAttribute。当然我们也可以不利用CLR的attribute,而采用手工创建。
与Binding一样,每个Contract也包含有Name和Namespace,用于在Service的元数据中作为唯一性识别。此外,Contract中还包含了ContractBehavior的集合,ContractBehavior类型可以用于修改或扩展contract的行为。类ContractDescription的组成如下图所示:
正如在ContractDescription中包含的IContractBehavior一样,WCF专门提供了行为Behavior,它可以对客户端和服务端的一些功能进行修改或者扩展。例如ServiceMetadataBehavior用于控制Service是否发布元数据。相似的,security behavior用于控制安全与授权,transaction behavior则控制事务。
除了前面提到的ContractBehavior,还包括ServiceBehavior和ChannelBehaivor。ServiceBehavior实现了IServiceBehavior接口,ChannelBehaivor则实现了IChannleBehavior接口。
由于WCF需要管理的是服务端与客户端的通信。对于服务端,WCF提供了类ServiceDescription用于描述一个WCF Service,;而针对客户端,WCF管理的是发送消息时需要使用到的通道Channel,类ChannelDescription描述了这样的客户端通道。
ServiceDescription类的组成如下图所示:
我们可以利用代码的方式创建ServiceDescription对象,也可以利用WCF的Attribute,或者使用工具SvcUtil.exe。虽然可以显式的创建它,但通常情况下,它会作为运行中的Service一部分而被隐藏于后(我在后面会提到)。
ChannelDescription类的组成与ServiceDescription大致相同,但它仅仅包含了一个ServiceEndpoint,用于表示客户端通过通道通信的目标Endpoint。当然,施加到ChannelDescription的Behavior也相应的为IChannelBehavior接口类型,如图所示:
定义一个WCF Service非常简单,以Hello World为例,定义的Service可能如下:
using System.ServiceModel
[ServiceContract]
public class HelloWorld
{
[OperationContract]
public void Hello()
{
Console.WriteLine(“Hello World!”);
}
}
System.ServiceModel是微软为WCF提供的一个新的类库,以用于面向服务的程序设计。在开发WCF应用程序时,需要先添加对System.ServiceModel的引用。WCF中的大部分类和接口也都是在命名空间System.ServiceModel下。
我们为HelloWorld类标记了[ServiceContract],这就使得该类成为了一个WCF Service,而其中的方法Hello()则因为标记了[OperationContract],而成为该Service的一个Operation。
不过WCF推荐的做法是将接口定义为一个Service,这使得WCF Service具有更好的灵活性,毕竟对于一个接口而言,可以在同时有多个类实现该接口,这也就意味着可以有多个Service Contract的实现。那么上面的例子就可以修改为:
[ServiceContract]
public interface IHello
{
[OperationContract]
void Hello();
}
而类HelloWorld则实现该IHello接口:
public class HelloWorld:IHello
{
public void Hello()
{
Console.WriteLine(“Hello World!”);
}
}
注意在实现了IHello接口的类HelloWorld中,不再需要在类和方法中标注ServiceContractAttribute和OperationContractAttribute了。
前面我已经提过,一个WCF Service必须有host作为它运行的环境。这个host可以是ASP.Net,可以是Windows Service,也可以是一个普通的应用程序,例如控制台程序。下面就是一个Host的实现:
using System.ServiceModel
public class HostApp
{
static void Main(string[] args)
{
ServiceHost host = new ServiceHost(typeof(HelloWorld), new Uri(“http://localhost:8080/HelloService”));
host.AddServiceEndpoint(typeof(IHello), new BasicHttpBinding(),”Svc”);
host.Open();
Console.WriteLine(“Start Your Service.”);
Console.ReadKey();
host.Close();
}
}
在这个HostApp中,我们为HelloWorld创建了一个ServiceHost对象。通过它就可以创建WCF运行时(Runtime),WCF Runtime是一组负责接收和发送消息的对象。ServiceHost可以创建SerivceDescription对象,利用SerivceDescription,ServiceHost为每一个ServiceEndpoint创建一个EndpointListener。ServiceHost的组成如下图:
EndpointListener侦听器包含了listening address,message filtering和dispatch,它们对应ServiceEndpoint中的EndpointAddress,Contract和Binding。在EndpointListener中,还包含了一个Channel Stack,专门负责发送和接收消息。
注意在创建ServiceHost时,传递的type类型参数,不能是interface。因此,我在这里传入的是typeof(HelloWorld)。ServiceHost类的AddServiceEndpoint()方法实现了为Host添加Endpoint的功能,其参数正好是Endpoint的三部分:Address,Bingding和Contract。(此时的IHello即为ServiceContract,其方法Hello为OperationContract)。
ServiceHost的Open()方法用于创建和打开Service运行时,而在程序结束后我又调用了Close()方法,来关闭这个运行时。实际上以本例而言,该方法可以不调用,因为在应用程序结束后,系统会自动关闭该host。但作为一种良好的编程习惯,WCF仍然要求显式调用Close()方法,因为Service运行时其本质是利用Channel来完成消息的传递,当打开一个Service运行时的时候,系统会占用一个Channel,调用完后,我们就需要释放对该通道的占用。当然我们也可以用using语句来管理ServiceHost资源的释放。
定义好了一个WCF Service,并将其运行在Host上后,如何实现它与客户端的通信呢?典型的情况下,服务端与客户端均采用了Web Service Description Language(WSDL),客户端可以通过工具SvcUtil.exe生成对应于该WCF Service的Proxy代码,以完成之间的消息传递,如图所示:
SvcUtil.exe是由WinFx Runtime Component SDK所提供的,如果安装SDK正确,可以在其中找到该应用工具。生成客户端Proxy代码的方法很简单,首先需要运行服务端Service。然后再命令行模式下运行下面的命令:
SvcUtil http://localhost:8080/HelloService
这样会在当前目录下产生两个文件output.cs和output.config。前者最主要的就是包含了一个实现了IHello接口的Proxy对象,这个代理对象名为HelloProxy,代码生成的结果如下:
[System.CodeDom.Compiler.GeneratedCodeAttribute(”System.ServiceModel”, “3.0.0.0″)]
public partial class HelloProxy : System.ServiceModel.ClientBase, IHello
{
public HelloProxy()
{
}
public HelloProxy(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public HelloProxy(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public HelloProxy(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public HelloProxy(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
public void Hello()
{
base.InnerProxy.Hello();
}
}
(注:本程序在WinFx 2006 February CTP版本下运行通过)
至于后者,则是WCF Service的配置信息,主要包含的是Endpoint中Address,Binding以及Contract的配置(在后续文章我会详细介绍)。
现在客户端就可以直接使用HelloProxy对象,来完成与服务端的通信了:
public class ClientApp
{
static void Main(string[] args)
{
using (HelloProxy proxy = new HelloProxy())
{
proxy.Hello();
}
Console.ReadKey();
}
}
除了可以使用SvcUtil工具产生客户端代码,同样我们也可以利用代码的方式来完成客户端。客户端在发送消息给服务端时,其通信的基础是Service的Endpoint,WCF提供了System.ServiceModel.Description.ServiceEndpoint类,通过创建它来实现两端的通信。在前面,我还提到“对于客户端而言,WCF管理的是发送消息时需要使用到的通道Channel”,为此,WCF提供了ChannelFactory(其命名空间为System.ServiceModel.Channel),专门用于创建客户端运行时(runtime)。ChannelFactory与ServiceHost相对应,它可以创建ChannelDescription对象。与服务端ServiceHost不同的是,客户端并不需要侦听器,因为客户端往往是建立连接的“发起方”,并不需要侦听进来的连接。因此客户端的Channel Stack会由ChannelDescription创建。
ChannelFactory和ServiceHost都具有Channel Stack,而服务端与客户端的通信又是通过channel来完成,这就意味着,利用ChannelFactory,客户端可以发送消息到服务端。而客户端本身并不存在Service对象,因此该Service的Proxy,是可以通过Channel来得到的。所以客户端的代码可以修改如下:
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Channel
public class ClientApp
{
static void Main(string[] args)
{
ServiceEndpoint httpEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IHello)), new BasicHttpBinding(), new EndpointAddress(“http://localhost:8080/HelloService/Svc”));
using (ChannelFactory factory = new ChannelFactory(httpEndPoint))
{
//创建IHello服务的代理对象;
IHello service = factory.CreateChannel();
service.Hello();
}
Console.ReadKey();
}
}
(注:本程序在WinFx 2006 February CTP版本下运行通过)
对于上面的代码,我们有两点需要注意:
1、采用这种方式,前提条件是客户端能够访问IHello接口。这也印证了之前我所叙述的最好使用interface来定义Service的好处。此外,为了保证部署的方便,有关Service的interface最好单独编译为一个程序集,便于更好的部署到客户端。
2、客户端必须知道服务端binding的方式以及address。
对于服务端而言,我们也可以直接在浏览器中打开该Service,在地址栏中输入http://localhost:8080/HelloService,如下图:
点击链接:http://localhost:8080/HelloService?wsdl,我们可以直接看到HelloService的WSDL。注意到在这里我并没有使用IIS,实际上WCF内建了对httpsys的集成,允许任何应用程序自动成为HTTP listener。
参考:
1、David Chappell,Introducing Windows Communication Foundation
2、Aaron Skonnard,Learn The ABCs Of Programming Windows Communication Foundation
3、Microsoft Corporation,Windows Communication Foundation Architecture Overview
Source: Bruce Zhang » Windows Communication Foundation之旅(Part Two)