C语言使用Protobuf进行网络通信
笔者前面博文 Go语言网络游戏服务器模块化编程介绍了Go语言在开发网络游戏时如何进行模块化编程,在其中使用了Protobuf进行网络通信。在Protobuf官方实现中并没有生成C语言的实现,不过有一个开源的 protobuf-c可以使用。
先来看看 protobuf-c生成的代码,假如如下PB:
1syntax = "proto3";
2package netmsg;
3
4enum NetCmdID
5{
6 CmdNone = 0;
7 Ping = 1;
8 Pong = 2;
9}
10
11message PingPong
12{
13 uint32 time = 1;
14}
生成的C代码类似这样:
1typedef struct Netmsg__PingPong Netmsg__PingPong;
2
3struct Netmsg__PingPong
4{
5 ProtobufCMessage base;
6 uint32_t time;
7};
8#define NETMSG__PING_PONG__INIT \
9 { PROTOBUF_C_MESSAGE_INIT (&netmsg__ping_pong__descriptor) \
10, 0 }
11
12/* Netmsg__PingPong methods */
13void netmsg__ping_pong__init
14 (Netmsg__PingPong *message);
15size_t netmsg__ping_pong__get_packed_size
16 (const Netmsg__PingPong *message);
17size_t netmsg__ping_pong__pack
18 (const Netmsg__PingPong *message,
19 uint8_t *out);
20size_t netmsg__ping_pong__pack_to_buffer
21 (const Netmsg__PingPong *message,
22 ProtobufCBuffer *buffer);
23Netmsg__PingPong *
24 netmsg__ping_pong__unpack
25 (ProtobufCAllocator *allocator,
26 size_t len,
27 const uint8_t *data);
28void netmsg__ping_pong__free_unpacked
29 (Netmsg__PingPong *message,
30 ProtobufCAllocator *allocator);
31
32extern const ProtobufCMessageDescriptor netmsg__ping_pong__descriptor;
可以看到,PingPong
在生成的C代码中结构是原样输出的,而函数中又是以C风格ping_pong
输出的;同时包名netmsg
在结构中首字母是大写,而在函数中又是全部小写。
如果要解析PB,它提供了一个API:
1PROTOBUF_C__API
2ProtobufCMessage *
3protobuf_c_message_unpack(
4 const ProtobufCMessageDescriptor *descriptor,
5 ProtobufCAllocator *allocator,
6 size_t len,
7 const uint8_t *data);
这里需要传入PB的描述符结构ProtobufCMessageDescriptor *
,针对PingPong消息即netmsg__ping_pong__descriptor
的地址。调用后会返回一个ProtobufCMessage *
,只需要强转成Netmsg__PingPong*
即可。这个返回值在使用完后需要调用函数protobuf_c_message_free_unpacked
释放内存:
1PROTOBUF_C__API
2void
3protobuf_c_message_free_unpacked(
4 ProtobufCMessage *message,
5 ProtobufCAllocator *allocator);
笔者想要的理想情况是与前面博文 Go语言网络游戏服务器模块化编程中的一样,可以在网络消息处理函数中直接写需要处理的具体的PB消息。
在处理它时,直接在参数中写生成的C结构指针:
1void onPing(user *u, Netmsg__PingPong *pb) {
2}
直接使用一个宏DEFINE_HANDLER
来映射网络消息ID与处理函数之间的关系,比如这样写:
1DEFINE_HANDLER(Ping, PingPong, ping_pong);
宏的第一个参数是消息ID,第二个参数是C结构的写法,第三个参数是函数以及描述符的写法。
为了方便我们只写一次映射关系,DEFINE_HANDLER
宏有两个实现,一个是作处理函数的注册用,另一个是实现网络消息的解析并调用自定义的处理函数。
DEFINE_HANDLER
宏作为处理函数注册用
DEFINE_HANDLER
宏调用自定义函数regNetMsg
来注册消息处理函数:
1typedef void (*fnCB)(user *u, char *buf, uint32_t len);
2static void regNetMsg(Netmsg__NetCmdID cmd, fnCB cb);
1#define DEFINE_HANDLER(NetCmdID, PbStructType, PbDescType) \
2 extern void on_##PbStructType(user *u, char *buf, uint32_t len); \
3 regNetMsg(NETMSG__NET_CMD_ID__##NetCmdID, on_##PbStructType);
DEFINE_HANDLER
宏实现网络消息的解析并调用自定义的处理函数
DEFINE_HANDLER
宏内依次调用protobuf_c_message_unpack
,自定义的消息处理函数,protobuf_c_message_free_unpacked
即可:
1#define DEFINE_HANDLER(NetCmdID, pbStructType, pbDescType) \
2void on_##pbStructType(user *u, char *buf, uint32_t len) {\
3Netmsg__##pbStructType* pb = \
4protobuf_c_message_unpack(&netmsg__##pbDescType##__descriptor,\
5len - sizeof(uint16_t), (const uint8_t *)&buf[sizeof(uint16_t)]);\
6extern void on##NetCmdID(user *u, Netmsg__##pbStructType *pb);\
7on##NetCmdID(u, pb); \
8protobuf_c_message_free_unpacked((ProtobufCMessage *)pb, nullptr);
同一个宏如何实现两个功能?
假定我们的映射代码为net_msg.h
1DEFINE_HANDLER(Ping, PingPong, ping_pong);
DEFINE_HANDLER
的实现为reg_msg.h
,在其中使用一个宏来判断一下是第一种功能还是第二种功能,在调用前定义功能宏:
1#define DEF_FUNC
2#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);
。
1typedef struct Netmsg__PingPong Netmsg__PingPong;
2typedef struct Netmsg__PingPong netmsg__ping_pong;
我曾经给官方提过一个issue,希望能添加,但官方未响应。可以自行修改代码:
1void MessageGenerator::
2GenerateStructTypedef(google::protobuf::io::Printer* printer) {
3 printer->Print("typedef struct $classname$ $classname$;\n",
4 "classname", FullNameToC(descriptor_->full_name(), descriptor_->file()));
5
6 for (int i = 0; i < descriptor_->nested_type_count(); i++) {
7 nested_generators_[i]->GenerateStructTypedef(printer);
8 }
9}
为:
1void MessageGenerator::
2GenerateStructTypedef(google::protobuf::io::Printer* printer) {
3 printer->Print("typedef struct $classname$ $classname$;\n",
4 "classname", FullNameToC(descriptor_->full_name(), descriptor_->file()));
5
6 printer->Print("typedef struct $classname$ ",
7 "classname", FullNameToC(descriptor_->full_name(), descriptor_->file()));
8
9 printer->Print("$lcclassname$;\n",
10 "lcclassname", FullNameToLower(descriptor_->full_name(), descriptor_->file()));
11
12 for (int i = 0; i < descriptor_->nested_type_count(); i++) {
13 nested_generators_[i]->GenerateStructTypedef(printer);
14 }
15}
然后使用修改过的protoc-gen-c
来编译PB。
下面给出完成的reg_msg.h
代码,支持GCC和Clang编译器:
1#ifdef USE_REG
2#undef USE_REG
3
4#define HANDLER_BODY(NetCmdID, PbStructType) \
5 netmsg__##PbStructType *pb = \
6 (netmsg__##PbStructType *)protobuf_c_message_unpack( \
7 &netmsg__##PbStructType##__descriptor, nullptr, \
8 len - sizeof(uint16_t), (const uint8_t *)&buf[sizeof(uint16_t)]); \
9 extern void on##NetCmdID(user *u, netmsg__##PbStructType *pb); \
10 on##NetCmdID(u, pb); \
11 protobuf_c_message_free_unpacked((ProtobufCMessage *)pb, nullptr);
12
13#ifdef __clang__
14#define DEFINE_HANDLER(NetCmdID, PbStructType) \
15 static void (^on_##PbStructType)(user * u, char *buf, uint32_t len) = \
16 ^void(user * u, char *buf, uint32_t len) { \
17 HANDLER_BODY(NetCmdID, PbStructType) \
18 }; \
19 regNetMsg(NETMSG__NET_CMD_ID__##NetCmdID, on_##PbStructType);
20#else
21#ifdef DEF_FUNC
22#define DEFINE_HANDLER(NetCmdID, PbStructType) \
23 void on_##PbStructType(user *u, char *buf, uint32_t len) { \
24 HANDLER_BODY(NetCmdID, PbStructType) \
25 }
26#elif defined(REG_MSG)
27#undef DEFINE_HANDLER
28#define DEFINE_HANDLER(NetCmdID, PbStructType) \
29 extern void on_##PbStructType(user *u, char *buf, uint32_t len); \
30 regNetMsg(NETMSG__NET_CMD_ID__##NetCmdID, on_##PbStructType);
31#endif
32#endif
33
34#include "net_msg.h"
35
36#endif
调用:
1static swiss_map_t *mapNetMsg = nullptr;
2
3#ifdef __clang__
4#ifdef _WIN32
5void *const __imp__NSConcreteGlobalBlock[32] = {nullptr};
6void *const __imp__NSConcreteStackBlock[32] = {nullptr};
7#else
8void *const _NSConcreteGlobalBlock[32] = {nullptr};
9void *const _NSConcreteStackBlock[32] = {nullptr};
10#endif
11// 这里必须使用Clang的块语法
12typedef void (^fnCB)(user *u, char *buf, uint32_t len);
13#else
14typedef void (*fnCB)(user *u, char *buf, uint32_t len);
15#endif
16
17static void regNetMsg(Netmsg__NetCmdID cmd, fnCB cb) {
18 swiss_map_insert(mapNetMsg, &cmd, (void *)&cb);
19}
20
21// 由于GCC不支持静态嵌套函数,所以需要将解析消息的函数定义与消息的注册分开
22// 而clang支持block语法,可以使用它写一个静态函数,可以直接在一起写
23#if !defined(__clang__) && defined(__GNUC__)
24#define USE_REG
25#define DEF_FUNC
26#include "reg_msg.h"
27#endif
28
29void RegNetMsg() {
30 mapNetMsg =
31 new_swiss_map(sizeof(uint16_t), hashInt16, sizeof(fnCB), equal_int16);
32
33#if !defined(__clang__) && defined(__GNUC__)
34 #undef DEF_FUNC
35 #define REG_MSG
36#endif
37#define USE_REG
38#include "reg_msg.h"
39}
这样就可以非常方便地在netmsg.h
中映射网络消息处理函数了:
1DEFINE_HANDLER(ReqLogin, req_login);
2DEFINE_HANDLER(Ping, ping_pong);
如果对你有帮助,欢迎点赞收藏!
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2025/2025-07-08-C语言使用Protobuf进行网络通信/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。