有一段Xml序列化的代码,基于2.0 Runtime传递到Server转换正常。当客户端在4.0 Runtime下调用,Server返回格式错误。序列化代码如下:
XmlSerializer xmlSerializer = new XmlSerializer(typeof(T)); using (MemoryStream memoryStream = new MemoryStream()) { XmlTextWriter xmlWriter = new XmlTextWriter(memoryStream, Encoding.UTF8); xmlSerializer.Serialize(xmlWriter, graph, ns); return Encoding.UTF8.GetString(memoryStream.ToArray()).Trim(); }
用Microsoft Network Monitor监视4.0 Runtime下发送的数据内容,转换后的Xml头部多出了3个字节:EF(239) BB(187) BF(191),它是UTF-8的preamble(encoding bites),字符值为62579。.NET Framework 2.0 String的Trim函数内部会去除一组预定义的空白字符集,其中就包括了62579。反编译代码如下(最后一个字符):
public string Trim() { return this.TrimHelper(string.WhitespaceChars, 2); } string.WhitespaceChars = new char[] { '\t', '\n', '\v', '\f', '\r', ' ', '\u0085', '\u00a0', '\u1680', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200a', '', '\u2028', '\u2029', '\u3000', '' };
.NET Framework 4.0 String的Trim函数,反编译代码如下:
public string Trim() { return this.TrimHelper(2); }
4.0版本String的Trim函数内部不再去除一组预定义的空白字符集,直接导致了之前Xml序列化无法去除UTF-8编码后的preamble。解决方法就是根据不同字符编码尝试去除转换后的preamble。代码如下:
/// <summary> /// 修正编码前言 /// </summary> /// <param name="value">字符串</param> /// <param name="encoding">编码</param> /// <returns>修正字符串</returns> public static String TrimPreamble(this String value, Encoding encoding) { if (String.IsNullOrEmpty(value)) return value; String encodingString = encoding.GetString(encoding.GetPreamble()); if (value.Length <= encodingString.Length) return value; for (Int32 i = 0; i < encodingString.Length; ++i) { if (value[i] != encodingString[i]) return value; } return value.Remove(0, encodingString.Length); }
在我的实现版本并没有使用StartWith或IndexOf函数是有原因的,比如上面的函数可以简写成:
/// <summary> /// 修正编码前言 /// </summary> /// <param name="value">字符串</param> /// <param name="encoding">编码</param> /// <returns>修正字符串</returns> public static String TrimPreamble(this String value, Encoding encoding) { if (String.IsNullOrEmpty(value)) return value; String encodingString = encoding.GetString(encoding.GetPreamble()); if (value.StartsWith(encodingString)) value = value.Remove(0, encodingString.Length); return value; }
当客户端基于.NET Framework 2.0,发送字符串到基于.NET Framework 4.0运行的Server,preamble的偏移在实际字符的-1位。StartWith、IndexOf函数的匹配并不是针对String实际可见的char array逐一匹配,由.NET平台决定字符串在内存的存储方式,它们会返回字符已存在,但并不存在于可见的char array中。具体原因可以查看System.Globalization.CompareInfo.InternalFindNLSStringEx函数。
不使用XmlTextWriter可以避免encoding bites,比如:StringWriter。其缺点是无法指定Encoding,但可以继承StringWriter并重新定义它的构造函数。
/// <summary> /// 编码字符写入器 /// </summary> public sealed class EncodingStringWriter : StringWriter { private Encoding _encoding; /// <summary> /// 构造函数 /// </summary> /// <param name="sb">字符建造器</param> /// <param name="encoding">编码</param> public EncodingStringWriter(StringBuilder sb, Encoding encoding) : base(sb) { _encoding = encoding; } /// <summary> /// 编码 /// </summary> public override Encoding Encoding { get { return _encoding; } } }