C++中消息自动派发之一 About JSON

1. 闲序

  游戏服务器之间通信大多采用异步消息通信。而消息打包常用格式有:google protobuff,facebook thrift, 千千万万种自定义二进制格式,和JSON。前三种都是二进制格式,针对C++开发者都是非常方便的,效率和包大小(数据冗余度)也比较理想。而JSON是字符串协议,encode和decode需要不小的开销。500字节json字符串解析大约需要1ms左右。JSON在脚本语言中非常常见,比如WEB应用、Social Game等,原因是web应用通过多进程分摊了JSON解析的CPU开销,而且这些应用实时性不强。JSON相对于二进制协议有点就是它是自描述的,调试JSON消息非常的方便,如果消息出错简单的将消息log到文件,肉眼即可分辨真伪(眼力不行,有工具相帮http://www.jsoneditoronline.org/,更多工具参见http://json.org/)。事实上json由于是字符串,压缩传输也可以达到比较理想的压缩比。

  我们的Social Game 客户端都是Flash,Flash 工程师们非常喜欢使用Json,几款游戏Flash和Php通信都是使用Json。新的游戏支持实时对战,后台使用c++实现,我们仍然采用JSON。在后台计算时为了保证实时性,我们一般把json解析放到网络线程(多线程),解析成c++的struct 特定类型再post到逻辑线程(单线程)处理。这样Json的解析可以分摊到多个CPU上,并且不浪费主逻辑线程cpu。

  目前遇到的问题是,如果每增加一个接口,就增加一个struct,再在网络处理逻辑函数中增加json解析代码(包括错误处理),再跟flash联调协议。还有一个挺烦人的时接口文档每次都要更新,如果直接把定义struct的头文件给flash,但是貌似不太优雅,还是有份文档比较正式。

  我参考了一下google protobuf 和 facebook thrift,想设计如下消息定义方式。

2. 定义idl文件

  interface description language ?其实我只有消息格式描述,并无接口,但是idl比较容易接受。

  假如说需要一个消息描述student的数据,那么使用 我定义idl描述其内容如下,student.idl

复制代码
//! 定义student消息格式,版本号1
stuct student_t(version=1)
{
//! 描述student需要子类型book
  struct book_t(version=1)
{
//! book中包含两个字段,ages 16位数字,content字符串,可为空,默认值为”oh nice“
        int16  pages;
      string content(default="Oh Nice!");
 }
//! 定义年龄,分数,姓名,都是基本类型
//! 定义friends为数组,单项类型为字符串,对应json数组
//! 定义books为字典,key为字符串,项为book结构,对应json对象结构
  int8 age;
  float grade(default=0);
  string name;
  array<string> friends;
  dictionary<string, book_t> books;
};
复制代码

3. 使用idl 代码生成器生成消息定义c++ 头文件

   idl_generator.py student.idl -l cpp -o msg_def.h

      生成msg_def.h

      idl_generator.py@ http://ffown.googlecode.com/svn/trunk/fflib/lib/generator/

4. 使用生成的C++ 消息头文件

  生成的头文件内容是:

  

复制代码
struct student_t
{
struct book_t
{
int16_t pages;
string contents;
};
int8_t age;
float grade;
string name;
vector<string> friends;
map<string, book_t> books;
};
//! 模板类,T为回调对象类型,每种msg 类型T中都需要定义相应的handle函数, R代表请求的socket类型指针,这里使用泛型表示
template<typename T, typename R>
class msg_dispather_t
{
typedef runtime_error msg_exception_t;//!请求格式出错,抛出异常
typedef rapidjson::Document json_dom_t;   //! 使用rapidjson库实现json解析,但是某个时刻可能替换该库,故typedef
typedef rapidjson::Value json_value_t; //! rapidjson源代码:http://code.google.com/p/rapidjson/
typedef R socket_ptr_t;  //! 请求socket
typedef int (msg_dispather_t<T, R>::*reg_func_t)(const json_value_t&, socket_ptr_t); //! 消息对应的解析函数
public:
msg_dispather_t(T& msg_handler_):
m_msg_handler(msg_handler_)
{
m_reg_func["student_t"] = &msg_dispather_t<T, R>::student_t_dispacher;//! 所有的消息都在构造时注册解析函数,解析函数是通过idl自动生成的
}
int dispath(const string& json_, socket_ptr_t sock_);//! 接口函数,使用者只需单点接入dispatch,消息会自动派发到msg_handler特定的handle函数

private:
int student_t_dispacher(const json_value_t& jval_, socket_ptr_t sock_)//! 每个消息都会自动生成特定的消息解析函数,前缀为消息名称
{
student_t s_val;
const json_value_t& age = jval_["age"];
const json_value_t& grade = jval_["grade"];
const json_value_t& name = jval_["name"];
const json_value_t& friends = jval_["friends"];
const json_value_t& books = jval_["books"];
char buff[512];

if (false == age.IsNumber())
{
snprintf(buff, sizeof(buff), "student::age[int] field needed");
throw msg_exception_t(buff);
}
s_val.age = age.GetInt();
if (false == grade.IsDouble())
{
snprintf(buff, sizeof(buff), "student::grade[float] field needed");
throw msg_exception_t(buff);
}
s_val.grade = grade.GetDouble();
if (false == name.IsString())
{
snprintf(buff, sizeof(buff), "student::name[string] field needed");
throw msg_exception_t(buff);
}
s_val.name = name.GetString();
if (false == friends.IsArray())
{
snprintf(buff, sizeof(buff), "student::friends[Array] field needed");
throw msg_exception_t(buff);
}
for (rapidjson::SizeType i = 0; i < friends.Size(); i++)
{
const json_value_t& val = friends[i];
if (false == val.IsString())
{
snprintf(buff, sizeof(buff), "student::friends field at[%u] must string", i);
throw msg_exception_t(buff);
}
s_val.friends.push_back(val.GetString());
}
if (false == books.IsObject())
{
snprintf(buff, sizeof(buff), "student::books[Object] field needed");
throw msg_exception_t(buff);
}
rapidjson::Document::ConstMemberIterator it = books.MemberBegin();
for (; it != books.MemberEnd(); ++it)
{
student_t::book_t book_val;
const json_value_t& name = it->name;
if (false == name.IsString())
{
snprintf(buff, sizeof(buff), "student::books[Object] key must [string]");
throw msg_exception_t(buff);
}

const json_value_t& val = it->value;
if (false == val.IsObject())
{
snprintf(buff, sizeof(buff), "student::books[Object] value must [Object]");
throw msg_exception_t(buff);
}

const json_value_t& book_pages = val["pages"];
const json_value_t& book_contens = val["contents"];
if (false == book_pages.IsNumber())
{
snprintf(buff, sizeof(buff), "student::books::pages[Number] field needed");
throw msg_exception_t(buff);
}
book_val.pages = book_pages.GetInt();
if (false == book_contens.IsString())
{
snprintf(buff, sizeof(buff), "student::books::book_contens[String] field needed");
throw msg_exception_t(buff);
}
book_val.contents = book_contens.GetString();
s_val.books[name.GetString()] = book_val;
}

m_msg_handler.handle(s_val, sock_);//! 由于msg_handler中重载了针对所有消息的handle函数,此函数会被正确的派发到逻辑层
return 0;
}

private:
T& m_msg_handler;
map<string, reg_func_t> m_reg_func;
};

template<typename T, typename R>
int msg_dispather_t<T, R>::dispath(const string& json_, socket_ptr_t sock_)
{
json_dom_t document; // Default template parameter uses UTF8 and MemoryPoolAllocator.
if (document.Parse<0>(json_.c_str()).HasParseError())
{
throw msg_exception_t("json format not right");
}
if (false == document.IsObject() && false == document.Empty())
{
throw msg_exception_t("json must has one field");
}

const json_value_t& val = document.MemberBegin()->name;
const char* func_name = val.GetString();
typename map<string, reg_func_t>::const_iterator it = m_reg_func.find(func_name);

if (it == m_reg_func.end())//! 查找解析派发函数是否存在
{
char buff[512];
snprintf(buff, sizeof(buff), "msg not supported<%s>", func_name);
throw msg_exception_t(buff);
return -1;
}
reg_func_t func = it->second;

(this->*func)(document.MemberBegin()->value, sock_);
return 0;
}
复制代码

 

 5. 逻辑层处理消息

  逻辑层不需要编写繁杂的json解析和错误处理,只要没有触发异常,消息会自动派发到msg_handler中的handle函数,所以逻辑层只需针对每一个消息类型

都重载一个handle函数即可,示例处理代码如下:

复制代码
class msg_handler_t
{
public:
typedef int socket_ptr_t;
public:
void handle(const student_t& s_, socket_ptr_t sock_)
{
cout << "msg_handler_t::handle:\n";
cout << "age:" << int(s_.age) << " grade:" << s_.grade << " friends:"<< s_.friends.size() << " name:"
<< s_.name << " books:" << s_.books.size() <<"\n";
}
};

int main(int argc, char* argv[])
{
try
{
string tmp = "{\"student_t\":{\"age\":123,\"grade\":1.2,\"name\":\"bible\",\"friends\":[\"a\",\"b\"],"
"\"books\":{\"bible\":{\"pages\":123,\"contents\":\"oh nice\"}}}}";
msg_handler_t xxx;
msg_dispather_t<msg_handler_t, msg_handler_t::socket_ptr_t> p(xxx);
p.dispath(tmp, 0);
}
catch(exception& e)
{
cout <<"e:"<< e.what() <<"\n";
}
}
复制代码

示例代码: http://ffown.googlecode.com/svn/trunk/fflib/lib/generator/

6. More 

  1> json解析目前使用 rapidjson,号称效率极佳,此处用它最大的好处是只需包含头文件即可使用

  2> 分析解析idl 文件程序使用python编写(正在编写中)

  3> idl 定义中支持namespace 为佳,但考虑复杂性,第一版本暂不支持。

    4> 本篇只实现了json to struct,实际上 struct to struct 也很容易实现,json 字符串的第一个字符为'{',而如果采用二进制消息,第一个字符表示消息类型的字符串长度(一个字节足以),如"sdudent_t",那么首字节应该为9,并且设定首字节首位为1,那么描述类型的字符串长度最大为128个字符(足以了)。放到下篇再搞,睡了。

 
 
标签: structjsonthriftprotobuff
3
0
 
(请您对文章做出评价)
 
« 博主前一篇:智能指针shared_ptr【无锁设计基于GCC】
» 博主后一篇:C++中消息自动派发之二 About IDL解析器
posted @ 2012-02-14 23:03 知然 阅读(1266) 评论(11编辑 收藏
 

 
 回复 引用 查看   
#1楼 2012-02-17 18:07 yu_yu      
思想很好,注册,回调
只是使用还不太方便,没有做到应用层只关注业务,注册新的业务还要修改已有的代码~
有部分原因是没有reflection
 回复 引用 查看   
#2楼[楼主2012-02-18 11:16 知然      
@yu_yu
注册新的业务不需要修改已有代码,只需重载一个handle函数处理新的消息
 回复 引用 查看   
#3楼 2012-02-19 19:50 yu_yu      
student_t_dispacher 解析是在msg_dispacher_t里,也就是说要增加新的业务,那还得改动代码,是不是可以在考虑下呢?
 回复 引用 查看   
#4楼 2012-02-20 00:16 yswang      
先顶下楼主,楼上不要纠结那个dispacher了,c++本来就不适合做反射,楼主的精华也不在那。
顶:
开发工具化是很好的方向
拍些砖:
1.不需要额外的定义语言,用c++就行,简单好维护兼容二进制消息
2.你测了rapid的效率了吗,和其他比有多少提高?
3.里面的异常都在if下抛出,能不能直接返回错误,抛异常会影响效率
4.消息回调有点丑。
5.没看到makefile,速速更新上来。
 回复 引用 查看   
#5楼 2012-02-20 00:20 yswang      
从项目角度上说,小项目,会带来额外的解析器维护成本,如果没发展成熟应该很难让第三方投入使用。
 回复 引用 查看   
#6楼[楼主2012-02-20 00:28 知然      
@yu_yu
msg_dispacher_t 是由python脚本分析idl文件自动生成的代码,脚本http://ffown.googlecode.com/svn/trunk/fflib/lib/msg_generator/
接下来我会介绍一下这个脚本
 回复 引用 查看   
#7楼[楼主2012-02-20 00:33 知然      
@yswang
我个人还未测试rapid,目前其也只到了0.1版,我这里使用它主要是它可以直接引用头文件,有时间一定测试一下。关于异常,建议很好,不过目前该版本还在测试,这个可以等到优化环节完成。Makefile就不跟上了,不需要引入其他库,可以直接g++
http://ffown.googlecode.com/svn/trunk/fflib/lib/msg_generator/, g++ test.cpp 即可使用示例代码
 回复 引用 查看   
#8楼[楼主2012-02-20 00:36 知然      
@yswang
这里解析器是非常关键的环节,一个报错友好的解析器还真是需要很多功夫,我即使使用python脚本仍然要花很大时间完成
 回复 引用 查看   
#9楼 2012-02-20 00:38 yswang      
@知然
makefile主要那个要有脚本自动在编译代码之前生产c++文件,好自动编译
 回复 引用 查看   
#10楼 2012-02-20 00:39 yswang      
@知然
顶下楼主,我等懒人就只能靠楼主啦
 回复 引用 查看   
#11楼[楼主2012-02-20 08:51 知然      
@yswang
蛮好的建议,如果每次make都要重新生成一遍代码,那么会使许多文件重新编译,还需要在自动脚本中加一些‘check’,后期我会尝试一下

 

 
posted @ 2012-05-30 20:34  姚康  阅读(710)  评论(0编辑  收藏  举报