Sproto c++
全篇是我skynet学习的收尾部分,大概搞懂了底层逻辑,标志着对skynet 和 sproto核心有了初步认识;这里基于github上一个项目,该项目过老,现将其适配更新后的sproto,用在这自己写的一个网络游戏项目里面,CS通信有了质的飞跃。
sproto 主要逻辑都实现在sproto.c中,开发者云风给需要绑定的使用者设计了callback 和 绑定语言对应的数据结构ud,所以使用者只需设计这两个参数即可。
- callback的任务是根据不同的数据类型返回在特定语言的字节数
- ud 在encode时负责数据的输入,decode时负责数据的输出
基于github的开源项目 sproto-cpp
主要时用paser将sproto schema 语言写的字符串生成二进制文件,在C中直接调用该文件,如此就和lua无关了。
但该项目比较久远,sproto做了些更新,我看到的几点有:double类型、控制流程的几个宏(主要时输出判断循环终止,如SPROTO_CB_ERROR、SPROTO_CB_NIL循环break)。
我修改的地方,在encode回调函数里。解码回调函数貌似不用改,能正常使用。
1、整数
case SPROTO_TINTEGER:{
...
if(args->extra){
int64_t v = (int64_t)(round(field_value * args->extra));
field_value = v;
}
...
}
2、递归叶子判断
case SPROTO_TSTRUCT:
{
SprotoMessage* submsg = ep->msg->GetStructField(tagname, index);
if (submsg == NULL){
std::cout << "SPROTO_TSTRUCT : error" <<std::endl;
//return 0;
return SPROTO_CB_NIL;// update,定义为-2
}
}
sproto 的组织方式
- 我以为,从sproto_dump()C语言函数的输出可以看出:
分为两种,数据类型(type) 和 消息类型(protocol)。
数据类型可用 . 来自定义;消息类型可用已有的数据类型来组织,其格式一定是包含request和response的表,来表明数据接收的格式和发出的格式(针对lua而言);对C++的binding,事实上消息类型由自定义的类来确定,在编码时只用到sproto.pb二进制中自定义的数据类型就可以了;那是否意味着在C++ 的sproto中不需要用到消息类型了呢?否,因为和skynet lua通信,交换的数据包先是“>s2”编码带长度的吧,然后是skynet中规定的package头 和 主体的信息拼接而成,package头包括type 和session,type就是消息类型中对应的tag,所以消息类型必须。
.package {
type 0 : integer
session 1 : integer
}
- C端处理lua发来的数据包
首先,数据包需要先处理2字节的长度信息再对剩余的二进制字段sproto_unpack得到二进制串bin,然后把bin分为两部分,header和content,header长度由.package编码后的二进制串长度确定,这得事先就用参数记好。然后,先将header 进行decode,得到type 和session(有个问题,session是0代表nil吗,还是0也是有效session,这里假设是前者),用type值通过query_proto函数查询得到 struct protocol 结构体指针,里面有其request和response对应的sproto_type。然后可以用sproto_type来解码content。
if header.type then
-- request
local proto = queryproto(self.__proto, header.type)
local result
if proto.request then
result = core.decode(proto.request, content)
end
if header_tmp.session then
return "REQUEST", proto.name, result, gen_response(self, proto.response, header_tmp.session), header.ud
else
return "REQUEST", proto.name, result, nil, header.ud
end
else
-- response
local session = assert(header_tmp.session, "session not found")
local response = assert(self.__session[session], "Unknown session")
self.__session[session] = nil
if response == true then
return "RESPONSE", session, nil, header.ud
else
local result = core.decode(response, content)
return "RESPONSE", session, result, header.ud
end
end
根据这段host:dispatch的lua源代码可知,根据type(也就是tag)判断是否需要回复(request(非 nil) 或 response(nil值)),也侧面说明了不存在tag为nil的protocol。在消息为request类型的情况,根据session判断是(非nil)否(nil值)有回复函数;在response类型情况,根据session判断收到的回复数据发往何处。
-
封装C端发送给lua的数据包
先encode 头header,附上所用的protocol的tag,如果需要恢复,就把session设为非0值。然后encode 所用的协议及填充内容得到content。把两端二进制数据连接起来得到bin。对bin进行sproto_pack,再加上两字节的长度信息,大端。 -
C中的nil
在设置session时,如要设为nil,可以做如下改动,把0值定为nil:
bool Package::GetIntegerField(const char* name, int index,
int64_t& value)
{
if (strcmp(name, "type") == 0)
{
value = type;
return true;
}
else if (strcmp(name, "session") == 0)
{
if (session == 0) {//规定session为0就为空,对应lua的nil
return false;
}
else
{
value = session;
return true;
}
}
else
{
return false;
}
}
在绑定的encode函数里,会:
if (!ep->msg->GetIntegerField(tagname, index, field_value)) {
std::cout << "SPROTO_TINTEGER : NIL" << std::endl;
return SPROTO_CB_NIL;
}
在C中,解码时,若数据项为nil,则不会改变message结构体中的对应变量值。
- sproto_pack和sproto_unpack的输出长度问题
经过实验发现,两者的输出可能不同,unpack的输出可能会大与pack的输出,但这并不影响数据的解码,且恰好以为这一特性,加之sproto_decode的输入数据size可以大于数据编码长度本身,可以动态地确定编码串中某一类消息所占的长度偏移,从而从左往右依次解码数据。如在package包中,有无session对其编码后的长度有影响,故在解码时根据decode的输出来判断其编码长度。