67
WCF 全面解析(上册) 6 消息 Message虽然我们一直在强调 WCF 基于消息通信的本质,但是一般的编程人员却 感受不到消息的存在,这是因为 WCF 设计的目标就是在内部实现消息通信的 所有细节,并为最终的编程人员提供一个完全面向对象的应用编程接口。他 们面对的是接口,却不知道服务契约对于服务的描述;面对的是数据类型, 却不知道数据契约对序列化的作用;面对的是方法调用和返回值的获取,却 不了解底层消息交换的过程。只有从消息交换层面来认识 WCF 才能洞悉其本 质,也只有对整个消息处理流程有清晰的认识才能创建高效的 WCF 应用。

第6 章 消息 Message - cnblogs.com · WCF 全面解析(上册) 第 6 章 消息 (Message) 虽然我们一直在强调. WCF. 基于消息通信的本质,但是一般的编程人员却

  • Upload
    others

  • View
    2

  • Download
    0

Embed Size (px)

Citation preview

WCF 全面解析(上册)

第 6 章 消息

(Message)

虽然我们一直在强调 WCF 基于消息通信的本质,但是一般的编程人员却

感受不到消息的存在,这是因为 WCF 设计的目标就是在内部实现消息通信的

所有细节,并为最终的编程人员提供一个完全面向对象的应用编程接口。他

们面对的是接口,却不知道服务契约对于服务的描述;面对的是数据类型,

却不知道数据契约对序列化的作用;面对的是方法调用和返回值的获取,却

不了解底层消息交换的过程。只有从消息交换层面来认识 WCF 才能洞悉其本

质,也只有对整个消息处理流程有清晰的认识才能创建高效的 WCF 应用。

第 6 章 消息(Message)

WCF 全面解析(上册)

232

6.1 SOAP 与 WS-Addressing

从 WCF 的应用领域来看,WCF 主要有两个分支:一个是通过提供基于 SOAP 消息的服

务解决企业级应用的通信问题;另一个则是为纯 Web 应用提供 REST(Representational State

Transfer)服务。本书上、下册主要以介绍 SOAP 为主。W3C 为 SOAP 消息制定了同名的规

范(SOAP 1.1 和 SOAP 1.2),而 WS-Addressing 则提供了一种与具体传输无关的消息寻址机

制。SOAP 和 WS-Addressing 是整个 WS-*协议簇的基础。在本书中会涉及很多 WS 规范,

这些规范都建立在 SOAP 和 WS-Addressing 上。

6.1.1 SOAP

SOAP 最初是“Simple Object Access Protocol”的简称,这是为 Web 服务制定的一种结

构化信息交换协议,但是今天的 SOAP 已经不具有了当初的含义,已经不再是数据交换协议。

我们如今将 SOAP 看成是一个单词,表示的就是符合 W3C 制定的 SOAP 规范的消息。到目

前为止,W3C 一共制定了 SOAP 1.1 和 SOAP 1.2 两个版本的规范,它们对应的命名空间分

别为:

SOAP 1.1,http://schemas.xmlsoap.org/soap/envelope

SOAP 1.2,http://www.w3.org/2003/05/soap-envelope

下面的 XML 体现了一个典型 SOAP 消息的结构。整个消息被封装在一个称为 SOAP 封

套的<Envelope>元素中。封套元素除了具有一个必需的<Body>表示消息主体之外,还可以

包含多个表示消息报头的<Header>元素。<Envelope>、<Body>和<Header>均采用基于 SOAP

版本的命名空间。

<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">

<env:Header>

<n:alertcontrol xmlns:n="http://example.org/alertcontrol">

<n:priority>1</n:priority>

<n:expires>2001-06-22T14:00:00-05:00</n:expires>

</n:alertcontrol>

</env:Header>

<env:Body>

<m:alert xmlns:m="http://example.org/alert">

<m:msg>Pick up Mary at school at 2pm</m:msg>

</m:alert>

</env:Body>

</env:Envelope>

6.1.2 WS-Addressing

SOAP 规范消息的结构,而 WS-Addressing 则规范消息交换中的寻址机制。W3C 在 2004

6.1 SOAP 与 WS-Addressing

WCF 全面解析(上册)

233

年8月和2005年8月发布了两个版本,分别被称为WS-Addressing 2004和WS-Addressing 1.0。

两个版本的 WS-Addressing 对应的命名空间分别为:

http://schemas.xmlsoap.org/ws/2004/08/addressing

http://www.w3.org/2005/08/addressing

整个 WS-Addressing 规范主要包括 WS-Addressing Core、WS-Addressing SOAP 绑定和

WS-Addressing WSDL 绑定三个部分。其核心规范为消息交换提供了一种与具体传输无关的

抽象寻址机制,而两个绑定则将这种抽象机制和具体的 SOAP 与 WSDL 进行绑定。

WS-Addressing Core 主要定义了两个核心的构造(Construct),即终结点引用(Endpoint

Reference)和消息寻址属性(Message Addressing Property)。它定义了一整套消息寻址属性

用于表示源终结点和目标终结点,以及对消息提供统一的标识。

下面这个例子体现了寻址机制在 SOAP 中的应用,它包含了定义在 WS-Addressing 中的

几个典型的报头,体现了该消息是从源终结点 http://example.com/business/client1 发送给目标

终结点 http://example.com/fabrikam/Purchasing 的消息。

<S:Envelope xmlns:S="http://www.w3.org/2003/05/soap-envelope"

xmlns:wsa="http://www.w3.org/2005/08/addressing">

<S:Header>

<wsa:MessageID>

http://example.com/6B29FC40-CA47-1067-B31D-00DD010662DA

</wsa:MessageID>

<wsa:ReplyTo>

<wsa:Address>

http://example.com/business/client1

</wsa:Address>

</wsa:ReplyTo>

<wsa:To>http://example.com/fabrikam/Purchasing</wsa:To>

<wsa:Action>http://example.com/fabrikam/SubmitPO</wsa:Action>

</S:Header>

<S:Body>...</S:Body>

</S:Envelope>

终结点引用

对于 WS-*的终结点,我们基本上可以与 WCF 的终结点等同起来,表示消息发送、接

收和转发的“端点”。WS-Addressing Core 通过一个由如下三要素构成的 XML 构造描述了针

对某个终结点的引用。

地址:每个终结点引用必须包含一个通过绝对 URI 表示终结点的地址,它可以是物理

地址,也可以是逻辑地址。

引用参数:每个终结点引用可以包含零到多个引用参数,而每个引用参数可以看成是对

终结点的辅助性描述。

元数据:每个终结点引用可以包含一组元数据。

第 6 章 消息(Message)

WCF 全面解析(上册)

234

一个具有上述三要素的完整终结点引用可以通过如下的 XML 元素表示,其中 wsa 表示

的命名空间是针对 WS-Addressing 1.0 的命名空间(http://www.w3.org/2005/08/addressing)。

<wsa:EndpointReference>

<wsa:Address>xs:anyURI</wsa:Address>

<wsa:ReferenceParameters>xs:any*</wsa:ReferenceParameters>

<wsa:Metadata>xs:any*</wsa:Metadata>

</wsa:EndpointReference>

消息寻址属性

WS-Addressing Core 定义了一系列用于辅助两个终结点正常交互的消息寻址属性

(Message Addressing Property)。下面给出了以 XML 元素表示的 8 个消息寻址属性的

定义。

<wsa:To>xs:anyURI</wsa:To>

<wsa:From>wsa:EndpointReferenceType</wsa:From>

<wsa:ReplyTo>wsa:EndpointReferenceType</wsa:ReplyTo>

<wsa:FaultTo>wsa:EndpointReferenceType</wsa:FaultTo>

<wsa:Action>xs:anyURI</wsa:Action>

<wsa:MessageID>xs:anyURI</wsa:MessageID>

<wsa:RelatesTo RelationshipType="xs:anyURI"?>xs:anyURI</wsa:RelatesTo>

<wsa:ReferenceParameters>xs:any*</wsa:ReferenceParameters>

对于这些消息寻址属性,并不是所有的都是必需的。在进行基于 SOAP 消息的通信中,

它们以报头的形式附加到 SOAP 消息上。

<To>(可选):以 URI 的形式表示消息发送的目标地址,如果没有显式指定,则采用默

认地址 http://www.w3.org/2005/08/addressing/anonymous。

<From>(可选):以终结点引用的形式表示源终结点。这是一个基本上不怎么使用的属

性。

<ReplyTo>(可选):以终结点引用的形式表示接收/回复消息的终结点。如果没有显式

指定终结点地址,则会采用默认的 http://www.w3.org/2005/08/addressing/anonymous。

<FaultTo>(可选):以终结点引用的形式表示接收错误消息的终结点。

<Action> (必需):以 URI 的形式表示消息的意图,比如调用服务操作。

<MessageID>(可选):以 URI 的形式表示消息的唯一标识。

< RelatesTo >(可选):表示关联消息的<MessageID>,比如将回复消息的< RelatesTo >

属性设置为请求消息的<MessageID>,从而将两者关联起来。

< ReferenceParameters >(可选):可以以任何 XML 元素形式提供额外的辅助信息。

6.2 消息

WCF 全面解析(上册)

235

6.2 消息

不同的 SOAP 版本(SOAP 1.1 和 SOAP 1.2)对组成 SOAP 消息的元素具有不同的要求。

对于不同版本的 WS-Addressing,绑定到 SOAP 消息上的相关报头也不尽相同。消息针对不

同版本的 SOAP 和 WS-Addressing 的支持反映在消息版本上。

6.2.1 消息版本

消息通过 System.ServiceModel.Channels.Message 类型表示。如下面的代码所示,Message

实际上是一个抽象类,它具有一个类型为 System.ServiceModel.Channels.MessageVersion 的只

读属性 Version 表示消息版本。

public abstract class Message : IDisposable

{

//其他成员

public abstract MessageVersion Version { get; }

}

SOAP 和 WS-Addressing 的版本共同决定了消息版本,或者说消息版本体现了针对不同

SOAP 和 WS-Addressing 的支持,这可以通过 MessageVersion 的定义看出来。如下面的代码

所示,MessageVersion 具有 Envelope 和 Addressing 两个只读属性,分别表示 SOAP 和

WS-Addressing 版本。

public sealed class MessageVersion

{

//其他成员

public EnvelopeVersion Envelope { get; }

public AddressingVersion Addressing { get; }

}

SOAP 版 本 通 过 System.ServiceModel.EnvelopeVersion 类 型 表 示 。 WCF 通 过

EnvelopeVersion 的静态只读属性 Soap11 和 Soap12 分别表示 SOAP 1.1 和 SOAP 1.2,另一个

静态属性 None 则表示非 SOAP 消息。

public sealed class EnvelopeVersion

{

//其他成员

public static EnvelopeVersion None { get; }

public static EnvelopeVersion Soap11 { get; }

public static EnvelopeVersion Soap12 { get; }

}

表示WS-Addressing版本的System.ServiceModel.Channels.AddressingVersion 类型也具有

类似的定义。两个静态只读属性 WSAddressingAugust2004 和 WSAddressing10 分别针对 WS-

Addressing 2004 和 WS-Addressing 1.0,而另一个静态属性 None 则表示消息不支持

WS-Addressing。

第 6 章 消息(Message)

WCF 全面解析(上册)

236

public sealed class AddressingVersion

{

//其他成员

public static AddressingVersion None { get; }

public static AddressingVersion WSAddressingAugust2004 { get; }

public static AddressingVersion WSAddressing10 { get; }

}

MessageVersion 也定义了如下的静态属性表示一些预定义的消息版本。其中

Soap11WSAddressingAugust2004 和 Soap11WSAddressing10 表示的是 SOAP 1.1 分别与

WS-Addressing 2004 和 WS-Addressing 1.0 的组合,而 Soap12WSAddressingAugust2004 和

Soap12WSAddressing10 自然就表示 SOAP 1.2 分别与 WS-Addressing 2004 和 WS-Addressing

1.0 的组合。

public sealed class MessageVersion

{

//其他成员

public static MessageVersion Default { get; }

public static MessageVersion None { get; }

public static MessageVersion Soap11 { get; }

public static MessageVersion Soap11WSAddressingAugust2004 { get; }

public static MessageVersion Soap11WSAddressing10 { get; }

public static MessageVersion Soap12 { get; }

public static MessageVersion Soap12WSAddressingAugust2004 { get; }

public static MessageVersion Soap12WSAddressing10 { get; }

}

为了确认其他几个静态属性代表的是怎样的 SOAP 和 WS-Addressing 的组合,我们编写

了如下的测试程序对它们进行分解。

static void Main()

{

DeCompose(MessageVersion.Default, "MessageVersion.Default");

DeCompose(MessageVersion.None, "MessageVersion.None");

DeCompose(MessageVersion.Soap11, "MessageVersion.Soap11");

DeCompose(MessageVersion.Soap12, "MessageVersion.Soap12");

}

static void DeCompose(MessageVersion version, string name)

{

string envelopeVersion = "EnvelopeVersion.None";

string addressingVersion = "AddressingVersion.None";

if (version.Envelope == EnvelopeVersion.Soap11)

{

envelopeVersion = "EnvelopeVersion.Soap11";

}

if (version.Envelope == EnvelopeVersion.Soap12)

{

envelopeVersion = "EnvelopeVersion.Soap12";

}

if (version.Addressing == AddressingVersion.WSAddressingAugust2004)

{

addressingVersion = "AddressingVersion.WSAddressingAugust2004";

}

6.2 消息

WCF 全面解析(上册)

237

if (version.Addressing == AddressingVersion.WSAddressing10)

{

addressingVersion = "AddressingVersion.WSAddressing10";

}

Console.WriteLine("{0,-22}: {1}", name, envelopeVersion);

Console.WriteLine("{0,-22}: {1}", "", addressingVersion);

Console.WriteLine();

}

运行上面的程序,我们会得到如下的输出结果。由此可见,MessageVersion.Default 代表

的是 SOAP 1.2 和 WS-Addressing 1.0 的组合,相当于静态属性 Soap12WSAddressing10。而

对于 None、Soap11 和 Soap12 这三个静态只读属性表示的消息版本来说,对应的

WS-Addressing 版本为 AddressingVersion.None。

MessageVersion.Default : EnvelopeVersion.Soap12

: AddressingVersion.WSAddressing10

MessageVersion.None : EnvelopeVersion.None

: AddressingVersion.None

MessageVersion.Soap11 : EnvelopeVersion.Soap11

: AddressingVersion.None

MessageVersion.Soap12 : EnvelopeVersion.Soap12

: AddressingVersion.None

MessageVersion 还定义了如下两个静态的 CreateVersion 方法,通过指定代表 SOAP 和

WS-Addressing版本的EnvelopeVersion和AddressingVersion对象创建相应的MessageVersion。

对于第一个重载,默认采用 WS-Addressing 1.0。

public sealed class MessageVersion

{

//其他成员

public static MessageVersion CreateVersion(

EnvelopeVersion envelopeVersion);

public static MessageVersion CreateVersion(EnvelopeVersion

envelopeVersion, AddressingVersion addressingVersion);

}

6.2.2 如何创建消息

由于 Message 是个抽象类型,我们不能直接通过 new 操作符创建 Message 对象,不过

Message 提供了一系列静态的 CreateMessage 方法用于消息的创建。接下来我们会通过实例

演示的形式介绍多种不同的消息创建方式。为了方便读者查看的创建的消息结构,我们定义

了如下一个静态的 WriteMessage 方法,该方法将 Message 对象序列化后写入指定的文件中,

最后将消息文件打开。

static void WriteMessage(Message message, string fileName)

{

using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))

{

第 6 章 消息(Message)

WCF 全面解析(上册)

238

message.WriteMessage(writer);

}

Process.Start(fileName);

}

创建空消息

下面的代码片段表示定义在 Message 中最简单的静态 CreateMessage 方法重载,两个参

数(version 和 action)分别代表消息版本和基于 WS-Addressing 的<Action>报头值。

public abstract class Message : IDisposable

{

//其他成员

public static Message CreateMessage(MessageVersion version,

string action);

}

我 们 通 过 如 下 的 代 码 调 用 这 个 CreateMessage 方 法 基 于 给 定 的 消 息 版 本

(MessageVersion.Default)和<Action>报头值创建一个 Message 对象。

string action = "http://www.artech.com/ICaculator/Add";

using(Message message =

Message.CreateMessage(MessageVersion.Default,action))

{

WriteMessage(message,"message.xml");

}

上面的代码执行之后,会生成具有如下结构的 XML 文件。由此可见,通过该静态方法

CreateMessage 会创建一个仅仅包含<Action>报头的空消息。(S601)

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://www.w3.org/2003/05/soap-envelope">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/ICaculator/Add

</a:Action>

</s:Header>

<s:Body />

</s:Envelope>

将对象序列化成消息的主体

如果我们希望将一个可序列化对象的内容直接作为所创建消息的主体,则可以直接调用

如下一个 CreateMessage 重载。该重载在上一个 CreateMessage 重载的基础上增加了一个表

示消息主体的 body 参数,该参数只能接受一个可序列化对象,而默认采用的序列化器为

DataContractSerializer。

public abstract class Message : IDisposable

{

//其他成员

public static Message CreateMessage(MessageVersion version, string action,

object body);

}

6.2 消息

WCF 全面解析(上册)

239

我们定义了一个表示订单的 Order 类型。如下面的代码所示,这是一个数据契约,它将

为我们创建的消息提供主体部分的内容。

[DataContract(Namespace = "http://www.artech.com")]

public class Order

{

[DataMember(Name = "OrderNo", Order = 1)]

public Guid ID{ get; set; }

[DataMember(Name = "OrderDate", Order = 2)]

public DateTime Date{ get; set; }

[DataMember(Order = 3)]

public string Customer{ get; set; }

[DataMember(Order = 4)]

public string ShipAddress{ get; set; }

}

在如下所示的消息创建程序中,我们创建了一个 Order 对象并将其作为参数调用静态方

法 CreateMessage。

Order order = new Order

{

ID = Guid.NewGuid(),

Date = DateTime.Today,

Customer = "张三",

ShipAddress = "江苏省 苏州市 星湖街 328号"

};

string action = "http://www.artech.com/IOrder/sumbit";

using (Message message = Message.CreateMessage(MessageVersion.Default,

action, order))

