1. 程式人生 > >golang中使用消息名稱創建protobuf消息

golang中使用消息名稱創建protobuf消息

mem 很好 兩個 因此 uuid gae [] not found port

golang 中根據 protobuf message name 動態實例化 protobuf 消息,消息內容通過輸入 json 文件指定

背景:

項目中使用 protobuf 作為 rpc 調用協議,計劃用 golang 實現一個壓測工具,希望能夠指定 message name 和 json 動態的構建 protobuf 消息;從 json 解析到 golang protobuf message 可以用 jsonpb,這裏使用 UnmarshalString,函數簽名

func UnmarshalString(str string, pb proto.Message) error

str 是 json 字符串,message 是自定義的 proto messgae 接口。於是剩下需要做的是通過 message name 獲取對應的 proto.Message 接口,搜索了一下,對於 golang 沒有找到很好的方法,檢查了 protoc 生成的 golang 消息文件,可以按以下方式根據 message name 獲取到 message type,然後利用 golang reflect 包實例畫消息;

解決方式:

簡單來說,就是需要根據 message name 獲取到 message type, 然後利用 golang 反射實例化消息結構。從 message name 獲取 message type,最直觀的是維護一個 map[string]reflect.Type 的字典,protoc 生成的 golang 代碼已經包含這個字典,自定義的 message 會通過 proto.RegisterType 註冊到 protoTypes 和 revProtoTypes 這兩個結構中,並提供 MessageName 和 MessageType 用來通過 name 獲取 type 或者反之, 相關代碼在 proto/properties.go 中, 由此可以實現通過 message name 獲取到 message type 進而實例化消息的功能。

其它包括 enum 類型,extensions 都有相應的註冊/獲取函數 proto.RegisterEnum, proto.RegisterExtension;

示例:

以下以一個 rpc 消息定義為例實現從消息名稱實例化一個消息實例,完整代碼見 https://github.com/wuyidong/parse_pb_by_name_golang

以下一個簡單 protobuf 做 rpc 協議的簡單例子,我們在 package rpc 中定義了協議的一般格式,由協議頭(Head)和消息本身(Body)組成,Body 全部為可選字段,用於填充具體的協議,Head 為固定格式, 其中 Head.message_type 用於標識 Body 所帶有的協議類型,服務端根據 Head.message_type 路由到具體的處理過程,具體的協議如 CreateAccountRequest/CreateAccountResponse 等都作為 rpc.Body 的可選字段。

rpc.proto --> rpc 消息格式

package rpc;

message RPCMessage  {
    // 消息頭部
    required Head head = 1;
    // 消息內容
    required Body body = 2;
};

message Head {
    // 請求 uuid
    required string session_no = 1;
    // 請求消息類型
    required int32 message_type = 2;
};

message Body {
    extensions 1000 to max;
};

message ResponseCode {
    required int32 retcode = 1;            // 返回碼
    optional string error_messgae = 2;     // 返回失敗時,錯誤信息
};

  account.proto --> 賬戶相關操作

package rpc.account;

import "rpc.proto";

enum MessageType {
    CREATE_ACCOUNT_REQUEST = 1001;
    CREATE_ACCOUNT_RESPONSE = 1002;
    DELETE_ACCOUNT_REQUEST = 1003;
    DELETE_ACCOUNT_RESPONSE = 1004;
    // ...
};

extend rpc.Body {
    optional CreateAccountRequest create_account_request = 1001;
    optional CreateAccountResponse create_account_response = 1002;
    // ...
};

// account 相關操作接口
message CreateAccountRequest {
    required string email = 1;
    optional string name = 2;    // 不指定則為 email
    optional string passwd = 3;  // 初始密碼為 email
};

message CreateAccountResponse {
    required ResponseCode rc = 1;
};
// ...

  proto 代碼編譯之後,rpc.account 包被命名為 rpc_account, 以 CreateAccountRequest 為例,可以看到 protoc 編譯後生成了如下 golang 代碼:

// CreateAccountRequest 結構體定義
type CreateAccountRequest struct {
	Email            *string `protobuf:"bytes,1,req,name=email" json:"email,omitempty"`
	Name             *string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
	Passwd           *string `protobuf:"bytes,3,opt,name=passwd" json:"passwd,omitempty"`
	XXX_unrecognized []byte  `json:"-"`
}

// proto.Message 接口指定的函數
func (m *CreateAccountRequest) Reset()                    { *m = CreateAccountRequest{} }
func (m *CreateAccountRequest) String() string            { return proto.CompactTextString(m) }
func (*CreateAccountRequest) ProtoMessage()               {}

// extension type 定義
var E_CreateAccountRequest = &proto.ExtensionDesc{
	ExtendedType:  (*rpc.Body)(nil),
	ExtensionType: (*CreateAccountRequest)(nil),
	Field:         1001,
	Name:          "rpc.account.create_account_request",
	Tag:           "bytes,1001,opt,name=create_account_request",
	Filename:      "account.proto",
}

