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“

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个字符(足以了)。放到下篇再搞,睡了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

Python with提前退出:坑与解决方案

? 问题的起源 早些时候使用with实现了一版全局进程锁,希望实现以下效果: ? 全局进程锁本身不用多说,大部分都依靠外部的缓存来实现的,redis上用的是s...

2675
来自专栏积累沉淀

Java设计模式(十四)----模板方法模式

模板方法模式 一、概述 二、结构 三、具体案例 四、优缺点和设计思想 一、概述 模板方法模式是类的行为模...

1875
来自专栏技术小黑屋

译文:理解Java中的弱引用

不久之前,我面试了一些求职Java高级开发工程师的应聘者。我常常会面试他们说,“你能给我介绍一些Java中得弱引用吗?”,如果面试者这样说,“嗯,是不是垃圾回收...

852
来自专栏余林丰

Java线程安全性中的对象发布和逸出

发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系。 什么是发布?简...

1899
来自专栏進无尽的文章

如何优化冗长的条件语句

【1】尽量少用 else 尽量多用 if reture 的语法方式。 【2】字典的逻辑对应转化作用。 【3】用多态替代条件语句 【4】策略模式,继承重写,...

691
来自专栏竹清助手

深入理解 Laravel Eloquent(三)——模型间关系(关联)

Eloquent 是一个 ORM,全称为 Object Relational Mapping,翻译为 “对象关系映射”(如果只把它当成 Database A...

653
来自专栏Python爬虫与算法进阶

学点算法之队列的学习及应用

约瑟夫问题 约瑟夫问题 有 n 个囚犯站成一个圆圈,准备处决。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过 k-1个...

3487
来自专栏CSDN技术头条

JavaScript内存管理机制以及四种常见的内存泄漏解析

几个星期前,我们开始编写深入研究JavaScript工作原理的系列文章。通过阅读这些文章,你可以了解到JavaScript的构建块及其交互原理,从而能够编写出更...

17610
来自专栏草根专栏

.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象

本文是第2篇, 介绍的是如何避免在构建对象时写出不易测试的代码. 本文的概念性内容大部分都来自Misko Hevery的这篇博客文章.

892
来自专栏恰同学骚年

设计模式的征途—21.迭代器(Iterator)模式

我们都用过电视机遥控器,通过它我们可以进行开机、关机、换台、改变音量等操作。我们可以将电视机看做一个存储电视频道的集合对象,通过遥控器可以对电视机中的频道集合进...

592

扫描关注云+社区