{

WriteMessage(message, "message.xml");

}

下面的 XML 片段代表上面的程序执行后生成的消息结构,作为主体部分的 XML 正是

Order 对象通过 DataContractSerializer 序列化后生成的内容。(S602)

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://www.w3.org/2003/05/soap-envelope">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/IOrder/sumbit

</a:Action>

</s:Header>

<s:Body>

<Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.artech.com">

<OrderNo>0c8004e5-f7bd-434e-9323-05725c495b5e</OrderNo>

<OrderDate>2011-11-24T00:00:00+08:00</OrderDate>

<Customer>张三</Customer>

<ShipAddress>江苏省 苏州市 星湖街 328号</ShipAddress>

</Order>

</s:Body>

</s:Envelope>

如果我们通过相同的方式创建一个非 SOAP 消息,那么指定的对象序列化后的 XML 就

第 6 章 消息(Message)

WCF 全面解析(上册)

240

是整个消息的内容。现在我们对上面的程序略作修改,在调用 CreateMessage 方法的时候将

消息版本指定为 MessageVersion.None。

string action = "http://www.artech.com/IOrder/sumbit";

using (Message message = Message.CreateMessage(MessageVersion.None,

action, order))

{

WriteMessage(message, "message.xml");

}

程序执行后,下面这个 XML 片段就代表了整个消息的结构,而这就是 Order 对象被序

列化后的 XML。(S603)

<Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.artech.com">

<OrderNo>d397393f-b318-40c9-aa83-728d9e0cf8af</OrderNo>

<OrderDate>2011-11-24T00:00:00+08:00</OrderDate>

<Customer>张三</Customer>

<ShipAddress>江苏省 苏州市 星湖街 328号</ShipAddress>

</Order>

通过 BodyWriter 将内容写入消息

除了直接通过指定可序列化对象的方式提供消息主体的内容外,被创建的消息主体还可

以通过一个类型为 System.ServiceModel.Channels.BodyWriter 的对象写入。如下面的代码片

段所示,Message 具有一个参数类型为 BodyWriter 的 CreateMessage 重载。BodyWriter 是一

个抽象类,消息主体内容的写入在抽象方法 OnWriteBodyContents 中实现,而通过构造函数

初始化的只读属性 IsBuffered 表示 OnWriteBodyContents 方法是否需要调用多次而选择对内

容进行缓存。

public abstract class Message : IDisposable

{

//其他成员

public static Message CreateMessage(MessageVersion version,

string action, BodyWriter body);

}

public abstract class BodyWriter

{

//其他成员

protected BodyWriter(bool isBuffered);

protected abstract void OnWriteBodyContents(XmlDictionaryWriter writer);

public bool IsBuffered { get; }

}

我们创建了如下一个名为 XmlReaderBodyWriter 的自定义 BodyWriter。在重写的方法

OnWriteBodyContents 中,通过 XmlTextReader 将作为消息主体内容的 XML 读出来写入当前

节点。

public class XmlReaderBodyWriter : BodyWriter

{

public String FileName{get; private set;}

6.2 消息

WCF 全面解析(上册)

241

public XmlReaderBodyWriter(String fileName)

: base(false)

{

this.FileName = fileName;

}

protected override void OnWriteBodyContents(XmlDictionaryWriter writer)

{

using (XmlReader reader = new XmlTextReader(this.FileName))

{

while (!reader.EOF)

{

writer.WriteNode(reader, false);

}

}

}

}

我们通过如下所示的方式来测试自定义 BodyWriter 在创建 Message 对象时对主体内容

的写入。首先将创建的 Order 对象传入 CreateMessage 方法创建一个消息,并将消息内容写

入文件(message1.xml)。由于指定的消息版本为 MessageVersion.None,所以被写入文件的

消息仅仅包含 Order 对象被序列化后的内容。然后我们基于这个文件创建自定义的

XmlReaderBodyWriter 对象,并将其作为 CreateMessage 的参数创建一个消息。

Order order = new Order

{

ID = Guid.NewGuid(),

Date = DateTime.Today,

Customer = "张三",

ShipAddress = "江苏省 苏州市 星湖街 328号"

};

string action = "http://www.artech.com/IOrder/sumbit";

using (Message message = Message.CreateMessage(MessageVersion.None, action,

order))

{

WriteMessage(message, "message1.xml");

}

XmlReaderBodyWriter bodyWriter = new XmlReaderBodyWriter("message1.xml");

using (Message message = Message.CreateMessage(MessageVersion.Soap11, action,

bodyWriter))

{

WriteMessage(message, "message2.xml");

}

下面的 XML 片段表示最终生成的 SOAP 消息的内容。由于我们指定的消息版本为

MessageVersion.Soap11,所以这是一个基于 SOAP 1.1 的消息,这可以从命名空间看出来。

(S604)

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">

<s:Header>

<Action s:mustUnderstand="1"

xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">

http://www.artech.com/IOrder/sumbit</Action>

</s:Header>

<s:Body>

<Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance"

第 6 章 消息(Message)

WCF 全面解析(上册)

242

xmlns="http://www.artech.com">

<OrderNo>867b2026-f502-495a-a40d-74fa343544ef</OrderNo>

<OrderDate>2011-11-24T00:00:00+08:00</OrderDate>

<Customer>张三</Customer>

<ShipAddress>江苏省 苏州市 星湖街 328号</ShipAddress>

</Order>

</s:Body>

</s:Envelope>

除了基于 SOAP 版本的命名空间存在差别之外,消息的<Action>报头的命名空间和之前

基于默认的消息版本(MessageVersion.Default)创建的 SOAP 也是不一样的。前面已经介绍

过了,通过只读属性 MessageVersion.Soap11 和 MessageVersion.Soap12 表示的 MessageVersion

的 Addressing 属性都是 AddressingVersion.None,所以并不支持 WS-Addressing。在这种情况

下的<Action>报头和 WS-Addressing 没有关系,因为它采用的是微软为其定义的命名空间

http://schemas.microsoft.com/ws/2005/05/addressing/none。

通过 XmlReader 将内容读到消息中

如果说基于 BodyWriter 创建消息是采用一种“推”的模式将内容写入消息,那么基于

XmlReader 的方式就是采用一种“拉”的模式。Message 中定义了 4 个基于 XmlReader 的

CreateMessage 重载,其中两个是直接利用 XmlReader 的,其余两个则是通过 XmlReader 的

子类 XmlDictionaryReader 写入消息内容的。

public abstract class Message : IDisposable

{

//其他成员

public static Message CreateMessage(MessageVersion version, string action,

XmlDictionaryReader body);

public static Message CreateMessage(MessageVersion version, string action,

XmlReader body);

public static Message CreateMessage(XmlDictionaryReader envelopeReader,

int maxSizeOfHeaders, MessageVersion version);

public static Message CreateMessage(XmlReader envelopeReader, int

maxSizeOfHeaders, MessageVersion version);

}

如下面的代码片段所示,我们采用和上面一样的方式序列化创建的 Order 对象并写入一

个物理文件中。然后创建一个用于读取该文件的 XmlTextReader,并传入 CreateMessage 方

法创建相应的消息。

Order order = new Order

{

ID = Guid.NewGuid(),

Date = DateTime.Today,

Customer = "张三",

ShipAddress = "江苏省 苏州市 星湖街 328号"

};

string action = "http://www.artech.com/IOrder/sumbit";

using (Message message = Message.CreateMessage(MessageVersion.None, action,

order))

{

6.2 消息

WCF 全面解析(上册)

243

WriteMessage(message, "message1.xml");

}

using(XmlReader reader = new XmlTextReader("message1.xml"))

using (Message message = Message.CreateMessage(

MessageVersion.Soap11WSAddressing10, action,reader))

{

WriteMessage(message, "message2.xml");

}

上面这段代码执行之后,最终被创建的消息将具有如下的结构。看起来和前面通过自定

义 BodyWriter 创建的消息内容一样,实际上消息的 <Action> 报头采用了真正的

WS-Addressing 1.0 的命名空间,这都是源于在调用 CreateMessage 方法的时候将消息版本设

置为 MessageVersion.Soap11WSAddressing10。

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/IOrder/sumbit

</a:Action>

</s:Header>

<s:Body>

<Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.artech.com">

<OrderNo>02a6e41a-9aa0-48cd-8253-da0ae053809c</OrderNo>

<OrderDate>2011-11-24T00:00:00+08:00</OrderDate>

<Customer>张三</Customer>

<ShipAddress>江苏省 苏州市 星湖街 328号</ShipAddress>

</Order>

</s:Body>

</s:Envelope>

创建错误消息

Message 定义了如下 3 个 CreateMessage 方法专门用于创建错误消息(Fault Message)。

对于一个正常的服务调用来说,如果服务能够正常处理接收的请求,则会发送一个普通的

SOAP 消息作为回复;如果在处理过程中出现异常,则会回复一个错误消息。

public abstract class Message : IDisposable

{

//其他成员

public static Message CreateMessage(MessageVersion version,

FaultCode faultCode, string reason, string action);

public static Message CreateMessage(MessageVersion version, string action,

object body, XmlObjectSerializer serializer);

public static Message CreateMessage(MessageVersion version,

FaultCode faultCode, string reason, object detail, string action);

public static Message CreateMessage(MessageVersion version,

MessageFault fault, string action);

}

对于这 3 个方法的参数列表,类型为 System.ServiceModel.FaultCode 的 faultCode 参数表

示错误代码,System.ServiceModel.Channels.MessageFault 类型的 fault 参数则是对错误消息主

第 6 章 消息(Message)

WCF 全面解析(上册)

244

体内容的封装,而字符串参数 reason 用于指定错误原因。我们照例编写相应的代码来实现针

对错误消息的创建。

FaultCode code = FaultCode.CreateSenderFaultCode("calcuError",

"http://www.artech.com");

FaultReasonText reasonText1 = new FaultReasonText(

"Divided by zero!", "en-US");

FaultReasonText reasonText2 = new FaultReasonText("试图除以零!", "zh-CN");

FaultReason reason = new FaultReason(

6.2 消息

WCF 全面解析(上册)

245

new FaultReasonText[] { reasonText1, reasonText2 });

MessageFault fault = MessageFault.CreateFault(code,reason);

string action = "http://www.artech.com/divideFault";

using (Message message = Message.CreateMessage(MessageVersion.Default,

fault, action))

{

WriteMessage(message, "message.xml");

}

上面这段程序执行之后,会生成具有如下 XML 结构的错误消息。由于在下册的第 1 章

“异常处理(Exception Handling)”中会对 FaultCode、FaultReasonText、FaultReason 和

MessageFault 进行详细的介绍,在这里就不对这段代码作过多的解释和说明了。(S606)

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://www.w3.org/2003/05/soap-envelope">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/divideFault

</a:Action>

</s:Header>

<s:Body>

<s:Fault>

<s:Code>

<s:Value>s:Sender</s:Value>

<s:Subcode>

<s:Value xmlns:a="http://www.artech.com">a:calcuError</s:Value>

</s:Subcode>

</s:Code>

<s:Reason>

<s:Text xml:lang="en-US">Divided by zero!</s:Text>

<s:Text xml:lang="zh-CN">试图除以零!</s:Text>

</s:Reason>

</s:Fault>

</s:Body>

</s:Envelope>

6.2.3 消息的基本操作

上面我们对创建消息的多种方式进行了详细的讲述,接下来讨论针对一个具体的消息可

以采用哪些具体的操作,这些操作具有怎样的限制以及给消息本身带来怎样的影响。具体来

说,消息的基本操作包括如下 4 种。

读消息:读取整个消息的内容或有选择地读取报头或主体部分内容。

写消息:将整个消息的内容或主体部分内容写入文件或流。

拷贝消息:通过消息拷贝生成另一个具有相同内容的新消息。

关闭消息:关闭消息,回收一些非托管资源。

上述这些消息的基本操作都和消息状态密切相关,消息状态决定了可以采取的操作,而

消息操作伴随着消息状态的改变。

第 6 章 消息(Message)

WCF 全面解析(上册)

246

消息状态机

图 6-1 展示的是基于消息的状态机,从中我们可以得出下面的一些关于 Message 对象状

态转换的规则:

消息的读、写和拷贝操作只能作用在状态为 Created 的消息上。

消息的读、写和拷贝将消息状态从 Created 转换成 Read、Written 和 Copied。

所有状态的消息都可以直接关闭,关闭后消息状态转换为 Closed。

图 6-1 Message 对象状态机

消息状态通过具有如下定义的 System.ServiceModel.Channels.MessageState 枚举表示,5

个枚举值分别对应着消息状态机中的 5 种状态。消息的当前状态可以通过 Message 的 State

属性得到。如下面的代码所示,Message 具有一个 Close 方法用于关闭当前消息并释放相应

的资源。Message 同时实现了接口 IDisposable,而实现的 Dispose 方法最终会调用 Close 方

法。当我们在操作消息的时候,对于不再使用的消息调用其 Dispose 或者 Close 方法进行资

源回收,或者像之前演示的一样,采用 using 模式创建消息对象。

public enum MessageState

{

Created,

Read,

Written,

Copied,

Closed

}

public abstract class Message : IDisposable

{

public void Close();

void IDisposable.Dispose()

{

this.Close();

}

public MessageState State { get; }

}

6.2 消息

WCF 全面解析(上册)

247

消息的读取

读取消息主体部分的内容是最为常见的操作。如果主体部分的内容对应一个可以序列化

的对象,则可以通过 GetBody<T>方法读取消息主体内容并反序列化生成相应的对象。在默

认的情况下采用 DataContractSerializer 进行反序列化,也可以显式指定用于反序列化的序列

化器。而通过另一个方法 GetReaderAtBodyContents 会得到一个 XmlDictionaryReader 对象,

可以通过这个对象进一步提取消息主体部分的内容。

public abstract class Message : IDisposable

{

//其他成员

public T GetBody<T>();

public T GetBody<T>(XmlObjectSerializer serializer);

public XmlDictionaryReader GetReaderAtBodyContents();

}

我们通过如下一段程序来演示对消息的读取。先基于创建的 Order 对象创建一个消息,

再调用 GetBody<Order>读取消息主体内容并反序列化成 Order 对象,最后将通过读取消息创

建的 Order 对象的相关属性打印出来。此外,在执行消息读取操作前后,还输出了消息的当

前状态。

Order order = new Order

{

ID = Guid.NewGuid(),

Date = DateTime.Today,

Customer = "张三",

ShipAddress = "江苏省 苏州市 星湖街 328号"

};

string action = "http://www.artech.com/crm/CreateCustomer";

Message message = Message.CreateMessage(MessageVersion.Default,

action, order);

Console.WriteLine("消息当前状态为:{0}", message.State);

order = message.GetBody<Order>();

Console.WriteLine("从消息中读取到订单信息");

Console.WriteLine("\t{0,-2}: {1}", "单号", order.ID);

Console.WriteLine("\t{0,-2}: {1}", "日期",

order.Date.ToString("yyyy-MM-dd"));

Console.WriteLine("\t{0,-2}: {1}", "客户", order.Customer);

Console.WriteLine("\t{0,-2}: {1}", "地址", order.ShipAddress);

Console.WriteLine("消息当前状态为:{0}", message.State);

上面的这段程序执行之后,在控制台上会具有如下的输出。输出结果反映了通过读取消

息主体内容生成的对象和用于创建消息的对象具有相同的数据。对于一个刚刚通过调用静态

方法 CreateMessage 创建的消息来说,它的状态为 Created;而当 GetBody<T>方法执行之后,

状态转换成 Read。(S607)

消息当前状态为:Created

从消息中读取到订单信息

第 6 章 消息(Message)

WCF 全面解析(上册)

248

单号: 5be107e4-4405-4bce-adc0-6181dde849be

日期: 2011-11-24

客户: 张三

地址: 江苏省 苏州市 星湖街 328号

消息当前状态为:Read

在介绍消息状态机的时候,我们强调消息的 3 种基本操作(读、写和拷贝)都只能作用

于状态为 Created 的消息。换言之,一个消息只能被操作一次,因为相应的操作作用于状态

为 Created 的消息都将导致其状态的改变。同样是针对上面这个消息读取的例子,我们对其

作如下的修改,让两次读取操作作用于同一个消息。

...

order = message.GetBody<Order>();

order = message.GetBody<Order>();

...

当第二次消息读取操作执行的时候就会抛出如图 6-2 所示的 InvalidOperationException

异常,提示“此消息不支持该操作,因为它已被读取”。

图 6-2 重复读取消息导致的异常

消息的写入

在 Message 类中定义了一系列的 WriterXxx 方法用于实现消息的写操作。通过这些方法,

我们可以将整个消息或消息主体部分的内容写入 XmlWriter 或 XmlDictioanryWriter 中,并最

终写入文件或流。

public abstract class Message : IDisposable

{

//其他成员

public void WriteBody(XmlDictionaryWriter writer);

public void WriteBody(XmlWriter writer);

public void WriteBodyContents(XmlDictionaryWriter writer);

public void WriteMessage(XmlDictionaryWriter writer);

public void WriteMessage(XmlWriter writer);

public void WriteStartBody(XmlDictionaryWriter writer);

public void WriteStartBody(XmlWriter writer);

public void WriteStartEnvelope(XmlDictionaryWriter writer);

}

我们在前面演示消息创建时定义的辅助方法 WriteMessage 就是通过调用 WriteMessage

方法将消息内容写入一个指定的 XML 文件中的。同消息的读取一样,写操作只能作用于状

态为 Created 的消息。成功执行了消息写入操作后,状态转换为 Written。

6.2 消息

WCF 全面解析(上册)

249

消息的拷贝

读/写操作只能作用于状态为 Created 的消息,而读/写本身会改变消息的状态,这就导

致一个消息只能被使用一次。这个问题可以通过消息的拷贝来解决,它允许我们在对消息

进行读/写操作之前对原消息作一个拷贝,可以通过这个拷贝重建消息以满足多次读/写的

需要。

如下面的代码所示,Message 具有一个 CreateBufferedCopy 方法用于创建自己的拷贝,

该 拷 贝 以 一 个 类 型 为 System.ServiceModel.Channels.MessageBuffer 的 对 象 返 回 。

CreateBufferedCopy 接受一个整型的 maxBufferSize 参数表示拷贝允许的最大大小,如果超过

