原文参考:https://gocn.io/article/319
在协议解析中,C++的模板有比较大的作用,有时候我们希望丢弃所有的包,只留下特定类型的包。参考SRS的代码SrsRtmpClient::connect_app2:
类型系统的设计,SrsConnectAppResPacket
继承自SrsPacket
:
class SrsPacket;
class SrsConnectAppResPacket : public SrsPacket
协议栈提供了expect_message
模板函数,接收特定类型的包:
SrsCommonMessage* msg = NULL;
SrsConnectAppResPacket* pkt = NULL;
if ((ret = protocol.expect_message<SrsConnectAppResPacket>(&msg,&pkt)) != ERROR_SUCCESS) {
return ret;
}
SrsAmf0Any* data = pkt->info->get_property("data");
SrsAmf0EcmaArray* arr = data->to_ecma_array();
SrsAmf0Any* prop = arr->ensure_property_string("srs_server_ip");
string srs_server_ip = prop->to_str();
在向服务器发送了ConnectApp
后,就等待ConnectAppRes
响应包,丢弃所有的其他的。这个时候,类型SrsConnectAppResPacket
就作为了一个参数,也就是C++的模板。如果是GOLANG怎么实现呢?没有直接的办法的,因为没有泛型。
在GOLANG中,也需要定义个interface,参考Packet,当然也是有ConnectAppResPacket
实现了这个接口(Message
是原始消息,它的Payload可以Unmarshal为Packet
):
type Message struct { Payload []byte }
type Packet interface {} // Message.Payload = Packet.Marshal()
type ConnectAppResPacket struct { Args amf0.Amf0 }
第一种方法,协议栈只需要收取Message,然后解析Message为Packet,收到packet后使用类型转换,判断不是自己需要的包就丢弃:
func (v *Protocol) ReadMessage() (m *Message,err error) func (v *Protocol) DecodeMessage(m *Message) (pkt Packet,err error)
不过这两个基础的API,User在使用时,比较麻烦些,每次都得写一个for循环:
var protocol *Protocol
for {
var m *Message
m,_ = protocol.ReadMessage()
var p Packet
p,_ = protocol.DecodeMessage(m)
if res,ok := p.(*ConnectAppResPacket); ok {
if data,ok := res.Args.Get("data").(*amf0.EcmaArray); ok {
if data,ok := data.Get("srs_server_ip").(*amf0.String); ok {
srs_server_ip = string(*data)
}
}
}
}
比较方便的做法,就是用回调函数,协议栈需要提供个ExpectPacket
方法:
func (v *Protocol) ExpectPacket(filter func(m *Message,p Packet)(ok bool)) (err error)
这样可以利用回调函数可以访问上面函数的作用域,直接转换类型和设置目标类型的包:
var protocol *Protocol
var res *ConnectAppResPacket
_ = protocol.ExpectPacket(func(m *Message,p Packet) (ok bool){
res,ok = p.(*ConnectAppResPacket)
})
if data,ok := res.Args.Get("data").(*amf0.EcmaArray); ok {
if data,ok := data.Get("srs_server_ip").(*amf0.String); ok {
srs_server_ip = string(*data)
}
}
这样已经比较方便了,不过还是需要每次都给个回调函数。要是能直接这样用就好了:
var protocol *Protocol
var res *ConnectAppResPacket
_ = protocol.ExpectPacket(&res)
if data,ok := data.Get("srs_server_ip").(*amf0.String); ok {
srs_server_ip = string(*data)
}
}
这样也是可以做到的,不过协议栈函数要定义为:
func (v *Protocol) ExpectPacket(ppkt interface{}) (err error)
在函数内部,使用reflect判断类型是否符合要求,设置返回值。代码参考ExpectPacket,下面是一个简要说明:
func (v *Protocol) ExpectPacket(ppkt interface{}) (m *Message,err error) {
// 由于ppkt是**ptr,所以取类型后取Elem(),就是*ptr,用来判断是否实现了Packet接口。
ppktt := reflect.TypeOf(ppkt).Elem()
// ppktv是发现匹配的包后,设置值的。
ppktv := reflect.ValueOf(ppkt)
// 要求参数必须是实现了Packet,避免传递错误的值进来。
if required := reflect.TypeOf((*Packet)(nil)).Elem(); !ppktt.Implements(required) {
return nil,fmt.Errorf("Type mismatch")
}
for {
m,err = v.ReadMessage()
pkt,err = v.DecodeMessage(m)
// 判断包是否是匹配的那个类型,如果不是就丢弃这个包。
if pktt = reflect.TypeOf(pkt); !pktt.AssignableTo(ppktt) {
continue
}
// 相当于 *ppkt = pkt,类似C++中对指针的指针赋值。
ppktv.Elem().Set(reflect.ValueOf(pkt))
break
}
return
}
遗憾的就是这个参数ppkt类型不能是Packet
,因为会有类型不匹配;也不能是*Packet
,因为在GOLANG中传递接口的引用本身是很奇怪的;这个参数只能是interface{}
。不过用法也很简单,只是需要注意参数的传递。
var res *ConnectAppResPacket
// 这是正确的做法,传递res指针的地址,相当于指针的指针。
_ = protocol.ExpectPacket(&res)
// 这是错误的做法,会在ExpectPacket检查返回错误,没有实现Packet接口
_ = protocol.ExpectPacket(res)
用起来还不错。