// 註冊定義結構到 proto
func init() {
	proto.RegisterType((*CreateAccountRequest)(nil), "rpc.account.CreateAccountRequest")
	proto.RegisterEnum("rpc.account.MessageType", MessageType_name, MessageType_value)
	proto.RegisterExtension(E_CreateAccountRequest)
}

其中 init 函數中三個 Register*** 函數將 CreateAccountRequest 相關信息註冊到 proto 包中:

// github.com/golang/protobuf/proto/properties.go 中
func RegisterEnum(typeName string, unusedNameMap map[int32]string, valueMap map[string]int32) {
	if _, ok := enumValueMaps[typeName]; ok {
		panic("proto: duplicate enum registered: " + typeName)
	}
	enumValueMaps[typeName] = valueMap
}

// EnumValueMap returns the mapping from names to integers of the
// enum type enumType, or a nil if not found.
func EnumValueMap(enumType string) map[string]int32 {
	return enumValueMaps[enumType]
}


func RegisterType(x Message, name string) {
	if _, ok := protoTypes[name]; ok {
		// TODO: Some day, make this a panic.
		log.Printf("proto: duplicate proto type registered: %s", name)
		return
	}
	t := reflect.TypeOf(x)
	protoTypes[name] = t
	revProtoTypes[t] = name
}

// MessageName returns the fully-qualified proto name for the given message type.
func MessageName(x Message) string {
	type xname interface {
		XXX_MessageName() string
	}
	if m, ok := x.(xname); ok {
		return m.XXX_MessageName()
	}
	return revProtoTypes[reflect.TypeOf(x)]
}

// MessageType returns the message type (pointer to struct) for a named message.
func MessageType(name string) reflect.Type { return protoTypes[name] }

//  github.com/golang/protobuf/proto/extensions.go 中
// RegisterExtension is called from the generated code.
func RegisterExtension(desc *ExtensionDesc) {
	st := reflect.TypeOf(desc.ExtendedType).Elem()
	m := extensionMaps[st]
	if m == nil {
		m = make(map[int32]*ExtensionDesc)
		extensionMaps[st] = m
	}
	if _, ok := m[desc.Field]; ok {
		panic("proto: duplicate extension registered: " + st.String() + " " + strconv.Itoa(int(desc.Field)))
	}
	m[desc.Field] = desc
}

// RegisteredExtensions returns a map of the registered extensions of a
// protocol buffer struct, indexed by the extension number.
// The argument pb should be a nil pointer to the struct type.
func RegisteredExtensions(pb Message) map[int32]*ExtensionDesc {
	return extensionMaps[reflect.TypeOf(pb).Elem()]
}

對照 Register*** 的實現,可以看到通過 E_CreateAccountRequest 類型是註冊到了 extensionMaps 下,這是個兩層的map, map[extendedType]map[messageField]ExtensionType,messageFlied 為 rpc.Body 的字段標識,因此我們根據 RegisteredExtensions(rpc.Body) 可以獲取到 rpc.Body 下所有的 extension 消息類型,messageFlied 則和我們之前在 MessageType 中定義的對應各消息的枚舉類型一致,可以通過 EnumValueMap(rpc.account.MessageType)[rpc.account.create_account_request] 取到,因此可以通過消息名稱獲取到消息對應的 ExtensionDesc 類型, 其中 ExtensionType 即為消息類型。對應的我們用以下代碼通過給定消息名稱實例化一個消息結構:

// message id -> *proto.ExtensionDesc
// 記錄 rpc.Body 的拓展消息
var RPCMessageBodyExtensions map[int32]*proto.ExtensionDesc

func init() {
	RPCMessageBodyExtensions = proto.RegisteredExtensions((*rpc.Body)(nil))
}

// some utils for UMessage

// msgName: rpc.account.create_account_request
func GetRPCMessageObjectByName(msgName string) (msg proto.Message, err error) {
	msgType := reflect.TypeOf(GetRPCMessageExtension(msgName).ExtensionType)
	if msgType == nil {
		err = fmt.Errorf("can‘t find message type")
		return
	}
	// msgType is pointer
	msg = reflect.Indirect(reflect.New(msgType.Elem())).Addr().Interface().(proto.Message)
	return
}

// msgName: rpc.account.create_account_request
// namePrefix: rpc.account
// name: create_account_request
func GetNamePrefix(msgName string) (prefix string) {
	items := strings.Split(msgName, ".")
	prefix = strings.Join(items[0:len(items)-1], ".")
	return
}

func GetName(msgName string) (name string) {
	items := strings.Split(msgName, ".")
	name = items[len(items)-1]
	return
}

func GetRPCMessageId(msgName string) (msgId int32) {
	msgTypeName := GetNamePrefix(msgName) + ".MessageType"
	mapMsgNameId := proto.EnumValueMap(msgTypeName)
	msgId = mapMsgNameId[strings.ToUpper(GetName(msgName))]
	return
}

func GetRPCMessageExtension(msgName string) (extension *proto.ExtensionDesc) {
	msgId := GetRPCMessageId(msgName)
	extension = RPCMessageBodyExtensions[msgId]
	return
}

golang中使用消息名稱創建protobuf消息