该值,则会抛出 ArgumentOutOfRangeException 异常。

public abstract class Message : IDisposable

{

//其他成员

public MessageBuffer CreateBufferedCopy(int maxBufferSize);

}

如果拥有一个通过调用 Message 对象的 CreateBufferedCopy 方法得到的 MessageBuffer

对象,就可以在任何需要的时候通过调用如下所示的 CreateMessage 方法重建一个新的

Message 对象。重建的消息状态为 Created,可以直接被读 /写。如下面的代码所示,

MessageBuffer 和 Message 一样实现了 IDisposable 接口,并定义了 Close 方法。Dispose 和

Close 用于回收 MessageBuffer 引用的资源。如果在确认 MessageBuffer 对象不再需要的时候,

请不要忘记调用它们。

public abstract class MessageBuffer : IXPathNavigable, IDisposable

{

//其他成员

public abstract Message CreateMessage();

public abstract void Close();

void IDisposable.Dispose()

{

this.Close();

}

}

对于前面演示的消息重复读取的例子,我们可以通过如下的方式防止因无效状态而导致

出现 InvalidOperationException 异常。(S608)

...

using (MessageBuffer messageBuffer =

message.CreateBufferedCopy(int.MaxValue))

{

using (Message newMessage = messageBuffer.CreateMessage())

{

order = newMessage.GetBody<Order>();

...

}

第 6 章 消息(Message)

WCF 全面解析(上册)

250

using (Message newMessage = messageBuffer.CreateMessage())

{

order = newMessage.GetBody<Order>();

...

}

}

6.3 消息报头与消息属性

SOAP 消息由一个必需的表示主体的<Body>元素和若干个表示报头的<Header>元素构

成。Message 通过如下所示的只读属性 Headers 表示消息报头集合,而该属性返回一个类型

为 System.ServiceModel.Channels.MessageHeaders 的集合对象。

public abstract class Message : IDisposable

{

//其他成员

public abstract MessageHeaders Headers { get; }

}

public sealed class MessageHeaders : IEnumerable<MessageHeaderInfo>,

IEnumerable

{

//省略成员

}

如上面的代码片段所示,MessageHeaders 实际上是一组 MessageHeaderInfo 对象的集合,

我们就先从 MessageHeaderInfo 类型说起。

6.3.1 MessageHeaderInfo

用于封装消息报头基本信息的 System.ServiceModel.Channels.MessageHeaderInfo 类型是

一个抽象类,它具有如下所示的属性定义。表示名称和命名空间的 Name 与 Namespace 属性

共同标识一个消息报头,而 IsReferenceParameter 则表示消息报头是否是某个终结点引用的

参数。关于终结点引用在前面讲解 WS-Addressing 的时候有过相应的介绍。

public abstract class MessageHeaderInfo

{

public abstract string Name { get; }

public abstract string Namespace { get; }

public abstract string Actor { get; }

public abstract bool MustUnderstand { get; }

public abstract bool Relay { get; }

public abstract bool IsReferenceParameter { get; }

}

至于 Actor、MustUnderstand 和 Relay 三个只读属性在 SOAP 1.1 和 SOAP 1.2 中均有详

细的介绍,要明白它们各自代表的含义,还得从 SOAP 消息的处理模型说起。

6.3 消息报头与消息属性

WCF 全面解析(上册)

251

在整个消息的路由处理模型中,用于处理消息的节点(在 SOAP 协议中被称为 SOAP

Node)主要扮演着三种角色,即最初发送者(Initial Sender)、最终接收者(Ultimate Receiver)

和消息中介(Intermediary)。对于发送的 SOAP 消息来说,其主体部分一般是针对最终接收

者的;而作为消息报头则可能是发送给最终接收者,也可以是发送给某个消息中介。

消息报头通过 Actor(Actor 是基于 SOAP 1.1 的,在 SOAP 1.2 中已经换成了 Role)属

性表示该报头针对的接收消息节点的角色。消息报头的Actor通过URI的形式表示,SOAP 1.2

定义了如下三种角色:

http://www.w3.org/2003/05/soap-envelope/role/next

http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver

http://www.w3.org/2003/05/soap-envelope/role/none

第一个代表“下一个”节点,可以是消息中介也可以是最终接收者;第二个专指消息的

最终接收者;而最后一个则表示相应的消息报头并不是针对具体的某个节点,这样的报头不

需要被处理,消息中介直接将它们转发给下一个节点。

消息报头的 Actor 指定了处理报头的节点类型,但是有些报头必须被处理节点理解并有

效处理,有些则是可以被忽略的,这通过 MustUnderstand 属性来体现。MustUnderstand 属性

被设置为 True 的消息报头被称为强制(Mandatory)消息报头。如果消息节点与强制消息报

头的 Actor 属性相匹配,那么它必须能够理解整个消息报头携带的内容并对其进行相应的处

理,否则应该回复一个错误消息。

另一个布尔类型的属性 Relay 表示消息报头是否是一个中继(Relayable)报头。中继报

头主要是针对消息中介而言的,如果消息中介与中继报头的 Actor 属性相匹配,并且其

MustUnderstand 属性为 False,那么它不需要真正处理该报头而只需要将其转发。

6.3.2 MessageHeader

我们针对消息报头的操作主要是针对System.ServiceModel.Channels.MessageHeader类型

进行的。如下面的代码片段所示,MessageHeader 是一个继承自 MessageHeaderInfo 的抽象类,

它通过重写 MessageHeaderInfo 的四个属性为它们定义了默认值。其中 Actor 属性的默认值

为空字符串,而 MustUnderstand、Relay 和 IsReferenceParameter 三个属性在默认的情况下均

为 False。

public abstract class MessageHeader : MessageHeaderInfo

{

//其他成员

public override string Actor { get; }

public override bool MustUnderstand { get; }

public override bool Relay { get; }

public override bool IsReferenceParameter { get; }

}

第 6 章 消息(Message)

WCF 全面解析(上册)

252

由于 MessageHeader 是一个抽象类,不能直接通过 new 操作符创建 MessageHeader 对象,

所以和 Message 一样定义了如下一系列静态的 CreateHeader 方法来进行 MessageHeader 的创

建。对于这些 CreateHeader 方法重载,表示名称、命名空间和报头值的三个参数(name、ns

和 value)是必需的,我们可以调用相应的重载指定定义在 MessageHeaderInfo 中的相干属性。

public abstract class MessageHeader : MessageHeaderInfo

{

//其他成员

public static MessageHeader CreateHeader(string name, string ns,

object value);

public static MessageHeader CreateHeader(string name, string ns,

object value, bool mustUnderstand);

public static MessageHeader CreateHeader(string name, string ns,

object value, XmlObjectSerializer serializer);

//其他 CreateHeader重载

}

CreateHeader 方法中指定的 value 参数表示一个可序列化对象,它被序列化后生成的

XML 作为消息报头的值。在默认的情况下序列化工作通过 DataContractSerializer 来完成,我

们可以根据需要指定相应的序列化器。

MessageHeader 还具有如下一些额外的方法成员,其中 IsMessageVersionSupported 方法

用 于 判 断 当 前 的 消 息 报 头 是 否 支 持 指 定 的 消 息 属 性 ; 而 WriteHeaderContents/

WriteHeaderContents/WriteStartHeader 方法则借助于 XmlDictionaryWriter 实现了针对整个消

息报头或者报头值的写操作。

public abstract class MessageHeader : MessageHeaderInfo

{

//其他成员

public virtual bool IsMessageVersionSupported(

MessageVersion messageVersion);

public void WriteHeader(XmlDictionaryWriter writer,

MessageVersion messageVersion);

public void WriteHeader(XmlWriter writer, MessageVersion messageVersion);

public void WriteHeaderContents(XmlDictionaryWriter writer,

MessageVersion messageVersion);

public void WriteStartHeader(XmlDictionaryWriter writer,

MessageVersion messageVersion);

}

6.3.3 MessageHeader<T>

为了方便我们创建消息报头,WCF 还定义了一个具有如下定义的 MessageHeader<T>类

型,泛型参数是作为消息报头值对象的类型。可读/写属性 Content 表示作为消息报头值的对

象,它也可以在构造函数中指定。

public class MessageHeader<T>

{

public MessageHeader();

6.3 消息报头与消息属性

WCF 全面解析(上册)

253

public MessageHeader(T content);

public MessageHeader(T content, bool mustUnderstand, string actor,

bool relay);

public MessageHeader GetUntypedHeader(string name, string ns);

public T Content { get; set; }

public string Actor { get; set; }

public bool MustUnderstand { get; set; }

public bool Relay { get; set; }

}

虽然 MessageHeader<T>具有 MessageHeader 从基类 MessageHeaderInfo 继承的 Actor、

MustUnderstand 和 Relay 三个属性,但是它们并不是父子关系,所以不能直接将

MessageHeader<T> 对象添加到消息报头列表中。 GetUntypedHeader 方法实现了从

MessageHeader<T>到 MessageHeader 之间的转换,而在调用该方法的时候需要指定报头名称

和命名空间。

6.3.4 MessageHeaders

最后我们来看看通过 Message 的 Headers 返回的表示报头列表的 MessageHeaders 类型。

如下面的代码所示,MessageHeaders 在实现 IEnumerable<MessageHeaderInfo>的基础上还定

义了一些额外的属性。

public sealed class MessageHeaders : IEnumerable<MessageHeaderInfo>,

IEnumerable

{

//其他成员

public string Action { get; set; }

public Uri To { get; set; }

public EndpointAddress From { get; set; }

public EndpointAddress ReplyTo { get; set; }

public EndpointAddress FaultTo { get; set; }

public UniqueId MessageId { get; set; }

public UniqueId RelatesTo { get; set; }

public MessageVersion MessageVersion { get; }

public UnderstoodHeaders UnderstoodHeaders { get; }

}

MessageHeaders 具有如上定义的一组可读/写的属性,它们正好和 WS-Addressing 中定

义的 7 个消息寻址属性相匹配。也就是说,针对这 7 个 WS-Addressing 的寻址报头,我们无

须手工创建 MessageHeader 或者 MessageHeaderInfo 对象,只需要通过属性设置即可实现。

而只读属性 MessageVersion 和 UnderstoodHeaders 分别返回消息版本和所有 MustUnderstand

属性为 True 的消息报头集合。

我们通过如下的代码来演示如何为消息添加报头。通过调用 Message 的静态方法

CreateMessage 创 建 了 版 本 为 Soap11WSAddressingAugust2004 的 消 息 , 然 后 通 过

MessageHeaders相应的属性设置了WS-Addressing的6个基于消息寻址属性的报头(<Action>

报头已经在构建消息的时候指定)。

第 6 章 消息(Message)

WCF 全面解析(上册)

254

最后创建了 Foo、Bar 和Baz 三个自定义报头,其中 Foo 和Bar 是针对最终接收者的(SOAP 1.1

规定如果没有对 Actor 属性进行显式设置,则消息报头默认就是针对最终接收者的),而 Baz

是针对所有消息节点的中继报头。除了报头 Bar 的 MustUnderstand 属性被设置为 True 之外,

Foo 和 Baz 的该属性都设置为 False。

string action = "http://www.artech.com/crm/AddCustomer";

using (Message message =

Message.CreateMessage(MessageVersion.Soap11WSAddressingAugust2004,

action))

{

string ns = "http://www.artech.com/crm";

EndpointAddress address =

new EndpointAddress("http://www.artech.com/crm/client");

message.Headers.To =

new Uri("http://www.artech.com/crm/customerservice");

message.Headers.From = address;

message.Headers.ReplyTo = address;

message.Headers.FaultTo = address;

message.Headers.MessageId = new UniqueId(Guid.NewGuid());

message.Headers.RelatesTo = new UniqueId(Guid.NewGuid());

MessageHeader<string> foo = new MessageHeader<string>("ABC");

MessageHeader<string> bar = new MessageHeader<string>("abc", true,

"", false);

MessageHeader<string> baz = new MessageHeader<string>("123", false,

"http://schemas.xmlsoap.org/soap/actor/next", true);

message.Headers.Add(foo.GetUntypedHeader("Foo",ns));

message.Headers.Add(bar.GetUntypedHeader("Bar", ns));

message.Headers.Add(baz.GetUntypedHeader("Baz", ns));

WriteMessage(message, "message.xml");

}

上面的程序执行之后,会生成具有如下 XML 结构的 SOAP 消息。从命名空间可以看出,

这是一个针对 SOAP 1.1 和 WS-Addressing 2004 的消息。(S609)

<s:Envelope xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"

xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/crm/AddCustomer

</a:Action>

<a:To s:mustUnderstand="1">

http://www.artech.com/crm/customerservice

</a:To>

<a:From>

<a:Address>http://www.artech.com/crm/client</a:Address>

</a:From>

<a:ReplyTo>

<a:Address>http://www.artech.com/crm/client</a:Address>

</a:ReplyTo>

<a:FaultTo>

<a:Address>http://www.artech.com/crm/client</a:Address>

</a:FaultTo>

<a:MessageID>

6.3 消息报头与消息属性

WCF 全面解析(上册)

255

urn:uuid:750eefd6-d9ba-479a-971a-a22dcbbb8e5b

</a:MessageID>

<a:RelatesTo>

urn:uuid:5656d95d-3446-4bc0-a666-b03c9a9086ae

</a:RelatesTo>

<Foo xmlns="http://www.artech.com/crm">ABC</Foo>

<Bar s:mustUnderstand="1" xmlns="http://www.artech.com/crm">abc</Bar>

<Baz s:actor="http://schemas.xmlsoap.org/soap/actor/next"

xmlns="http://www.artech.com/crm">123</Baz>

</s:Header>

<s:Body />

</s:Envelope>

如果我们希望看看同样的消息针对 SOAP 1.2 和 WS-Addressing 1.0 具有怎样的 XML 结

构,则可以按照如下的方式将消息版本设置为 Soap12WSAddressing10。除此之外,由于表

示消息节点角色的 http://schemas.xmlsoap.org/soap/actor/next 是基于 SOAP 1.1 的,因此需要

替换成基于 SOAP 1.2 的 URI。

...

using (Message message =

Message.CreateMessage(MessageVersion.Soap12WSAddressing10, action))

{

...

string ultimateReceiver =

"http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver";

string next = "http://www.w3.org/2003/05/soap-envelope/role/next";

MessageHeader<string> foo = new MessageHeader<string>("ABC", false,

ultimateReceiver,false);

MessageHeader<string> bar = new MessageHeader<string>("abc", true,

ultimateReceiver, false);

MessageHeader<string> baz = new MessageHeader<string>("123", false, next ,

true);

...

}

执行上面这段程序后,会生成具有如下 XML 结构的基于 SOAP 1.2 和 WS-Addressing 1.0

的消息。除了命名空间改变之外,细心的读者可能注意到了 SOAP 1.1 中报头的 actor 属性在

SOAP 1.2 中已经变成了 role。(S610)

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://www.w3.org/2003/05/soap-envelope">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/crm/AddCustomer

</a:Action>

<a:To s:mustUnderstand="1">

http://www.artech.com/crm/customerservice

</a:To>

<a:From>

<a:Address>http://www.artech.com/crm/client</a:Address>

</a:From>

<a:ReplyTo>

<a:Address>http://www.artech.com/crm/client</a:Address>

</a:ReplyTo>

<a:FaultTo>

<a:Address>http://www.artech.com/crm/client</a:Address>

</a:FaultTo>

第 6 章 消息(Message)

WCF 全面解析(上册)

256

<a:MessageID>

urn:uuid:5ca052b1-b7c6-4ed1-9ab5-6bb4a7262d41

</a:MessageID>

<a:RelatesTo>

urn:uuid:e9e7e8c1-c5f1-41e4-8548-1e8fd6606a55

</a:RelatesTo>

<Foo

s:role="http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver"

xmlns="http://www.artech.com/crm">ABC</Foo>

<Bar

s:role="http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver"

s:mustUnderstand="1" xmlns="http://www.artech.com/crm">abc</Bar>

<Baz s:role="http://www.w3.org/2003/05/soap-envelope/role/next"

s:relay="1" xmlns="http://www.artech.com/crm">123</Baz>

</s:Header>

<s:Body />

</s:Envelope>

6.3.5 消息属性

除了一组消息报头集合之外,消息还具有一个消息属性集合。消息报头会最终序列化作

为 SOAP 消息的<Header>报头,但是消息属性仅仅是附加到 Message 对象上的一组对象,并

不作为 SOAP 消息的一部分。附加到消息上的属性一般仅限于本地使用,比如可以将某些状

态信息附加到消息上供本地信道栈的信道使用。

消息具有的属性集合通过只读属性 Properties 表示。如下面的代码所示,该属性的类型

System.ServiceModel.Channels.MessageProperties 就是一个键/值类型分别为 String 和 Object

的字典。

public abstract class Message : IDisposable

{

//其他成员

public abstract MessageProperties Properties { get; }

}

public sealed class MessageProperties :

IDictionary<string, object>,

ICollection<KeyValuePair<string, object>>,

IEnumerable<KeyValuePair<string, object>>,

IEnumerable,

IDisposable

{

//省略成员

}

除了附加在消息上供本地使用的消息属性之外,WCF 还定义了两个重要的消息属性实

现针对 HTTP 请求消息和回复消息的控制。它们对应的类型分别是 HttpRequestMessageProperty

和 HttpResponseMessageProperty,定义在命名空间 System.ServiceModel 下。如果采用 HTTP

传输方式,我们可以利用这两个消息属性对 HTTP 请求消息和回复消息进行定制,比如添加

查询字符串和 HTTP 报头(比如 Cookie)。

如下面的代码片段所示,HttpRequestMessageProperty 具有如下 4 个属性。其中 Headers

6.3 消息报头与消息属性

WCF 全面解析(上册)

257

属性表示HTTP请求的报头集合;Method属性表示HTTP请求方法(Verb),默认值为“POST”;

QueryString 属性表示查询字符串,默认返回一个空字符串;布尔类型的属性

SuppressEntityBody 表示是否忽略 HTTP 请求消息主体,而只发送其报头部分。静态只读属

性 Name 表示 HttpRequestMessageProperty 对象在消息属性集合中的名称,其值为

“httpRequest”。

public sealed class HttpRequestMessageProperty

