第七章 消息协定
7.1 消息协定概述
通常情况下,在定义消息的架构时只使用数据协定就足够,但是有时需要精确控制如何将类型映射到通过网络传输的SOAP消息。对于这种情况,通常解决方案是插入自定义的SOAP标头。
此外还可以定义消息头和正文的安全属性,通过确定是否对这些元素进行数字签名和加密,消息样式的操作可提供这种控制。消息样式的操作最多具有一个参数和一个返回值,其中参数和返回值的类型都是消息类型,即这两种类型可直接序列化为指定的SOAP消息结构。
消息协定可以是用MessageContractAttribute标记的任何类型或Message类型。
如下所示:
[OperationContract] BankingTransactionResponse Process(BankingTransaction bt); [OperationContract] Void Store(BankingTransaction bt);
以上示例的两个消息样式的操作均为有效的操作。
那么该如何定义消息协定呢?请看以下示例:
[MessageContract] Public class BankingTranscation { [MessageHeader] Public Operation operation; [MessageHeader] Public DateTime transactionDate; [MessageBodyMember] Private Account sourceAccount; }
通过上边的示例我们可以看出,若要为某一类型定义消息协定即定义该类型和SOAP信封之间的映射,请对该类型的应用MessageContractAttribute属性,然后对类型中药成为SOAP标头的成员使用MessageHeaderAttribute属性,对成为消息的SOAP正文部分成员使用MessageBodyMemberAttribute属性即可。
此外还需要注意的是,可以对所有的字段/属性/事件应用MessageHeaerAttribute和MessageBodyMemeberAttribute,且不受字段/属性/事件的访问限制。
演示示例:
- 新建一个WCF应用服务程序。修改后的目录结构如下:
- 在ICalculatorService.cs文件中,定义访问接口,消息协定类MyMessage
具体代码如下:
[ServiceContract(Namespace="Http://Microsoft.WCF.Message")] public interface ICalculatorService { [OperationContract(Action = "Http://Test/MyMessage action", ReplyAction = "Http://Test/MyMessage action")] MyMessage Calculator(MyMessage request); } [MessageContract] public class MyMessage { private string operation; private double n1; private double n2; private double result; public MyMessage(){} public MyMessage(string operation ,double n1,double n2,double result) { this.n1=n1; this.n2=n2; this.result=result; this.operation=operation; } public MyMessage(MyMessage message) { this.n1=message.n1; this.n2=message.n2; this.operation=message.operation; this.result=message.result; } [MessageHeader] public string Operation { get{return operation;} set{operation=value;} } [MessageBodyMember] public double N1 { get{return n1;} set{n1=value;} } [MessageBodyMember] public double N2 { get{return n2;} set{n2=value;} } [MessageBodyMember] public double Result { get{return result;} set{result=value;} } [MessageHeader(MustUnderstand=true)] public string str;
2. 在CalculatorService.svc文件中,定义CalculatorService类,该类实现上一步中定义的ICalculatorService接口。具体代码如下:
public class CalculatorService : ICalculatorService { public MyMessage Calculator(MyMessage request) { MyMessage myMessage = new MyMessage(request); switch (myMessage.Operation) { case "+": myMessage.Result = myMessage.N1 + myMessage.N2; break; case "-": myMessage.Result = myMessage.N1 - myMessage.N2; break; case "*": myMessage.Result = myMessage.N1 * myMessage.N2; break; case "/": myMessage.Result = myMessage.N1 / myMessage.N2; break; default: myMessage.Result = 0.0D; break; } return myMessage; } }
3. 对配置文件进行一些配置设置,代码如下:
在<System.serviceModel>节点下新增如下内容:
<services> <service name="Message.CalculatorService" behaviorConfiguration="Message.CalculatorServiceBehaviors"> <endpoint binding="wsHttpBinding" contract="Message.ICalculatorService"/> <endpoint binding="mexHttpBinding" contract="IMetadataExchange" address="mex"/> </service> </services>
4. 将上一步生成文件进行发布,部署,然后利用C盘Windows SDKs文件夹下的Svcutil.exe工具生成客户端调用类和App.Config配置文件。(关于Svcutil.exe工具的使用请参考上一篇文章)
5. 新建控制台应用程序,作为调用服务的客户端,并添加上一步生产的客户端调用类文件和配置文件。项目目录如下:
6. 在Client项目的Program.cs文件中,调用服务。代码如下:
static void Main(string[] args) { CalculatorServiceClient calculator = new CalculatorServiceClient(); MyMessage request = new MyMessage("+","Olive", 1.2, 3.4, 0); MyMessage respons = ((ICalculatorService)calculator).Calculator(request); Console.WriteLine("N1={0},N2={1},Operation is:{2},Result={3}", respons.N1, respons.N2, respons.Operation,respons.Result); Console.WriteLine("Calculator is Over!"); Console.Read(); }
7. 演示结果如下:
总结:该例主要用来演示如何创建消息协定,并调用消息服务。
7.2 在消息协定内部使用自定义类型
每个单独的消息头和消息正文部分均使用为消息所使用的服务协定选择的序列化引擎进行序列化,默认的序列化引擎XmlFormatter可以显示的处理或隐式出来具有消息协定的任何类型。
此外,还可以在消息协定内部使用数组,可以通过直接在数组上使用MessageHeaderAttribute或MessageBodyMemberAttribute来修饰数组类型的字段或属性等,当然也可以通过MessageHeaderArrayAttribute来修饰。
示例如下:
[MessageContract] Public class MyMessage { [MessageHeader] Public int numRecords; [MessageHeader] Public int[] Counts; MessageHeaderArray] Public int [] Records; }
这两种不同的修饰方法,产生的结果如下:
<MyMessage> <numRecords>116</numRecords> <Counts> <int>1</int> <int>2</int> </Counts> <Records>3</Records> <Record>4</Records> </MyMessage>
通过生成的结果可知,使用MessageHeaderAttribute属性修饰的字段比通过MessageHeaderArrayAttribte属性修饰的字段多了一个消息信封<Counts>但是这两种修饰方式产生的效果是一样的。
演示示例:
这个演示示例我们直接在上一个示例中添加一些东西就可以了。
- 在上一个示例中的ICalculatorService.cs文件中添加如下数据协定
[DataContract] public class ExtType { public ExtType() { } public ExtType(string _name1,string _name2) { this.name1=_name1; this.name2=_name2; } private string name1; private string name2; [DataMember] public string Name1 { get{ return name1;} set{name1=value;} } [DataMember] public string Name2 { get{return name2;} set{name2=value;} }
2. 然后在该文件的消息协定类MyMessage中添加如下属性:
[MessageBodyMember] public ExtType ExtType { get { return et; } set { et = value; } }
3. 重新生成发布,然后又Svcutil.exe工具重新生成客户端类和配置文件,并在客户端项目中将原有的Client.cs文件和App.config文件删除,添加刚刚生成的新的文件。
4. 在客户端项目中的Program.cs添加如下代码:
static void Main(string[] args) { CalculatorServiceClient calculator = new CalculatorServiceClient(); MyMessage request = new MyMessage(); request.N1 = 1.2; request.N2 = 3.4; request.Result = 0; request.Operation = "+"; request.NewString = "Spartacus"; Message1.ExtType et = new Message1.ExtType(); et.Name2 = "Only"; et.Name1 = "For"; request.ExtType = et; MyMessage respons = ((ICalculatorService)calculator).Calculate(request); Console.WriteLine("N1={0},N2={1},Operation is:{2},Result={3}", respons.N1, respons.N2, respons.Operation, respons.Result); Console.WriteLine("The ExtType's Name1 is : {0} ,and Name2 is : {1}", respons.ExtType.Name1, respons.ExtType.Name2); Console.WriteLine("Calculator is Over!"); Console.Read(); }
5. 编译运行,结果如下:
7.3 对消息部分进行签名和加密
消息协定可以通过在MessageHeaderAttribute和MessageBodyMemberAttribute属性上设置MessageContractMemberAttribute.ProtectionLevel属性的不同的值,来指示消息头或正文是否应该进行数字签名和加密。
System.Net.Security.ProtectionLevel有三个枚举值,分别为:
None:不加密或签名
Sign:仅数字签名
EncryptAndSign:加密并数字签名
除了通过设置ProtectionLevel属性值来进行签名和加密以外,还有以下几点需要注意:
- 必须正确配置绑定和行为,如果没有正确配置就使用这些安全功能,则验证时会引发异常。
- 对于消息头会分别为每个消息头确定其保护级别。
- 对象消息正文部分,保护级别为“最低保护级别”,正文的保护级别由所有正文的最高ProtectionLevel属性值确定,在设置时应设置为最低保护级别。
演示示例:
[MessageContract] Public class PatientRecord { [MessageHeader(ProtectionLevel=None)] Public int recordID; [MessageHeader(ProtectionLevel=Sign)] Public string patientName; [MessageBodyMember(ProtectionLevel=None)] Public string comments; [MessageBodyMember(ProtectionLevel=EncryptAndSign)] Public string SSN; }
注释:
上边的示例代码中,我们可以得知,recordID标头未受保护,patientName标头未signed,因为消息正文部分SSN已经应用EncryptAndSign,所以所有的消息正文部分都进行了加密和签名。
7.4 控制标头和正文部分的名称和命名空间
在消息协定的SOAP表示形式中,每个标头和正文部分都映射为一个具有名称和命名空间的Xml元素,如果想修改默认的名称可以通过System.ServiceModel.MessageContractMemberAttribute.Name和NameSpace属性实现。
示例如下:
[MessageContract] Public class BankingTransaction { [MessageHeader(NameSpace=”http://www.cnblogs.com/Olive116”)] Public bool IsGoodMan; [MessageBodyMember(Name=”Author”)] Public string writer; }
注释:IsGoodMan 标头位于代码中指定的命名空间中,正文部分writer由名为Author的Xml元素表示。
这里的Name和NameSpace属性和数据协定中的Name和Namespace属性有些相似的地方。
7.5 控制是否包装SOAP正文部分
默认的情况下,SOAP的正文部分会在包装元素内部进行序列化,如果想要取消包装,可以通过将IsWrapped属性设置为false来实现,若要控制包装元素的名称和命名空间,可以使用WrapperName和WrapperNamespace属性来实现。
7.6 SOAP标头属性
SOAP标准定义了下列可能存在于标头上的属性:
l Actor/Role(SOAP 1.1中为Actor,SOAP 1.2中为Role)
--指定使用给定标头节点的统一资源标识符(URL)
l MustUnderstand
--指定处理标头的节点是否必须理解该标头
l Relay
--指定要将标头中继到下游节点
除了MustUnderstand节点以外,WCF不会对传入消息的这些属性作任何的处理。
SOAP标头属性的设置有以下三种方式:
- 通过静态方式设置标头属性
示例如下:
[MessageContract] Public class BankingTransaction { [MessageHeader(Actor=”Http://www.cnblogs.com/Olive116”,MustUnderstand=true)] Public bool IsAudited; }
2. 通过动态方式设置标头属性
[MessageContract] Public class BankingTransaction { [MessageHeader] Public bool IsAudited; }
……
……
BankingTransaction bt=new BankingTransaction();
bt.IsAudited=true;
bt.IsAudited.Actor=”Http://www.cnblogs.com/Olive116”;
bt.IsAudited.MustUnderstand=true;
3. 通过动静态结合的方式设置标头属性,此时,标头属性默认为静态设置的值,但是可以在以后使用动态机制进行重写.
示例如下:
[MessageContract] Public class BankingTransaction { [MessageHeader(MustUnderstand=false)] Public bool IsAudited; } …… …… BankingTransaction bt=new BankingTransaction(); bt.IsAudited=true; bt.IsAudited.Actor=”Http://www.cnblogs.com/Olive116”; bt.IsAudited.MustUnderstand=true;
7.7 SOAP正文部分顺序
默认情况下SOAP正文采用字母顺序,但是可以通过System.ServiceModel.MessageBodyMemberAttribute.Order属性进行设置,
这里需要特别注意的是以数据协定不同,在消息协定中,基类型正文成员不排列在派生类正文成员之前。
示例如下:
[MessageContract] Public class BankingTransaction { [MessageHeader] Public bool IsAudited; [MessageBodyMember(Order=1)] Public string MemberOrder1; [MessageBodyMember(Order=2)] Public string MemberOrder2; [MessageBodyMember(Order=3)] Public string MemberOrder3; }
生成消息正文的顺序分别为MemberOrder1,MemberOrder2,MemberOrder3.
7.8 消息协定版本管理
应用程序的新版本可能会向消息中添加额外的标头,在新版本应用程序向旧版本应用程序发送消息时,系统必须处理额外的标头,同样反向操作时系统必须处理缺少的标头。
标头版本管理的一些规则:
l WCF不反对缺少标头,相应的成员将保留其默认值
l WCF会忽略意外的额外标头,但是额外标头中如果有MustUnderstand属性且值为true,在这种情况下,由于存在无法处理但必须理解的标头,因此会引发异常。
消息正文的版本管理规则跟标头的版本管理规则大体上是类似的,即忽略缺少和附加的消息正文部分。
此外还有一些消息的性能注意事项,一般来讲每个消息头和正文部分相互独立的进行序列化,因此可以为每个标头和正文重新声明相同的命名空间,为提高性能,可以将多个标头和正文部分合并成一个标头或正文部分。
示例如下:
[MessageContract] Public class BankingTransaction { [MessageHeader] Public Operation operation; [MessageBodyMember] Public string BodyMember1; [MessageBodyMember] Public string BodyMember2; [MessageBodyMember] Public string BodyMember3; }
这样的性能是很差的,我们可以对此进行稍加修饰对消息正文进行合并,如下:
[MessageContract] Public class BankingTransaction { [MessageHeader] Public Operation operation; [MessageBodyMember] Public BodyMemberDetails details; } [DataContract] Public class BodyMemberDetails { [DataMember] Public string BodyMember1; [DataMember] Public string BodyMember2; [DataMember] Public string BodyMember3; }