跟我一起从零开始学WCF系列课程(2):设计和实现服务协定
这几天下班后电脑被老婆占着看《胜女的时代》,所以该系列更新得有点慢哦。还是赶紧多赚点钱帮她台本本吧,这样我们就不用抢了。好了,言归正传,该系列第二章终于出炉了…
本章内容主要包括:
2.1 服务协定
2.1.1 WCE术语
2.1.2 创建服务协定
2.1.3 三种消息模式
2.1.4 DEMO演示:创建不同消息模式的WCF服务
2.2 数据协定
2.2.1 创建数据协定
2.2.2 DEMO演示:创建数据协定
2.3 Out和Ref参数
2.3.1 Out和Ref参数
2.3.2 DEMO演示:Out和Ref参数
2.1 服务协定
2.1.1 WCE术语
a.消息
–消息是一个独立的数据单元,它可能由几个部分组成,包括消息正文和消息头。
b.服务
–服务是一个构造,它公开一个或多个终结点,其中每个终结点都公开一个或多个服务操作。
c.终结点
–终结点是用来发送或接收消息(或执行这两种操作)的构造。终结点包括一个定义消息可以发送到的目的地的位置(地址)、一个描述消息应如何发送的通信机制规范(绑定)以及对于可以在该位置发送或接收(或两者皆可)的一组消息的定义(服务协定)— 该定义还描述了可以发送何种消息。
–WCF 服务作为一个终结点集合向外界公开。
endpoint-服务端为客户端提供一系列服务方法的集合,方法可以包装在一个类和不同类里面,客户端连接到到服务端某个类,这个类实现很多方法,这个类就叫做终结点,也可以理解为客户端调用服务端的入口。
2.1.2 创建服务协定
a. 类或接口都可以定义服务协定,建议使用接口,因为接口可以直接对服务协定建模。
服务协定接口具有托管接口的所有优点:
–服务协定接口可以扩展任何数量的其他服务协定接口。
–一个类可以通过实现服务协定接口来实现任意数量的服务协定。(C#是单继承,但可以实现多个接口)
–可以通过更改接口实现来修改服务协定的实现,而让服务协定保持不变。
–可以通过实现旧接口和新接口来确定服务的版本。老客户端连接到原始版本,而新客户端则可以连接到较新的版本。
b. 定义服务协定
–在类或接口上使用ServiceContract属性标记
c.定义服务操作
–在方法上使用OperationContract属性对其进行标记
d.参数和返回值
–每个操作都有一个返回值和一个参数,即使它们为void(因为WCF服务的消息模式是请求/应答的模式)。可以使用局部方法将对对象的引用从一个对象传递到另一个对象,但与局部方法不同的是,服务操作不会传递对对象的引用, 它们传递的只是对象的副本copy。(因为在本机编程,在同一台电脑的内存里,传个地址过去就可以了,但WCF是面向internet的,客户端和服务器端不在同一台电脑,甚至不在一个国家,不能传递地址)
–这一点很重要,这是因为参数或返回值中使用的每个类型都必须是可序列化的(可以翻译成文本),换言之,该类型的对象必须能够转换为字节流,并能够从字节流转换为对象(反序列化)。
–默认情况下,基元类型(整型、字符串、数值型)是可序列化的,.NET Framework 中的很多类型都是可序列化的。
2.1.3 三种消息模式(请求/答复、单向、双工)
a.请求/答复
–通过请求/答复模式,请求发送方(客户端应用程序)将接收与请求相关的答复。这是默认的模式,因为它既支持传入操作(一个或多个参数传递到该操作中),也支持返回操作(该操作将一个或多个输出值传回给调用方)。
[OperationContract] string Hello(string greeting);
–请注意,除非指定其他基础消息模式,否则,即使服务操作返回void(在Visual Basic 中为Nothing),也属于请求/答复消息交换。
–操作的结果是:除非客户端异步调用操作,否则客户端将停止处理,直到收到返回消息(.net framework来完成),即使该消息正常情况下为空时也是如此。
缺点:
–如果执行操作需要很长的时间,则会降低客户端性能和响应能力。
优点:
–响应消息中可返回SOAP 错误(服务方法执行过程中的错误),这表明可能在通信或处理中发生了一些与服务有关的错误状况。
b.单向
–如果WCF 服务应用程序的客户端不必等待操作完成,并且不处理SOAP 错误,则该操作可以指定单向消息模式。
–单向操作是客户端调用操作并在WCF 将消息写入网络后继续进行处理的操作。通常这意味着,除非在出站消息中发送的数据极其庞大,否则客户端几乎立即继续运行(除非发送数据时出错)。此种类型的消息交换模式支持从客户端到服务应用程序的类似于事件的行为。
–若要为返回void 的操作指定单向消息交换,请将IsOneWay 属性设置为true,默认为false
[OperationContract(IsOneWay=true)] void Hello(string greeting);
此方法与前面的请求/答复示例相同,但是,将IsOneWay属性设置为true 意味着尽管方法相同,服务操作也不会发送返回消息,而客户端将在出站消息抵达通道层时立即返回。
c.双工
–双工模式的特点是,无论使用单向消息发送还是请求/答复消息发送方式,服务和客户端均能够独立地向对方发送消息。对于必须直接与客户端通信或向消息交换的任意一方提供异步体验(包括类似于事件的行为)的服务来说,这种双向通信形式非常有用
–由于存在与客户端通信的附加机制,双向模式比请求/答复或单向模式要略为复杂
–若要设计双工协定,还必须设计回调协定(客户端需要建模,公开哪些方法可以被服务端回调),并将该回调协定的类型分配给标记服务协定的ServiceContract属性(attribute)的CallbackContract 属性(property)。
– 若要实现双工模式,您必须创建第二个接口,该接口包含在客户端调用的方法声明。
[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples", SessionMode=SessionMode.Required, CallbackContract=typeof(ICalculatorDuplexCallback))] public interface ICalculatorDuplex { [OperationContract(IsOneWay = true)] void Clear(); } public interface ICalculatorDuplexCallback { [OperationContract(IsOneWay = true)] void Equals(double result); [OperationContract(IsOneWay = true)] void Equation(string eqn); }
2.1.4 DEMO演示:创建不同消息模式的WCF服务
我们在上一章建的WCF服务的基础上做点修改来演示请求/答复和单向消息模式。
a.请求/答复
新建工程DefaultWcfServiceLibrary.sln,默认创建的例子就是使用接口来实现的WCF服务,实际上就是请求/答复模式,因为没有添加(IsOneWay=true),为了更好地反映请求/答复模式客户端直到收到返回消息才执行下一步,否则将停止处理,我们在service1.cs的GetData方法中添加System.Threading.Thread.Sleep(10000)让进程停止10秒,通过F5测试可以发现客户端也是停止10秒后,才执行语句return string.Format("You entered: {0}", value),返回"You entered: 5"。
public string GetData(int value) { //进程暂停10秒 System.Threading.Thread.Sleep(10000); return string.Format("You entered: {0}", value); }
我们也可以往工程中添加一个Form程序TestMsgModel,包括一个调用服务方法GetData的按钮事件,同时可以通过发现Discovery的方法添加Service1服务引用(Service Reference)。同样可以测试出上述结果,代码如下:
//测试请求/答复消息模式 private void btnTestRAMSgModel_Click(object sender, EventArgs e) { ServiceReference1.Service1Client sc = new WindowsFormsApplication1.ServiceReference1.Service1Client(); string strOutput = sc.GetData(5); MessageBox.Show(strOutput); }
b.单向
为了测试单向消息模式,我们在IService1.cs添加服务方法TestSingleMsgModel,同时声明IsOneWay=true,然后在在Service1.cs中实现TestSingleMsgModel,代码如下
[ServiceContract] public interface IService1 { [OperationContract] string GetData(int value); [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); // TODO: Add your service operations here [OperationContract(IsOneWay=true)] void TestSingleMsgModel(string strInput); }
public class Service1 : IService1 { public string GetData(int value) { //进程暂停10秒 System.Threading.Thread.Sleep(10000); return string.Format("You entered: {0}", value); } public CompositeType GetDataUsingDataContract(CompositeType composite) { if (composite.BoolValue) { composite.StringValue += "Suffix"; } return composite; } public void TestSingleMsgModel(string strInput) { System.Threading.Thread.Sleep(10000); } }
然后在客户端Form程序TestMsgModel中更新服务引用(右键引用名,点击“Update Service Reference”),添加按钮事件btnTestSingleMsgModel_Click,设置FTestMsgModel程序为启动项,F5测试,点击Test Single Msg Model按钮,立即弹出对话框“Invoke Successfully!”,并不会实现Service1.cs中TestSingleMsgModel方法的进程停止10秒钟,这说明单向操作是客户端调用操作并在WCF 将消息写入网络后几乎立即继续进行处理的操作。
public partial class Form1 : Form { ServiceReference1.Service1Client sc = new WindowsFormsApplication1.ServiceReference1.Service1Client(); public Form1() { InitializeComponent(); } //测试请求/答复消息模式 private void btnTestRAMSgModel_Click(object sender, EventArgs e) { string strOutput = sc.GetData(5); //消息框将在10秒后弹出 MessageBox.Show(strOutput); } //测试单向消息模式 private void btnTestSingleMsgModel_Click(object sender, EventArgs e) { sc.TestSingleMsgModel("Hello,WCF!"); //消息框立即弹出 MessageBox.Show("Invoke Successfully!"); } }
测试结果如下:
图2.1
下载demo: DefaultWcfServiceLibrary.rar
c. 双工
我们创建一个新WCF服务:WcfServiceLibrary2 来实现加减乘除的计算器功能。为了实现双工,我们在服务端实现清除Clear、加AddTo、减AddTo、乘MultiplyBy和除DivideBy的方法,然后在客户端实现服务端定义的回调方法:输出值Equals和输出连续计算之后的总方程式Equation。本例中客户端使用ConsoleApplication来实现的,具体参考代码示例或下载DEMO,里面有相关注释。
// 服务方法(加减乘除)都是一系列连续的调用,所以需要SessionMode会话的服务,CallbackContract定义客户端方法的接口类型 [ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples", SessionMode = SessionMode.Required, CallbackContract = typeof(ICalculatorDuplexCallback))] public interface ICalculatorDuplex { [OperationContract(IsOneWay = true)] void Clear(); [OperationContract(IsOneWay = true)] void AddTo(double n); [OperationContract(IsOneWay = true)] void SubtractFrom(double n); [OperationContract(IsOneWay = true)] void MultiplyBy(double n); [OperationContract(IsOneWay = true)] void DivideBy(double n); } // 客户端服务接口,用于声明客户端回调的方法,并不是服务,所以没有ServiceContract修饰 public interface ICalculatorDuplexCallback { // 公开、单向的方法,服务端调客户端,不管客户端怎么回应,服务端立即结束 [OperationContract(IsOneWay = true)] void Equals(double result); [OperationContract(IsOneWay = true)] void Equation(string eqn); }
// 服务行为修饰,获取或设置服务对象何时创建,开启会话的实例模式(一个会话才创建一个实例) [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class CalculatorService : ICalculatorDuplex { double result; string equation; ICalculatorDuplexCallback callback = null; public CalculatorService() { result = 0.0D; equation = result.ToString(); // 获取调用当前上下文里面回调的接口的类型(客户端实例调用当前操作的回调通道) callback = OperationContext.Current.GetCallbackChannel(); } public void Clear() { // 客户端调用Clear方法时,服务端回调客户端的Equation方法 callback.Equation(equation + " = " + result.ToString()); result = 0.0D; equation = result.ToString(); } public void AddTo(double n) { result += n; equation += " + " + n.ToString(); // 服务端回调客户端的Equals方法 callback.Equals(result); } public void SubtractFrom(double n) { result -= n; equation += " - " + n.ToString(); // 服务端回调客户端的Equals方法 callback.Equals(result); } public void MultiplyBy(double n) { result *= n; equation += " * " + n.ToString(); // 服务端回调客户端的Equals方法 callback.Equals(result); } public void DivideBy(double n) { result /= n; equation += " / " + n.ToString(); // 服务端回调客户端的Equals方法 callback.Equals(result); } }
public class CallbackHandler : ICalculatorDuplexCallback { public void Equals(double result) { Console.WriteLine("Result({0})", result); } public void Equation(string eqn) { Console.WriteLine("Equation({0})", eqn); } } class Program { static void Main(string[] args) { // Construct InstanceContext to handle messages on callback interface InstanceContext instanceContext = new InstanceContext(new CallbackHandler()); // Create a client ServiceReference1.CalculatorDuplexClient client = new ServiceReference1.CalculatorDuplexClient(instanceContext); Console.WriteLine("Pressto terminate client once the output is displayed."); Console.WriteLine(); // Call the AddTo service operation. double value = 100.00D; client.AddTo(value); // Call the SubtractFrom service operation. value = 50.00D; client.SubtractFrom(value); // Call the MultiplyBy service operation. value = 17.65D; client.MultiplyBy(value); // Call the DivideBy service operation. value = 2.00D; client.DivideBy(value); // Complete equation client.Clear(); Console.ReadLine(); //Closing the client gracefully closes the connection and cleans up resources client.Close(); } }
注意:
1)在创建服务端程序WcfServiceLibrary2 之后(未创建客户端程序ConsoleApplication1之前),F5调试,出现红色感叹号,不能调试,是因为双工的消息模式,必须要有客户端实现回调方法。
2)服务端应用配置文件App.config的终结点endpoint的节点内容,修改成支持回调的绑定方式和接口协定。
<!--默认banding方式是HttpBinding,不支持回调,要改成支持回调的wsDualHttpBinding--> <endpoint address="" binding="wsDualHttpBinding" contract="WcfServiceLibrary2.ICalculatorDuplex">
简单地说,双工需要在服务端定义回调方法,并在客户端实现,服务端服务方法中调用回调方法。测试结果如下:
图2.2
下载DEMO: WcfServiceLibrary2.rar
2.2 数据协定
2.2.1 创建数据协定
a.数据协定
- 面向服务的应用程序(例如Windows Communication Foundation(WCF) 应用程序)设计为与Microsoft 平台和非 Microsoft 平台上的最大可能数量的客户端应用程序进行互操作。
- 为了获得最大可能的互操作性,建议您使用DataContract 和DataMember 属性对您的类型进行标记,以创建数据协定。
- 数据协定是服务协定的一部分,用于描述您的服务操作交换的数据。
b.看一下前面用到的自定义类型CompositeType的例子:
//整个数据类型用DataContract修饰 [DataContract] public class CompositeType { bool boolValue = true; string stringValue = "Hello "; //属性用DataMember修饰 [DataMember] public bool BoolValue { get { return boolValue; } set { boolValue = value; } } }
c.数据类型的其他约束:
- 数据协定是可选的样式协定:除非您显式应用数据协定属性,否则不会序列化任何类型或数据成员。也就是说当自定义的数据类型想在WCF服务端和客户端传递的时候,一定要像上例中显示的加上DataContrat和DataMember属性,不加的话,WCF不会认为它们是需要公开的数据类型,不会序列化的。
- 数据协定与托管代码的访问范围无关:可以对私有数据成员进行序列化,并将其发送到其他位置,以便可以公开访问它们。也就是说与本身自定义类的访问范围没有关系的,只要加上DataMember去修饰它,它就会帮你序列化。
- WCF 处理用于启用操作功能的基础 SOAP 消息的定义,并处理数据类型到消息正文的序列化和从消息正文进行的反序列化。数据类型一旦序列化,您就无需在设计操作时考虑基础消息交换基础结构。意思是我们只要按照DataContract和DataMember的方式来定义想要传递的数据类型就可以了,至于底层是怎样转换或者反转换的过程,我们不需要过多考虑,要让开发者快速实现WCF。
- 可以使用其他序列化机制。标准ISerializable, SerializableAttribute和IXmlSerializable 机制都可用于处理数据类型到基础SOAP 消息的序列化,这些消息可将数据类型从一个应用程序带到另一个应用程序。当DataContract和DataMember的序列化方式不能满足我们需求的时候可以采用这些机制自己去定义。
2.2.2 DEMO演示:创建数据协定
在上一章WcfServiceLibrary1工程中使用的自定义类型CompositeType就是数据协定的例子,它在IService1.cs中定义了两个属性:bool类型的属性
BoolValue,和
string类型的属性
StringValue;然后看Service1.cs中的实现:根据
BoolValue的值决定是否在
StringValue末尾加上字符串
"Suffix";以及在客户端程序WindowsFormsApplication1中如何给两个属性赋值,和调用GetDataUsingDataContract方法返回一个新的CompositeType类型值,输出它的CompositeType值,就这么简单。代码参考上一章:跟我一起从零开始学WCF系列课程(1):WCF概述
2.3 Out和Ref参数
2.3.1 Out和Ref参数
- 大部分情况下,您可以使用in 参数(Visual Basic 中为ByVal)、out 和 ref 参数(Visual Basic 中为 ByRef)。由于out 和 ref 参数都指示数据是从操作返回的,类似如下的操作签名会指定需要请求/答复操作,即使操作签名返回void 也是如此
[ServiceContract] public interface IMyContract { [OperationContract] void PopulateData(ref CustomDataType data); }
- 使用out 或ref 参数要求操作具有基础响应消息,才可以将已修改的对象传回。如果操作是单向操作(IsOneWay=True),则将在运行时引发InvalidOperationException 异常。
2.3.2 DEMO演示:Out和Ref参数
为了演示,我们在上节第一个例子WcfServiceLibrary1的基础上做修改(为避免混淆,这里命名为WcfOutRefParam),我们将GetData方法增加Out和Ref参数(strRef,strOut),实现中将strRef修改后传出,在其末尾添加后缀"---changed by wcf method",strOut传出字符串"from wcf method",客户端初始化strRef为"ABC",然后调用GetData方法,再输出strRef和strOut的值,即"ref 参数值:ABC---changed by wcf method out 参数值:from wcf method“。
[OperationContract] string GetData(int value,ref string strRef,out string strOut);
public string GetData(int value,ref string strRef,out string strOut) { //System.Threading.Thread.Sleep(10000); strRef += "---changed by wcf method"; strOut = "from wcf method"; return string.Format("You entered: {0}", value); }
private void button1_Click(object sender, EventArgs e) { ServiceReference1.Service1Client sc = new WindowsFormsApplication1.ServiceReference1.Service1Client(); string strRef = "ABC"; string strOut; sc.GetData(5, ref strRef, out strOut); MessageBox.Show("ref 参数值:"+strRef + " out 参数值:" + strOut); }
下载DEMO:WcfOutRefParam.rar