{

public WebHeaderCollection Headers { get; }

public string Method { get; set; }

public string QueryString { get; set; }

public bool SuppressEntityBody { get; set; }

public static string Name { get; }

}

HttpResponseMessageProperty 具有如下的定义,其中 Headers、SuppressEntityBody 和

Name 与 HttpRequestMessageProperty 的同名属性具有相同的含义。作为消息属性的

HttpResponseMessageProperty 对象在消息属性集合中的名称为“httpResponse”。StatusCode

属性以 System.Net.HttpStatusCode 枚举的形式返回 HTTP 回复的状态码,而 StatusDescription

属性则是对状态的描述。布尔类型的属性 SuppressPreamble 表示是否忽略消息序文(Message

Preamble)。

public sealed class HttpResponseMessageProperty

{

public WebHeaderCollection Headers { get; }

public HttpStatusCode StatusCode { get; set; }

public string StatusDescription { get; set; }

public bool SuppressEntityBody { get; set; }

public bool SuppressPreamble { get; set; }

public static string Name { get; }

}

对于针对消息报头和属性集合的操作,既可以直接作用于具体的消息,也可以通过表示

当前服务操作调用上下文的 OperationContext 来实现。如下面的代码片段所示,

OperationContext 具有 4 个属性,其中 IncomingMessageHeaders 和 IncomingMessageProperties

属性返回入栈消息的报头集合和属性集合,而 OutgoingMessageHeaders 和 OutgoingMessage

Properties 属性则返回出栈消息的报头集合和属性集合。

public sealed class OperationContext : IExtensibleObject<OperationContext>

{

//其他成员

public MessageHeaders IncomingMessageHeaders { get; }

public MessageProperties IncomingMessageProperties { get; }

public MessageHeaders OutgoingMessageHeaders { get; }

public MessageProperties OutgoingMessageProperties { get; }

}

笔者曾经在项目中遇到过这样一个使用消息属性的场景:部属的服务受 IBM WebSeal

第 6 章 消息(Message)

WCF 全面解析(上册)

258

的保护,客户端在第一次进行服务调用之前必须发送一个包含用户名和密码的 HTTPS 请求,

Web Seal 经过认证后会生成一个安全令牌以 Cookie 的形式返回。客户端缓存这个安全令牌,

并将以 Cookie 的形式附加到每次服务调用的 HTTP 请求消息上以通过 Web Seal 的认证。

接下来我们通过实例演示如何通过消息属性的方式实现将安全令牌以 Cookie 的形式发

送出去。为了演示通过消息属性对回复消息的控制,我们会将服务端当前的时间戳同样以

Cookie 的形式返回给客户端。我们还是使用之前频繁使用的具有三层结构(Service.Interface、

Service 和 Client)的计算服务的例子。

如下所示的是服务 CalculatorService 的定义。在 Add 操作方法中,如果当前的回复消息

不具有 HttpResponseMessageProperty 消息属性,则创建一个新的对象并添加到它的属性集合

中,否则直接获取现有的 HttpResponseMessageProperty。最后将基于当前时间戳的 Cookie

添加到 HttpResponseMessageProperty 的报头集合中。

[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]

public class CalculatorService : ICalculator

{

public double Add(double x, double y)

{

HttpResponseMessageProperty messageProperty;

if (OperationContext.Current.OutgoingMessageProperties.ContainsKey(

HttpResponseMessageProperty.Name))

{

messageProperty =

(HttpResponseMessageProperty)OperationContext.Current.OutgoingMessagePrope

rties[HttpResponseMessageProperty.Name];

}

else

{

messageProperty = new HttpResponseMessageProperty();

OperationContext.Current.OutgoingMessageProperties.Add(

HttpResponseMessageProperty.Name, messageProperty);

}

messageProperty.Headers.Add(HttpResponseHeader.SetCookie,

"Timestamp=" + Stopwatch.GetTimestamp());

return x + y;

}

}

客户端通过如下的代码实现针对安全令牌 Cookie 的发送,在这里安全令牌通过一个

GUID 表示,相关的实现与服务端添加 Cookie 完全一致。(S611)

string securityToke = Guid.NewGuid().ToString();

using (ChannelFactory<ICalculator> channelFactory = new

ChannelFactory<ICalculator>("calculatorservice"))

{

ICalculator proxy = channelFactory.CreateChannel();

using (OperationContextScope contextScope =

new OperationContextScope(proxy as IContextChannel))

{

HttpRequestMessageProperty messageProperty;

if (OperationContext.Current.OutgoingMessageProperties.ContainsKey(

HttpRequestMessageProperty.Name))

6.3 消息报头与消息属性

WCF 全面解析(上册)

259

{

messageProperty =

(HttpRequestMessageProperty)OperationContext.Current.OutgoingMessageProper

ties[HttpRequestMessageProperty.Name];

}

else

{

messageProperty = new HttpRequestMessageProperty();

OperationContext.Current.OutgoingMessageProperties.Add(HttpRequestMessageP

roperty.Name, messageProperty);

}

messageProperty.Headers.Add(HttpRequestHeader.Cookie,

"SecurityToken=" + securityToke);

proxy.Add(1, 2);

}

}

为了查看作为针对服务调用的 HTTP 请求和回复是否具有相应的 Cookie,我们可以借助

Fiddler 来拦截和查看请求消息与回复消息。也可以通过在本册的第 2 章“地址(Address)”

中频繁使用的 tcpTrace 来查看。下面是拦截到的请求消息的内容,可以看到指定的安全令牌

以 Cookie 的形式出现在 HTTP 报头中。

POST /calculatorservice HTTP/1.1

Content-Type: text/xml; charset=utf-8

Cookie: SecurityToken=c7cdd6a5-338e-4ded-80de-eebd4d115fb5

Host: 127.0.0.1:3721

Content-Length: 152

Expect: 100-continue

Accept-Encoding: gzip, deflate

Connection: Keep-Alive

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">

<s:Body>

<Add xmlns="http://www.artech.com/">

<x>1</x>

<y>2</y>

</Add>

</s:Body>

</s:Envelope>

而在如下所示的回复消息中,同样具有包含服务端时间戳的 Cookie(Set-Cookie:

Timestamp=79601057852)。

HTTP/1.1 100 Continue

HTTP/1.1 200 OK

Content-Length: 176

Content-Type: text/xml; charset=utf-8

Server: Microsoft-HTTPAPI/2.0

Set-Cookie: Timestamp=79601057852

Date: Fri, 25 Nov 2011 07:58:08 GMT

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">

<s:Body>

<AddResponse xmlns="http://www.artech.com/">

<AddResult>3</AddResult>

</AddResponse>

第 6 章 消息(Message)

WCF 全面解析(上册)

260

</s:Body>

</s:Envelope>

6.3 消息报头与消息属性

WCF 全面解析(上册)

261

6.3.6 实例演示:通过消息报头和消息属性实现上下文信息的

传播(S612)

接下来我们演示一个综合利用消息报头和消息属性的例子。在这个例子中我们利用消息

报头将客户端的一些上下文信息传递到服务端,而服务端从消息报头中提取上下文信息并进

行反序列化,最后将反序列化后的上下文对象作为消息属性附加到请求消息中。

我们将用于存储客户端上下文信息的容器定义成如下一个 ApplicationContext 类型的字

典 。 如 下 面 的 代 码 所 示 , 我 们 通 过 应 用 CollectionDataContractAttribute 特 性 将

ApplicationContext 定义成一个集合数据契约。

ApplicationContext 通过一个应用了 ThreadStaticAttribute 特性的 current 字段表示客户端

当前上下文,所以这个上下文是基于当前线程的。ApplicationContext 的核心是表示当前上下

文的静态属性 Current,读取该属性采用的逻辑为:对于客户端来说直接返回字段 current 的

值;服务端先确定请求消息是否具有表示上下文的消息属性,如果有则直接返回,否则确认

请求消息是否具有表示上下文的消息报头,如果有则将其提取并放到属性集合中返回。

[CollectionDataContract(

Namespace = "http://www.artech.com",

ItemName = "context",

KeyName = "name",

ValueName = "value")]

public class ApplicationContext : Dictionary<string, object>

{

public const string HeaderName = "ApplicationContext";

public const string Namespace = "http://www.artech.com";

public const string PropertyName = "ApplicationContext";

[ThreadStatic]

private static ApplicationContext current;

public static ApplicationContext Current

{

get

{

if (null == OperationContext.Current)

{

return current;

}

MessageProperties incomingProperties =

OperationContext.Current.IncomingMessageProperties;

if (null != incomingProperties &&

incomingProperties.ContainsKey(PropertyName))

{

return (ApplicationContext) incomingProperties[PropertyName];

}

MessageHeaders incomingHeaders =

OperationContext.Current.IncomingMessageHeaders;

if (null != incomingHeaders &&

incomingHeaders.FindHeader(HeaderName, Namespace) > -1)

第 6 章 消息(Message)

WCF 全面解析(上册)

262

{

ApplicationContext context =

incomingHeaders.GetHeader<ApplicationContext>(HeaderName,

Namespace);

incomingHeaders.Add(PropertyName, context);

return context;

}

return current;

}

set { current = value; }

}

}

然后我们定义如下一个实现了接口 IDisposable的ApplicationContextScope类型限制上下

文的作用范围。在 ApplicationContextScope 构造函数中创建一个 ApplicationContext 对象作

为当前上下文,而在 Dispose 方法中将当前上下文还原成之前的状态。

public class ApplicationContextScope: IDisposable

{

private ApplicationContext originalContext = ApplicationContext.Current;

public ApplicationContextScope()

{

ApplicationContext.Current = new ApplicationContext();

}

public void Dispose()

{

ApplicationContext.Current = originalContext;

}

}

客户端最终需要将作为当前上下文的 ApplicationContext 对象序列化后放到请求消息的

某个确定的消息报头。为了方便编程,我们定义如下一个针对 OperationContext 的扩展方法

AttachApplicationContext。

public static class Extensions

{

public static void AttachApplicationContext(

this OperationContext context)

{

if (null == ApplicationContext.Current)

{

return;

}

MessageHeader<ApplicationContext> header =

new MessageHeader<ApplicationContext>(ApplicationContext.Current);

OperationContext.Current.OutgoingMessageHeaders.Add(

header.GetUntypedHeader(ApplicationContext.HeaderName,

ApplicationContext.Namespace));

}

}

我们还是通过之前创建的计算服务的例子来演示上下文的传递。在如下所示的服务类型

CalculatorService 的定义中,在 Add 方法返回运算结果之前会打印出当前所有的上下文信息。

public class CalculatorService : ICalculator

{

public double Add(double x, double y)

6.4 消息契约

WCF 全面解析(上册)

263

{

Console.WriteLine("接收到来自客户端的上下文");

foreach (var item in ApplicationContext.Current)

{

Console.WriteLine("{0,-3}: {1}",item.Key, item.Value);

}

return x + y;

}

}

如下面表示服务调用的代码所示,整个服务调用是在一个 ApplicationContextScope 中进

行的。在执行服务调用之前,为当前上下文添加了 Foo、Bar 和 Baz 三个上下文条目,然后

调用上面定义的扩展方法 AttachApplicationContext 将当前上下文作为一个消息报头放到请

求消息中。

using (ChannelFactory<ICalculator> channelFactory =

new ChannelFactory<ICalculator>("calculatorservice"))

{

ICalculator proxy = channelFactory.CreateChannel();

using(ApplicationContextScope appContextScope =

new ApplicationContextScope())

using (OperationContextScope opContextScope =

new OperationContextScope(proxy as IContextChannel))

{

ApplicationContext.Current.Add("Foo", "ABC");

ApplicationContext.Current.Add("Bar", "abc");

ApplicationContext.Current.Add("Baz", "123");

OperationContext.Current.AttachApplicationContext();

proxy.Add(1, 2);

}

}

当整个实例程序运行后,服务端会打印出当前上下文的所有条目。从下面的输出结果可

以看出,客户端的上下文“自动”地传递到服务端。

接收到来自客户端的上下文

Foo: ABC

Bar: abc

Baz: 123

这样一个上下文传递的应用完全可以通过 WCF 的扩展来实现,可以避免在客户端手工

地将当前上下文附加到请求消息上。具体的实现可以参阅下册的第 9 章“扩展(Extension)”。

6.4 消息契约

数据契约定义了数据对象被序列化后的结构,在服务调用中作为参数或者返回值的数据

契约对象最终作为请求/回复消息主体的一部分。但是有时候我们期望数据对象的部分成员

作为消息报头,部分成员作为消息主体。比如说,我们定义一个采用流的方式进行文件上传

的服务。除了以流的方式传输以二进制表示的文件内容外,还需要传输一个额外的基于文件

属性的信息(比如文件格式、文件大小等)。一般的做法是将传输文件的内容流作为 SOAP

第 6 章 消息(Message)

WCF 全面解析(上册)

264

的主体,将其属性内容作为 SOAP 的报头进行传递。

这样对数据类型的要求可以通过消息契约来实现。此外,消息契约还允许我们针对某个

单独的数据成员实施消息保护(加密/签名)策略。数据契约通过 MessageContractAttribute、

MessageHeaderAttribute 和 MessageBodyMemberAttribute 三个特性来定义,它们均定义在命

名空间 System.ServiceModel 下。

6.4.1 MessageContractAttribute

被定义成消息契约的数据类型需要应用具有如下定义的 MessageContractAttribute 特性,

被定义成消息契约的可以是类,也可以是结构体。

[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class,

AllowMultiple = false)]

public sealed class MessageContractAttribute : Attribute

{

//其他成员

public bool HasProtectionLevel { get; }

public ProtectionLevel ProtectionLevel { get; set; }

public bool IsWrapped { get; set; }

public string WrapperName { get; set; }

public string WrapperNamespace { get; set; }

}

与消息保护级别相关的 ProtectionLevel/HasProtectionLevel 属性对于我们来说已经不再

陌生,用于定义服务契约的 ServiceContractAttribute 和 OperationContractAttribute 特性均具有

这两个属性。但是 DataContractAttribute 没有这两个属性,因为无法实现对代表操作某个参

数或者返回值对应的 XML 单独实施加密或者签名。

IsWrapped 属性表示是否将表示消息主体成员的数据包装到一个根节点下,而

WrapperName 和 WrapperNamespace 属性则表示该根节点的名称和命名空间。在默认的情况

下,IsWrapped 属性设为 True,如果不曾对 WrapperName 和 WrapperNamespace 进行显式设

置,那么消息契约类型的名称将作为根节点的元素名称,而命名空间为 http://tempuri.org/。

现在我们通过实例演示的方式看看针对一个消息契约对象生成的消息结构和应用在类

型上的 MessageContractAttribute 的定义有何关系。我们首先需要解决的是如何将一个消息契

约对象转换成消息,而这可以通过 System.ServiceModel.Description.TypedMessageConverter

对象来实现。

如下面的代码所示,TypedMessageConverter 是一个抽象类,FromMessage 和 ToMessage

实现了消息契约对象和消息之间的相互转换。TypedMessageConverter 定义了一系列 Create

静态方法来创建 TypedMessageConverter 对象。在 Create 方法中必须指定消息契约的类型和

作为<Action>报头的字符串,也可以指定默认的命名空间和消息版本。

public abstract class TypedMessageConverter

{

public static TypedMessageConverter Create(Type messageContract,

6.4 消息契约

WCF 全面解析(上册)

265

string action);

public static TypedMessageConverter Create(Type messageContract,

string action, DataContractFormatAttribute formatterAttribute);

public static TypedMessageConverter Create(Type messageContract,

string action, XmlSerializerFormatAttribute formatterAttribute);

public static TypedMessageConverter Create(Type messageContract,

string action, string defaultNamespace);

public static TypedMessageConverter Create(Type messageContract,

string action, string defaultNamespace,

DataContractFormatAttribute formatterAttribute);

public static TypedMessageConverter Create(Type messageContract,

string action, string defaultNamespace,

XmlSerializerFormatAttribute formatterAttribute);

public abstract object FromMessage(Message message);

public abstract Message ToMessage(object typedMessage);

public abstract Message ToMessage(object typedMessage,

MessageVersion version);

}

通过消息契约对象生成的消息具有怎样的结构还取决于所采用的消息格式化器,它最终

决定了所采用的序列化器类型。通过本册的第 5 章“序列化(Serialization)”的介绍,我们

知道可以通过将 DataContractFormatAttribute 和 XmlSerializerFormatAttribute 特性应用在契约

接口/类中的操作方法上来控制最终所采用的消息格式化器类型,所以部分 Create 方法重载

接受这两种特性类型的参数。在默认的情况下采用的依然是 DataContractSerializer 序列化器。

我们先创建如下一个辅助方法 GenerateMessage<T>,泛型参数表示消息契约类型。该方

法将指定的消息契约对象转换成消息,并将其写入到一个 XML 文件中以便及时查看生成的

消息结构。

public static void GenerateMessage<T>(T typedMessage, string action,

string ns, string fileName)

{

TypedMessageConverter converter = TypedMessageConverter.Create(

typeof(T), action, ns);

using (Message message = converter.ToMessage(typedMessage))

{

using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))

{

message.WriteMessage(writer);

}

}

Process.Start(fileName);

}

然后我们将如下一个表示员工信息的 Employee 类型定义成消息契约。其中表示员工 ID

号的 Id 属性被定义成消息报头成员,而其他属性则被定义成消息主体成员。

[MessageContract]

public class Employee

{

[MessageHeader]

public string Id { get; set; }

[MessageBodyMember]

第 6 章 消息(Message)

WCF 全面解析(上册)

266

public string Name { get; set; }

[MessageBodyMember]

public string Gender { get; set; }

[MessageBodyMember]

public string Department { get; set; }

}

现在我们通过如下的代码将创建的 Employee 对象通过 GenerateMessage<Employee>方

法生成一个消息。在调用该方法的时候,指定了作为<Action>报头的字符串和默认的命名空

间。

Employee employee = new Employee

{

Id = Guid.NewGuid().ToString(),

Name = "张三",

Gender = "男",

Department = "销售部"

};

string action = "http://www.artech.com/hr/AddEmployee";

string ns = "http://www.artech.com/hr";

GenerateMessage<Employee>(employee, action, ns, "employee.xml");

