如何将Protobuf反序列化为自定义对象?gRPC流场景优化
gRPC流数据传输优化方案分析
问题背景
我正在研究gRPC的流数据传输实现,需要从gRPC服务器向客户端发送流数据。已定义greet.proto如下:
message protoTagValueDouble { int64 TagId = 1; google.protobuf.Timestamp DateTime = 2; google.protobuf.Timestamp DateTimeUtc = 3; google.protobuf.Timestamp TimeStampUtc = 4; int32 OpcQuality = 5; string Tagname = 6; double Value =7; } message Response { repeated protoTagValueDouble items = 1; }
服务端与客户端均使用标记了[ProtoContract]的自定义TagValueDouble类(更新于2024.02.15):
[ProtoContract] public class TagValueDouble { [ProtoMember(1)] public long TagId { get; set; } [ProtoMember(2)] public DateTime? DateTime { get; set; } [ProtoMember(3)] public DateTime? DateTimeUtc { get; set; } [ProtoMember(4)] public DateTime? TimeStampUtc { get; set; } [ProtoMember(5)] public int OpcQuality { get; set; } [ProtoMember(6)] public string Tagname { get; set; } [ProtoMember(7)] public double? Value { get; set; } }
目前通过New()逐个将Protobuf消息映射为TagValueDouble对象,虽能正常运行但性能不足,希望采用更高效的序列化/反序列化方式实现Response到List<TagValueDouble>的转换。客户端尝试了如下写法:
await foreach (var response in serverData.ResponseStream.ReadAllAsync(stoppingToken)) { var bytes = response.ToByteArray(); using var stream = new MemoryStream(bytes); { var x = Serializer.Deserialize<List<TagValueDouble>>(stream); } }
但不清楚服务端该如何对应实现,想确认此思路是否可行,或是方案存在错误?
方案分析与修正
你的思路存在几个关键问题,直接这样实现会失败:
- 序列化格式不匹配:客户端用
Serializer.Deserialize<List<TagValueDouble>>是protobuf-net的序列化格式,但服务端发送的是gRPC自动生成的Response对象(基于Google.Protobuf序列化),两者结构完全不同,直接转换会导致反序列化失败。 - 冗余操作拖慢性能:客户端已经拿到gRPC自动反序列化后的
Response对象,再调用ToByteArray()重新序列化、再用protobuf-net反序列化,完全是多此一举,反而会降低性能。
正确的优化方向
方向一:直接复用gRPC生成的类
如果追求性能,最直接的方式是放弃自定义TagValueDouble类,直接使用gRPC根据.proto生成的protoTagValueDouble和Response类。gRPC代码生成器已做过大量性能优化,序列化/反序列化效率远高于手动映射。
方向二:让自定义类与Protobuf定义兼容
如果必须使用自定义TagValueDouble类,需确保它的protobuf-net标记与.proto定义完全对齐,然后调整gRPC服务定义直接传输目标类型:
- 修改
.proto,将响应改为流形式的单个数据对象:
service GreetService { rpc StreamData (Empty) returns (stream protoTagValueDouble); }
- 配置gRPC使用protobuf-net作为序列化器,服务端直接发送
TagValueDouble对象:
public async Task StreamData(Empty request, IServerStreamWriter<TagValueDouble> responseStream, ServerCallContext context) { while (!context.CancellationToken.IsCancellationRequested) { var dataList = GetTagValueDoubleList(); // 获取待发送数据 foreach (var item in dataList) { await responseStream.WriteAsync(item); } await Task.Delay(1000, context.CancellationToken); } }
- 客户端直接读取流中的
TagValueDouble对象,无需额外序列化操作:
await foreach (var item in client.StreamDataAsync(new Empty(), cancellationToken: stoppingToken)) { // 直接使用item }
方向三:优化手动映射性能
如果以上方式不可行,可通过以下手段优化手动映射:
- 使用对象池复用
TagValueDouble实例,减少GC开销 - 手动编写映射代码或用表达式树生成映射逻辑,替代反射工具
- 批量处理
protoTagValueDouble对象,减少循环次数
内容的提问来源于stack exchange,提问作者Roman
相关产品推荐
相关产品推荐

