首页 > 代码库 > mac TeamTalk开发点点滴滴之一——DDLogic框架分解上
mac TeamTalk开发点点滴滴之一——DDLogic框架分解上
DDLogic框架着重解决如下这几个点:
- 基于Task的任务调度
- 事件的订阅与发布
- pdu通信协议以及拆装包过程
- 基于WSAAsyncSelect模型的网络异步I/O TCP/IP长连接
- 业务模块拆分以及模块与模块之间通过接口交互
- 持久化数据以及基于此数据之上的一层数据监听机制(类似IDE工具调试的 Watch)
下面针对每个点分别做描述:
1 基于Task的任务调度(Task 调度)
任何应用程序都会存在一个个需要处理的业务,只有如此你的应用程序才是活的,才能完成用户的业务需求。这些任务或是后台计算性、或是网络通信的拆包/装包、又或是前端交互的如动画计算,可以说整个应用程序就是由这样的一个个task跑起来的。那么如何来合理的调度这些任务呢?之前写过的一篇《TT和chrome线程模型对比分析》(http://www.cnblogs.com/kuiadao/p/3740513.html),同学们可以去看下,这里把任务调度相关的文字直接挪过来下。
一图胜千言,先上下DDLogic的执行逻辑模型图如下:
这张图告诉我们几点:
1.TT是多线程的,线程分为UI主线程、网络异步I/O线程、逻辑任务执行器线程池、http线程池等
1.1 主线程(UI线程):负责界面的显示和交互,以及借助消息循环来做事件的派发 1.2 网络异步I/O线程:负责TCP/IP长连接以及消息服务器数据包的收发 1.3 逻辑任务执行器线程池:一个简单的可伸缩的任务执行池,FIFO task list thread线程执行一些正常任务, Priority queue thread可以执行一些优先级调度或者dependency调度,Priority queue thread也可以在某个重任务把常驻线程耗掉的时候,开启一个新线程来执行后续饥渴任务。 1.4 http线程池:由于除主线程外所有子线程都没有MessagePump,逻辑任务执行器线程池只能负责一些后台计算性的任务(因为如果在逻辑执行器里面执行http任务,有可能会被同步http求给卡住倒置后续的任务不能够得到及时响应),所以只能再做个http线程池来专门处理http相关的任务
2.任务执行单位——Task
2.1task的创建和执行是分开的(command模式),可以在任何的线程中创建一个task,然后通过调用TaskPool的pushTask将任务放到TaskPool的线程池中执行2.2 整个过程只有在pushTask的时候才加锁,等到开始执行的时候是无锁的,所以在设计task的时候,开发者需要考虑到task中的数据对象管辖的范围2.3 task执行过程中产生的事件通知都是利用主线程的消息循环dispatch出去的(这一点与chrome有很大的不同)
这块接下来的目标会尽量和chrome的思想靠齐,特别是在线程任务的设计上chrome允许创建的每个线程都有执行各种任务的能力,并且也为之创建了各种的任务执行队列来异步执行,这样的轮子便于整个项目功能和业务的分解。
Task调度的实现代码分析将放到《mac TeamTalk开发点点滴滴之四——NSOperation与Task》做深入的阐述,敬请期待。
2 事件的订阅与发布 (Event Watch机制)
在一个框架里面有一套统一的、方便使用的事件订阅与发布是非常有必要的。看过一些优秀的开源代码、框架都有各自的不同程度不同方式的实现,如libevent的event-driven,一个高性能的服务器网络库;如.net framework 委托与事件;如delphi(object pascal) VCL的回调函数指针与事件等,同学们可以自行去研究下,特别是libevent的实现值得一看。DDLogic对于这块的设计需要达到这样的效果——即观察者可以通过监听某个业务模块的某个唯一属性(MKN=module key name)的变化,当该属性发生变化的时候,观察者能够及时的获得同步或者异步方式的处理。基于此目的mac TT和windows TT分别用不同的技术达到了DDLogic的设计需求。
mac TT
mac TT依托于强大的OC运行时库支持动态创建类、c语言原始的函数指针、函数调用在运行时才去做二进制重定位即编译时调用者不需要确保被调用函数的存在,实现Event机制的方式可以多种多样,我知道的有协议与委托、类别与委托、C语言的函数指针与回调、target/action、键值观察(KVO)、RunRoop(和windows的消息循环差不多),还有NS库提供的NotificationCenter等。PS:同学们可以去膜拜下《深入浅出Cocoa》(http://blog.csdn.net/column/details/cocoa.html 深入浅出Cocoa)。
首先,先看下DDLogic Event Watch机制的使用好有个初步感受,描述如下:
1 首先将众多事件根据业务模块(module)来拆分,如会话module里面定义的事件属性包括:
//module key names static NSString* const MKN_DDSESSIONMODULE_GROUPMSG = @"DDSESSIONMODULE_GROUPMSG"; //群消息到达static NSString* const MKN_DDSESSIONMODULE_SINGLEMSG = @"DDSESSIONMODULE_SGINGLEMSG"; //个人息到达
2 需要监听事件的地方调用如下,实现
[[DDLogic instance] addObserver:MODULE_ID_SESSION name: MKN_DDSESSIONMODULE_SINGLEMSG observer:self selector:@selector(onHandleSingleMsg:)];onHandleSingleMsg函数,即具体的事件处理函数。
3 在群信息/个人信息到达的时候发布事件,调用如下发布通知
[self uiAsyncNotify:MKN_DDSESSIONMODULE_SINGLEMSG userInfo:userInfo];
咋样上面使用起来很简单吧,典型的观察者模式接口设计。再完善一点可以像.net framework、Delphi VCL可视化订阅事件一样,将事件源的定义和事件和事件处理函数的绑定集成到xcode上去。
接下来讲讲DDLogic Event Watch机制在mac上是如何实现的。DDLogic是借助了上文描述的NS库提供的NSNotificationCenter来实现,其实和NSNotificationCenter原生态的使用没啥区别,所以有些同学会问了NSNotificationCenter 接口使用文档。我这里想着重回答下同学们的一个疑问:因为肯定有会有同学问,本身NSNotificationCenter就已经很好用了而且你的框架也是简单包装了下而已,为啥要这样做呢?这里我的解释也不想套用啥高大上的理论,我自己的理解是:
- DDLogic去包装NSNotificationCenter主要目的是定制一套统一的规则即定义module key
name、监听module key name的事件通知与处理、以及统一的事件发布。 - 对于框架的层面不应该与某种技术选型耦合太深,就拿NSNotificationCenter技术选型来讲,当未来的某一天这套通知机制不够用的时候,可以方便的替换掉选择更适合的技术选型,这个时候可以尽量把替换封装在框架内而不用因此去重构业务层代码。
- 在技术选型上做一层适配,其实还有个好处是可以对你的技术选型做一个定制,比如你选择了NSNotificationCenter技术,但是发现NSNotificationCenter库很强大支持各种场景,但是你的项目其实不需要那么重,通过适配是可以降低使用者对NSNotificationCenter的学习成本。
- 还有一点是开发mac TT DDLogic的时候,windows TT的框架已经成型了,为了保持一致的使用体验,我就特地去包装了下,宽恕我吧^_^
windows TT
windows平台由于没有类似NSNotificationCenter这样的优秀的平台库,不可避免对于DDLogic Event Watch机制的封装需要自己去造轮子,当然会麻烦许多工作量也上升了一个指数。使用方式和上面写的差不多,这里就不重复写了,大家可以去看下具体的源码。
接下来讲讲DDLogic Event Watch机制在windows上是具体实现。它借助了
1. 一层三元组[module_id,module_item,module_tag]来组成一个数据集(DataSet也可以称作Document)。 2. fastDelegate(一套开源的用c++实现的委托,比成员函数指针回调效率更高,有兴趣的可以自己去研究下(http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible))实现函数回调即调用到具体的事件处理函数。 3. 操作系统的消息循环,包装成事件通知。
一图胜千言如下图:
通过图示具体实现如下:
1. 创建一个无窗口的句柄用来作为异步事件派发的基础,即底层最终是借助windows操作系统的消息循环来封装上层的Event事件的派发的,支持同步/异步派发(即SendMessage/PostMessage)。2. 生成一个全局唯一的三元组 DataSet实例用来存储各个业务模块观察者关心的唯一属性,类似mac TT的MKN(module keyname),存储格式按照三元组[module_id,module_item,module_tag],module_id对应业务模块ID,module_item对应登陆者信息,module_tag则对应MKN。3.在需要监听事件的地方调用 logic::GetLogic()->addWatch(this ,MAKE_DELEGATE(this,&SessionChat::OnEvaluateWatch,serv::DID_EVALUTATE_CONFIG) OnEvaluateWatch函数,即具体的事件处理函数。4.在发布事件的地方调用 logic::GetLogic()->asyncPostEvent(serv::DID_EVALUTATE_CONFIG ,module_item,TAG_EVALUTATE_CONFIG,pData);
3 PDU通信协议以及拆装包过程
PDU通信协议走的是二进制协议——即固定长度的协议头(16个字节) + 协议体方式。协议头包括整个协议包的大小、版本、模块号(moduleid)、命令号(commandid)等。模块号(moduleid)和业务模块对应,commandid对应具体的网络传输命令,这样做的好处是通过包头就可以知道这个包是属于那个业务模块处理的。对于协议这块的技术选型,我们当时也讨论了许多,我、大子腾、大子烨分别都提出了各自的解决方案,最终选择了大子腾的PDU协议,这个过程考虑的因素很多,所以我准备专门写一篇blog来分写下当时的情景,另外这篇博文还会分析PDU通信协议和chrome的对比,敬请期待...这里就不再深入描述了。
接下来讲下协议的拆包/装包与DDLogic的分层吧,虽然和具体通信协议交集不是那么大,但是想想还是放这里比较适。
如图:
从这幅图可以简单看出:
1.协议层:协议的拆包和协议任务的分配都封装在协议层,对业务层是透明的 2.协议层:协议拆包完成后,会生成一个task放入任务执行池,做任务派发的工作 3.业务层:根据module_id会分派到相应的业务模块 4.业务层:收到通知后,根据协议里面的command_id处理具体业务