执行上面这段代码后,将会生成具有如下 XML 结构的消息。从中我们可以清楚地看到,

该消息确实包含一个对应着 Employee 的 Id 属性的<Id>报头,而 Employee 对象的契约三个

属性被封装到<Employee>元素中并作为消息主体的子元素。生成的消息还透露出另一个信

息,那就是在没有指定消息版本的情况下,采用 SOAP 1.2+WS-Addressing 1.0。

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://www.w3.org/2003/05/soap-envelope">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/hr/AddEmployee

</a:Action>

<h:Id xmlns:h="http://www.artech.com/hr">

90f66f76-9d43-4bb0-ada7-ab348638324c

</h:Id>

</s:Header>

<s:Body>

<Employee xmlns="http://www.artech.com/hr">

<Department>销售部</Department>

<Gender>男</Gender>

<Name>张三</Name>

</Employee>

</s:Body>

</s:Envelope>

如果我们将应用在 Employee 类型上的 MessageContractAttribute 特性的 IsWrapped 属性

设置为 False,则会生成如下一个 SOAP 消息。在这个消息中,Employee 的三个消息主体成

员的值直接作为了消息主体的直接子元素。(S613)

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"

xmlns:s="http://www.w3.org/2003/05/soap-envelope">

<s:Header>

<a:Action s:mustUnderstand="1">

http://www.artech.com/hr/AddEmployee</a:Action>

6.4 消息契约

WCF 全面解析(上册)

267

<h:Id xmlns:h="http://www.artech.com/hr">

16d304c9-8d2b-4a6e-9835-9df175dddf51

</h:Id>

</s:Header>

<s:Body>

<Department xmlns="http://www.artech.com/hr">销售部</Department>

<Gender xmlns="http://www.artech.com/hr">男</Gender>

<Name xmlns="http://www.artech.com/hr">张三</Name>

</s:Body>

</s:Envelope>

6.4.2 MessageHeaderAttribute

MessageHeaderAttribute 和 MessageBodyMemberAttribute 分别用于定义消息报头成员和

消息主体成员,它们是具有如下定义的 System.ServiceModel.MessageContractMemberAttribute

的子类。Name 和 Namespace 属性分别表示消息契约成员的名称和命名空间,而

HasProtectionLevel 和 ProtectionLevel 则是我们熟悉的与消息保护级别相关的两个属性。前面

所说的可以专门针对消息契约的某个单独的成员实施消息保护策略就体现在这里。

public abstract class MessageContractMemberAttribute : Attribute

{

public bool HasProtectionLevel { get; }

public ProtectionLevel ProtectionLevel { get; set; }

public string Name { get; set; }

public string Namespace { get; set; }

}

MessageHeaderAttribute 继承自 MessageContractMemberAttribute,它仅仅定义了如下三

个可读/写的属性。对于这些属性,正是对应定义在 MessageHeaderInfo 中的同名属性。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,

AllowMultiple=false, Inherited=false)]

public class MessageHeaderAttribute : MessageContractMemberAttribute

{

public string Actor { get; set; }

public bool MustUnderstand { get; set; }

public bool Relay { get; set; }

}

6.4.3 MessageBodyMemberAttribute

MessageBodyMemberAttribute 应用于属性或字段成员,应用了该特性的属性或字段的内

容将出现在 SOAP 的主体部分。MessageBodyMemberAttribute 的定义显得尤为简单,仅仅具

有一个 Order 对象,用于控制成员在 SOAP 消息主体中出现的位置。默认的排序规则是基于

字母排序的。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,

Inherited = false)]

public class MessageBodyMemberAttribute : MessageContractMemberAttribute

{

第 6 章 消息(Message)

WCF 全面解析(上册)

268

public int Order { get; set; }

}

可能细心的读者会问,为什么 MessageHeaderAttribute 中没有这样的 Order 属性呢?原

因很简单,MessageHeaderAttribute 定义的是 SOAP 报头,而 SOAP 消息报头集合中的每个

报头元素与次序无关。而 MessageBodyMemberAttribute 则是定义 SOAP 主体的某个元素,

主体成员之间的次序是契约的一个重要组成部分。

6.4.4 消息契约与操作

消息契约类型可以作为操作的输入参数和返回值,但是如果将消息契约类型作为输入参

数,则意味着它将是操作的唯一参数;消息契约类型也不能作为操作方法的引用参数和输出

参数。对于如下一个契约接口 IEmployeeManager,用于创建员工记录的 CreateEmployee 操

作除了具有一个定义成消息契约的 Employee 类型的参数之外,还具有一个表示添加的员工

是否属于兼职的参数 partTime,这样的契约定义是不合法的。

[ServiceContract(Namespace ="http://www.artech.com/")]

public interface IEmployeeManager

{

[OperationContract]

void CreateEmployee(Employee emplyee, bool partTime);

}

如果我们对实现了该契约接口的服务进行寄宿,则会抛出如图 6-3 所示的

InvalidOperationException 异常,提示“操作 CreateEmployee 具有特性为 MessageContractAttribute

的参数或返回类型。要使用 Message Contract 表示请求消息,操作必须有一个特性为

MessageContractAttribute 的参数。若要使用 Message Contract 表示响应消息,操作的返回值必

须是特性为 MessageContractAttribute 的类型,且不能有 out 或 ref 参数。”

图 6-3 消息契约操作包含额外参数导致的异常

之所以基于消息契约的操作具有这个限制,是因为操作的请求消息(消息契约类型作为

参数)或者回复消息(消息契约类型作为返回值)是通过消息契约来描述的,消息主体只能

包含消息契约对象自身的成员。

在本册的第 4 章“契约(Contract)”中,我们谈到 WCF 通过 OperationDescription 来表

6.4 消息契约

WCF 全面解析(上册)

269

述服务契约的操作。OperationDescription 的 Messages 属性是一个 MessageDescription 对象的

集合,而每个 MessageDescription 是对基于该操作的请求/回复消息的描述。对于参数/返回

值 为 消 息 契 约 的 操 作 来 说 , 描 述 请 求 / 回 复 消息 的 MessageDescription 对 象 的

HasProtectionLevel/ProtectionLevel 对 应 着 MessageContractAttribute 的 同 名 属 性 , 而

MessageType 返回的正是消息契约的类型。

public class OperationDescription

{

public MessageDescriptionCollection Messages { get; }

}

public class MessageDescriptionCollection : Collection<MessageDescription>

{

//省略成员

}

public class MessageDescription

{

//其他成员

public MessageHeaderDescriptionCollection Headers { get; }

public MessageBodyDescription Body { get; }

public bool HasProtectionLevel { get; }

public ProtectionLevel ProtectionLevel { get; set; }

public Type MessageType { get; set; }

}

MessageDescription 的 Headers 和 Body 分别表示消息报头集合和主体的描述。对于消息

契 约 操 作 来 说 , MessageDescription 针 对 消 息 报 头 和 主 体 的 描 述 信 息 来 源 于

MessageHeaderAttribute 和 MessageBodyAttribute 的定义。为了演示 MessageDescription 与消

息契约定义之间的匹配关系,我们对消息契约 Employee 进行了如下的修改。通过

MessageContractAttribute 将消息保护级别设置为 Sign,并为消息主体成员定义不同的名称。

[MessageContract(IsWrapped = false, ProtectionLevel = ProtectionLevel.Sign)]

public class Employee

{

[MessageHeader(Name = "EmployeeId")]

public string Id { get; set; }

[MessageBodyMember(Name = "EmployeeName", Order = 1)]

public string Name { get; set; }

[MessageBodyMember(Name = "Sex", Order = 2)]

public string Gender { get; set; }

[MessageBodyMember(Name = "Dept", Order = 1)]

public string Department { get; set; }

}

我们定义了如下一个契约接口 IEmployeeManager,它的操作 CreateEmployee 将消息契

约 Employee 作为唯一的输入参数。

[ServiceContract(Namespace="http://www.artech.com/")]

public interface IEmployeeManager

{

[OperationContract]

void CreateEmployee(Employee employee);

}

第 6 章 消息(Message)

WCF 全面解析(上册)

270

现在我们编写如下程序打印出代表 CreateEmployee 操作请求消息的 MessageDescription

对象的相关信息,比如消息保护等级、消息类型,以及所有消息报头和主体成员的名称。

ContractDescription contract =

ContractDescription.GetContract(typeof(IEmployeeManager));

OperationDescription operation = contract.Operations[0];

MessageDescription message = operation.Messages[0];

Console.WriteLine("{0, -15}: {1}", "ProtectionLevel",

message.ProtectionLevel);

Console.WriteLine("{0, -15}: {1}", "MessageType", message.MessageType.Name);

Console.WriteLine("{0,-15}: {1}", "Headers", message.Headers[0].Name);

for (int i = 1; i < message.Headers.Count; i++ )

{

Console.WriteLine("{0,-15}: {1}", "", message.Headers[i].Name);

}

Console.WriteLine("{0,-15}: {1}", "Body Parts", message.Body.Parts[0].Name);

for (int i = 1; i < message.Body.Parts.Count; i++)

{

Console.WriteLine("{0,-15}: {1}", "", message.Body.Parts[i].Name);

}

从如下所示的输出结果可以看出,MessageDescription 的相关属性和我们在定义消息契

约时针对 MessageContractAttribute、MessageHeaderAttribute 和 MessageBodyMemberAttribute

三个特性的相关设置是完全匹配的。而 MessageDescription 的 MessageType 类型正是消息契

约的类型。(S614)

ProtectionLevel : Sign

MessageType : Employee

Headers : EmployeeId

Body Parts : Dept

: EmployeeName

: Sex

6.5 XML 编码

消息在通过传输层发送之前必须经过编码(Encode),通过传输层接收到消息先得经过

解码(Decode)才能被继续处理。消息(主要指 SOAP 消息)就是一段 XML,所以消息的

编码/解码本质上就是针对 XML 的编码/解码。在上面介绍 BodyWriter,以及 Message 和

MessageHeader 的写操作时都提到了一个 XmlDictionaryWriter 对象,实际上 XML 的编码工

作就是由它来实现的。既然有 XmlDictionaryWriter,自然就有 XmlDictionaryReader,后者实

现XML解码。顾名思义,XmlDictionaryWriter和XmlDictionaryReader就是基于XmlDictionary

的 Writer 和 Reader,我们先来介绍一下 XmlDictionary。

6.5.1 XmlDictionary

XmlDictionary 是一个字典,它作为编码和解码双方共享的一份“词汇表”。这样的说法

可能有点抽象,我们不妨做一个类比。比如我说“WCF 是.NET 平台下基于 SOA 的消息通

6.5 XML 编码

WCF 全面解析(上册)

271

信框架”。对于各位读者来说,这句话很好理解。如果我向对计算机一窍不通的人说这句话,

对方是无论如何也不能理解的。

读者和我之所以能够通过这样的语言进行交流,是因为我们之间具有相似的知识背景,

在我们之间共享着相同的词汇表,对每个单词的含义具有一致的理解。而别人不能理解,是

因为我和他之间的信息不对称,如果要让他理解,我必须用他所能理解的方式进行交流。在

这种情形之下,我可能要花很多文字对这句话的一些术语进行详细的解释,比如什么是.NET

平台,什么是 SOA,什么又是通信框架。

所以交流的前提是双方具有相同的“词汇表”,双方就某个主题共享的相同“词汇”越

多,交流越容易,说的话就越简洁。数据的编码也像我们日常的沟通和交流一样,编码的一

方是“说”的一方,解码的一方是“听”的一方。说的一方按照它所掌握的“词汇表”对信

息进行编码,对方只有具有相同的“词汇表”才能正常地解码。如果这个“词汇表”越详尽,

编码后的内容容量就越小。而 XmlDictionary 就是这样的一个词汇表。

XmlDictionary 对应的类型是 System.Xml.XmlDictionary 。如下面的代码所示,

XmlDictionary 实现了 System.Xml.IXmlDictionary 接口,XmlDictionary 本质上就是一个

System.Xml.XmlDictionaryString 的集合。而 XmlDictionaryString 则可以看成是键-值对,其

键和值的类型分别为整型和字符串。

public class XmlDictionary : IXmlDictionary

{

//其他成员

public XmlDictionary();

public XmlDictionary(int capacity);

public virtual XmlDictionaryString Add(string value);

public virtual bool TryLookup(int key, out XmlDictionaryString result);

public virtual bool TryLookup(string value,

out XmlDictionaryString result);

public virtual bool TryLookup(XmlDictionaryString value,

out XmlDictionaryString result);

}

public interface IXmlDictionary

{

//其他成员

bool TryLookup(int key, out XmlDictionaryString result);

bool TryLookup(string value, out XmlDictionaryString result);

bool TryLookup(XmlDictionaryString value,

out XmlDictionaryString result);

}

public class XmlDictionaryString

{

//其他成员

public IXmlDictionary Dictionary { get; }

public static XmlDictionaryString Empty { get; }

public int Key { get; }

public string Value { get; }

}

下面的程序演示了如何为 XmlDictionary 对象添加 XmlDictionaryString 元素。我们只需

第 6 章 消息(Message)

WCF 全面解析(上册)

272

要调用 XmlDictionary 的 Add 方法直接将作为 Value 的字符串添加到字典中就行。由于

XmlDictionary 不曾实现 IEnumerable 或者 IEnumerable<XmlDictionaryString>接口,我们不能

对其进行遍历。为了能够输出被添加的 XmlDictionaryString 对象的 Key,我们将通过 Add

方法的返回值添加到一个列表中,并最终通过遍历列表的形式打印出添加的 Key 和 Value。

List<XmlDictionaryString> dictionaryStringList =

new List<XmlDictionaryString>();

XmlDictionary dictionary = new XmlDictionary();

dictionaryStringList.Add(dictionary.Add("Employee"));

dictionaryStringList.Add(dictionary.Add("Id"));

dictionaryStringList.Add(dictionary.Add("Name"));

dictionaryStringList.Add(dictionary.Add("Gender"));

dictionaryStringList.Add(dictionary.Add("Department"));

Console.WriteLine("{0,-4}{1}", "Key", "Value");

Console.WriteLine(new string('-', 20));

foreach (XmlDictionaryString item in dictionaryStringList)

{

Console.WriteLine("{0,-4}{1}", item.Key, item.Value);

}

上面的代码执行之后,在控制台上会有如下的输出。由此可见,当我们直接指定

XmlDictionaryString 的 Value 调用 XmlDictionary 的 Add 方法的时候,在内部将会通过一个

自增长的整数作为 XmlDictionary 的 Key。(S615)

Key Value

--------------------

0 Employee

1 Id

2 Name

3 Gender

4 Department

试想这样一个问题,我们现在需要对如上所示的表示员工信息的 XML 进行编码。如果

有 了 上 面 创 建 的 XmlDictionary , 由 于 所 有 的 XML 元 素 名 称 都 对 应 于 某 个

XmlDictionaryString 的 Value,所以在进行编码的时候完全可以将它们替换成用整数表示的

XmlDictionaryString 的 Key,解码的时候再还原成 XmlDictionaryString 的 Value。这无疑会使

编码后的内容变得很少,如果需要对编码的内容进行网络传输,则会极大地节省网络带宽。

<Employee xmlns:i="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.artech.com/">

<Department>销售部</Department>

<Gender>男</Gender>

<Id>fab6664b-5ecd-498f-9ff3-d1c1ac991af8</Id>

<Name>张三</Name>

</Employee>

6.5.2 XmlDictionaryWriter

XML 的编码工作最终通过具有如下定义的 System.Xml.XmlDictionaryWriter 来完成。如

6.5 XML 编码

WCF 全面解析(上册)

273

下面的代码片段所示,XmlDictionaryWriter 实际上是 XmlWriter 的子类,它提供了一系列

WriteXxx 方法以实现针对不同类型的 XML 节点的编码。

public abstract class XmlDictionaryWriter : XmlWriter

{

//其他成员

public virtual void WriteArray(string prefix, string localName,

string namespaceUri, bool[] array, int offset, int count);

public void WriteAttributeString(XmlDictionaryString localName,

XmlDictionaryString namespaceUri, string value);

public void WriteElementString(XmlDictionaryString localName,

XmlDictionaryString namespaceUri, string value);

public virtual void WriteNode(XmlDictionaryReader reader, bool defattr);

public virtual void WriteQualifiedName(XmlDictionaryString localName,

XmlDictionaryString namespaceUri);

public void WriteStartAttribute(XmlDictionaryString localName,

XmlDictionaryString namespaceUri);

public void WriteStartElement(XmlDictionaryString localName,

XmlDictionaryString namespaceUri);

public virtual void WriteString(XmlDictionaryString value);

public virtual void WriteValue(Guid value);

public virtual void WriteXmlAttribute(string localName, string value);

public virtual void WriteXmlnsAttribute(string prefix,

string namespaceUri);

}

从上面的代码片段可以看到,XmlDictionaryWriter 是一个抽象类,我们通过调用它的静

态方法 CreateXxx 创建相应的 XmlDictionaryWriter。如下面的代码片段所示,定义在

XmlDictionaryWriter 中用于创建具体 XmlDictionaryWriter 的方法共有 3 组,它们分别返回 3

种不同类型的 XmlDictionaryWriter。而这 3 种具体的 XmlDictionaryWriter 类型体现了 WCF

支持的 3 种典型的 XML 编码方式。

public abstract class XmlDictionaryWriter : XmlWriter

{

//XmlUTF8TextWriter

public static XmlDictionaryWriter CreateTextWriter(Stream stream);

public static XmlDictionaryWriter CreateTextWriter(Stream stream,

Encoding encoding);

public static XmlDictionaryWriter CreateTextWriter(Stream stream,

Encoding encoding, bool ownsStream);

//XmlBinaryWriter

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream);

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream,

IXmlDictionary dictionary);

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream,

IXmlDictionary dictionary, XmlBinaryWriterSession session);

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream,

IXmlDictionary dictionary, XmlBinaryWriterSession session,

bool ownsStream);

//XmlMtomWriter

public static XmlDictionaryWriter CreateMtomWriter(Stream stream,

Encoding encoding, int maxSizeInBytes, string startInfo);

public static XmlDictionaryWriter CreateMtomWriter(Stream stream,

Encoding encoding, int maxSizeInBytes, string startInfo,

第 6 章 消息(Message)

WCF 全面解析(上册)

274

string boundary, string startUri, bool writeMessageHeaders,

bool ownsStream);

}

