- 基于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线程池等
<code> 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相关的任务</code>
2.任务执行单位——Task
<code>2.1task的创建和执行是分开的(command模式),可以在任何的线程中创建一个task,然后通过调用TaskPool的pushTask将任务放到TaskPool的线程池中执行2.2 整个过程只有在pushTask的时候才加锁,等到开始执行的时候是无锁的,所以在设计task的时候,开发者需要考虑到task中的数据对象管辖的范围2.3 task执行过程中产生的事件通知都是利用主线程的消息循环dispatch出去的(这一点与chrome有很大的不同)</code>
这块接下来的目标会尽量和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里面定义的事件属性包括:
<code>//module key names static NSString* const MKN_DDSESSIONMODULE_GROUPMSG = @"DDSESSIONMODULE_GROUPMSG";//群消息到达static NSString* const MKN_DDSESSIONMODULE_SINGLEMSG = @"DDSESSIONMODULE_SGINGLEMSG"; //个人息到达</code>
2 需要监听事件的地方调用如下,实现
<code>[[DDLogic instance] addObserver:MODULE_ID_SESSION name: MKN_DDSESSIONMODULE_SINGLEMSG observer:self selector:@selector(onHandleSingleMsg:)];onHandleSingleMsg函数,即具体的事件处理函数。</code>
3 在群信息/个人信息到达的时候发布事件,调用如下发布通知
<code>[self uiAsyncNotify:MKN_DDSESSIONMODULE_SINGLEMSG userInfo:userInfo];</code>
咋样上面使用起来很简单吧,典型的观察者模式接口设计。再完善一点可以像.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上是具体实现。它借助了
<code> 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. 操作系统的消息循环,包装成事件通知。</code>
一图胜千言如下图:
通过图示具体实现如下:
<code>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);</code>
3 PDU通信协议以及拆装包过程
PDU通信协议走的是二进制协议——即固定长度的协议头(16个字节) + 协议体方式。协议头包括整个协议包的大小、版本、模块号(module id)、命令号(command id)等。模块号(module id)和业务模块对应,command id对应具体的网络传输命令,这样做的好处是通过包头就可以知道这个包是属于那个业务模块处理的。对于协议这块的技术选型,我们当时也讨论了许多,我、大子腾、大子烨分别都提出了各自的解决方案,最终选择了大子腾的PDU协议,这个过程考虑的因素很多,所以我准备专门写一篇blog来分写下当时的情景,另外这篇博文还会分析PDU通信协议和chrome的对比,敬请期待…这里就不再深入描述了。
接下来讲下协议的拆包/装包与DDLogic的分层吧,虽然和具体通信协议交集不是那么大,但是想想还是放这里比较适。
如图:
从这幅图可以简单看出:
<code> 1.协议层:协议的拆包和协议任务的分配都封装在协议层,对业务层是透明的 2.协议层:协议拆包完成后,会生成一个task放入任务执行池,做任务派发的工作 3.业务层:根据module_id会分派到相应的业务模块 4.业务层:收到通知后,根据协议里面的command_id处理具体业务 </code>
4 TCP/IP长连接
大部分客户端应用程序的网络I/O模型采用阻塞模式就够用了,如遇到UI和网络需要异步,很常用的一种实现方式是启用多线程将网络数据的收发放到工作者线程中去。但是对网于IM这种应用场景来说阻塞模式就不适用了,试想聊天过程中你和服务器之间的交互是多么的频繁,你可以同时和几十位用户一起聊天,为了不阻塞难道每次聊天收发信息都需要建立一个线程来实现吗?这当然是不现实的,所以我们需要选择非阻塞模式异步socket IO。下面分别讲讲mac pro 和 windows的网络异步I/O的实现。
mac TT
mac TT得益于oc提供的良好平台目前借助的是CFNetwork和NSStream类实现TCP/IP 异步I/O socket,利用CFNetwork创建socket通信通道,利用NSStream传递单向的数据流,具体实现如下: 通过在NSStream中增加一个类方法扩展用于建立TCP/IP连接的一系列过程。
<code>+ (<span class="hljs-keyword">void</span>)getStreamsToHostNamed:(<span class="hljs-built_in">NSString</span> *)hostName port:(<span class="hljs-built_in">NSInteger</span>)port inputStream:(<span class="hljs-built_in">NSInputStream</span> **)inputStream outputStream:(<span class="hljs-built_in">NSOutputStream</span> **)outputStream { <span class="hljs-built_in">CFHostRef</span> host; <span class="hljs-built_in">CFReadStreamRef</span> readStream; <span class="hljs-built_in">CFWriteStreamRef</span> writeStream; host = <span class="hljs-built_in">CFHostCreateWithName</span>(<span class="hljs-literal">NULL</span>, (__bridge <span class="hljs-built_in">CFStringRef</span>) hostName); <span class="hljs-built_in">CFStreamCreatePairWithSocketToCFHost</span>(<span class="hljs-literal">NULL</span>, host, (SInt32)port, &readStream, &writeStream); <span class="hljs-built_in">CFRelease</span>(host); ... } </code>
NSStream的两个派生类NSInputStream/NSOutputStream把整个socket通信抽象成了一个输入/输出流,通过oc平台的RunLoop将异步I/O事件通知到如下回调函数中:
<code>-<span class="hljs-params">(void)</span>stream:<span class="hljs-params">(NSStream *)</span>aStream handleEvent:<span class="hljs-params">(NSStreamEvent)</span>eventCode { switch<span class="hljs-params">(eventCode)</span> { ... } } </code>
回调通知的事件有:
<code><span class="hljs-keyword">typedef</span> <span class="hljs-built_in">NS_OPTIONS</span>(<span class="hljs-built_in">NSUInteger</span>, <span class="hljs-built_in">NSStreamEvent</span>) { <span class="hljs-built_in">NSStreamEventNone</span> = <span class="hljs-number">0</span>, <span class="hljs-built_in">NSStreamEventOpenCompleted</span> = <span class="hljs-number">1</span>UL << <span class="hljs-number">0</span>, <span class="hljs-comment">//输入or输出流打开成功即socket连接建立成</span> <span class="hljs-built_in">NSStreamEventHasBytesAvailable</span> = <span class="hljs-number">1</span>UL << <span class="hljs-number">1</span>, <span class="hljs-comment">//可以接受数据通知即输入缓冲区有内容了</span> <span class="hljs-built_in">NSStreamEventHasSpaceAvailable</span> = <span class="hljs-number">1</span>UL << <span class="hljs-number">2</span>, <span class="hljs-comment">//可以发送数据通知即输出缓冲区空了</span> <span class="hljs-built_in">NSStreamEventErrorOccurred</span> = <span class="hljs-number">1</span>UL << <span class="hljs-number">3</span>, <span class="hljs-comment">//错误通知</span> <span class="hljs-built_in">NSStreamEventEndEncountered</span> = <span class="hljs-number">1</span>UL << <span class="hljs-number">4</span> }; 以上具体代码可以参见mac tt代码<span class="hljs-built_in">NSStream</span>+<span class="hljs-built_in">NStreamAddtion</span>.m 以及 MGJMTalkClient.m </code>
咋样整个过程看下来是不都看不到socket的影子?这样做有啥好处呢?我自己的理解最大的好处是足够简单,对于调用者来说socket的整个过程是透明的,调用者不需要去理解操作系统对异步socket的I/O模型的支持,不需要去理解socket建立的整个过程等。类似的还有java的NIO甚至netty库都把整个socket过程隐藏在了一个流的概念中。 尽说好处了,差点忘记目前DDLogic的这种实现还有一个很大的问题(PS:是不是我们使用上有问题,同学们也可以帮忙看下),即connet TCP服务器的时候,回调事件里面肿么也收不到连接断失败的事件通知,导致整个TCP/IP流程不流畅,我们暂时采用了一个很龌龊的方式是:connet TCP服务器的时候设置个定时器,如果3秒钟没有收到连接建立成功的通知就认为连接失败了。这里的代码我担心也会为将来开发IOS TT埋下一个隐患,建议IOS开发同学去深入研究下或者寻找更好的技术选型。这里提供几个参考
- OC平台OS层的基于C的 BSD socket,这一层面提供的是socket原生态的方法,可以最大程度的控制网络编程,但是工作量也是最大的,和windows TT采用C/C++ 进行socket编程差不多。
- OC平台Core Foundation层提供的CFNetwork C ,对OS层的BSD socket做了一层简单的包装,并且和系统的run loop结合起来,使得异步socket I/O实现起来很方便。上面mac TT用的其实就是这一层,所以这里还是需要去深入研究上面的坑。
- OC平台最上层提供的Bonjour库,同学们可以自行去看下 Networking and Bonjour on iPhone
- 另辟蹊径不走OC平台提供的库,用libevent来实现,不过对于客户端来说使用该库可能略重,但是它良好的封装使得使用起来非常简单而且本身也是轻量级高性能的网络库,客户端选择POSIX select或windows select模型足够用了。
windows TT
windows TT是基于windows的WSAAsyncSelect模型建立的异步I/O,利用这个模型应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知,对于一个客户端程序已经足够用了。code projct有个对该模型很好的包装,同学们可以去看下( http://www.codeproject.com/Articles/3855/CAsyncSocketEx-Replacement-for-CAsyncSocket-with-p )。具体的实现windows并没有像oc平台这样好的抽象,但是实现起来其似乎也是差不多的思想,异步I/O消息通知的事件包括:FD READ、FDWRITE、FD FORCEREAD、FD CONNECT、FD ACCEPT、FD CLOSE这些,每个事件都相应的能通知到socket数据处理层就可以了。
比较下来两个系统平台对于TCP/IP异步socket IO的封装是差不多的,差别只是抽象的层次mac pro平台更加高一点,windows更加接近原生态的socket。 以上讲的是利用各自平台的网络库实现与服务器之间通信的技术,接下来一起看下在内存中收发数据的两个buffer,因为数据传递是异步的,发送/接收数据都有可能是还没有真正发送/接收成功,所以需要在socket数据处理层维护两块buffer——inBuffer(接收数据缓存)/outBuffer(发送数据缓存)。以outBuffer(发送数据缓存)为例子,当你调用sendSocketData的时候,由于操作系统发送缓存区满了导致调用失败 ,由于是异步socket IO,系统的send过程并不会等待系统的发送缓存区空了再发送数据,而是会让send过程失败,等到系统的发送缓存区空的时候通过一个可写的事件通知你,所以在sendSocketData过程send失败的情况下,你所需要做的就是将数据缓存到outBuffer(发送数据缓存)中,等到可写事件收到了再将outBuffer(发送数据缓存)的数据发送出去,上个流程图吧:
5 业务模块拆分以及模块与模块之间通过接口交互
任何应用程序从业务角度讲都不是单一的,是由许多业务组装起来的(比如mac TT有登陆业务、文件传输业务、消息管理业务、会话管理业务等),那么这些业务需要如何有机的结合起来完成一个应用程序的所有需求呢?同学们应该会首先想到MVC(Model、View、Controller)/MVP(Model、View、Presenter),嗯没错,在OC平台中本身就是按照MVC来实现具体业务的开发的,DDLogic在MVC基础之上再加了一个Module的概念,为的是和前面:基于Task的任务调度、pdu通信协议以及拆装包过程、事件的订阅与发布、持久化数据以及基于此数据之上的一层数据监听机制(类似IDE工具调试的 Watch)这些有机的结合起来,回头看看是否还记得前面PDU协议面的module id和存储格式按照三元里面的module id呢?先上个简单的图吧:
DDLogic的思路是这样的(以登陆业务模块为例子):
<code> <span class="hljs-number">1</span>.所有模块的对外接口都通过DDLogic Modules Manager来管理。 <span class="hljs-number">2</span>.模块与模块之间通过接口来调用, 模块内部实现对外不可见。比如外部只能调用DDloginModule的doLogin()来实现登陆操作,调用方是不知道具体如何实现登陆的。 <span class="hljs-number">3</span>.每个独立的业务创建成为业务<span class="hljs-class"><span class="hljs-keyword">module</span>——<span class="hljs-title">DDLoginModule</span>,有一个全局唯一的业务模块<span class="hljs-title">ID</span>——<span class="hljs-title">MODULE_ID_LOGIN</span></span> <span class="hljs-number">4</span>.调用业务模块的接口函数通过全局唯一业务模块ID——MODULE_ID_LOGIN来,DDLoginModule* loginModule = getDDModule(MODULE_ID_LOGIN);[loginModule doLogin]; <span class="hljs-number">5</span>.支持插件管理,n(n >=<span class="hljs-number">1</span>)个模块合作来组装成<span class="hljs-number">1</span>个插件,并且支持动态加载/卸载(未实现的目标) </code>
是不感觉DDLogic框架连这种东西也拿出来分享,没啥技术含量是个程序员都知道用类似的方式来拆分业务?是的你的感觉是对的,但是只对了一半,确实看起来没啥营养,但是请你再往下看你会发觉这个点才是整个DDLigic框架的精髓,如果说上面讲的每个设计点是DDLogic框架的一条条河流的话,那么这里就应该是它们的汇聚地,下面逐个点来分析
<code> <span class="hljs-number">1</span>.基于Task的任务调度:每个task的执行都会绑定一个module_id来知道具体是哪个模块的task在执行,并且通过module_id将任务执行的结果反馈给模块,这条河流就汇聚到<span class="hljs-class"><span class="hljs-keyword">module</span>了。</span> <span class="hljs-number">2</span>.事件的订阅与发布:每个事件都是通过指定module_id和MKN来订阅的,等到被订阅事件发布的时候同样通过指定module_id和MKN来通知出去,这条河流也汇聚到moudule了 <span class="hljs-number">3</span>.pdu通信协议以及拆装包过程:通过解析pdu协议头获取module_id和command_id,然后生成NetworkTask派发到相应的模块中区,这条河流也汇聚到<span class="hljs-class"><span class="hljs-keyword">module</span>了</span> <span class="hljs-number">4</span>.持久化数据以及基于此数据之上的一层数据监听机制(类似IDE工具调试的 Watch):通过储格式三元组[module_id,module_item,module_tag],这条河流也汇聚到<span class="hljs-class"><span class="hljs-keyword">module</span>了</span> </code>
6 持久化存储以及基于此数据模型数据监听机制(类似IDE调试工具的Watch)
DDLogic数据持久化用的是NSCoder可以支持基于业务模块的数据序列化/反序列化。基于数据模型的监听机制(暂且称作data watch机制)对一个应用程序来说是非常实用的,举个例子:你的好友管理模块的数据新增了一个好友,好友列表数据发生add事件,监听此数据变化的模块如好友列表控件、消息管理模块等都会收到相应的通知并作出及时的处理。
这块将放到《
mac TT开发点点滴滴之四——NSCode与DDlogic的结合》中做深入阐述,敬请期待。