Protobuf解组未知消息 [英] protobuf unmarshal unknown message
问题描述
我有一个接收协议错误消息的侦听器。但是,它不知道哪种类型的消息何时进入。因此,我尝试将其解组为interface{}
,以便稍后可以键入CAST:
var data interface{}
err := proto.Unmarshal(message, data)
if err != nil {
log.Fatal("unmarshaling error: ", err)
}
log.Printf("%v
", data)
但是,此代码无法编译:
cannot use data (type interface {}) as type proto.Message in argument to proto.Unmarshal:
interface {} does not implement proto.Message (missing ProtoMessage method)
如何在GO中解组并稍后类型强制转换"未知"的协议缓冲区消息?
推荐答案
首先是关于OP的问题的两个词,由他们提出:
proto.Unmarshal
无法解组为interface{}
。方法签名很明显,您必须传递proto.Message
参数,该参数是由具体的ProtoBuffer类型实现的接口。
在处理未出现在Any
中的原始协议缓冲区[]byte
有效负载时,理想情况下您至少需要一些内容(字符串、数字等.)与字节片一起使用,您可以使用它来映射到具体的协议消息。
然后您可以打开它并实例化适当的protocol buf具体类型,然后才能将该参数传递给Unmarshal
:
var message proto.Message
switch atLeastSomething {
case "foo":
message = &mypb.Foo{}
case "bar":
message = &mypb.Bar{}
}
_ = proto.Unmarshal(message, data)
现在,如果字节负载确实未知怎么办?
作为前言,考虑到这种情况在实践中应该很少发生。用于以您选择的语言生成ProtoBuffer类型的架构表示协定,并且通过接受ProtoBuffer有效负载,对于它的某些定义而言,您正在履行该协定。
不管怎样,如果由于某种原因,您必须以wire格式处理一个完全未知的、神秘的、协议缓冲区负载,您可以使用protowire
包从中提取一些信息。
2
)用于字符串、字节、重复字段和.子消息(reference)。
您可以检索负载内容,但您的语义肯定很弱。
代码
话虽如此,未知原型消息的解析器可能是这样的。其想法是利用protowire.ConsumeField
读取原始字节片。
数据模型可能如下所示:
type Field struct {
Tag Tag
Val Val
}
type Tag struct {
Num int32
Type protowire.Type
}
type Val struct {
Payload interface{}
Length int
}
和解析器:
func parseUnknown(b []byte) []Field {
fields := make([]Field, 0)
for len(b) > 0 {
n, t, fieldlen := protowire.ConsumeField(b)
if fieldlen < 1 {
return nil
}
field := Field{
Tag: Tag{Num: int32(n), Type: t },
}
_, _, taglen := protowire.ConsumeTag(b[:fieldlen])
if taglen < 1 {
return nil
}
var (
v interface{}
vlen int
)
switch t {
case protowire.VarintType:
v, vlen = protowire.ConsumeVarint(b[taglen:fieldlen])
case protowire.Fixed64Type:
v, vlen = protowire.ConsumeFixed64(b[taglen:fieldlen])
case protowire.BytesType:
v, vlen = protowire.ConsumeBytes(b[taglen:fieldlen])
sub := parseUnknown(v.([]byte))
if sub != nil {
v = sub
}
case protowire.StartGroupType:
v, vlen = protowire.ConsumeGroup(n, b[taglen:fieldlen])
sub := parseUnknown(v.([]byte))
if sub != nil {
v = sub
}
case protowire.Fixed32Type:
v, vlen = protowire.ConsumeFixed32(b[taglen:fieldlen])
}
if vlen < 1 {
return nil
}
field.Val = Val{Payload: v, Length: vlen - taglen}
// fmt.Printf("%#v
", field)
fields = append(fields, field)
b = b[fieldlen:]
}
return fields
}
示例输入和输出
给定如下原型架构:
message Foo {
string a = 1;
string b = 2;
Bar bar = 3;
}
message Bar {
string c = 1;
}
在GO中初始化为:
&test.Foo{A: "A", B: "B", Bar: &test.Bar{C: "C"}}
通过在上述代码中的循环末尾添加fmt.Printf("%#v
", field)
语句,它将输出以下内容:
main.Field{Tag:main.Tag{Num:1, Type:2}, Val:main.Val{Payload:[]uint8{0x41}, Length:1}}
main.Field{Tag:main.Tag{Num:2, Type:2}, Val:main.Val{Payload:[]uint8{0x42}, Length:1}}
main.Field{Tag:main.Tag{Num:1, Type:2}, Val:main.Val{Payload:[]uint8{0x43}, Length:1}}
main.Field{Tag:main.Tag{Num:3, Type:2}, Val:main.Val{Payload:[]main.Field{main.Field{Tag:main.Tag{Num:1, Type:2}, Val:main.Val{Payload:[]uint8{0x43}, Length:1}}}, Length:3}}
关于子消息
从上面可以看出,处理可能是也可能不是消息字段的protowire.BytesType
的想法是尝试递归解析它。如果成功,则保留结果msg
并将其存储在字段值中;如果失败,则按原样存储字节,然后可能是原型string
或bytes
。顺便说一句,如果我没有读错,这似乎就是Marc Gravell在Protogen code中所做的。
关于重复字段
上面的代码没有显式处理重复字段,但是解析完成后,重复字段将具有与Field.Tag.Num
相同的值。因此,将字段打包到片/数组中应该是微不足道的。
关于地图
上面的代码也不处理原型映射。我怀疑映射在语义上等同于重复的k/v对,例如:
message Pair {
string key = 1; // or whatever key type
string val = 2; // or whatever val type
}
如果我的假设是正确的,则可以使用给定的代码将映射解析为子消息。
关于oneof
%s
我还没有对此进行测试,但我预计有关联合类型的信息将完全丢失。字节负载将仅包含实际设置的值。
但是Any
怎么办?
Any
原型类型不适合图片。与看起来相反,Any
与JSON对象的map[string]interface{}
不是类似。原因很简单:Any
是一个定义很好的结构的原型消息,即(在围棋中):type Any struct {
// unexported fields
TypeUrl string // struct tags omitted
Value []byte // struct tags omitted
}
因此它更类似于Gointerface{}
的实现,因为它保存一些实际数据和该数据的类型信息。
它可以容纳任意的原型有效负载(以及它们的类型信息!)但它不能用于解码未知消息,因为Any
正好具有这两个字段,即类型url和字节负载。
总而言之,这个答案没有提供成熟的生产级解决方案,但它展示了如何在解码任意有效负载的同时尽可能多地保留原始语义。希望它能为您指明正确的方向。
这篇关于Protobuf解组未知消息的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!