逐步实现TCP服务端Step03-13:消息队列相关

消息队列就是存放消息的队列,而消息的表现形式为“对象”,也就是说这个队列实际上是消息对象队列。

我们希望队列能更通用一些,不仅可以存放消息对象,也可以存放X对象,Y对象和Z对象。Object是它们的一般情况。

对象与一般的内置类型不同,应专门用一个类来管理对象的生命期,这里用ObjectManager来管理Object对象。若要对一批对象实施管理,这些对象必须是可检索的,即:基于某些规则能找到指定的对象。鉴于类的功能要单一简单的原则,Object类并不提供与检索有关的信息,这部分内容交由Index类处理。使用的时候,只要将Object对象与Index对象关联起来即可。

其实,ObjectManager和Index与队列的逻辑无关,它们所做的是对队列元素Object对象的管理和维护。对Queue来说,这是一项基础服务,它就像一个仓库,可以提供Queue想要的那个Object,至于Object对象的排队问题,最终由Queue来处理。若Queue只用来存放内置数据类型,比如int的话,这项服务就没必要了。

Object类

  • id_:对象应该有一个标识。
  • start_mode_:启动模式,与SharedMemory类中的start_mode_作用相同,用来标识进程的启动方式,1-恢复模式,0-初始模式。
  • memory_:用于构造Object对象的内存区域,其具体指向由子类指定。

把new的访问权限设为protected,是为了隐藏对象的创建过程,Object的子类应定义一个Create方法用于创建对象。

Object类的子类

假设子类名为TestObject:

对于Object的子类,需要关注的是Init方法以及一个用于创建自身实例的Create方法。ObjectManager会调用Create来完成对Object实例的创建,然后调用Init方法对实例进行初始化。

Create方法的实现:

1
2
3
4
5
Object* TestObject::Create(void* memory)
{
    Object::memory_ = (unsigned char*)memory;
    return (Object*)(new TestObject);
}

对于每一子类来说,这个Create函数只有new的部分是不一样的,这里可以用宏定义来减少重复编码。类似于消息体类中的COMMON_METHOD宏。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define DECLARE_CREATE\
    public:\
        static Object* Create(void* memory);

#define IMPLEMENT_CREATE(CLASS_NAME)\
    Object* CLASS_NAME::Create(void* memory)\
    {\
        Object::memory_ = (unsigned char*)memory;\
        return (Object*)(new CLASS_NAME);\
    }

有关于TestObject类的实现及使用详见trial目录。

ObjectManager类

  • create_obj_func_:是用于创建Object对象的函数,这个函数在Object的子类中被定义。
  • indexs_:指向一块内存,该内存用于构造Index对象。Index对象是一次性创建好的。这一批index对象,对应于被管理的那批object对象。
  • objects_:指向一块内存,该内存用于构造Object对象。Object对象也是一次性创建的。
  • obj_cnt_:待管理的对象的总个数 。
  • obj_size_:单个对象的尺寸。
  • shared_memory_:SharedMemory对象,用于管理ObjectManager需要的内存的空间。
  • used_obj_cnt_:已使用的Object对象个数。
  • free_index_head_:首个空闲的Index对象的索引号。因为每个Index对象都关联了一个Object对象,空闲的Index对象,就表示其关联的那个Object对象尚未使用。

  • operator new:内存空间由shared_memory_对象分配。

  • CountSize:计算一个ObjectManager实例需要耗费多少内存。
  • 构造函数:在构造函数参数中传入单个对象的尺寸,要管理的对象的个数以及对象创建函数。构造函数中会由shared_memory_对象创建一块供Object对象们使用的空间,以及一块供Index对象们使用的空间。
  • Init:初始化。此处会调用FormatIndexs方法。
  • FormatIndexs:“格式化”索引。主要是建立各Index对象之间的关联。
  • FormatObjects:“格式化”对象。使用create_obj_func_函数构造指定数目的对象,并为每个对象都关联一个Index对象。图中,灰色矩形表示已被格式化的对象,白色矩形为未格式化的对象。
  • GetIndex:获取给定索引号的Index对象,若获得的对象为空闲状态,则返回NULL 。indexs_所指向的内存区域可视为以Index对象为元素的数组,基于数组标号可随机访问Index对象。
  • GetObject:与GetIndex方法类似。在找到Index对象后,再使用get_attached_object得到对应的Object对象。
  • CreateObject:虽然名为创建对象,但这个“创建”并非new出来一个Object对象。Object对象在一开始的时候已被批量创建,以后不会再创建。该方法所做的主要工作是把“空闲链表”中的首个元素卸载,并作为”已使用链表”的新的首元素,最后返回这个首元素的索引号。当然,这两个链表实际上是Index对象数组indexs_,逻辑上是两个链表,下图演示的工作过程表示的是逻辑关系,节点实际上是紧密排列的。
  • DestroyObject:与CreateObject的过程相反,将指定元素从”已使用链表”卸载,作为“空闲链表”的新首元素。

