CppExplore

一切像雾像雨又像风

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  29 随笔 :: 0 文章 :: 280 评论 :: 0 Trackbacks

作者:CppExplore 网址:http://www.cppblog.com/CppExplore/
《技术系列综述(一)》介绍了网络层部分。网络层基本都是多路复用函数作为运行的主线程,使用管道或者sockpair与之通讯,这是网络层线程的固有特点,和业务线程的呈现方式完全不同。经过网络层以后,数据开始流向业务线程,现在就顺着数据流向往上看。
一 业务线程
《技术系列之 线程(一)》有对线程的一个入门描述,里面的消息队列只是示例,真实可用的可以看《技术系列之 线程(二)》
(1)业务线程的划分
在业务层面,数据结构仍然是服务器程序的核心。整理出业务需要的数据结构,据此划分线程,保证每个数据结构都在它所属的线程中被修改。如果其它线程想修改该结构,则需要向本线程发送消息,由本线程修改。当然这只是理想情况,有时候处于性能的需要或者和网络层的交互,需要跨线程访问其它线程的数据,此时则需要加锁,这也是线程间交互陷入混乱的一个开端,这种情况要限制在一个很小可控的范围内。
(2)带消息队列的线程实现
OA/CRM/WorkFlow的系统的关键在于业务的整理、面向对象的设计。而网络服务器不同,服务器的业务相对清晰,相关性强,它设计的关键在于数据模型的整理、线程的划分。可以说线程是服务器的骨架。这里引入几乎每个服务器都会有的一个基础模块:带消息队列的线程类。《技术系列之 线程(二)》实现了一个简单的线程消息队列。在该消息队列基础之上进一步封装,实现带消息队列的线程类。该类的静态类图如下:

ThreadQueue参见《技术系列之 线程(二)》
start方法中参数默认为1,大于1则是线程池的实现,里面以线程的方式启动run函数,线程号存入vector,供停止时用。putq方法在其它线程中调用,向该线程发送消息,run方法中循环调用getq获取消息,调用deal_msg处理。实际的业务线程只需要继承该类即可成为带消息队列的线程。
(3)监控线程
基于上面这个基础模块,可以进一步开发监控线程,监控线程定时向各个线程发送心跳消息,各普通线程收到心跳信息后向监控线程回复,如果某个线程在一定时间内没有回复心跳,则可以采取进一步的修复处理。该方案可以作为系统安全的一个备选方案。
以上思想同样适用于嵌入式平台,很多嵌入式平台使用多进程协同处理消息,进程之间使用共享内存或者系统消息队列通讯,思想大同小异,并且同样可以设计监控进程。
二 消息映射
(1)消息定义以及处理示例
有了线程消息队列,也有就有消息传递,接下来就是业务线程获取到消息处理消息。一个业务线程可能要处理多种消息,为区分不同的消息,引入消息类型。如下:

enum MsgType
{
    MSG_TYPE_1=
65,//64一下预留,用于统一的管理控制
    MSG_TYPE_2,
    ..
    MSG_TYPE_MAX
}
;
struct Msg
{
    MsgType type;
    MsgData data;
}
;

业务线程在deal_msg方法中处理消息,很容易想到的处理流程如下:
switch(msg->type)
{
    
case MSG_TYPE_1:
         do_msg_type_1_();
         
break;
    
case MSG_TYPE_2:
         do_msg_type_2_();
         
break;
    ..
    
default:
             do_default_msg_();
             
break;
}


(2)从代码熵引入查表法
这里引入“代码熵”的概念,用于描述代码的混乱程度。每千行代码中,出现一个“else”,代码熵加1,出现一个“case”,代码熵也加1。如果1k行代码的熵大于7,我们就认为这个代码已经开始变的混乱了。
因此当消息类型不多的时候,使用case是个不错的选择,当处理的消息大于7个并且有扩充趋势的时候,我们就要想另外的办法来代替这种switch...case...的写法。
消除else和case最直接的想法是查表法,使用数组下标标记消息类型,数组保存消息处理方法指针。这里不使用map,查询需要o(lgn)不如数组来的直接,另外设计期间就已明确知道消息的类型以及对应的处理函数,不需要动态增减,也不需要使用vector,直接简单的方法就是定义一个static的数组表。
(3)消息映射
直接的数组展现方式不携带语义,展现方式不直观,维护和扩充都让人头大。这里可用借助宏包裹数组,提供可读的展现方式,如下:

BEGIN_MESSAGE_MAP(SessionManager,SessionMsg)
    ON_MESSAGE(MSG_TYPE_1, SessionManager::do_msg_type_1_)
    ON_MESSAGE(MSG_TYPE_2, SessionManager::do_msg_type_2_)
    
END_MESSAGE_MAP()