CreateTextWriter 方法返回一个 XmlUTF8TextWriter 对象,它采用纯文本的编码方式。

虽然命名为 XmlUTF8TextWriter,但只是在默认的情况下采用 UTF 编码方式而已,我们可以

指定不同的 Encoding 类型。CreateBinaryWriter 方法返回实现二进制 XML 编码的

XmlBinaryWriter 对象,而 CreateMtomWriter 方法返回的 XmlMtomWriter 基于 MTOM

( Message Transmission Optimization Mechanism )编 码方式。 XmlUTF8TextWriter 、

XmlBinaryWriter 和 XmlMtomWriter 均是内部类型。

XmlUTF8TextWriter

由于基于纯文本的编码与平台无关,所以它能为不同的厂商所支持,这和 SOA 跨平台

的诉求一致。BasicHttpBinding、WSHttpBinding/WS2007HttpBinding 和 WSDualHttpBinding

在默认的情况下均采用这种编码方式。

WCF 最终通过 XmlUTF8TextWriter 实现基于文本的消息编码。XmlUTF8TextWriter 通过

调用定义在 XmlDictionaryWriter 中的静态方法 CreateTextWriter 来创建。如下面的代码所示,

XmlDictionaryWriter 定义了 3 个 CreateTextWriter 方法重载。

public abstract class XmlDictionaryWriter : XmlWriter

{

//其他成员

public static XmlDictionaryWriter CreateTextWriter(Stream stream);

public static XmlDictionaryWriter CreateTextWriter(Stream stream,

Encoding encoding);

public static XmlDictionaryWriter CreateTextWriter(Stream stream,

Encoding encoding, bool ownsStream);

}

CreateTextWriter 方法的参数 stream 便是经过编码的内容需要写入的流。encoding 表明

采用的字符编码方式,目前只支持 UTF8 和 Unicode 两种编码方式,默认的编码方式为 UTF8。

如果指定其他的编码方式,则会抛出一个 XmlException 异常。

布尔类型参数 ownsStream 表明 XmlUTF8TextWriter 对象是否拥有对应的 Stream 对象。

如果将此参数显式地设置为 True,则意味着创建的 XmlUTF8TextWriter 是 Stream 的拥有者,

关闭 XmlUTF8TextWriter 对象将自动将 Stream 对象关闭。该参数的默认值为 True。

接下来我们通过一个简单的实例来演示如何以文本的方式对一个对象被序列化后的

XML 进行编码。我们先创建如下一个静态的辅助方法 WriteObject<T>,泛型参数 T 为被序

列化对象的类型;参数 graph 和 createWriter 分别表示被序列化的对象和用于创建

XmlDictionaryWriter 对象的委托。

static void WriteObject<T>(T graph,

Func<Stream, XmlDictionaryWriter> createWriter)

{

using (MemoryStream stream = new MemoryStream())

6.5 XML 编码

WCF 全面解析(上册)

275

{

using (XmlDictionaryWriter writer = createWriter(stream))

{

DataContractSerializer serializer =

new DataContractSerializer(typeof(T));

serializer.WriteObject(writer,graph);

}

long count = stream.Position;

byte[] bytes = stream.ToArray();

StreamReader reader = new StreamReader(stream);

stream.Position = 0;

string content = reader.ReadToEnd();

Console.WriteLine("字节数为:{0}\n", count);

Console.WriteLine("编码后的二进制表示为:{0}\n",

BitConverter.ToString(bytes));

Console.WriteLine("编码后的文本表示为:{0}", content);

}

}

在如上的方法中,我们会基于 MemoryStream 创建 XmlDictionaryWriter 对象,然后通过

DataContractSerializer 将指定的对象序列化后写入 XmlDictionaryWriter 对象中。最后打印出

经过编码后的字节数和二进制与文本表示。

我们创建了如下一个表示信息的数据类型 Employee。如下面的代码片段所示,这是一

个数据契约,表示员工 ID、姓名、性别和部门的属性被定义成数据契约成员。

[DataContract(Namespace ="http://www.artech.com/")]

public class Employee

{

[DataMember(Order = 1)]

public string Id { get; set; }

[DataMember(Order = 2)]

public string Name { get; set; }

[DataMember(Order = 3)]

public string Gender { get; set; }

[DataMember(Order = 4)]

public string Department { get; set; }

}

在一个控制台程序中我们创建了如下一个 Employee 对象,然后调用上面定义的

WriteObject<T>方法对该对象进行序列化和编码。而用于创建 XmlDictionaryWriter 的

createWriter 委托创建的是 XmlUTF8TextWriter。

Employee employee = new Employee

{

Id = "001",

Name = "张三",

Gender = "男",

Department = "销售部"

};

WriteObject<Employee>(employee, stream =>

XmlDictionaryWriter.CreateTextWriter(stream, Encoding.UTF8, false));

上面的程序执行之后,在控制台上将会有如下的输出,这和我们采用 UTF8 对一段文本

第 6 章 消息(Message)

WCF 全面解析(上册)

276

内容进行编码没有什么两样,我们只需要记住编码后的字节数为 189 就行。(S616)

字节数为:189

编码后的二进制表示为:3C-45-6D-70-6C-6F-79-65-65-20-78-6D-6C-6E-73-3D-22-68-74-74-70-3A-2F-2F-77

-77-77-2E-61-72-74-65-63-68-2E-63-6F-6D-2F-22-20-78-6D-6C-6E-73-3A-69-3D-2

2-68-74-74-70-3A-2F-2F-77-77-77-2E-77-33-2E-6F-72-67-2F-32-30-30-31-2F-58-

4D-4C-53-63-68-65-6D-61-2D-69-6E-73-74-61-6E-63-65-22-3E-3C-49-64-3E-30-30

-31-3C-2F-49-64-3E-3C-4E-61-6D-65-3E-E5-BC-A0-E4-B8-89-3C-2F-4E-61-6D-65-3

E-3C-47-65-6E-64-65-72-3E-E7-94-B7-3C-2F-47-65-6E-64-65-72-3E-3C-44-65-70-

61-72-74-6D-65-6E-74-3E-E9-94-80-E5-94-AE-E9-83-A8-3C-2F-44-65-70-61-72-74

-6D-65-6E-74-3E-3C-2F-45-6D-70-6C-6F-79-65-65-3E

编码后的文本表示为:<Employee

xmlns="http://www.artech.com/"xmlns:i="http://www.w3.org/2001/XMLSche

ma-instance"><Id>001</Id><Name>张三</Name><Gender>男</Gender><Department>销

售部</Department></Employee>

XmlBinaryWriter

基于二进制编码的XmlBinaryWriter通过XmlDictionaryWriter 的CreateBinaryWriter方法

来创建。如下面的代码所示,CreateBinaryWriter 和用于创建 XmlUTF8TextWriter 的方法

CreateTextWriter 具有不同的参数列表。它多了一个 IXmlDictionary 的 dictionary 参数和

System.Xml.XmlBinaryWriterSession 类型的 session 参数。

public abstract class XmlDictionaryWriter : XmlWriter

{

//其他成员

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream);

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream,

IXmlDictionary dictionary);

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream,

IXmlDictionary dictionary, XmlBinaryWriterSession session);

public static XmlDictionaryWriter CreateBinaryWriter(Stream stream,

IXmlDictionary dictionary, XmlBinaryWriterSession session,

bool ownsStream);

}

现在我们直接通过如下的代码以 XmlBinaryWriter 的方式对 Employee 对象序列化后的

XML 进行编码。

Employee employee = new Employee

{

Id = "001",

Name = "张三",

Gender = "男",

Department = "销售部"

};

WriteObject<Employee>(employee, stream =>

XmlDictionaryWriter.CreateBinaryWriter(stream,null,null,false));

从如下所示的程序执行后的输出可以看出,通过二进制进行编码比通过文本编码得到的

6.5 XML 编码

WCF 全面解析(上册)

277

字节数要少。关于二进制编码对内容的“压缩优势”在这个例子中体现得还不是很明显,如

果被编码的 XML 很大,我们会看到二进制编码明显的优势。(S617)

字节数为:134

编码后的二进制表示为:40-08-45-6D-70-6C-6F-79-65-65-08-16-68-74-74-70-3A-2F-2F-77-77-77-2E-61-72

-74-65-63-68-2E-63-6F-6D-2F-09-01-69-29-68-74-74-70-3A-2F-2F-77-77-77-2E-7

7-33-2E-6F-72-67-2F-32-30-30-31-2F-58-4D-4C-53-63-68-65-6D-61-2D-69-6E-73-

74-61-6E-63-65-40-02-49-64-99-03-30-30-31-40-04-4E-61-6D-65-B7-04-20-5F-09

-4E-40-06-47-65-6E-64-65-72-99-03-E7-94-B7-40-0A-44-65-70-61-72-74-6D-65-6

E-74-B7-06-00-95-2E-55-E8-90-01

编码后的文本表示为:<<不可读的编码内容>>

在介绍 XmlDictionary 的时候,我们说通过它在编码/解码双方共享一个“词汇表”可以

极大地“节省”编码后的字节,现在就来演示一下 XmlDictionary 的威力。在如下的代码中,

我们创建了一个 XmlDictionary 对象,并将 Employee 对象被系列化后的所有 XML 元素名添

加到该 XmlDictionary 中。在调用 XmlDictionaryWriter 的 CreateBinaryWriter 方法时将该

XmlDictionary 作为其参数,同时作为参数的还有另一个创建的 XmlBinaryWriterSession 对

象。

Employee employee = new Employee

{

Id = "001",

Name = "张三",

Gender = "男",

Department = "销售部"

};

var session = new XmlBinaryWriterSession();

XmlDictionary dictionary = new XmlDictionary();

dictionary.Add("Employee");

dictionary.Add("Id");

dictionary.Add("Name");

dictionary.Add("Gender");

dictionary.Add("Department");

WriteObject<Employee>(employee, stream =>

XmlDictionaryWriter.CreateBinaryWriter(stream, dictionary,

session, false));

执行上面这段代码,我们会在控制台上看到如下的输出结果。针对相同 XML 的编码,

在没有使用 XmlDictionary 的时候编码后的字节数为 134,使用了 XmlDictionary 之后变成了

41。编码后的内容之所以得到如此大的压缩,原因在于所有的 XML 元素名在编码之前都应

被替换成以整数表示的 XmlDictionaryString 的 Key。(S618)

字节数为:41

编码后的二进制表示为:42-01-0A-03-0B-01-69-05-42-07-99-03-30-30-31-42-09-B7-04-20-5F-09-4E-42-0B

第 6 章 消息(Message)

WCF 全面解析(上册)

278

-99-03-E7-94-B7-42-0D-B7-06-00-95-2E-55-E8-90-01

编码后的文本表示为:<<不可读的编码内容>>

6.5 XML 编码

WCF 全面解析(上册)

279

XmlMtomWriter

在很多分布式应用场景中,我们会通过 SOAP 消息传输一些大规模的二进制数据(比如

上传文件、图片、MP3 甚至是视频)。对于这些大块的二进制内容,如果采用二进制的编码

方式,固然能够获得最好的编码压缩率,保证数据的快速传输,但是却不能获得跨平台的能

力。如果采用纯文本的编码方式,基于 Base64 的编码方式会使编码后的内容显得非常冗余,

而且这些冗余的数据会直接置于 SOAP 消息的主体中,使得 SOAP 消息十分庞大,从而影响

SOAP 消息的正常传输。

为了解决这样的问题,MTOM(Message Transmission Optimization Mechanism,消息传

输优化机制)应运而生。MTOM 兼具文本编码的跨平台能力(因为 MTOM 是 W3C 制定的

一个规范),又具有 Binary 编码高压缩率的优势。要想深入了解 MTOM,读者可以访问 W3C

的官方网站下载相关的文档。在这里仅仅是对该机制的实现作一个简单的介绍。

对于 MTOM 编码方式,二进制的内容仍然按照 Base64 的方式进行编码,不过最终会对

包含<base64binary>的元素进行传输优化。可以视这种优化为基于 XOP(XML-Binary

Optimizated Packaging)的标准的高压缩率的编码方法。

SOAP 消息在传输的时候,通过一种称为 MIME Multipart/Related XOP 数据包的形式发

送。XOP 数据包是经过对<base64binary>元素进行优化编码后的内容,Multipart/Related XOP

就是多个关联的 XOP 数据包。每个 XOP 数据包和 SOAP 消息本身是分离的,也就是说,

XOP 数据包并不内嵌于 SOAP 消息中,而是作为其附件(Attachment)单独传送的。SOAP

消息保留一份 XOP 数据包的引用。

基于 MTOM 的编码通过 XmlMtomWriter 来完成,而它通过 XmlDictionaryWriter 的

CreateMtomWriter 静态方法创建。如下面的代码所示,XmlDictionaryWriter 具有两个重载的

CreateMtomWriter 方法。

public abstract class XmlDictionaryWriter : XmlWriter

{

//其他成员

public static XmlDictionaryWriter CreateMtomWriter(Stream stream,

Encoding encoding, int maxSizeInBytes, string startInfo);

public static XmlDictionaryWriter CreateMtomWriter(Stream stream,

Encoding encoding, int maxSizeInBytes, string startInfo,

string boundary, string startUri, bool writeMessageHeaders,

bool ownsStream);

}

当我们通过 XmlMtomWriter 对 XML 进行编码的时候,最终生成的是一个具有报头和主

体的 MIME Multipart/Related XOP 数据包,XML 的内容经过编码被放到主体部分。字符串

类型的参数 startInfo 表示该 XML 对应 Content-Type 的 type 属性,对于 SOAP 自然就是

“application/soap+xml”。字符串类型的 boundary 和 startUri 参数分别表示分隔符和

Content-ID;而布尔类型的 writeMessageHeaders 参数表示是否写入 MIME Multipart/Related

第 6 章 消息(Message)

WCF 全面解析(上册)

280

XOP 数据包的报头内容。

我们对之前的编码测试程序作了如下的修改,调用 XmlDictionaryWriter 的

CreateMtomWriter 方法创建的 XmlMtomWriter 对象对 Employee 对象序列化的 XML 进行编

码。在调用 CreateMtomWriter 方法的时候,对 startInfo、boundary 和 startUri 等参数作了显

式设置。

Employee employee = new Employee

{

Id = "001",

Name = "张三",

Gender = "男",

Department = "销售部"

};

string startInfo = "application/soap+xml";

string boundary = "http://www.artech.com/boundary";

string startUri = "http://www.artech.com/contentid";

WriteObject<Employee>(employee, stream =>

XmlDictionaryWriter.CreateMtomWriter(stream, Encoding.UTF8,

int.MaxValue, startInfo, boundary, startUri,true,false));

程序的执行结果与上面截然不同。由于 MTOM 只有在针对大规模的二进制数据的传输

时才能显示出优化的能力,对于文本内容反而因为多了很多必需的结构化描述信息,使得最

终编码后的数据包都基于纯文本编码方式而冗余。(S619)

字节数为:619

编码后的二进制表示为:4D-49-4D-45-2D-56-65-72-73-69-6F-6E-3A-20-31-2E-30-0D-0A-43-6F-6E-74-65-6E

-74-2D-54-79-70-65-3A-20-6D-75-6C-74-69-70-61-72-74-2F-72-65-6C-61-74-65-6

4-3B-74-79-70-65-3D-22-61-70-70-6C-69-63-61-74-69-6F-6E-2F-78-6F-70-2B-78-

6D-6C-22-3B-62-6F-75-6E-64-61-72-79-3D-22-68-74-74-70-3A-2F-2F-77-77-77-2E

-61-72-74-65-63-68-2E-63-6F-6D-2F-62-6F-75-6E-64-61-72-79-22-3B-73-74-61-7

2-74-3D-22-3C-68-74-74-70-3A-2F-2F-77-77-77-2E-61-72-74-65-63-68-2E-63-6F-

6D-2F-63-6F-6E-74-65-6E-74-69-64-3E-22-3B-73-74-61-72-74-2D-69-6E-66-6F-3D

-22-41-70-70-6C-69-63-61-74-69-6F-6E-2F-73-6F-61-70-2B-78-6D-6C-22-0D-0A-0

D-0A-2D-2D-68-74-74-70-3A-2F-2F-77-77-77-2E-61-72-74-65-63-68-2E-63-6F-6D-

2F-62-6F-75-6E-64-61-72-79-0D-0A-43-6F-6E-74-65-6E-74-2D-49-44-3A-20-3C-68

-74-74-70-3A-2F-2F-77-77-77-2E-61-72-74-65-63-68-2E-63-6F-6D-2F-63-6F-6E-7

4-65-6E-74-69-64-3E-0D-0A-43-6F-6E-74-65-6E-74-2D-54-72-61-6E-73-66-65-72-

2D-45-6E-63-6F-64-69-6E-67-3A-20-38-62-69-74-0D-0A-43-6F-6E-74-65-6E-74-2D

-54-79-70-65-3A-20-61-70-70-6C-69-63-61-74-69-6F-6E-2F-78-6F-70-2B-78-6D-6

C-3B-63-68-61-72-73-65-74-3D-75-74-66-2D-38-3B-74-79-70-65-3D-22-41-70-70-

6C-69-63-61-74-69-6F-6E-2F-73-6F-61-70-2B-78-6D-6C-22-0D-0A-0D-0A-3C-45-6D

-70-6C-6F-79-65-65-20-78-6D-6C-6E-73-3D-22-68-74-74-70-3A-2F-2F-77-77-77-2

E-61-72-74-65-63-68-2E-63-6F-6D-2F-22-20-78-6D-6C-6E-73-3A-69-3D-22-68-74-

74-70-3A-2F-2F-77-77-77-2E-77-33-2E-6F-72-67-2F-32-30-30-31-2F-58-4D-4C-53

-63-68-65-6D-61-2D-69-6E-73-74-61-6E-63-65-22-3E-3C-49-64-3E-30-30-31-3C

-2F-49-64-3E-3C-4E-61-6D-65-3E-E5-BC-A0-E4-B8-89-3C-2F-4E-61-6D-65-3E-3C-4

7-65-6E-64-65-72-3E-E7-94-B7-3C-2F-47-65-6E-64-65-72-3E-3C-44-65-70-61-72-