再说一下,Index对象组织成的逻辑结构,不论是链表或者顺序表或者其它形式,都只是ObjectManager类出于自身管理需要而设定的。这里的逻辑结构只服务于对Object对象的管理,对于ObjectManager类的设计者来说,他并不关心这些Object对象会被使用者组织成何种形式,比如队列。对于Queue的设计者来说,只是使用了ObjectManager提供的对象管理服务,将这些对象组织成队列的逻辑,应由Queue的设计者来实现。

Index类

  • attached_obj_:该Index对象对应的Object对象。
  • prev_index_:该节点的前驱节点的索引号。
  • next_index_:该节点的后继节点的索引号。
  • used_flag_:使用标记。用于标识当前节点是否空闲,0-空闲,1-已用。
  • additional_data_table_:附加数据表,实际上是一个int型数组。在实际使用中,可能需要Index对象存放一些数据以达到某种目的。

  • AttachObject:与Object对象建立关联,其实是将Object对象的指针存起来。

  • SetRecordToADT:向扩展数据表中写入数据。
  • GetRecordFromADT:从扩展数据表中查询数据。
  • ClearAllRecordInADT:清空扩展数据表。

为何需要prev_index_、next_index_和additional_data_table_ ?

从Index类设计者的角度来看,使用Index类的人有可能把Index组织成顺序表。顺序表本身就带有了顺序关系,比如,存储于3号位的Index对象,与其相邻的前一个对象,一定存储于2号位,同理,2号位的后面一定是3号位。若使用者满足于这种默认顺序,就不需要prev_index_和next_index_属性:

若使用者需要改变现有的顺序关系,就需要额外的信息来说明。此时,prev_index_和next_index_是必要的,当然,也可以直接挪动对象本身以改变次序,比如,将原本存储于3号位的对象存到2号位上:
其实,prev_index_和next_index_就是一组附加的数据,只是我们把它具体化为当前节点的前驱索引和后继索引值。若使用者需要附加一些自己的数据,可在additional_data_table_中添加。additional_data_table_不做语义上的说明,只是给出了空间,让使用者自己去填充。还是上面的这个例子,假设不使用prev_index_和next_index_,在additional_data_table_的0号位中存放后继索引值,在1号位中存放前驱索引值,可达到同样的效果:

Queue类

提供基础服务的类已经完成,Queue只要专注于队列的逻辑即可。

  • adt_index_:实现队列逻辑的时候,会用Index对象的additional_data_table_的0号位存储一些数值。adt_index_用于记录使用的位置号,其值始终为0 。
  • head_element_:队首元素。队列中的基本数据单元称为“元素”。使用方在向ObjectManager取对象的时候是基于ID号(或者说索引号)的。head_element_所存放的就是那个处于队首的对象的ID号。
  • tail_element_:与head_element_类似,它是队尾元素。
  • object_manager_:ObjectManager对象指针。

  • Init:传入一个ObjectManager对象的指针,为Queue提供服务。

  • SetNextElement:为当前元素设置下一个元素。第一个参数element_id是当前元素的id,第二个参数是其后继元素的id。 其实,这个方法就是将后继元素的id记录到了当前元素对应的Index对象的additional_data_table_的0号位中。
  • GetNextElement:获取指定元素的后继元素。其实是把additional_data_table_的0号位中的数据读出来。
  • DeleteElement:它主要服务于元素出队操作,出队时将元素删除,本质上还是对additional_data_table_的0号位中的数值进行修改。这个删除,只是在队列层面的删除,它是逻辑上的操作,不会动ObjectManager中的对象。

    假设队列中的元素ID为10,6,3,7,8,现将ID为的3元素删除:

    对于队列来说,只会做删除队首元素的操作:

  • PushElementBack:入队新元素。还是以上面的那个队列为例:

  • PopElementFront:将队首元素出队。其实就是执行了DeleteElement(head_element_)删除队首元素。

小结

这里的实现与通常的Queue没有本质区别,它就是一个单向链表。上面有很大一部分其实是在描述ObjectManager的实现,其实它与Queue没什么关系,它只是对象的管理机构。Queue并未维护一组元素节点,而是通过在Index对象中附加信息的方式,令那些由ObjectManager管辖的对象,在逻辑上行成了队列。这么做就使Queue的逻辑部分与数据完全分离了。

本次改动的地方比较多,除了队列的实现和使用以外,还用上了之前已经封装好了的类。

s和c都使用了消息系列类,处理Echo消息:

<==  index  ==>