GOLANG將型別作為引數,用反射設定指標的指標,實現類似模板功能

winlin發表於2017-05-09

在協議解析中,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 := res.Args.Get("data").(*amf0.EcmaArray); ok {
    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中傳遞介面的指標也是不可以的,會導致型別錯誤(**ConnectAppResPacket並不能匹配*Packet);這個引數只能是interface{}。不過用法也很簡單,只是需要注意引數的傳遞。

var res *ConnectAppResPacket
// 這是正確的做法,傳遞res指標的地址,相當於指標的指標。
_ = protocol.ExpectPacket(&res)
// 這是錯誤的做法,會在ExpectPacket檢查返回錯誤,沒有實現Packet介面
_ = protocol.ExpectPacket(res)

用起來還不錯。

相關文章