74-6D-65-6E-74-3E-E9-94-80-E5-94-AE-E9-83-A8-3C-2F-44-65-70-61-72-74-6D-65

-6E-74-3E-3C-2F-45-6D-70-6C-6F-79-65-65-3E-0D-0A-2D-2D-68-74-74-70-3A-2F-2

F-77-77-77-2E-61-72-74-65-63-68-2E-63-6F-6D-2F-62-6F-75-6E-64-61-72-79-2D-

2D-0D-0A

6.5 XML 编码

WCF 全面解析(上册)

281

编码后的文本表示为:MIME-Version: 1.0

Content-Type:

multipart/related;type="application/xop+xml";boundary="http://www.artech.c

om/boundary"

;start="<http://www.artech.com/contentid>";start-info="Application/soap+xm

l"

--http://www.artech.com/boundary

Content-ID: <http://www.artech.com/contentid>

Content-Transfer-Encoding: 8bit

Content-Type:

application/xop+xml;charset=utf-8;type="Application/soap+xml"

<Employee xmlns="http://www.artech.com/"

xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Id>001

</Id><Name>张三</Name><Gender>男</Gender><Department>销售部</Department></Employee>

--http://www.artech.com/boundary--

6.5.3 XmlDictionaryReader

有 XmlDictionaryWriter 就必然有 XmlDictionaryReader,XmlDictionaryWriter 对 XML 进

行编码,XmlDictionaryReader 则对编码后的内容进行解码,还原编码前的 XML。三种不同

的解码方式(文本、二进制和 MTOM)对应着 XmlUTF8TextReader、XmlBinaryReader 和

XmlMtomReader 三种具体的 XmlDictionaryReader,它们通过定义在 XmlDictionaryReader 中

的静态方法 CreateTextReader、CreateBinaryReader 和 CreateMtomReader 进行创建。

XmlDictionaryReader 具有一个 XmlDictionaryWriter 不具有的特性,那就是可以约束读

取的 XML 的复杂度。这些约束可以抵御某种类型的拒绝服务(DoS)攻击,这些攻击试图

利用消息复杂性来占用计算资源。XML 复杂度约束通过 XmlDictionaryReader 的类型为

System.Xml.XmlDictionaryReaderQuotas 的 Quotas 来控制,如下面的代码片段所示,在

XmlDictionaryReader 的 CreateTextReader、CreateBinaryReader CreateMtomReader 方法中均包

含相应的参数用于对该属性进行初始化。

public abstract class XmlDictionaryReader : XmlReader

{

//其他成员

public static XmlDictionaryReader CreateTextReader(Stream stream,

XmlDictionaryReaderQuotas quotas);

public static XmlDictionaryReader CreateTextReader(byte[] buffer,

XmlDictionaryReaderQuotas quotas);

//其他 CreateTextReader方法

public static XmlDictionaryReader CreateBinaryReader(Stream stream,

XmlDictionaryReaderQuotas quotas);

public static XmlDictionaryReader CreateBinaryReader(byte[] buffer,

XmlDictionaryReaderQuotas quotas);

//其他 CreateBinaryReader方法

public static XmlDictionaryReader CreateMtomReader(Stream stream,

Encoding encoding, XmlDictionaryReaderQuotas quotas);

第 6 章 消息(Message)

WCF 全面解析(上册)

282

public static XmlDictionaryReader CreateMtomReader(Stream stream,

Encoding[] encodings, XmlDictionaryReaderQuotas quotas);

//其他 CreateMtomReader方法

public virtual XmlDictionaryReaderQuotas Quotas { get; }

}

通 过 XmlDictionaryReaderQuotas 类 型 表 示 的 XML 复 杂 度 约 束 指 标 分 别 用

XmlDictionaryReaderQuotas 的 5 个属性表示。其中 MaxArrayLength 和 MaxBytesPerRead 表

示允许的最大数组长度和每次读取的最大字节数,默认值分别为 16384 和 4096;MaxDepth

和 MaxStringContentLength 分别表示最大的嵌套层次和读取的字符串最大长度,默认值分别

为 32 和 8192;MaxNameTableCharCount 表示 XmlDictionaryReader 使用的名称表允许的最

大字符数,默认值为 16384。

public sealed class XmlDictionaryReaderQuotas

{

[DefaultValue(0x4000)]

public int MaxArrayLength { get; set; }

[DefaultValue(0x1000)]

public int MaxBytesPerRead { get; set; }

[DefaultValue(0x20)]

public int MaxDepth { get; set; }

[DefaultValue(0x4000)]

public int MaxNameTableCharCount { get; set; }

[DefaultValue(0x2000)]

public int MaxStringContentLength { get; set; }

}

在 XmlDictionaryReader 基于某个类型的编码方式读取相应的 XML 过程中,如果读取的

XML 复杂度超过了定义在 XmlDictionaryReaderQuotas 中的某个指标,那么读取工作将会中

止并抛出异常。由于这主要是应付拒绝服务(DoS)网络攻击,对 XML 复杂度约束只存在

于 XmlDictionaryReader 中,而 XmlDictionaryWriter 并无此约束。

6.6 消息编码

接下来我们介绍 WCF 框架本身是如何进行消息编码与解码的。消息编码与解码是在信

道层实现的,而具体完成编码/解码工作的是一个名为消息编码器(MessageEncoder)的组件。

6.6.1 消息编码器

消息编码器通过具有如下定义的 System.ServiceModel.Channels.MessageEncoder类表示。

如下面的代码所示,MessageEncoder是一个抽象类,定义了两组ReadMessage和WriteMessage

6.6 消息编码

WCF 全面解析(上册)

283

方法,分别实现针对消息的解码和编码。

public abstract class MessageEncoder

{

//其他成员

public Message ReadMessage(ArraySegment<byte> buffer,

BufferManager bufferManager);

public Message ReadMessage(Stream stream, int maxSizeOfHeaders);

public abstract Message ReadMessage(ArraySegment<byte> buffer,

BufferManager bufferManager, string contentType);

public abstract Message ReadMessage(Stream stream, int maxSizeOfHeaders,

string contentType);

public abstract void WriteMessage(Message message, Stream stream);

public ArraySegment<byte> WriteMessage(Message message,

int maxMessageSize, BufferManager bufferManager);

public abstract ArraySegment<byte> WriteMessage(Message message,

int maxMessageSize, BufferManager bufferManager, int messageOffset);

}

除了这两组核心方法之外,MessageEncoder 还具有如下一些属性和方法。方法

GetProperty<T>我们已经很熟悉了,用于获取指定类型的信道栈的属性。属性 ContentType、

MediaType 和 MessageVersion 分别返回消息的 MIME 内容类型、媒体内容类型和版本。而方

法 IsContentTypeSupported 用于判断当前消息编码器是否支持指定的 MIME 内容。

ContentType 一般在 MediaType 基础上加上对字符集的设置,比如“application/soap+xml;

charset= ' utf8 '”。

public abstract class MessageEncoder

{

//其他成员

public virtual T GetProperty<T>() where T: class;

public virtual bool IsContentTypeSupported(string contentType);

public abstract string ContentType { get; }

public abstract string MediaType { get; }

public abstract MessageVersion MessageVersion { get; }

}

WCF 支持文本、二进制和 MTOM 编码三种编码方式,它们分别对应着

TextMessageEncoder、BinaryMessageEncoder 和 MtomMessageEncoder 三种具体的消息编码

器。这三种具体的消息编码器分别使用 XmlUTF8TextWriter/XmlUTF8TextReader 、

XmlBinaryWriter/XmlBinaryReader 和 XmlMtomWriter/XmlMtomReader 实现具体的消息编码

和解码工作。

6.6.2 消息编码器工厂

代表三种具体的消息编码器类型的 TextMessageEncoder、BinaryMessageEncoder 和

MtomMessageEncoder 都是内部类型,而 MessageEncoder 也不像 Message 和 XmlDictionary

Wrtier/XmlDictionaryReader 一样定义相应的 CreateXxx 方法,那么我们通过怎样的方式来创

第 6 章 消息(Message)

WCF 全面解析(上册)

284

建它们呢?

具体的消息编码器是通过相应的消息编码器工厂来创建的,消息编码器工厂通过具有如

下定义的 System.ServiceModel.Channels.MessageEncoderFactory 类型来表示。如下面的代码

片段所示,MessageEncoderFactory 也是一个抽象类,消息编码器的提供通过属性 Encoder

来实现,而 CreateSessionEncoder 则直接返回该属性的值。MessageVersion 用于获取消息版

本。

public abstract class MessageEncoderFactory

{

public virtual MessageEncoder CreateSessionEncoder();

public abstract MessageEncoder Encoder { get; }

public abstract MessageVersion MessageVersion { get; }

}

为了分别创建 TextMessageEncoder、BinaryMessageEncoder 和 MtomMessageEncoder 这

三种具体的消息编码器,WCF 定义了对应的 MessageEncoderFactory,即 TextMessageEncoder

Factory、BinaryMessageEncoderFactory 和 MtomMessageEncoderFactory。这三个具体的消息

编码器工厂也是内部类型。

6.6.3 消息编码绑定元素

消息的编码/解码工作最终是在信道层实现的,绑定对象缔造了整个信道栈,而消息编

码/解码最终体现了构成绑定的一个绑定元素。我们称这个与消息编码/解码相关的绑定元素

为消息编码绑定元素,它通过 System.ServiceModel.Channels.MessageEncodingBindingElement

类型表示。如下面的代码片段所示,抽象类 MessageEncodingBindingElement 具有抽象方法

CreateMessageEncoderFactory,用于返回创建消息编码器的 MessageEncoderFactory。

基于不同消息编码/解码方式的实现,WCF 定义了 TextMessageEncodingBindingElement、

BinaryMessageEncodingBindingElement 和 MtomMessageEncodingBindingElement 三个具体的

消息编码绑定元素。如下面的代码片段所示,它们重写了 CreateMessageEncoderFactory 抽象

方法,分别返回上面所介绍的 TextMessageEncoderFactory、BinaryMessageEncoderFactory 和

MtomMessageEncoderFactory 三种具体的消息编码器工厂。

public sealed class TextMessageEncodingBindingElement :

MessageEncodingBindingElement, ...

{

//其他成员

public override MessageEncoderFactory CreateMessageEncoderFactory();

}

public sealed class BinaryMessageEncodingBindingElement :

MessageEncodingBindingElement, ...

{

//其他成员

public override MessageEncoderFactory CreateMessageEncoderFactory();

6.6 消息编码

WCF 全面解析(上册)

285

}

public sealed class MtomMessageEncodingBindingElement :

MessageEncodingBindingElement, ...

{

//其他成员

public override MessageEncoderFactory CreateMessageEncoderFactory();

}

6.6.4 消息编码与绑定

由于消息编码器工厂是由相应的消息编码绑定元素创建的,绑定具有怎样的消息编码绑

定元素最终决定了信道层采用何种消息编码/解码方式。

系统绑定

对于我们常用的系统绑定来说,NetTcpBinding、NetNamedPipeBinding 和 NetMsmqBinding

在内部采用 BinaryMessageEncodingBindingElement 绑定元素,这决定了它们总是采用二进制

消息编码方式。原因在于这三种绑定主要面向基于局域网的通信,通过二进制编码可获得最

好的性能优势。

而对于面向 Internet 和基于 WS 的 BasicHttpBinding、WSHttpBinding/WS2007HttpBinding

和 WSDualHttpBinding(非面向 Internet)来说,则可采用文本编码和 MTOM 编码方式。如

下面的代码片段所示,BasicHttpBinding 和基于 WS 绑定的基类 WSHttpBindingBase 均具有

一个可读/写的 MessageEncoding 属性。该属性返回一个 System.ServiceModel.WSMessage

Encoding 枚举,两个枚举值 Text 和 MTOM 分别代表文本编码和 MTOM 编码。

public class BasicHttpBinding : Binding,...

{

[DefaultValue(0)]

public WSMessageEncoding MessageEncoding { get; set; }

}

public abstract class WSHttpBindingBase : Binding, ...

{

[DefaultValue(0)]

public WSMessageEncoding MessageEncoding { get; set; }

}

public enum WSMessageEncoding

{

Text,

Mtom

}

从应用在 BasicHttpBinding 和 WSHttpBindingBase 的 MessageEncoding 属性上的

DefaultValueAttribute 特性可以看出,该属性的默认值为 Text,意味着文本编码是默认采用的

消息编码方式。对于 BasicHttpBinding 和几种 WS 绑定,我们可以通过编程和配置的方式控

制其消息编码方式。

第 6 章 消息(Message)

WCF 全面解析(上册)

286

自定义绑定

对于自定义绑定来说,我们可以自由地组合构成绑定的所有绑定元素,也就是说,我们

可以根据需要选择相应的消息编码绑定元素。由于我们推荐采用配置的方式来定义自定义绑

定,所以很有必要了解上述三种消息编码绑定元素具有怎样的配置。要具体了解它们的配置

结构,最直接的方式莫过于查看它们对应的配置元素类型的定义。TextMessageEncoding

BindingElement 对应的配置元素通过具有如下定义的 System.ServiceModel.Configuration.

TextMessageEncodingElement 表示。

public sealed class TextMessageEncodingElement :

BindingElementExtensionElement

{

[ConfigurationProperty("maxReadPoolSize", DefaultValue=0x40)]

public int MaxReadPoolSize { get; set; }

[ConfigurationProperty("maxWritePoolSize", DefaultValue=0x10)]

public int MaxWritePoolSize { get; set; }

[ConfigurationProperty("messageVersion",

DefaultValue="Soap12WSAddressing10")]

public MessageVersion MessageVersion { get; set; }

[ConfigurationProperty("readerQuotas")]

public XmlDictionaryReaderQuotasElement ReaderQuotas { get; }

[ConfigurationProperty("writeEncoding", DefaultValue="utf-8")]

public Encoding WriteEncoding { get; set; }

}

TextMessageEncodingElement 的属性定义不仅体现了基于文本的消息编码绑定元素

TextMessageEncodingBindingElement 具有的 5 个配置属性,还体现了配置属性的类型和默认

值。

MaxReadPoolSize(maxReadPoolSize):表示无须分配新的 XmlDictionaryReader 便可同时

读取的最大消息数,默认值为 64。

MaxWritePoolSize(maxWritePoolSize):表示无须分配新的 XmlDictionaryWriter 便可同时

写入的最大消息数,默认值为 16。

MessageVersion(messageVersion):消息版本,默认值为 Soap12WSAddressing10,即 SOAP

1.2+WS-Addressing 1.0。

ReaderQuotas(readerQuotas):控制消息的复杂度约束。

WriteEncoding(writeEncoding):采用的文本编码类型,默认值为 utf-8,即默认采用 UTF8

编码方式。

TextMessageEncodingElement 的 ReaderQuotas 属性类型为具有如下定义的 System.

ServiceModel.Configuration.XmlDictionaryReaderQuotasElement,它的目的在于设置 XmlDictionary

Reader 用于约束读取的 XML 复杂度的 XmlDictionaryReaderQuotas,所以它具有与

6.6 消息编码

WCF 全面解析(上册)

287

XmlDictionaryReaderQuotas 一致的属性定义。所不同的是,定义在 XmlDictionaryReader

QuotasElement 中的 5 个属性默认值均为 0,如果没有对它们进行显式设置或者将其显式设置

为 0,最后的复杂度约束指标将会采用 XmlDictionaryReaderQuotas 的默认值。

public sealed class XmlDictionaryReaderQuotasElement : ConfigurationElement

{

[ConfigurationProperty("maxArrayLength", DefaultValue=0)]

public int MaxArrayLength { get; set; }

[ConfigurationProperty("maxBytesPerRead", DefaultValue=0)]

public int MaxBytesPerRead { get; set; }

[ConfigurationProperty("maxDepth", DefaultValue=0)]

public int MaxDepth { get; set; }

[ConfigurationProperty("maxNameTableCharCount", DefaultValue=0)]

public int MaxNameTableCharCount { get; set; }

[ConfigurationProperty("maxStringContentLength", DefaultValue=0)]

public int MaxStringContentLength { get; set; }

}

基于二进制消息编码/解码的绑定元素 BinaryMessageEncodingBindingElement 对应的配

置元素类型为 System.ServiceModel.Configuration.BinaryMessageEncodingElement。我们可以

通过配置属性 MaxReadPoolSize、MaxWritePoolSize 设置无须分配新的 XmlDictionaryReader/

XmlDictionaryWriter 就能读取/写入的消息数,通过 ReaderQuotas 设置 XML 复杂度约束。这

些配置属性同样定义在 TextMessageEncodingElement 中,但 BinaryMessageEncodingElement

具有一个额外的配置属性 MaxSessionSize 表示编码缓冲区的大小(以字节为单位),默认值

为 2048。

public sealed class BinaryMessageEncodingElement :

BindingElementExtensionElement

{

[ConfigurationProperty("maxReadPoolSize", DefaultValue=0x40)]

public int MaxReadPoolSize { get; set; }

[ConfigurationProperty("maxWritePoolSize", DefaultValue = 0x10)]

public int MaxWritePoolSize { get; set; }

[ConfigurationProperty("maxSessionSize", DefaultValue=0x800)]

public int MaxSessionSize { get; set; }

[ConfigurationProperty("readerQuotas")]

public XmlDictionaryReaderQuotasElement ReaderQuotas { get; }

}

我们最后来看看基于 MTOM 消息编码的绑定元素 MtomMessageEncodingBinding

Element 所对应的配置元素类型 System.ServiceModel.Configuration.MtomMessageEncoding

Element。如下面的代码片段所示,MtomMessageEncodingElement 除了具有 TextMessage

EncodingElement 的所有配置属性外,还具有一个额外的属性 MaxBufferSize 用于表示最大缓

冲区的大小,默认值为 65536。

第 6 章 消息(Message)

WCF 全面解析(上册)

288

public sealed class MtomMessageEncodingElement :

BindingElementExtensionElement

