服务器用protobuffer (之后简称pb)做协议包体使用了两个月, 确实体验到了很多方便的地方(接口代码易写易维护, 内部编码高效,传输快等)
但不可否认的是C++在使用它的时候存在一些很难发现的坑。
c++ pb包体序列化/反序列化的方法有三种,分别是(从输入流, 文件流, 和string)中序列化
对于网络传输的话, 用到只可能是string序列化/反序列化
也就是在正常网络编程中用到的接口函数如下:
string code= getStringFromClient();
ProtoTest::OnGame rcvBody;
rcvBody.ParseFromString(code);
......
ProtoTest::OnGame rspBody;
string codeBody;
rspBody.SerializeToString(&codeBody);
SendStringToClient(codeBody);
自己游戏正在用的服务器是基于已有框架搭建的
每一个messageID通过函数指针对应一个回调函数
所以收发数据的接口必须按如下如下格式:
int32_t process_request_playgame(CMessage& message, const char* body, const int32_t length)
int32_t send_response_to_client(const char* body, int16_t nLength, int16_t nMessageID, int32_t iSequence)
所以在收数据的时候必须把c风格字符串(body包体)赋值为string再反序列化
发数据的时候把序列化后的string赋值给C字符串(.c_str()) 再发送
第一个坑调了一天多, 还耽误了客户端很多时间, 因为没有把c风格字符串赋值给string
导致部分包体解出来(反序列化后)各字段对应的数据不正确。
解决方案就是给string赋值一下再解包…:
ProtoTest::OnGame rcvBody;
string code = "";
code.assign(&body[0], length);
rcvBody.ParseFromString(code);
第二个坑和第三个坑总结来说就是需要设置pb包体中 bytes 字段和 string 字段的长度。
pb中bytes字段可以用于处理多字节的语言字符、如中文; 也就是一些编码过的字符数组可以用它来传。
message OnGame{
required bytes gameData = 1;
required int32 m_iRoomID = 2;
required int32 m_iTableID = 3;
required int32 m_icmd = 4; //gameData process CMD
required int32 gameDataLength = 5;
}
OnGame包体中gamedata用来存序列化后的内层包体(通过m_icmd区分)。
以前set的时候是按如下写的:
int32_t CTable::send_game_data_to_player(int32_t cmd, int32_t iPlayerID,const char* pszGameData, int32_t iGameDataSize)
....
rspBody.set_gamedata(pszGameData);
....
然而这样写实际上客户端反序列化得到的数据不一定是对的。大概是因为char数组往下取,取到\0才会结束,而pszGameData很有可能中间包含多个\0,或者不包含;
看一下pb编译包体得到的.h文件中的函数接口:
void set_gamedata(const ::std::string& value);
#if LANG_CXX11
void set_gamedata(::std::string&& value);
#endif
void set_gamedata(const char* value);
void set_gamedata(const void* value, size_t size);
应该使用第四个。
同理看了前两个坑第三个坑也和c字符串指针和长度有关。
message Friend_resGetInfo{ //收
required int32 resultID = 1;
optional string m_NameOther = 2;
optional int32 sex = 3;
optional int32 exp = 4;
optional int32 battleCnt = 5;
optional int32 winCnt = 6;
optional bool isOnline = 7;
}
一个很普通的包体, 回复给客户端的代码如下:
void CPlayer::pack_res_getRole(int32_t msgid, int32_t resultID, stRoleInfo* m_stRoleInfo2 ){
ProtoTest::Friend_resGetInfo rspBody;
rspBody.set_resultid(resultID);
if(resultID == success){
rspBody.set_m_nameother(m_stRoleInfo2->name);
rspBody.set_sex(m_stRoleInfo2->sex);
rspBody.set_exp(m_stRoleInfo2->exp);
rspBody.set_battlecnt(m_stRoleInfo2->battleCnt);
rspBody.set_wincnt(m_stRoleInfo2->winCnt);
rspBody.set_isonline(true);
}
string codeBody1;
rspBody.SerializeToString(&codeBody1);
int16_t outLen = codeBody1.length();
send_response_to_client(codeBody1.c_str(), outLen, msgid, 0);
}
很难看出这段代码执行多次会导致服务器崩溃, gdb的core定位是如下:
(gdb) where
#0 0x00007fc75b7f95e5 in raise () from /lib64/libc.so.6
#1 0x00007fc75b7fadc5 in abort () from /lib64/libc.so.6
#2 0x00007fc75b8374f7 in __libc_message () from /lib64/libc.so.6
#3 0x00007fc75b83cf3e in malloc_printerr () from /lib64/libc.so.6
#4 0x00007fc75b83fdd0 in _int_free () from /lib64/libc.so.6
#5 0x00000000004735ff in ProtoTest::Friend_resGetInfo::SharedDtor() () at /usr/local/include/google/protobuf/arenastring.h:258
#6 0x00000000004760fc in ProtoTest::Friend_resGetInfo::~Friend_resGetInfo() () at package.pb.cc:17443
#7 0x0000000000422a3e in GameJayo::Server::LogicServer::CPlayer::pack_res_getRole(int, int, GameJayo::Server::LogicServer::stRoleInfo*) () at logicserver_player.cpp:1156
很显然析构Friend_resGetInfo对象的时候失败了 , 然后这个bug让自己查到吐血查了一天…
最后(baoge)发现m_nameOther字段接口函数如下:
void set_m_nameother(const ::std::string& value);
#if LANG_CXX11
void set_m_nameother(::std::string&& value);
#endif
void set_m_nameother(const char* value);
void set_m_nameother(const char* value, size_t size);
我使用的是第三个…
最后总结一句: c++给string赋值c字符串的时候一定一定要指定长度。