QQ交流群:198941541
解包器必须从ascs::i_unpacker继承,i_unpacker接口定义如下:
template<typename MsgType>
class i_unpacker
{
public:
typedef MsgType msg_type;
typedef const msg_type msg_ctype;
typedef std::list<msg_type> container_type;
typedef ASCS_RECV_BUFFER_TYPE buffer_type;
bool stripped() const {return _stripped;}
void stripped(bool stripped_) {_stripped = stripped_;}
protected:
i_unpacker() : _stripped(true) {}
virtual ~i_unpacker() {}
public:
virtual void reset() = 0;
virtual void dump_left_data() const {}
//heartbeat must not be included in msg_can, otherwise you must handle heartbeat at where you handle normal messages.
virtual bool parse_msg(size_t bytes_transferred, container_type& msg_can) = 0;
virtual size_t completion_condition(const asio::error_code& ec, size_t bytes_transferred) {return 0;}
virtual buffer_type prepare_next_recv() = 0;
//this default implementation is meaningless, just satisfy compilers
virtual char* raw_data(msg_type& msg) const {return const_cast<char*>(msg.data());}
virtual const char* raw_data(msg_ctype& msg) const {return msg.data();}
virtual size_t raw_data_len(msg_ctype& msg) const {return msg.size();}
private:
bool _stripped;
};
有些接口已经有了默认实现,可能原因是它们不是必须的,注意默认实现并不一定有意义,只是为了让不使用它们的用户,在不重写它们时,也能通过编译而已,下面我们来详细介绍每一个接口:
bool stripped() const {return _stripped;}
void stripped(bool stripped_) {_stripped = stripped_;}
是否剥离消息,即返回的消息,是否包涵协议部分,默认为剥离消息(即不包涵协议部分),当然这只是我的建议,因为最终解包器是用你来写,你也可以不遵守这个约定,ascs并不关心这个属性。
virtual void reset() = 0;
在对象被重用时,解包出错时由框架调用,所有解包器都会有自己的成员变量,所以这个函数必须重写,还得好好写,否则可能造成你的解包器在解包出错之后再也解不出新消息来。
virtual void dump_left_data() const
在on_unpack_error里面由框架调用,推荐的功能是打印所有未解出消息的数据便于调试,可以不重写。
virtual buffer_type prepare_next_recv() = 0;
在开始数据读取之前,asio需要一个缓存对象,比如asio::mutable_buffer,你必须返回一个有效的缓存,且其生命周期(真正缓存的生命周期,不是缓存对象)必须持续到 parse_msg调用之后。
virtual size_t completion_condition(const asio::error_code& ec, size_t bytes_transferred)
是否结束数据读取(并开始消息解析——调用parse_msg),返回0代表结束(你应该至少收到了一个完整的消息才返回0,这里说至少是考虑到粘包问题,可能一次读取就能得到多个消息,但你也不能故意等到读取到多个消息之后才返回0,这样会让前面的消息得不到及时的处理),非0表示在下次调用completion_condition之前,最多可以读取的数据字节数(可以返回一个非常大的值而不用考虑缓存溢出,asio会先保证缓存的安全,其次再参考你提供的读取大小),下次调用completion_condition的时候,库会告诉总共读取的字节数(bytes_transferred),在completion_condition返回0之前,所有回调completion_condition里面的bytes_transferred都累加的,返回0之后bytes_transferred将归零0并重新累计(这是asio的设计)。库会先调用prepare_next_recv,然后调用一次completion_condition,显然这次调用里面的bytes_transferred为0(归零0并重新累计),解包器返回一个最多可以读取的数据字节数,然后asio开始异步读取数据(调用socket的async_read_some),这一读会得到两个结果,一个是缓存(prepare_next_recv返回的缓存)还没有写满,此时asio将再次调用completion_condition,由解包器来决定是继续读(返回大于0)还是结束读(返回0),继续读即重新上面的过程,结束读的话,库会结束这次async_read并开始解包(调用解包器的parse_msg);另一个是缓存已经写满,此时在低版本asio下面,asio仍然会再调用一次completion_condition(多余),此时completion_condition显然应该返回0(你的解包器应该考虑这种情况),如果在高版本asio下面,asio不会再次调用completion_condition,而是直接结束async_read,然后库开始解包(调用解包器的parse_msg)。建议你的解包器支持这两种asio行为,所以在写解包器的时候,头脑中保持一点,有可能completion_condition并没有被调用(第一次除外)就直接到了parse_msg了,如果你在completion_condition(非第一次)里面做了一些逻辑,一定要在parse_msg里面也有相应的逻辑。举个例子,你在completion_condition里解了包头(假设包头是长度)以确定什么时候返回0(结束这次async_read并开始解包),并将消息长度保存在解包器的一个成员变量里面,这个逻辑显然在第一次completion_condition里面是做不了的(还没有接收到任何数据),于是你在非第一次的completion_condition里面解包头,然后在parse_msg里面就可以直接得到消息长度了,可是在高版本asio下面,你在parse_msg里面得到的消息长度可能就不正确了(仅是一个初始值),这是因为asio只调用了一次completion_condition(缓存就被写满了),你在completion_condition里面的逻辑(非第一次)并没有运行。
virtual bool parse_msg(size_t bytes_transferred, container_type& msg_can) = 0;
真正的解包函数,多个消息通过容器返回。bytes_transferred就是返回0的那一次completion_condition调用里面的bytes_transferred。如果解包出错,请返回false(然后ascs将调用你重写的reset接口和dump_left_data接口,其中后者是在on_unpack_error里面调用的,如果你重写了它但仍然需要调用dump_left_data接口,就需要你自己调用,或者在on_unpack_error里面调用父类同名函数),注意对于解包出错之前已经解出来的消息,仍然可以通过msg_can返回,ascs仍然会为你派发这些消息。
char* raw_data(msg_type& msg) const
const char* raw_data(msg_ctype& msg) const
size_t raw_data_len(msg_ctype& msg) const
从一个解包好的消息里面,得到真正的数据部分(排除协议数据),不是任意协议都可以实现这三个接口(不拷贝数据的情况下),框架不会使用它们,放在这里只是推荐你去实现(如果可能的话)。
对于心跳消息,解包器可以返回也可以直接过滤掉,都不影响ascs对于心跳超时的判断,如果你选择返回,那在消息派发时(比如on_msg_handle),你要能够排除掉它。
所有接口都无需考虑多线程安全的问题(除了最后3个,ascs并不使用它们,所以线程安全要看谁使用它们,怎么使用它们)。
解包器必须考虑分包粘包问题,是一个很容易出错和出性能瓶颈的地方,请小心对待,特别是分包,你需要保存残留数据,并拼接在下次接收数据的前面,可以参考我默认提供的那几个解包器。
不同于打包器,运行时替换解包器是比较麻烦的,因为解包器有自己的成员变量(比如缓存),替换不好会造成ascs引用到非法地址,我们将在下一篇里面专门讲运行时替换解包器。
上一篇:ascs 简明开发教程(14) 下一篇:ascs 简明开发教程(16)