契约就是双方或多方就某个问题达成的一种的共识 服务提供者通过契约的形式将服务公布出来 服务消费者根据契约来消费 这样通过契约这个中间者就可以规范服务提供的内容 而服务消费者也可以根据契约来使用服务端提供的服务 消费者不需要关心服务内部的功能是如何实现的 它只需要根据契约提供的信息来调用服务即可
web服务描述语言
web服务描述语言 简称WSDL(Web Service Description Language) 如果希望服务契约被基于不同平台的客户端所理解 就应该使用一种与平台无关的标准对服务进行描述 而WSDL就是基于这样的需求而产生的一种用于描述服务的标记语言 一份WSDL文档具有对服务契约的描述信息
定义服务契约
我们使用ServiceContract特性来将接口或者类声明成一个契约 如
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/")] 6 public interface ICalculator 7 { 8 [OperationContract] 9 double Add(double x, double y); 10 } 11 }
ServiceContract的name属性表示契约的名字(如果不指定 则契约默认的名字为接口的名字 ) namespace属性表示契约的命名空间 (如果不指定 则契约默认的命名空间为 http://tempuri.org
服务契约作为WSDL文档的一部分 对应着契约ServiceContract的name和namespace 比如此例中的ICalculator接口被声明为一个服务契约 name为CalculatorService namespace为http://www.artech.com/ 则WSDL文档的<portType>元素对应着ServiceContract的name和namespace
1 <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://www.artech.com/"> 2 <wsdl:portType name="tns:CalculatorService"></wsdl:portType> 3 </wsdl:definitions>
定义服务
实现契约接口的类通过ServiceBehavior特性来声明 如
1 using System.ServiceModel; 2 3 namespace Service 4 { 5 [ServiceBehavior(Name = "CalculatorService", Namespace = "http://www.artech.com/")] 6 public class CalculatorService:ICalculator 7 { 8 public double Add(double x, double y) 9 { 10 var result = x + y; 11 return result; 12 } 13 } 14 }
ServiceBehavior的name和namespace对应着WSDL文档的wsdl:definitions元素的name和targetNamespace属性 如
1 <wsdl:definitions name="CalculatorService" targetNamespace="http://www.artech.com/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"> 2 </wsdl:definitions>
终结点的bindingName和bindingNamespace
服务端终结点有bindingName和bindingNamespace 这两个属性仅仅用于指定WSDL文档中关于绑定的描述而无关其它 由于这两个属性是关于服务的描述 所以客户端的终结点无此属性
1 <endpoint bindingName="myBasicHttpBinding" bindingNamespace="http://www.artech.com/"/>
对应的WSDL文档如下 其中wsdl:bingding元素的name值是终结点的bindingName加下划线加服务的名称
1 <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://www.artech.com/"> 2 <wsdl:binding name="tns:myBasicHttpBinding_CalculatorService"></wsdl:binding> 3 </wsdl:definitions>
终结点的contract
终结点的contract属性并不是指契约类型 如
1 <endpoint address="http://127.0.0.1/service" binding="WSHttpBinding" contract="Service.Interface.ICalculator" />
contract属性指的是定义服务契约的ServiceContract特性的ConfigurationName参数的值 如果没有指定ConfigurationName的值 那么默认该参数的值就是契约接口所在命名空间.契约类的名字 如例子中的Service.Interface.ICalculator 如果假设将ConfigurationName设为CalculatorContract 则服务端和客户端终结点的contract就是CalculatorContract 如
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/", ConfigurationName = "CalculatorContract")] 6 public interface ICalculator 7 { 8 [OperationContract] 9 double Add(double x, double y); 10 } 11 }
对服务契约做如上配置 则服务端和客户端终结点配置的contract属性为
1 <endpoint address="http://127.0.0.1/service" binding="WSHttpBinding" contract="CalculatorContract" />
ServiceContract除了Name、Namespace、ConfigurationName还具有如下几个参数
SessionMode:表示实现了当前契约的服务应采用何种会话模式 值为一个SessionModel枚举 枚举可能的值有:Allowed、NotAllowed、Required(关于会话参看实例与会话)
ProtectionLevel:表示传输信息的保护级别 值为一个ProtectionLevel枚举 枚举可能的值有:EncryptAndSign、None、Sign(关于信息保护参看传输安全)
CallbackContract:在通过双向通信实现的回调中 服务契约中需要通过此属性指定用于回调操作的接口或类的类型
定义服务操作
使用ServiceContract使接口或类成为契约 使用OperationContract使接口或类的方法成为服务操作
OperationContract的属性如下
Name :定义服务操作的名称 默认是接口方法的名称
Action:定义客户端请求服务的WSDL文档中的<action>报头
ReplyAction:定义服务端回复信息的WSDL文档中的<action>报头
可以为服务操作指定一个名称 如
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/")] 6 public interface ICalculator 7 { 8 [OperationContract(Name="Add")] 9 double Add(double x, double y); 10 } 11 }
OperationContract的name参数指定服务操作的名称 如果操作有重载 则必须为重载方法指定一个name 如
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/")] 6 public interface ICalculator 7 { 8 [OperationContract(Name="Add")] 9 double Add(double x, double y); 10 11 [OperationContract(Name = "Add2")] 12 double Add(double x, double y,string msg); 13 } 14 }
服务契约对应WSDL文档的<portType> 而服务操作对应的是<portType>的子元素<operation> 如
1 <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://www.artech.com/"> 2 <wsdl:portType name="tns:CalculatorService"> 3 <wsdl:operation name="Add"></wsdl:operation> 4 <wsdl:operation name="Add2"></wsdl:operation> 5 </wsdl:portType> 6 </wsdl:definitions>
使用Action和ReplyAction分别可以定义客户端请求服务的WSDL文档中的<action>报头和服务端回复客户端的WSDL文档中的<action>报头 如
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/")] 6 public interface ICalculator 7 { 8 [OperationContract(Name = "Add", Action = "http://www.artech.com/CalculatorService/Add", ReplyAction = "http://www.artech.com/CalculatorService/AddResponse")] 9 double Add(double x, double y); 10 } 11 }
Action的值是契约的命名空间+/+契约名称+/+操作的名称 而ReplyAction的名称则是契约的命名空间+/+契约名称+/+操作的名称+Response Action和ReplyAction对应的WSDL文档元素是<input>和<output> 如
1 <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://www.artech.com/"> 2 <wsdl:portType name="tns:CalculatorService"> 3 <wsdl:operation name="Add"> 4 <wsdl:input wsaw:action="http://www.artech.com/CalculatorService/Add" message="tns:ICalculator_Add_InputMessage"/> 5 <wsdl:output wsaw:action="http://www.artech.com/CalculatorService/AddResponse" message="tns:ICalculator_Add_OutputMessage"/> 6 </wsdl:operation> 7 <wsdl:operation name="Add2"></wsdl:operation> 8 </wsdl:portType> 9 </wsdl:definitions>
当客户端发送请求时 它的SOAP消息的Action就是契约描述文档WSDL中的<wsdl:input>的action 如
1 <s:Evenlope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> 2 <s:header> 3 <a:Action s:mustunderstand="1"> 4 http://www.artech.com/CalculatorService/Add 5 </a:Action> 6 </s:header> 7 </s:Evenlope>
当服务端回复请求时 它的SOAP消息的Action就是契约描述文档WSDL中的<wsdl:output>的action 如
1 <s:Evenlope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> 2 <s:header> 3 <a:Action s:mustunderstand="1"> 4 http://www.artech.com/CalculatorService/AddResponse 5 </a:Action> 6 </s:header> 7 </s:Evenlope>
Action作为请求的识别信息在WCF服务端分发体系中具有重要的地位 在WCF - 地址一文中介绍过信道分发器和终结点分发器 终结点分发器是一个EndpointDispatcher类型的对象 它有两个表示消息筛选器的属性AddressFilter和ContractFilter AddressFilter用于筛选当前请求中的To报头地址(即SOAP消息中的<a:To>报头地址 该地址代表的就是终结点逻辑地址)如果报头地址和服务端终结点的逻辑地址一致 则匹配成功 而ContractFilter则用于筛选当前请求中的Action报头地址(即SOAP消息中的<a:Action>报头地址)通过这个报头地址 可以确定客户端所请求的是哪个服务操作 正是因为OperationContract的Action属性决定了服务操作的选择问题 因此它必须是唯一的 即OperationContract的name必须是唯一的 如果Add有重载版本 则必须为重载版本设定一个不同于原操作方法的name值
OperationContract的默认操作
可以将OperationContract的Action设为*号 这样如果没有SOAP消息中的<a:Action>报头地址 则会匹配Action为*号的服务操作 Action为*号的服务操作只能定义一个 表示默认的操作
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/")] 6 public interface ICalculator 7 { 8 [OperationContract(Name = "Add", Action = "*", ReplyAction = "*")] 9 double Add(double x, double y); 10 } 11 }
服务契约的继承
ServiceContractAttribute类不能被继承 但代表服务操作的方法的OperationContract却可以被继承 所以 接口B将继承A的Write方法
1 [ServiceContract(ConfigurationName = "AService")] 2 public interface A 3 { 4 [OperationContract] 5 void Write(); 6 } 7 8 [ServiceContract(ConfigurationName = "BService")] 9 public interface B:A 10 { 11 [OperationContract] 12 void Test(); 13 }
在客户端 我们可以为一个服务配置两个契约 比如例子中的B契约 它同时拥有A的Write操作 如果想在一个服务中既可以只调用A服务操作又可以调用B服务操作 则
1 <system.serviceModel> 2 <client> 3 <endpoint address="http://127.0.0.1/service" binding="ws2007HttpBinding" contract="AService" /> 4 <endpoint address="http://127.0.0.1/service" binding="ws2007HttpBinding" contract="BService" /> 5 </client> 6 </system.serviceModel>
ContractDescription类
用于描述契约的类 它描述通过ServiceContract特性定义的契约的信息 此类具有ServiceContract特性的所有属性
ContractDescription拥有ServiceContract特性的所有属性 属性如下:
Name:契约的名称
Namespace:契约的命名空间
ConfigurationName:契约的配置名称
ContractType:获取被定义成契约的接口或类的类型
Behaviors:一个KeyedByTypeCollection<IContractBehavior>集合 包含了对当前契约的所有行为的描述
Operations:一个OperationDescriptionCollection集合 此集合存储了契约的所有服务操作的描述 每个元素都是一个OperationDescription对象
使用此类的GetContract静态方法可以获取到此类的实例 如:
ContractDescription ContractDes=ContractDescription.GetContract(typeof(ICalculator))
OperationDescription类
用于描述契约的所有服务操作的类 即此类描述定义成契约接口或类且定义了OperationContract特性的所有方法的信息 此类具有OperationContract特性的部分属性 除了Action、ReplyAction、AsyncPattern属性) 属性如下:
Name:服务操作的名称
Action:获取请求服务操作的action地址
SyncMethod:获取服务操作说明的服务同步方法
BeginMethod:获取服务的异步操作的开始方法
EndMmethod:获取服务的异步操作的结束方法
DeclaringContract:获取包含当前服务操作的契约信息
Faults:获取异常处理的错误列表
KnowTypes:获取或设置在序列化过程中所需的已知类型
Behaviors:获取当前服务操作的所有行为
IsOneWay:获取当前服务操作采用的信息交换模式 如request-reply、dataGram
Message:一个MeesageDescription集合 表示关于请求和回复的描述信息
MessageDescription类
用于描述契约的服务操作中的请求和回复消息 它包含了SOAP消息中的报头<Header>、消息主体<Body> 它还可以表示针对整个消息或消息中的某个部分采用保护级别的描述 它的属性如下
Headers:SOAP消息的<Header>部分
Body:SOAP消息的<Body>部分
Direction: 一个MeesageDirection枚举 可能的值有Input和Output 分别表示消息是请求消息还是回复消息
Action:如果消息是请求消息 则返回OperationContract的Action属性的值 如果是回复消息 则返回OperationContract的ReplyAction属性的值
ProtectionLevel:获取消息保护信息
MeesageType:返回请求或回复所使用的消息契约类型 如果消息使用的是非契约的消息 则返回null
实例:通过MessageDescription分析消息结构
我们通过计算服务的例子来打印请求或回复消息的结构 首先配置一下Service.Interface的ICalculator接口
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/", ConfigurationName = "CalculatorService")] 6 public interface ICalculator 7 { 8 [OperationContract] 9 double Add(double x, double y); 10 } 11 }
打开Client项目的入口文件 定义两个方法showOperationMessage和showMessageBody方法 前者用于获取服务操作描述信息中的请求信息、回复信息的描述 后者用于打印SOAP消息的部分正文
1 using System.ServiceModel; 2 using Service.Interface; 3 using System.ServiceModel.Description; 4 5 namespace Client 6 { 7 class Program 8 { 9 static void showMessageBody(MessageDescription message) 10 { 11 Console.WriteLine(message.Direction == MessageDirection.Input ? "请求消息" : "回复消息"); 12 var body = message.Body; 13 Console.WriteLine("<tns:{0} xmlns:tns="{1}">", body.WrapperName, body.WrapperNamespace); 14 foreach (var part in body.Parts) 15 { 16 Console.WriteLine(" <tns:{0}>……</tns:{0}>", part.Name); 17 } 18 if (null != body.ReturnValue) 19 { 20 Console.WriteLine(" <tns:{0}>……</tns:{0}>", body.ReturnValue.Name); 21 } 22 Console.WriteLine("</tns:{0}>", body.WrapperName); 23 } 24 25 static void showOperationMessage(OperationDescription operation) 26 { 27 var inputMsg = operation.Messages[0];//取出请求信息的描述 28 showMessageBody(inputMsg); 29 var outputMsg = operation.Messages[1];//取出回复信息的描述 30 showMessageBody(outputMsg); 31 } 32 33 static void Main(string[] args) 34 { 35 ContractDescription constract = ContractDescription.GetContract(typeof(ICalculator)); 36 //从OperationDescriptionCollection中取出关于服务操作的描述信息 37 showOperationMessage(constract.Operations[0]); 38 } 39 } 40 }
输出结果可以看到服务操作Add的请求和回复的消息部分的结构 请求消息中Add操作的两个参数被封装在XML的Add元素里 而回复消息中回复的信息被封装在AddResonse元素中
我们将ICalculator稍作修改 使用一个out类型的参数作为回复 并将操作返回值设为void 如
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/", ConfigurationName = "CalculatorService")] 6 public interface ICalculator 7 { 8 [OperationContract] 9 void Add(double x, double y,out double z); 10 } 11 }
再次运行则会得到以下输出结果
在回复消息的结构中可以看到输出操作和返回值一样作为输出结果定义在了回复消息的结构中
再次修改ICalculator
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/", ConfigurationName = "CalculatorService")] 6 public interface ICalculator 7 { 8 [OperationContract] 9 void Add(ref double x, ref double y,out double z); 10 } 11 }
结果
无论ref、out、void 它们都有输出的功能 所以都被定义在了回复消息的结构中
通过ContractDescription类可以获取到关于契约的所有描述信息 关于服务操作的描述可以使用OperationDescription获取 关于请求和回复的描述信息则可以使用MessageDescription获取
服务操作的消息交换模式
request-reply请求-回复模式
服务操作即被定义为契约的接口的方法 客户端和服务端的消息交换模式在服务操作上默认表现为request-reply模式 请求消息的格式由服务操作的输入参数来决定 而恢复消息的格式则是由服务操作的返回值、输出参数来决定的 服务元数据是通过WSDL形式发布的 WSDL文档的<portType>元素表示契约 而其子元素<operation>则表示服务操作 <operation>通过<input>(输入信息)和<output>(输出信息)的有序组合来表示消息交换模式 而input表示的是服务操作的输入参数 output表示的是服务操作的输出参数 假设一个服务操作有ref、out参数和返回值 则input结构会包含输入参数和ref参数 因为ref是一个输入+输出的参数 所以output结构也会包含ref参数
datagram数据报模式
服务操作的消息交换模式除了request-reply模式 还有单向模式 即数据报模式 它既不接收回复也无需关心它发送的信息是否成功 此种模式只需在副操作上使用OperationContract的IsOneWay设为true即可开启单向消息模式 如此在服务发布的WSDL文档中 <portType>/<operation>就只包含input元素而无output元素 被定义为单向消息模式的服务操作不允许有返回值 即必须是void 且无ref、out类型的参数
duplex双工模式
如果服务操作采用双工模式 则意味着信息交换双方都可以向对方发送信息(请求回复模式下的客户端只能向服务端请求某资源而不能发信息 而数据报模式只能单向的发信息)这让服务端回调客户端成为可能
实例演示 通过双工通信模式实现回调
先打开Hostring项目的app.config 配置如下:
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <behaviors> 5 <serviceBehaviors> 6 <behavior name="exposeExceptionDetail"> 7 <serviceDebug includeExceptionDetailInFaults="true"/> 8 </behavior> 9 </serviceBehaviors> 10 </behaviors> 11 <services> 12 <service name="Service.CalculatorService" behaviorConfiguration="exposeExceptionDetail"> 13 <endpoint address="net.tcp://127.0.0.1:7777/calculatorservice" binding="netTcpBinding" contract="Service.Interface.ICalculator"/> 14 </service> 15 </services> 16 </system.serviceModel> 17 </configuration>
打开客户端的app.config 配置如下
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <client> 5 <endpoint address="net.tcp://127.0.0.1:7777/calculatorservice" binding="netTcpBinding" contract="Service.Interface.ICalculator" name="calculatorservice" /> 6 </client> 7 </system.serviceModel> 8 </configuration>
修改Service.Interface项目的ICalculator
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 //使用CallbackContract属性使服务端的服务操作回调客户端的服务操作 6 [ServiceContract(Name = "CalculatorService", Namespace = "http://www.artech.com/",CallbackContract=typeof(ICalculatorCallback))] 7 public interface ICalculator 8 { 9 [OperationContract] 10 void Add(double x, double y); 11 } 12 }
还需定义一个回调的契约 该契约的实现是由客户端来完成的
1 using System.ServiceModel; 2 3 namespace Service.Interface 4 { 5 [ServiceContract] 6 public interface ICalculatorCallback 7 { 8 [OperationContract] 9 void DisplayResult(double x,double y); 10 } 11 }
打开Service项目的CalculatorService这个实现了ICalculator契约的服务 修改如下
1 using Service.Interface; 2 using System.ServiceModel; 3 4 namespace Service 5 { 6 [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]//此处需要设并发模式为Reentrant 否则将造成死锁 7 public class CalculatorService : ICalculator 8 { 9 public void Add(double x, double y) 10 { 11 //使用对当前服务操作的上下文对象获取要使用的回调契约对象 12 var callback = OperationContext.Current.GetCallbackChannel<ICalculatorCallback>(); 13 //调用回调服务的服务操作 14 callback.DisplayResult(x, y); 15 } 16 } 17 }
接着需要在客户端定义实现回调契约的回调服务
1 public class CalculatorCallbackService:ICalculatorCallback 2 { 3 public void DisplayResult(double x, double y) 4 { 5 Console.WriteLine(x + y); 6 } 7 }
开启服务
1 using System.ServiceModel; 2 using Service; 3 4 namespace Hosting 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 ServiceHost host = new ServiceHost(typeof(CalculatorService)); 11 host.Open(); 12 Console.WriteLine("服务已开启……"); 13 Console.Read(); 14 } 15 } 16 }
在Client项目中调用服务
1 //将回调服务进行封装 2 InstanceContext callback = new InstanceContext(new CalculatorCallbackService()); 3 //双工信道工厂 将封装的回调服务作为参数 则当前客户端也会开启侦听信道侦听服务端的请求 4 DuplexChannelFactory<ICalculator> channelfactory = new DuplexChannelFactory<ICalculator>(callback, "calculatorservice"); 5 //服务代理 6 var proxy=channelfactory.CreateChannel(); 7 //当调用服务的操作时 服务操作会转而回调客户端的服务操作CalculatorCallbackService 8 proxy.Add(1, 2); 9 Console.Read();
此例子中如果没有为服务CalculatorService指定ServiceBehavior的并发模式 则会导致死锁 也可以设置Add操作和DisplayResult操作的OperationContract的IsOneWay属性设为true使其通信变为单向模式 来解决死锁 例子使用的绑定是支持双工通信的NetTcpBinding 将其换为wsDualHttpBinding同样可以实现双向通信 如果使用wsDualHttpBinding 解决死锁只能使用ConcurrencyMode不能使用IsOneWay=true
客户端异步操作
对于任何一个服务操作 无论它是怎样的消息交换模式 都可以通过配置对服务执行异步操作 右击客户端-添加服务引用-选择高级
通过添加服务引用生成的允许异步操作的代理对象同样是ClientBase<TChannel>的类型 与同步操作的ClientBase<TChannel>不同的是 该类会多出几个用于异步操作的方法 Beginxx、Endxx 在计算服务的例子中 通过为客户端添加异步调用服务 则生成的代理类的异步调用服务的操作的定义如下
1 public IAsyncResult BeginAdd(double x, double y, AsyncCallback callback, object asyncState) 2 public double EndAdd(System.IAsyncResult result) 3 public void AddAsync(double x, double y, object userState) 4 public event EventHandler<AddCompletedEventArgs> AddCompleted;
通过Beginxx、Endxx进行异步调用服务
1 using Client.CalculatorService; 2 3 namespace Client 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 CalculatorServiceClient proxy = new CalculatorServiceClient(); 10 IAsyncResult asyn= proxy.BeginAdd(1, 2, null, null); 11 var z=proxy.EndAdd(asyn); 12 proxy.Close(); 13 Console.WriteLine(z); 14 Console.Read(); 15 } 16 } 17 }
此方式虽然可以实现异步调用服务 在BeginAdd之后会根据异步操作可能执行的时间来确定EndAdd应该什么时候执行 但有可能在BeginAdd之后 执行时间过长导致线程阻塞 而此时EndAdd被调用 则会出现异常 解决办法是使用回调的方式进行异步调用
通过回调进行异步调用服务
Beginxx方法除了接收输入参数外 还接收一个AsyncCallback类型的委托函数 该委托会在Beginxx执行完成后回调该委托 即通过委托我们可以在实现异步操作完成后再调用Endxx方法来获取结果 这样就避免了执行时间过长而导致的异常行为 该委托有一个IAsync类型的参数 该参数表示的则是Beginxx方法的输入参数
1 using Client.CalculatorService; 2 3 namespace Client 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 CalculatorServiceClient proxy = new CalculatorServiceClient(); 10 proxy.BeginAdd(1, 2, asyncResult => { 11 var z=proxy.EndAdd(asyncResult); 12 proxy.Close(); 13 Console.WriteLine(z); 14 }, null); 15 Console.Read(); 16 } 17 } 18 }
Beginxx方法还有一个object类型的参数 该参数用于异步调用服务时提供额外的参数 这个参数可以通过AsyncCallback委托的IAsync类型的参数获取 如
1 using Client.CalculatorService; 2 3 namespace Client 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 CalculatorServiceClient proxy = new CalculatorServiceClient(); 10 proxy.BeginAdd(1, 2, asyncResult => { 11 var z=proxy.EndAdd(asyncResult); 12 proxy.Close(); 13 Console.WriteLine(asyncResult.AsyncState as string + z);//输出hello world3 14 }, "hello world"); 15 Console.Read(); 16 } 17 } 18 }
通过事件注册进行异步服务调用
使用代理类的AddCompleted事件可以异步调用服务 AddCompleted事件的AddCompletedEventArgs参数有三个属性
AddCompletedEventArgs.Cancelled :异步是否被取消
AddCompletedEventArgs.Error:异步时抛出的错误
AddCompletedEventArgs.Result:调用代理对象的AddAsync方法时传递的输入参数
AddCompletedEventArgs.UserState:调用代理对象的AddAsync方法时传递的额外参数
1 using Client.CalculatorService; 2 3 namespace Client 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 CalculatorServiceClient proxy = new CalculatorServiceClient(); 10 proxy.AddCompleted += (sender, a) => 11 { 12 var z=a.Result; 13 proxy.Close(); 14 Console.Write(z); 15 }; 16 proxy.AddAsync(1, 2); 17 Console.Read(); 18 } 19 } 20 }
传递额外参数
1 CalculatorServiceClient proxy = new CalculatorServiceClient(); 2 proxy.AddCompleted += (sender, a) => 3 { 4 var z=a.Result; 5 proxy.Close(); 6 Console.Write(a.UserState as string+z); 7 }; 8 proxy.AddAsync(1, 2,"hello world"); 9 Console.Read();
服务端异步操作
客户端通过勾选配置服务引用的异步选项可以实现异步调用服务 客户端也可以不使用异步 而在服务端定义实现异步的操作
打开Service.Interface项目 添加一个IFileReader的契约
1 using System.ServiceModel; 2 using System.IO; 3 4 namespace Service.Interface 5 { 6 [ServiceContract(Namespace = "http://www.artech.com/")] 7 public interface IFileReader 8 { 9 [OperationContract(AsyncPattern=true)]//定义服务操作为异步模式 10 IAsyncResult BeginRead(string fileName,AsyncCallback Callback,object userParams); 11 [OperationContract] 12 string EndRead(IAsyncResult asyncResult); 13 } 14 }
打开Service项目 添加一个实现IFileReader契约的服务类
1 using System.ServiceModel; 2 using System.IO; 3 using Service.Interface; 4 5 namespace Service 6 { 7 public class FileReaderService:IFileReader 8 { 9 private const string fileLocation = @"E:"; 10 private FileStream fileStream; 11 private byte[] buffer; 12 13 public IAsyncResult BeginRead(string fileName, AsyncCallback Callback, object userParams) 14 { 15 fileStream = new FileStream(fileLocation + fileName, FileMode.Open, FileAccess.Read, FileShare.Read); 16 buffer = new byte[fileStream.Length]; 17 return fileStream.BeginRead(buffer, 0, buffer.Length, Callback, userParams); 18 } 19 20 public string EndRead(IAsyncResult asyncResult) 21 { 22 fileStream.EndRead(asyncResult); 23 fileStream.Close(); 24 return Encoding.ASCII.GetString(buffer); 25 } 26 } 27 }
Hosting项目配置
1 <system.serviceModel> 2 3 <behaviors> 4 <serviceBehaviors> 5 <behavior name="metadataBehavior"> 6 <serviceMetadata httpGetEnabled="true" httpGetUrl="http://127.0.0.1:7777/filereaderservice/metadata" /> 7 </behavior> 8 </serviceBehaviors> 9 </behaviors> 10 11 <services> 12 <service behaviorConfiguration="metadataBehavior" name="Service.FileReaderService"> 13 <endpoint address="net.tcp://127.0.0.1:8888/filereaderservice" binding="netTcpBinding" contract="Service.Interface.IFileReader" /> 14 </service> 15 </services> 16 17 </system.serviceModel>
Client项目配置
1 <system.serviceModel> 2 <client> 3 <endpoint address="net.tcp://127.0.0.1:8888/filereaderservice" binding="netTcpBinding" 4 contract="Service.Interface.IFileReader" name="filereaderservice" /> 5 </client> 6 </system.serviceModel>
为客户端添加服务引用 不要选择异步 则生成的代理类只有Read方法 并没有BeginRead和EndRead异步方法 异步操作是在服务端定义的 所以客户端只管使用Read方法执行同步请求即可 而由服务端来异步完成文件的读取
1 using Client.FileReaderService; 2 3 namespace Client 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 FileReaderClient proxy = new FileReaderClient(); 10 var filecontent=proxy.Read("Test.txt"); 11 Console.WriteLine(filecontent); 12 Console.Read(); 13 } 14 } 15 }
服务运行时
信道分发器创建了终结点分发器 终结点分发器(EndpointDispatcher类)在初始化的时候则会根据契约生成一个该契约下的服务操作列表 一个DispatchOperationCollection集合(EndpointDispatcher.DispatchOperationCollection属性) 由于DispatchOperation表示的是契约的服务操作 所以它具有OperationDescription(服务操作的描述信息类)的属性 如Name、Action、ReplyAction、IsOneWay 终结点分发器初始化时 除了创建表示服务操作的DispatchOperationCollection外 终结点分发器在初始化还会创建一个"服务分发运行时"(或称为终结点分发运行时) 分发运行时由一个System.ServiceModel.Dispatcher.DispatchRuntime类表示(EndpointDispatcher.DispatchRuntime属性) 当请求通过信道分发器转交给终结点分发器后 所有的处理操作都将在这个DispatchRuntime中执行 DispatchRuntime具有一个Operations<string,DispatcherOperation>的字典属性用以表示所有的服务操作 键值就是服务操作的名称
客户端运行时
当使用ChannelFactory<TChannel>被开启后 会创建一个客户端运行时 用ClientRuntime表示 此类同样具有一个表示服务操作列表的Operations<string,ClientOperation>字典属性
操作的选择
服务端是如何匹配请求的服务操作的呢 当WCF服务开启时 服务端会自动创建一个以信道分发器和终结点分发器为核心的运行时框架 该框架会根据该服务的一个或多个终结点地址来创建一个或多个信道分发器 每个信道分发器又包含了自己的信道侦听器 信道侦听器用于侦听信道分发器所绑定的终结点地址的端口 当有请求进入信道分发器时 信道分发器的信道侦听器就会侦听到这个请求然后将请求的信息移交给信道分发器 接着信道分发器会根据请求信息进行消息筛选(Message Filter)进而确定要使用的是哪个终结点分发器 终结点分发器将请求的SOAP报头的<Action>与DispatchOperationCollection中的服务操作名称相匹配从而确定请求要使用的是哪个服务操作 但只有基于WS-Addressing的SOAP消息 才具有<Action>报头 为了解决针对不同消息类型请求的服务操作的选择问题 DispatchRuntime提供了一个OperationSelector属性 该属性表示服务操作选择器 该属性实现了System.ServiceModel.Dispatcher.IDispatchOperationSelector接口 该接口提供一个SelectOperation方法用于实现获取基于不同消息类型的请求的服务操作的名字 返回的是一个string类型的值 表示服务操作的名字 DispatchRuntime对于服务操作的选择属性定义如下
1 public sealed class DispatchRuntime 2 3 { 4 public IDispatchOperationSelector OperationSelector { get; set; } 5 6 }
IDispatchOperationSelector定义如下
1 public interface IDispatchOperationSelector 2 { 3 // 将本地的服务操作列表与传入的服务操作相匹配 4 // message:要与服务操作相关联的、传入的 System.ServiceModel.Channels.Message 5 string SelectOperation(ref Message message); 6 }
如果WCF默认的获取服务操作名字的机制无法满足需求 则可以考虑实现IDispatchOperationSelector接口 实现自己的SelectOperation方法 通过设置DispatchRuntime的OperationSelector属性指向一个自实现IDispatchOperationSelector接口即可
客户端运行时同样提供了一个OperationSelector属性 该属性实现了System.ServiceModel.Dispatcher.IClientOperationSelector接口 ClientRuntime对于服务操作的选择属性定义如下
public sealed class ClientRuntime { public IClientOperationSelector OperationSelector { get; set; } }
IClientOperationSelector接口实现如下
1 public interface IClientOperationSelector 2 { 3 // 获取一个值 指示是否需要参数来确定选择 如果需要参数 则为 true 否则为 false 4 bool AreParametersRequiredForSelection { get; } 5 6 //将本地的服务操作列表与传入的服务操作相匹配 7 // method:调用的方法 8 // parameters:传递给该方法的参数 9 string SelectOperation(MethodBase method, object[] parameters); 10 }
在默认情况下 WCF使用一个实现了IClientOperationSelector接口的MethodInfoOperationSelector选择器来匹配请求的服务操作