如果你熟悉MFC,一定很熟悉这种消息映射的定义方式。这种消息映射的定义方式,从可维护、可读方面比直接的数组更进了一步。宏BEGIN_MESSAGE_MAP、ON_MESSAGE的实现方式不再详写,如果读者实在想象不出来,可以参见《技术系列之 状态机(一)》中的状态机映射宏的定义方式。使用的时候在deal_msg中直接根据消息类型找到数组中的消息处理函数进行处理,如果你认为这样暴露了消息映射背后的数组结构,可以把这个寻找消息处理函数的工作也封装到基类IMsgThread中。
(4)成员函数委托
上面的消息映射宏展开后实际是一个静态数组,而方法do_msg_type_1_/do_msg_type_2_也必须是类的静态成员函数(普通类成员函数指针不能转化为普通函数指针)。通过类的静态成员函数访问类的非静态属性或者方法如下:在消息中携带该类指针handler,处理方法中取到handler指针转换类型,通过指针操作。
当代码中充斥大量通过静态成员函数访问对象私有属性的时候,这无疑是一种丑陋的写法(事实并没有这么严重)。
这里就该boost::function,boost::bind出场了。如果你喜欢,也可以直接写模版实现。也可以参见csdn文章成员函数指针与高性能的C++委托》
(5)题外话
1、统一的展现方式。
不仅变量的命名需要统一规范,方法的调用逻辑同样需要统一,这可以帮助你明确程序中数据的流向以及保证程序持续的扩充、维护。相对于简单的命名规范,方法调用逻辑的统一更为重要。
以线程类举例说2点(1)其它线程类不能直接调用其它线程的putq方法向对应线程发送消息。正确的做法是调用对应线程类的方法,由该方法负责向本线程发送消息。
(2)发送消息的方法/处理消息的方法职责要明确、命名要统一。
发送消息的方法负责把方法参数转化为消息内容,调用putq发送消息,该方法不得操作本类的任何私有属性。
处理消息的方法负责对消息做出处理、响应。
命名方面,比如发送消息的方法可以以ON_开头,处理消息的方法可以以DO_开头。
这些规范不应该只是规范,而应该是发自内心的需要。当然没有什么规范是必须的,你仍然可以使用你喜欢的或者认为可行的方式,如果你的方法在程序1w行、10w行、50w行的时候,仍能清晰表现程序的数据流向,仍有很好的可维护性、可扩充性。
2、开发领域的烙印。
不多说了。一句话:重要的是思想,不是平台和语言。

posted on 2008-11-07 15:33 cppexplore 阅读(4865) 评论(9)  编辑 收藏 引用

评论

# re: 【原创】技术系列综述(二) 2008-11-10 09:10 true
好文,挺起来  回复  更多评论
  

# re: 【原创】技术系列综述(二) 2008-11-10 09:21 true
提出一个疑问:业务线程肯定是多线程的实现,threadA,threadB,threadC,如果同一个client连续发送了2个待处理的包,是否保证对该client的处理由同一个线程实现?否则是否会出现threadA,threadB同时向client的socket写数据,导致乱序
  回复  更多评论
  

# re: 【原创】技术系列综述(二) [未登录] 2008-11-10 09:36 cppexplore
@true
业务线程也不见得是多线程实现,尤其是是处理有上下文关联关系(比如,rtsp协议,发了setup,才能发play,发送teardown之后才结束,不同阶段状态是不同的,http是无状态的协议就另说了)逻辑的业务线程 ,一般都是单线程。把握住系统瓶颈的所在,在关键业务瓶颈处加多线程。另外,如果真想实现你说的情况,可以绑定connection和上层业务线程,我觉得这不会有性能的提升,当然也要看你的业务应用场景。  回复  更多评论
  

# re: 【原创】技术系列综述(二) 2008-11-25 13:47 卡通服装
好文章,支持楼主多写些  回复  更多评论
  

# re: 【原创】技术系列综述(二) 2009-04-14 17:01 包装机
恩,好  回复  更多评论
  

# re: 【原创】服务器技术系列综述(二) 2009-07-27 13:05 套袋收缩机
不错,给你顶一个,  回复  更多评论
  

# re: 【原创】服务器技术系列综述(二) 2009-09-28 18:09 neou
不错,跟我的思路比较类似。 我的代码。

BEGIN_REQUEST_MAP(chunk_master_handler)
REQUEST_HANDLER(PChunkSnap, onChunkSnap)
REQUEST_HANDLER(PRegSlave, onRegSlave)
REQUEST_HANDLER(PMasterSnap, onMasterSnap)
END_REQUEST_MAP()


另外,服务器最重要的是要实现一个好用的buffer, 高效,内存分配快。   回复  更多评论
  

# re: 【原创】服务器技术系列综述(二) 2010-05-30 21:54 UustCodeIT
对于某些消息,业务线程处理完了还要向client返回结果,怎么把结果返回到网络层在发送给client?是不是在把消息从网络层向业务线程转移的时候还要提供消息所有者等信息。  回复  更多评论
  

# re: 【原创】服务器技术系列综述(二) [未登录] 2010-06-04 12:06 cppexplore
@UustCodeIT
session对象和 网络层的connection对象有一定对应关系的  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理