筆者前面博文Go語言網絡游戲服務器模塊化編程介紹了Go語言在開發網絡游戲時如何進行模塊化編程,在其中使用了Protobuf進行網絡通信。在Protobuf官方實現中并沒有生成C語言的實現,不過有一個開源的protobuf-c可以使用。
先來看看protobuf-c生成的代碼,假如如下PB:
syntax = "proto3";
package netmsg;enum NetCmdID
{CmdNone = 0;Ping = 1;Pong = 2;
}message PingPong
{uint32 time = 1;
}
生成的C代碼類似這樣:
typedef struct Netmsg__PingPong Netmsg__PingPong;struct Netmsg__PingPong
{ProtobufCMessage base;uint32_t time;
};
#define NETMSG__PING_PONG__INIT \{ PROTOBUF_C_MESSAGE_INIT (&netmsg__ping_pong__descriptor) \
, 0 }/* Netmsg__PingPong methods */
void netmsg__ping_pong__init(Netmsg__PingPong *message);
size_t netmsg__ping_pong__get_packed_size(const Netmsg__PingPong *message);
size_t netmsg__ping_pong__pack(const Netmsg__PingPong *message,uint8_t *out);
size_t netmsg__ping_pong__pack_to_buffer(const Netmsg__PingPong *message,ProtobufCBuffer *buffer);
Netmsg__PingPong *netmsg__ping_pong__unpack(ProtobufCAllocator *allocator,size_t len,const uint8_t *data);
void netmsg__ping_pong__free_unpacked(Netmsg__PingPong *message,ProtobufCAllocator *allocator);extern const ProtobufCMessageDescriptor netmsg__ping_pong__descriptor;
可以看到,PingPong
在生成的C代碼中結構是原樣輸出的,而函數中又是以C風格ping_pong
輸出的;同時包名netmsg
在結構中首字母是大寫,而在函數中又是全部小寫。
如果要解析PB,它提供了一個API:
PROTOBUF_C__API
ProtobufCMessage *
protobuf_c_message_unpack(const ProtobufCMessageDescriptor *descriptor,ProtobufCAllocator *allocator,size_t len,const uint8_t *data);
這里需要傳入PB的描述符結構ProtobufCMessageDescriptor *
,針對PingPong消息即netmsg__ping_pong__descriptor
的地址。調用后會返回一個ProtobufCMessage *
,只需要強轉成Netmsg__PingPong*
即可。這個返回值在使用完后需要調用函數protobuf_c_message_free_unpacked
釋放內存:
PROTOBUF_C__API
void
protobuf_c_message_free_unpacked(ProtobufCMessage *message,ProtobufCAllocator *allocator);
筆者想要的理想情況是與前面博文Go語言網絡游戲服務器模塊化編程中的一樣,可以在網絡消息處理函數中直接寫需要處理的具體的PB消息。
在處理它時,直接在參數中寫生成的C結構指針:
void onPing(user *u, Netmsg__PingPong *pb) {
}
直接使用一個宏DEFINE_HANDLER
來映射網絡消息ID與處理函數之間的關系,比如這樣寫:
DEFINE_HANDLER(Ping, PingPong, ping_pong);
宏的第一個參數是消息ID,第二個參數是C結構的寫法,第三個參數是函數以及描述符的寫法。
為了方便我們只寫一次映射關系,DEFINE_HANDLER
宏有兩個實現,一個是作處理函數的注冊用,另一個是實現網絡消息的解析并調用自定義的處理函數。
DEFINE_HANDLER
宏作為處理函數注冊用
DEFINE_HANDLER
宏調用自定義函數regNetMsg
來注冊消息處理函數:
typedef void (*fnCB)(user *u, char *buf, uint32_t len);
static void regNetMsg(Netmsg__NetCmdID cmd, fnCB cb);
#define DEFINE_HANDLER(NetCmdID, PbStructType, PbDescType) \extern void on_##PbStructType(user *u, char *buf, uint32_t len); \regNetMsg(NETMSG__NET_CMD_ID__##NetCmdID, on_##PbStructType);
DEFINE_HANDLER
宏實現網絡消息的解析并調用自定義的處理函數
DEFINE_HANDLER
宏內依次調用protobuf_c_message_unpack
,自定義的消息處理函數,protobuf_c_message_free_unpacked
即可:
#define DEFINE_HANDLER(NetCmdID, pbStructType, pbDescType) \
void on_##pbStructType(user *u, char *buf, uint32_t len) {\
Netmsg__##pbStructType* pb = \
protobuf_c_message_unpack(&netmsg__##pbDescType##__descriptor,\
len - sizeof(uint16_t), (const uint8_t *)&buf[sizeof(uint16_t)]);\
extern void on##NetCmdID(user *u, Netmsg__##pbStructType *pb);\
on##NetCmdID(u, pb); \
protobuf_c_message_free_unpacked((ProtobufCMessage *)pb, nullptr);
同一個宏如何實現兩個功能?
假定我們的映射代碼為net_msg.h
DEFINE_HANDLER(Ping, PingPong, ping_pong);
DEFINE_HANDLER
的實現為reg_msg.h
,在其中使用一個宏來判斷一下是第一種功能還是第二種功能,在調用前定義功能宏:
#define DEF_FUNC
#include "reg_msg.h"
這樣就可以實現只需要寫一份映射代碼,然后寫處理函數的簡便操作。但是這里有一點缺憾就是DEFINE_HANDLER
宏因為protobuf-c生成的代碼風格的緣故需要寫兩個,一個是結構的寫法PingPong
, 一個是函數以及描述符的寫法ping_pong
,而且包名的大小寫也不一致。其實要解決這個問題非常簡單,只需要在生成結構時代碼時,添加一個typedef
即可,比如PingPong
,添加一個typedef struct Netmsg__PingPong netmsg__ping_pong;
即可統一寫成DEFINE_HANDLER(Ping, ping_pong);
。
typedef struct Netmsg__PingPong Netmsg__PingPong;
typedef struct Netmsg__PingPong netmsg__ping_pong;
我曾經給官方提過一個issue,希望能添加,但官方未響應。可以自行修改代碼:
void MessageGenerator::
GenerateStructTypedef(google::protobuf::io::Printer* printer) {printer->Print("typedef struct $classname$ $classname$;\n","classname", FullNameToC(descriptor_->full_name(), descriptor_->file()));for (int i = 0; i < descriptor_->nested_type_count(); i++) {nested_generators_[i]->GenerateStructTypedef(printer);}
}
為:
void MessageGenerator::
GenerateStructTypedef(google::protobuf::io::Printer* printer) {printer->Print("typedef struct $classname$ $classname$;\n","classname", FullNameToC(descriptor_->full_name(), descriptor_->file()));printer->Print("typedef struct $classname$ ","classname", FullNameToC(descriptor_->full_name(), descriptor_->file()));printer->Print("$lcclassname$;\n","lcclassname", FullNameToLower(descriptor_->full_name(), descriptor_->file()));for (int i = 0; i < descriptor_->nested_type_count(); i++) {nested_generators_[i]->GenerateStructTypedef(printer);}
}
然后使用修改過的protoc-gen-c
來編譯PB。
下面給出完成的reg_msg.h
代碼,支持GCC和Clang編譯器:
#ifdef USE_REG
#undef USE_REG#define HANDLER_BODY(NetCmdID, PbStructType) \netmsg__##PbStructType *pb = \(netmsg__##PbStructType *)protobuf_c_message_unpack( \&netmsg__##PbStructType##__descriptor, nullptr, \len - sizeof(uint16_t), (const uint8_t *)&buf[sizeof(uint16_t)]); \extern void on##NetCmdID(user *u, netmsg__##PbStructType *pb); \on##NetCmdID(u, pb); \protobuf_c_message_free_unpacked((ProtobufCMessage *)pb, nullptr);#ifdef __clang__
#define DEFINE_HANDLER(NetCmdID, PbStructType) \static void (^on_##PbStructType)(user * u, char *buf, uint32_t len) = \^void(user * u, char *buf, uint32_t len) { \HANDLER_BODY(NetCmdID, PbStructType) \}; \regNetMsg(NETMSG__NET_CMD_ID__##NetCmdID, on_##PbStructType);
#else
#ifdef DEF_FUNC
#define DEFINE_HANDLER(NetCmdID, PbStructType) \void on_##PbStructType(user *u, char *buf, uint32_t len) { \HANDLER_BODY(NetCmdID, PbStructType) \}
#elif defined(REG_MSG)
#undef DEFINE_HANDLER
#define DEFINE_HANDLER(NetCmdID, PbStructType) \extern void on_##PbStructType(user *u, char *buf, uint32_t len); \regNetMsg(NETMSG__NET_CMD_ID__##NetCmdID, on_##PbStructType);
#endif
#endif#include "net_msg.h"#endif
調用:
static swiss_map_t *mapNetMsg = nullptr;#ifdef __clang__
#ifdef _WIN32
void *const __imp__NSConcreteGlobalBlock[32] = {nullptr};
void *const __imp__NSConcreteStackBlock[32] = {nullptr};
#else
void *const _NSConcreteGlobalBlock[32] = {nullptr};
void *const _NSConcreteStackBlock[32] = {nullptr};
#endif
// 這里必須使用Clang的塊語法
typedef void (^fnCB)(user *u, char *buf, uint32_t len);
#else
typedef void (*fnCB)(user *u, char *buf, uint32_t len);
#endifstatic void regNetMsg(Netmsg__NetCmdID cmd, fnCB cb) {swiss_map_insert(mapNetMsg, &cmd, (void *)&cb);
}// 由于GCC不支持靜態嵌套函數,所以需要將解析消息的函數定義與消息的注冊分開
// 而clang支持block語法,可以使用它寫一個靜態函數,可以直接在一起寫
#if !defined(__clang__) && defined(__GNUC__)
#define USE_REG
#define DEF_FUNC
#include "reg_msg.h"
#endifvoid RegNetMsg() {mapNetMsg =new_swiss_map(sizeof(uint16_t), hashInt16, sizeof(fnCB), equal_int16);#if !defined(__clang__) && defined(__GNUC__)#undef DEF_FUNC#define REG_MSG
#endif
#define USE_REG
#include "reg_msg.h"
}
這樣就可以非常方便地在netmsg.h
中映射網絡消息處理函數了:
DEFINE_HANDLER(ReqLogin, req_login);
DEFINE_HANDLER(Ping, ping_pong);
如果對你有幫助,歡迎點贊收藏!