WCF 序列化 – 一个案例研究






4.16/5 (13投票s)
一个案例研究,
目标
我开始这个案例研究,希望能了解是否可以覆盖 WCF 的默认序列化行为,并很快开始思考是否有办法改进和定制我正在从事的项目。
在我深入探讨我发现的内容之前,让我简要解释一下在 WCF 调用期间会发生什么。
当启动服务调用时,所有数据(方法参数)将通过序列化和反序列化过程传输到服务器,如下所示。
1) 在客户端,实体将被序列化为 XML 消息。
2) 此 XML 消息通过线路发送到服务器。
3) 在服务器端,XML 消息将被反序列化为实体形式。
当将对象从服务器返回到客户端时,也会发生相同的过程,反之亦然。
WCF 默认使用 DataContractSerializer 进行序列化。有关更多详细信息,请参阅http://msdn.microsoft.com/en-us/library/ms731073
这是一个名为 person 的实体的示例 XML 消息,该实体具有 Name 和 Address 属性。
<Person>
<Name>Jay Hamlin</Name>
<Address>123 Main Street</Address>
</Person>
从消息中可以看出,不仅数据,类名和字段名,即元数据也被传输了。由于消息是自描述的,因此它是与第三方服务通信的理想方式,这些服务可以根据元数据内容发现消息的数据类型。但是,如果您正在与自己的服务进行内部通信(SOA 应用程序通常都是这样),则并非真正需要。
现在,除了您在服务中编写的实际逻辑之外,屏幕后面还有两个主要操作,它们会对服务的性能产生潜在影响。
第一个当然是客户端和服务器两端的序列化和反序列化。第二个问题更为严重:实际的消息传输发生在网络上(无论是内部高速局域网还是通过互联网)。无论您的网络有多快,传输大数据(> 1MB)都需要时间。
因此,消息的大小(我将称之为有效负载)对于提高服务性能至关重要。
以下是本次练习的目标
- 找出是否有简单的方法可以覆盖 WCF 的序列化行为并实现自己的东西。
- 既然我们无法从消息中删除实际数据,那么我们可以使用哪些方法来减少消息中的元数据,同时仍然能够与第三方服务通信?
- 对于仅由您的客户端使用的自拥有的服务,是否存在一种方法可以完全从方程中删除元数据?
覆盖 WCF 序列化行为
实现自己的 WCF 序列化有 4 个简单步骤。
- 创建自己的序列化器类,实现 XmlObjectSerializer 并覆盖所有虚拟方法。
- 创建自己的操作行为类,实现 DataContractSerializerOperationBehavior 并覆盖所有虚拟方法以使用新创建的序列化器。
- 编写自己的 ContractBehaviourAttribute 类,实现 IContractBehavior。用您创建的序列化器替换默认的序列化器行为。
- 对于所有希望使用新序列化的服务接口,将新创建的属性添加到接口。
步骤 - 1(创建自己的序列化器)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.IO;
namespace CustomWCFSerialization
{
public class MySerializer : XmlObjectSerializer
{
const string localName = "MyObject";
Type type;
public MySerializer(Type type)
{
this.type = type;
}
public override bool IsStartObject(System.Xml.XmlDictionaryReader reader)
{
return reader.LocalName == localName;
}
public override object ReadObject(System.Xml.XmlDictionaryReader reader, bool verifyObjectName)
{
// Write your own De-Serialization logic
}
public override void WriteEndObject(System.Xml.XmlDictionaryWriter writer)
{
writer.WriteEndElement();
}
public override void WriteObjectContent(System.Xml.XmlDictionaryWriter writer, object graph)
{
// Write your own Serialization logic
}
public override void WriteStartObject(System.Xml.XmlDictionaryWriter writer, object graph)
{
writer.WriteStartElement(localName);
}
}
}
步骤 - 2(创建自己的操作行为)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Xml;
namespace CustomWCFSerialization
{
public class MyOperationBehavior : DataContractSerializerOperationBehavior
{
public MyOperationBehavior(OperationDescription operation) : base(operation) { }
public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
{
return new MySerializer(type);
}
public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
{
return new MySerializer(type);
}
}
}
步骤 - 3(创建自己的 ContractBehaviourAttribute)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.Reflection;
namespace CustomWCFSerialization
{
public class MyContractBehaviorAttribute : Attribute, IContractBehavior
{
public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
this.ReplaceSerializerOperationBehavior(contractDescription);
}
public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
this.ReplaceSerializerOperationBehavior(contractDescription);
}
public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
{
// Write your own type validation logic
}
private void ReplaceSerializerOperationBehavior(ContractDescription contract)
{
foreach (OperationDescription od in contract.Operations)
{
for (int i = 0; i < od.Behaviors.Count; i++)
{
DataContractSerializerOperationBehavior dcsob = od.Behaviors[i] as DataContractSerializerOperationBehavior;
if (dcsob != null)
{
od.Behaviors[i] = new MyOperationBehavior(od);
}
}
}
}
}
}
步骤 - 4(开始使用它)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace CustomWCFSerialization.BinarySerializationSample
{
[MyContractBehavior]
[ServiceContract]
public interface ICustomerService
{
[OperationContract]
void InsertCustomer(Customer customer);
[OperationContract]
Customer GetCustomer(int id);
[OperationContract]
List<Customer> GetCustomers();
}
}
我不会详细介绍,因为这不是案例研究的真正目标。请下载代码以获得更深入的理解。
方法 - 1(发送尽可能少的元数据)
如果您与第三方服务通信,您实际上无法避免发送元数据,否则他们将无法理解消息。另一件需要考虑的事情是服务在消息格式方面提供的支持。由于大多数第三方服务通常同时支持 XML 和 JSON 格式,因此采用任何其他新标准都没有意义。让我们看看同一个 person 实体的 JSON 消息是什么样的。
{"Person": { "Name": "Jay Hamlin", "Address": "123 Main Street", } }
仅从外观上看,我们可以看出此消息比 XML 消息小。具体小多少取决于您的实体和它有多少数据。但从逻辑上讲,我们可以说元数据内容本身就减少了一半,当然实际数据大小保持不变。
假设消息的 50% 是数据,50% 是元数据,我们可以说使用 JSON 将减少 25% 的消息有效负载。换句话说,网络延迟时间减少 25%。另请记住,由于我们谈论的是第三方服务,网络肯定会变慢,因此 25% 是一个重大的改进。
要实现 JSON 序列化,我们不需要创建自己的序列化器,因为 Microsoft 已经在 `System.Runtime.Serialization` 命名空间中提供了 `DataContractJsonSerializer`。因此,在实现此功能时,我们可以省略步骤 1。
步骤 - 1 - 无需
步骤 - 2
public class DataContractJsonSerializerOperationBehavior : DataContractSerializerOperationBehavior
{
public DataContractJsonSerializerOperationBehavior(OperationDescription operation) : base(operation) { }
public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
{
return new DataContractJsonSerializer(type);
}
public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
{
return new DataContractJsonSerializer(type);
}
}
步骤 - 3
public class JsonDataContractBehaviorAttribute : Attribute, IContractBehavior
{
public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
this.ReplaceSerializerOperationBehavior(contractDescription);
}
public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
this.ReplaceSerializerOperationBehavior(contractDescription);
}
public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
{
foreach (OperationDescription operation in contractDescription.Operations)
{
foreach (MessageDescription message in operation.Messages)
{
this.ValidateMessagePartDescription(message.Body.ReturnValue);
foreach (MessagePartDescription part in message.Body.Parts)
{
this.ValidateMessagePartDescription(part);
}
foreach (MessageHeaderDescription header in message.Headers)
{
this.ValidateJsonSerializableType(header.Type);
}
}
}
}
private void ReplaceSerializerOperationBehavior(ContractDescription contract)
{
foreach (OperationDescription od in contract.Operations)
{
for (int i = 0; i < od.Behaviors.Count; i++)
{
DataContractSerializerOperationBehavior dcsob = od.Behaviors[i] as DataContractSerializerOperationBehavior;
if (dcsob != null)
{
od.Behaviors[i] = new DataContractJsonSerializerOperationBehavior(od);
}
}
}
}
private void ValidateMessagePartDescription(MessagePartDescription part)
{
if (part != null)
{
this.ValidateJsonSerializableType(part.Type);
}
}
private void ValidateJsonSerializableType(Type type)
{
if (type != typeof(void))
{
if (!type.IsPublic)
{
throw new InvalidOperationException("Json serialization is supported in public types only");
}
ConstructorInfo defaultConstructor = type.GetConstructor(new Type[0]);
if (defaultConstructor == null && !type.IsPrimitive)
{
throw new InvalidOperationException("Json serializable types must have a public, parameterless constructor");
}
}
}
}
步骤 - 4
[JsonDataContractBehavior]
[ServiceContract]
public interface ICustomerService
{
[OperationContract]
void InsertCustomer(Customer customer);
[OperationContract]
Customer GetCustomer(int id);
[OperationContract]
List<Customer> GetCustomers();
}
请下载代码以查看实现细节。
请在练习末尾查找我收集的性能数据点。
方法 - 2(仅发送数据)
当与外部服务通信时,不能应用此方法,因为它们不知道您使用了哪些类和实体。此外,目前还没有定义仅用于数据序列化的标准。
如果您对消息的格式感到困惑,这里有一个示例,我使用简单的分隔字符串来定义 person 实体的数据。
Jay Hamlin;123 Main Street
很明显,消息不能比这更简单了。您接下来可能会问,服务将如何理解“Jay Hamlin”是 name 字段的值,“123 Main Street”是 Address 字段的值。让我们回到 C# 世界,您经常使用数组。现在,用分号分割字符串,您将得到一个数组。第 0 个元素始终是 Name,第 1 个元素始终是 Address。因此,如果您的对象中缺少一些值,您只需在 XML/JSON 中省略该字段。在这里,您必须提供一个空字符串或自定义定义的字符串,以确保在反序列化时,该值将被理解为 null。
因此,为了在客户端和服务器之间实现这种理解,您需要定义自己的序列化器,WCF 将使用该序列化器来理解消息。只有当服务属于您自己的生态系统时,您才能做到这一点。
在您认为这需要大量更改、大量硬编码或更改您的域模型之前……我将告诉您,我通过使用一些反射并以一种将所有这些都抽象于普通开发人员的方式实现了这一点。
实现与 JSON 所做的相同。唯一的变化是我们必须实现自己的序列化器(步骤 - 1)。由于所有其他步骤都已在上面解释,我将跳过它们。
public class StringDelimitedSerializer : XmlObjectSerializer
{
const string localName = "StringDelimitedObject";
Type type;
public StringDelimitedSerializer(Type type)
{
this.type = type;
}
public override bool IsStartObject(System.Xml.XmlDictionaryReader reader)
{
return reader.LocalName == localName;
}
public override object ReadObject(System.Xml.XmlDictionaryReader reader, bool verifyObjectName)
{
byte[] bytes = reader.ReadElementContentAsBase64();
DelimitedString stream = new DelimitedString(Encoding.UTF8.GetString(bytes));
return StringDelimitedSerializerHelper.DiscoverAndDeSerialize(stream, this.type);
}
public override void WriteEndObject(System.Xml.XmlDictionaryWriter writer)
{
writer.WriteEndElement();
}
public override void WriteObjectContent(System.Xml.XmlDictionaryWriter writer, object graph)
{
DelimitedString stream = new DelimitedString();
StringDelimitedSerializerHelper.DiscoverAndSerialize(stream, graph, this.type);
byte[] bytes = Encoding.UTF8.GetBytes(stream.ToString());
writer.WriteBase64(bytes, 0, bytes.Length);
}
public override void WriteStartObject(System.Xml.XmlDictionaryWriter writer, object graph)
{
writer.WriteStartElement(localName);
}
}
正如您所见,我使用了自己的助手类 'StringDelimitedSerializerHelper' 来执行实际的序列化逻辑。这个助手类将发现对象的类型信息,然后将对象序列化为分隔字符串。我还创建了自己的类 'DelimitedString',使方法更简洁。
此方法的好处也很明显,有效负载有潜力减少 50% 或更多。
请下载代码以查看我使用此方法创建的 2 个自定义序列化器(二进制和字符串分隔序列化器)的实现细节。
请在练习末尾查找我收集的性能数据点。
性能数据点
以下所有数据点均与 DataContractSerializer 进行比较。
序列化器名称 |
消息类型 |
架构 |
Data |
Payload(载荷) |
序列化时间 |
DataContractSerializer |
XML |
是 |
是 |
100% |
100% |
JsonSerializer |
JSON |
是 |
是 |
59% |
101% |
BinarySerializer(自创) |
二进制 |
否 |
是 |
32% |
321% |
StringDelimitedSerializer(自创) |
分隔字符串 |
否 |
是 |
24% |
140% |
JsonSerializer 的序列化时间仅增加了 1%,但有效负载/网络延迟减少了 41%,看起来是替换 Data Contract Serializer 的一个不错的选择。
StringDelimitedSerializer 的有效负载减少了 76%,但序列化时间增加了 40%。请注意,此序列化器是自写的,因此需要更多优化。此外,由于序列化时间通常比网络延迟小几个数量级,因此 40% 并非真正大的数字。
我建议您在自己的网络和自己的对象中生成统计数据,以获得更现实的视图。
如果您确实面临多个网络超时问题,除了应用自己的序列化之外,您还可以应用市场上的一些压缩工具。
*有效负载与网络延迟时间成正比。序列化时间是消息编码和解码时间。
免责声明
共享的代码不适用于生产环境,我建议您花时间对其进行彻底测试和优化,以获得性能和复杂数据类型的支持,然后再考虑使用它。
本文并非旨在说服您为 WCF 选择替代序列化。您应该下载示例,在生产环境网络上用自己的服务进行测试,并得出自己的指标来证明是否确实需要优化。