XML被JSON代替的时候,是因为JSON的更小的文件体积.
现在移步到手机,json 数据包也愈发显的不可接受了.满眼的都是 json 的属性名,真正有用的属性值却只占整个JSON包的一小部份.如果能不要"属性名称",那可太好了,但是那是不可能的.
老早就听说过 ProtoBuf ,一直没有用过.这两天耗了些时间研究了一下,成功的应用于服务端(WebApi) 和 客户端(Xamarin.Form) 上.
先贴两张图感受一下:
能小多少,和具体的类结构及数据完整性有关. 不过单从这份结果对比来看, 效果真的很吸引人!
如果将它应用于手机端, 可以给你的APP体验抬高不止一个档次!
1, 服务端实体类的声明 (WebApi):
NuGet -> ProtoBuf-net, 要安新版本的.
一开始, 我直接安装的 WebApiContrib.Formatting.ProtoBuf, 顺带安装 protobuf-net 2.0.0.448 版的, 结果在有循环引用的实体集上报错:
Possible recursion detected.
为什么会有循环引用? 你用过 EF吗? 搜了一圈, 有说什么 protobuf 只支持 Tree ,不支持图的.
Json.Net 序列化的时候,可以设置 LoopReferences = Ignore, 如果 protobuf 不支持这样的功能, 那真是有辱谷歌的大牙了.
后来看到 AsReferenceDefault 这个东西, 敲了一下, 没有这个属性, 才意思到下载的版本太老了.
服务端的数据实体定义:
1 [ProtoContract(AsReferenceDefault = true, ImplicitFields = ImplicitFields.AllFields)] 2 public partial class T_BRANCH 3 { 4 public decimal BRANCH_ID { get; set; } 5 6 public string BRANCH_CODE { get; set; } 7 ...
ImplicitFields.AllFields 的功效等于在每个属性上加 ProtoMember.
2, 注册 Protobuf 格式器
在 Global 里添加:
1 GlobalConfiguration.Configuration.Formatters.Add(new ProtoBufFormatter());
ProtoBufFormatter 的源码:
1 public class ProtoBufFormatter : MediaTypeFormatter { 2 private static readonly MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/x-protobuf"); 3 private static Lazy<RuntimeTypeModel> model = new Lazy<RuntimeTypeModel>(CreateTypeModel); 4 5 public static RuntimeTypeModel Model { 6 get { 7 return model.Value; 8 } 9 } 10 11 public ProtoBufFormatter() { 12 SupportedMediaTypes.Add(mediaType); 13 } 14 15 public static MediaTypeHeaderValue DefaultMediaType { 16 get { 17 return mediaType; 18 } 19 } 20 21 public override bool CanReadType(Type type) { 22 return CanReadTypeCore(type); 23 } 24 25 public override bool CanWriteType(Type type) { 26 return CanReadTypeCore(type); 27 } 28 29 public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger) { 30 var tcs = new TaskCompletionSource<object>(); 31 32 try { 33 object result = Model.Deserialize(stream, null, type); 34 tcs.SetResult(result); 35 } catch (Exception ex) { 36 tcs.SetException(ex); 37 } 38 39 return tcs.Task; 40 } 41 42 public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext) { 43 var tcs = new TaskCompletionSource<object>(); 44 45 try { 46 Model.Serialize(stream, value); 47 tcs.SetResult(null); 48 } catch (Exception ex) { 49 tcs.SetException(ex); 50 } 51 52 return tcs.Task; 53 } 54 55 private static RuntimeTypeModel CreateTypeModel() { 56 var typeModel = TypeModel.Create(); 57 typeModel.UseImplicitZeroDefaults = false; 58 return typeModel; 59 } 60 61 private static bool CanReadTypeCore(Type type) { 62 return true; 63 } 64 }
至此, 服务端完成了, 可以请求一下试试效果. 记得要加请求头: Accept ,值为:application/x-protobuf
3, PCL 实体类
一般,我都会将数据实体(POCO)单独分隔成一个类库, 方便各个项目使用, 因为这些POCO类库不涉及任何业务逻辑.
现在,在手机端遇到了一个问题, 用这些类库,将服务器上收到的数据反序列化,无论是 json 还是 protobuf , 都会报错.
跟踪了一下, 大至都是因为 : 加到类上的 Serializable; 加到属性上的 Required/StringLength 等 特性 造成的. 因为这些特性在 PCL 类库里是不受支持的.
将这些东西去掉当然是不可能的, 那怎么办呢 ?
如果是基于DbFirst / ModelFirst 的 EF 类库,那好办, 新增一个对应的PCL类库, 把 原库下面的 TT 模板复制一份到PCL库中, 改一下就可以了.
如果是 CodeFirst 的类库 , 那就有点小复杂:
1, 还是新建一个对应的 PCL 类库.
2, 将原库中的所有类文件都复制一份到PCL类库中.
3, 添加一个 attributes 目录, 将类库中所有用到的,不被PCL支持的特性 重新定义一遍, 比如 StringLengthAttribute:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace System.ComponentModel.DataAnnotations { 8 public class StringLengthAttribute : Attribute { 9 10 public StringLengthAttribute(int max) { 11 } 12 13 } 14 }
注意, 命名空间一定要和原来的一样.
4, 将这些PCL类库拿去代替原库,在手机中使用.
4, 手机端的 protobuf 类库
Protobuf-net 是有 PCL 版本的, 只不过在 NuGet 中添加的 Protobuf-net 是无法安装到 PCL 库中的, 需要先把 Protobuf-net 下载来, 然后手工添加引用:
不要添加非 PCL 版的 protobuf-net 到 PCL 类库中, 运行会报错.
5, WebApi Client 设定
1,添加上面的 ProtoBufFormatter.cs 到手机端的PCL类库中.
2, 在 ReadAsAsync 方法中使用 ProtoBufFormatter :
1 var a = await this.GetResult(token); 2 var reason = ""; 3 HttpStatusCode? status = null; 4 if (a != null) { 5 if (a.IsSuccessStatusCode) { 6 if (this.SupportProtoBuf) { 7 return await a.Content.ReadAsAsync<T>(new[] { new ProtoBufFormatter() }); 8 } else 9 return await a.Content.ReadAsAsync<T>(); 10 } else { 11 reason = a.ReasonPhrase; 12 status = a.StatusCode; 13 } 14 }
具体请参考:
https://github.com/gruan01/LbcTest/blob/master/Lbc.WebApi/MethodBase.cs
OK, 以上是在 Xamarin.Form 中使用 Protobuf 的全部过程.