{

[ConfigurationProperty("maxBufferSize", DefaultValue=0x10000)]

public int MaxBufferSize { get; set; }

[ConfigurationProperty("maxReadPoolSize", DefaultValue=0x40)]

public int MaxReadPoolSize { get; set; }

[ConfigurationProperty("maxWritePoolSize", DefaultValue=0x10)]

public int MaxWritePoolSize { get; set; }

[ConfigurationProperty("messageVersion",

DefaultValue="Soap12WSAddressing10")]

public MessageVersion MessageVersion { get; set; }

[ConfigurationProperty("readerQuotas")]

public XmlDictionaryReaderQuotasElement ReaderQuotas { get; }

[ConfigurationProperty("writeEncoding", DefaultValue="utf-8")]

public Encoding WriteEncoding { get; set; }

}

在如下的配置片段中,我们配置了一个名称为 myBinding 的自定义绑定,该绑定具有

MtomMessageEncodingBindingElement 绑定元素。对该绑定元素的每个属性均进行了显式设

置,不过被设置的都是默认值。

<configuration>

<system.serviceModel>

<bindings>

<customBinding>

<binding name="myBinding">

<mtomMessageEncoding maxBufferSize ="65536"

maxReadPoolSize ="64"

maxWritePoolSize ="16"

messageVersion ="Soap12WSAddressing10"

writeEncoding ="utf-8">

<readerQuotas maxArrayLength ="16384"

maxBytesPerRead ="4096"

maxDepth ="32"

maxStringContentLength ="8192"

maxNameTableCharCount ="16384"/>

</mtomMessageEncoding>

<httpTransport/>

</binding>

</customBinding>

</bindings>

</system.serviceModel>

</configuration>

6.6.5 消息编码的实现

其实到目前为止,我们还没有真正讨论 WCF 进行消息编码的流程。我们只知道消息编

码/解码都是在信道层中通过某个信道实现的,那么具体是哪个信道呢?

6.6 消息编码

WCF 全面解析(上册)

289

在本册的第 3 章“绑定(Binding)”中介绍信道栈时我们说信道栈具有两个必需的信道,

其中一个是用于消息编码/解码的消息编码信道,另一个是用于消息发送/接收的传输信道。

当时我们只是为了方便说明信道栈对消息的处理流程,而且很多 WCF 的书籍和资料在介绍

信道栈的时候也采用这样的说法。但是这种说法却是不对的,实际上并不存在消息编码信道,

用于创建消息编码信道的信道监听器和信道工厂自然也不存在。在整个绑定模型中,与消息

编码相关的只有消息编码绑定元素。

一般来说,绑定元素的主要目的是创建信道监听器和信道工厂,最后使用它们创建相应

的 信 道 对 消 息 作相 应 的处 理 。 但 是 消 息编 码 绑定 元 素 却 是 个 例外 , 在它 的

BuildChannelListener<TChannel>和 BuildChannelFactory<TChannel>中,仅仅将自己添加到当

前的绑定上下文(BindingContext)中。

在创建传输信道的时候,会从绑定上下文中获取消息编码绑定元素,并调用其

CreateMessageEncoderFactory 得到相应的消息编码器工厂。传输信道会利用该消息编码器工

厂创建的消息编码器在发送前和接收后对消息进行编码和解码。也就是说,消息的编码和解

码最终是通过传输信道完成的。如果看了我们接下来演示的这个实例,相信你会对此具有深

刻的认识。

6.6.6 实例演示:通过自定义消息编码器实现消息压缩(S620)

在本册的第 5 章“序列化(Serialization)”中我们通过自定义消息格式化器实现了对消

息的压缩,现在通过自定义消息编码器的方式来实现相同的压缩功能。前面定义的压缩消息

格式化器通过在序列化后和反序列化前对消息进行压缩和解压缩实现了压缩功能,这里我们

定义的压缩消息编码器则在编码前和解码后对消息进行压缩和解压缩达到相同的目的。

之前已经定义了如下一个 MessageCompressor 类型实现具体的消息压缩和解压缩。其中

构造函数的参数 algorithm 和 minMessageSize 分别表示压缩算法和实现压缩的消息主体最小

属性。

public class MessageCompressor

{

public MessageCompressor(CompressionAlgorithm algorithm,

int minMessageSize);

public Message CompressMessage(Message sourceMessage);

public Message DecompressMessage(Message sourceMessage);

}

CompressionMessageEncoder

我们创建了如下一个继承自 MessageEncoder 的类型为 CompressionMessageEncoder 的自

定义消息编码器。两个属性 InnerMessageEncoder 和 MessageCompressor 分别实现对消息的

编码/解码和压缩/解压缩。在实现的抽象只读属性 ContentType、MediaType 和 MessageVersion

第 6 章 消息(Message)

WCF 全面解析(上册)

290

中直接返回 InnerMessageEncoder 的同名属性。此外,我们还按照相同的方式重写了虚方法

GetProperty<T>。

public class CompressionMessageEncoder: MessageEncoder

{

public MessageEncoder InnerMessageEncoder { get; private set; }

public MessageCompressor MessageCompressor { get; private set; }

public CompressionMessageEncoder(MessageEncoder innerMessageEncoder,

MessageCompressor messageCompressor)

{

this.InnerMessageEncoder = innerMessageEncoder;

this.MessageCompressor = messageCompressor;

}

public override string ContentType

{

get { return this.InnerMessageEncoder.ContentType; }

}

public override string MediaType

{

get { return this.InnerMessageEncoder.MediaType; }

}

public override MessageVersion MessageVersion

{

get { return this.InnerMessageEncoder.MessageVersion; }

}

public override T GetProperty<T>()

{

return this.InnerMessageEncoder.GetProperty<T>();

}

public override Message ReadMessage(ArraySegment<byte> buffer,

BufferManager bufferManager, string contentType)

{

Message message = this.InnerMessageEncoder.ReadMessage(buffer,

bufferManager, contentType);

return this.MessageCompressor.DecompressMessage(message);

}

public override Message ReadMessage(Stream stream, int maxSizeOfHeaders,

string contentType)

{

Message message = this.InnerMessageEncoder.ReadMessage(stream,

maxSizeOfHeaders, contentType);

return this.MessageCompressor.DecompressMessage(message);

}

public override ArraySegment<byte> WriteMessage(Message message,

int maxMessageSize, BufferManager bufferManager, int messageOffset)

{

message = this.MessageCompressor.CompressMessage(message);

return this.InnerMessageEncoder.WriteMessage(message,

maxMessageSize, bufferManager, messageOffset);

}

public override void WriteMessage(Message message, Stream stream)

{

message = this.MessageCompressor.CompressMessage(message);

this.InnerMessageEncoder.WriteMessage(message, stream);

}

6.6 消息编码

WCF 全面解析(上册)

291

}

在用于解码的两个 ReadMessage 方法中,先通过 InnerMessageEncoder 实施解码后再通

过 MessageCompressor 实施解压缩。而对于旨在实现编码的 WriteMessage 方法中则正好相反,

即先通过MessageCompressor 实施压缩再通过 InnerMessageEncoder 将压缩后的消息进行编码。

第 6 章 消息(Message)

WCF 全面解析(上册)

292

CompressionMessageEncoderFactory

如下一个自定义消息编码器工厂 CompressionMessageEncoderFactory 用于创建我们自定

义的消息编码器 CompressionMessageEncoder。属性 InnerMessageEncoderFactory 用于创建真

正实现消息编码/解码的消息编码器。

public class CompressionMessageEncoderFactory : MessageEncoderFactory

{

public MessageEncoderFactory InnerMessageEncoderFactory

{ get; private set; }

public MessageCompressor MessageCompressor { get; private set; }

public CompressionMessageEncoderFactory(MessageEncoderFactory

innerMessageEncoderFactory, MessageCompressor messageCompressor)

{

this.InnerMessageEncoderFactory = innerMessageEncoderFactory;

this.MessageCompressor = messageCompressor;

}

public override MessageEncoder Encoder

{

get { return new CompressionMessageEncoder(

this.InnerMessageEncoderFactory.Encoder,

this.MessageCompressor); }

}

public override MessageVersion MessageVersion

{

get { return this.InnerMessageEncoderFactory.MessageVersion; }

}

}

在实现抽象只读属性 Encoder 时,直接通过 InnerMessageEncoderFactory 创建的消息编

码 器 创 建 CompressionMessageEncoder 对 象 。 而 MessageVersion 属 性 也 直 接 返 回

InnerMessageEncoderFactory 的同名属性。

CompressionTextMessageEncodingBindingElement

现在我们来创建相应的消息编码绑定元素,在这里只演示基于文本的编码方式,为此我

们将自定义的绑定元素命名为 CompressionTextMessageEncodingBindingElement,如果能够直

接继承 TextMessageEncodingBindingElement,那么自定义消息编码绑定元素就会很简单。可

惜 TextMessageEncodingBindingElement 是一个封闭(Sealed)的类型,所以我们转而采用如

下的定义方式。

public class CompressionTextMessageEncodingBindingElement :

MessageEncodingBindingElement

{

public TextMessageEncodingBindingElement TextEncodingElement

{ get; private set; }

public MessageCompressor MessageCompressor { get; private set; }

private CompressionTextMessageEncodingBindingElement() { }

public CompressionTextMessageEncodingBindingElement(

TextMessageEncodingBindingElement textEncodingElement,

CompressionAlgorithm algorithm, int minMessageSize)

6.6 消息编码

WCF 全面解析(上册)

293

{

this.TextEncodingElement = textEncodingElement;

this.MessageCompressor = new MessageCompressor(

algorithm, minMessageSize);

}

public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(

BindingContext context)

{

context.BindingParameters.Add(this);

return base.BuildChannelFactory<TChannel>(context);

}

public override IChannelListener<TChannel>

BuildChannelListener<TChannel>(BindingContext context)

{

context.BindingParameters.Add(this);

return base.BuildChannelListener<TChannel>(context);

}

public override MessageEncoderFactory CreateMessageEncoderFactory()

{

return new CompressionMessageEncoderFactory(

this.TextEncodingElement.CreateMessageEncoderFactory(),

this.MessageCompressor);

}

public override MessageVersion MessageVersion

{

get { return this.TextEncodingElement.MessageVersion; }

set { this.TextEncodingElement.MessageVersion = value; }

}

public override BindingElement Clone()

{

TextMessageEncodingBindingElement textEncodingElement =

new TextMessageEncodingBindingElement()

{

MaxReadPoolSize = this.TextEncodingElement.MaxReadPoolSize,

MaxWritePoolSize = this.TextEncodingElement.MaxWritePoolSize,

MessageVersion = this.TextEncodingElement.MessageVersion,

ReaderQuotas = this.TextEncodingElement.ReaderQuotas,

WriteEncoding = this.TextEncodingElement.WriteEncoding

};

return new CompressionTextMessageEncodingBindingElement()

{

TextEncodingElement = textEncodingElement,

MessageCompressor = this.MessageCompressor

};

}

}

属性 TextEncodingElement 的目的在于提供创建针对用于消息编码/解码的消息编码器工

厂。在 CreateMessageEncoderFactory 方法中,我们通过 TextEncodingElement 创建的消息编

码器工厂创建自定义的 CompressionMessageEncoderFactory。而在重写的 BuildChannel

Listener<TChannel>和 BuildChannelFactory<TChannel>方法中,直接将自身作为绑定参数添

加到当前绑定上下文中。

第 6 章 消息(Message)

WCF 全面解析(上册)

294

CompressionTextEncodingElement

为了实现通过配置的方式对自定义的消息编码绑定元素进行设置,我们定义如下一个配

置元素类型 CompressionTextEncodingElement。除了具有两个与压缩相关的配置元素

(Algorithm 和 MinMessageSize)之外,还具有一个 TextMessageEncodingElement 类型的属

性 TextEncoding 对 TextMessageEncodingBindingElement 部分进行设置。

public class CompressionTextEncodingElement : BindingElementExtensionElement

{

[ConfigurationProperty("textEncoding")]

public TextMessageEncodingElement TextEncoding

{

get { return (TextMessageEncodingElement)this["textEncoding"]; }

set { this["textEncoding"] = value; }

}

[ConfigurationProperty("algorithm",

DefaultValue = CompressionAlgorithm.GZip)]

public CompressionAlgorithm Algorithm

{

get { return (CompressionAlgorithm)this["algorithm"]; }

set { this["algorithm"] = value; }

}

[ConfigurationProperty("minMessageSize", DefaultValue="1024")]

public int MinMessageSize

{

get { return (int)this["minMessageSize"]; }

set { this["minMessageSize"] = value; }

}

public override Type BindingElementType

{

get { return typeof(CompressionTextMessageEncodingBindingElement); }

}

protected override BindingElement CreateBindingElement()

{

TextMessageEncodingBindingElement textBindingElement =

new TextMessageEncodingBindingElement();

if (null != this.TextEncoding)

{

this.TextEncoding.ApplyConfiguration(textBindingElement);

}

return new CompressionTextMessageEncodingBindingElement(

textBindingElement, this.Algorithm, this.MinMessageSize);

}

}

压缩绑定元素的应用

我们依旧采用验证基于自定义消息格式化器实现压缩的关于 MessengerService 的例子,

下面是契约接口和服务类型。

//契约接口

namespace Artech.WcfServices.Service.Interface

{

6.6 消息编码

WCF 全面解析(上册)

295

[ServiceContract(Namespace = "http://www.artech.com/")]

public interface IMessenger

{

[OperationContract(IsOneWay = true)]

void Send(string message);

}

}

//服务类型

namespace Artech.WcfServices.Service

{

public class MessengerService : IMessenger

{

public void Send(string message)

{

Console.WriteLine(message);

}

}

}

下面是服务端和客户端的配置。终结点采用了一个名称为 compressionBinding 的自定义

绑定,而该绑定采用了我们自定义的绑定元素 CompressionTextEncodingElement。我们为该

绑定元素的所有属性均进行了显式设置。

//服务端配置

<configuration>

<system.serviceModel>

<bindings>

<customBinding>

<binding name="compressionBinding">

<compressionTextEncoding algorithm ="Deflate"

minMessageSize ="1024">

<textEncoding maxReadPoolSize ="64"

maxWritePoolSize ="16"

messageVersion ="Soap12WSAddressing10"

writeEncoding = "utf-8">

<readerQuotas maxArrayLength ="16384"

maxBytesPerRead ="4096"

maxDepth ="32"

maxStringContentLength ="8192"

maxNameTableCharCount ="16384"/>

</textEncoding>

</compressionTextEncoding>

<httpTransport />

</binding>

</customBinding>

</bindings>

<extensions>

<bindingElementExtensions>

<add name="compressionTextEncoding"

type="Artech.MessageEncoding.Extension.Configuration.CompressionTextEncodi

ngElement, Artech.WcfServices.Service.Interface, Version=1.0.0.0,

Culture=neutral, PublicKeyToken=null" />

</bindingElementExtensions>

</extensions>

<services>

<service name="Artech.WcfServices.Service.MessengerService">

<endpoint address="http://127.0.0.1:3721/messengerservice"

第 6 章 消息(Message)

WCF 全面解析(上册)

296

binding="customBinding"

contract="Artech.WcfServices.Service.Interface.IMessenger"

bindingConfiguration="compressionBinding"/>

</service>

</services>

</system.serviceModel>

</configuration>

//客户端配置

<configuration>

<system.serviceModel>

<bindings>

<customBinding>

<binding name="compressionBinding">

<compressionTextEncoding algorithm ="Deflate"

minMessageSize ="1024">

<textEncoding maxReadPoolSize ="64"

maxWritePoolSize ="16"

messageVersion ="Soap12WSAddressing10"

writeEncoding ="utf-8">

<readerQuotas maxArrayLength ="16384"

maxBytesPerRead ="4096"

maxDepth ="32"

maxStringContentLength ="8192"

maxNameTableCharCount ="16384"/>

</textEncoding>

</compressionTextEncoding>

<httpTransport />

</binding>

</customBinding>

</bindings>

<extensions>

<bindingElementExtensions>

<add name="compressionTextEncoding"

type="Artech.MessageEncoding.Extension.Configuration.CompressionTextEncodi

ngElement, Artech.WcfServices.Service.Interface, Version=1.0.0.0,

Culture=neutral, PublicKeyToken=null" />

</bindingElementExtensions>

</extensions>

<client>

<endpoint name="messengerservice"

address="http://127.0.0.1:3721/messengerservice"

binding="customBinding"

contract="Artech.WcfServices.Service.Interface.IMessenger"

bindingConfiguration="compressionBinding"/>

</client>

</system.serviceModel>

</configuration>

通过上面的配置,我们分别将压缩算法和允许压缩的消息主体内容的最小容量设置为

Deflate 和 1024。只要消息主体内容大于 1024,消息主体就会被压缩。所以我们在进行消息

调用中,传入了 1000 个字符的字符串(加上 XML 元素会超过 1024)。

using (ChannelFactory<IMessenger> channelFactory =

new ChannelFactory<IMessenger>("messengerservice"))

{

6.6 消息编码

WCF 全面解析(上册)

297

string message = new string('a',1000);

IMessenger messenger = channelFactory.CreateChannel();

messenger.Send(message);

}

为了确认传输的消息被真正地压缩了,我们可以使用 Fiddler 消息拦截工具。不过如果

要使用 Fiddler,则需要将以 IP(127.0.0.1)表示的地址替换成本地机器名。我们也可以通过

在本册的第 2 章“地址(Address)”中使用的 tcpTrace 来查看消息内容。下面是通过 Fiddler

拦截的请求消息,消息的主体内容被封装在一个自定义的<CompressedBody>结点中。由于

内容经过压缩,所以是不可读的。消息中还包括一个表示压缩算法的<Compression>报头,

接收端通过该报头确定采用解压缩的算法。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">

<s:Header>

<Compression xmlns="http://www.artech.com/compression">

algorithm = "GZip"

</Compression>

</s:Header>

<s:Body>

<CompressedBody

xmlns="http://www.artech.com/compression">7L0HYBxJliUmL23Ke39K9UrX4HShCIBg

EyTYkEAQ7MGIzeaS7B1pRyMpqyqBymVWZV1mFkDM7Z28995777333nvvvfe6O51OJ/ff/z9cZm

QBbPbOStrJniGAqsgfP358Hz8iHr/Ol7P03aJcNp99NG/b1aO7d6+ursZZ3ebT+XhaLe5+dPR4

kTdNdpEfZT96fvT86Pn//fP4rpH4x3ehII7+nwAAAP//</CompressedBody>

</s:Body>

</s:Envelope>