由于TT原班人馬離職,文章丟失,暫留備份
轉載地址
mac TeamTalk開發點點滴滴之一——DDLogic框架分解上 - 刀哥的技術隨筆
mac TeamTalk開發點點滴滴之一——DDLogic框架分解下 - 刀哥的技術隨筆
DDLogic框架著重解決如下這幾個點:
- 1.基于Task的任務調度
- 2.事件的訂閱與發布
- 3.pdu通信協議以及拆裝包過程
- 4.基于WSAAsyncSelect模型的網絡異步I/O TCP/IP長連接
- 5.業務模塊拆分以及模塊與模塊之間通過接口交互
- 6.持久化數據以及基于此數據之上的一層數據監聽機制(類似IDE工具調試的 Watch)
下面針對每個點分別做描述:
1 基于Task的任務調度(Task 調度)
任何應用程序都會存在一個個需要處理的業務,只有如此你的應用程序才是活的,才能完成用戶的業務需求。這些任務或是后臺計算性、或是網絡通信的拆包/裝包、又或是前端交互的如動畫計算,可以說整個應用程序就是由這樣的一個個task跑起來的。那么如何來合理的調度這些任務呢?
之前寫過的一篇《TT和chrome線程模型對比分析》,同學們可以去看下,這里把任務調度相關的文字直接挪過來下。
一圖勝千言,先上下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.1 task的創建和執行是分開的(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》( 深入淺出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就已經很好用了而且你的框架也是簡單包裝了下而已,為啥要這樣做呢?
這里我的解釋也不想套用啥高大上的理論,我自己的理解是:
1 DDLogic去包裝NSNotificationCenter主要目的是定制一套統一的規則即定義module key
name、監聽module key name的事件通知與處理、以及統一的事件發布。2 對于框架的層面
不應該與某種技術選型耦合太深
,就拿NSNotificationCenter技術選型來講,當未來的某一天這套通知機制不夠用的時候,可以方便的替換掉選擇更適合的技術選型,這個時候可以盡量把替換封裝在框架內而不用因此去重構業務層代碼。3 在技術選型上做一層適配,其實還有個好處是可以對你的技術選型做一個定制,比如你選擇了NSNotificationCenter技術,但是發現NSNotificationCenter庫很強大支持各種場景,但是你的項目其實不需要那么重,通過適配是可以降低使用者對NSNotificationCenter的學習成本。
4 還有一點是開發mac TT DDLogic的時候,windows TT的框架已經成型了,為了保持一致的使用體驗,我就特地去包裝了下,寬恕我吧_
**windows TT **
windows平臺由于沒有類似NSNotificationCenter這樣的優秀的平臺庫,不可避免對于DDLogic Event Watch機制的封裝需要自己去造輪子,當然會麻煩許多工作量也上升了一個指數。使用方式和上面寫的差不多,這里就不重復寫了,大家可以去看下具體的源碼。
接下來講講DDLogic Event Watch機制在windows上是具體實現。它借助了
- 一層三元組[module_id,module_item,module_tag]來組成一個數據集(DataSet也可以稱作Document)。
- fastDelegate(一套開源的用c++實現的委托,比成員函數指針回調效率更高,有興趣的可以自己去研究下(http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible))實現函數回調即調用到具體的事件處理函數。
- 操作系統的消息循環,包裝成事件通知。
一圖勝千言如下圖:
通過圖示具體實現如下:
- 創建一個無窗口的句柄用來作為異步事件派發的基礎,即底層最終是借助windows操作系統的消息循環來封裝上層的Event事件的派發的,支持同步/異步派發(即SendMessage/PostMessage)。
- 生成一個全局唯一的三元組 DataSet實例用來存儲各個業務模塊觀察者關心的唯一屬性,類似mac TT的MKN(module key
name),存儲格式按照三元組[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通信協議以及拆裝包過程
(協議數據單元(Protocol Data Unit))
PDU通信協議走的是二進制協議——即固定長度的協議頭(16個字節) + 協議體方式
。協議頭包括整個協議包的大小、版本、模塊號(moduleid)、命令號(commandid)等。模塊號(moduleid)
和業務模塊對應,commandid
對應具體的網絡傳輸命令,這樣做的好處是通過包頭就可以知道這個包是屬于那個業務模塊處理的。對于協議這塊的技術選型
,我們當時也討論了許多,我、大子騰、大子燁分別都提出了各自的解決方案,最終選擇了大子騰的PDU協議
,這個過程考慮的因素很多,所以我準備專門寫一篇blog來分寫下當時的情景,另外這篇博文還會分析PDU通信協議和chrome的對比,敬請期待...這里就不再深入描述了。
接下來講下協議的拆包/裝包與DDLogic的分層吧,雖然和具體通信協議交集不是那么大,但是想想還是放這里比較適。
如圖:
從這幅圖可以簡單看出:
1.協議層:協議的拆包和協議任務的分配都封裝在協議層
,對業務層是透明的
2.協議層:協議拆包完成后,會生成一個task放入任務執行池,做任務派發的工作
3.業務層:根據module_id會分派到相應的業務模塊
4.業務層:收到通知后,根據協議里面的command_id處理具體業務
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連接的一系列過程。
+ (void)getStreamsToHostNamed:(NSString *)hostName port:(NSInteger)port inputStream:(NSInputStream **)inputStream outputStream:(NSOutputStream **)outputStream
{
CFHostRef host;
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
host = CFHostCreateWithName(NULL, (__bridge CFStringRef) hostName);
CFStreamCreatePairWithSocketToCFHost(NULL, host, (SInt32)port, &readStream, &writeStream);
CFRelease(host);
...
}
NSStream的兩個派生類NSInputStream/NSOutputStream把整個socket通信抽象成了一個輸入/輸出流
,通過oc平臺的RunLoop將異步I/O事件通知到如下回調函數中:
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch(eventCode) {
...
}
}
回調通知的事件有:
typedef NS_OPTIONS(NSUInteger, NSStreamEvent) {
NSStreamEventNone = 0,
NSStreamEventOpenCompleted = 1UL << 0, //輸入or輸出流打開成功即socket連接建立成
NSStreamEventHasBytesAvailable = 1UL << 1, //可以接受數據通知即輸入緩沖區有內容了
NSStreamEventHasSpaceAvailable = 1UL << 2, //可以發送數據通知即輸出緩沖區空了
NSStreamEventErrorOccurred = 1UL << 3, //錯誤通知
NSStreamEventEndEncountered = 1UL << 4
};
以上具體代碼可以參見mac tt代碼NSStream+NStreamAddtion.m 以及 MGJMTalkClient.m(現在換成DDTcpClientManager.m)
咋樣整個過程看下來是不都看不到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、FD_WRITE、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的思路是這樣的(以登陸業務模塊為例子):
// 現在的好像是DDLoginManager.m
1.所有模塊的對外接口都通過DDLogic Modules Manager來管理。
2.模塊與模塊之間通過接口來調用,
模塊內部實現對外不可見。比如外部只能調用DDloginModule的doLogin()來實現登陸操作,調用方是不知道具體如何實現登陸的。
3.每個獨立的業務創建成為業務module——DDLoginModule,有一個全局唯一的業務模塊ID——MODULE_ID_LOGIN
4.調用業務模塊的接口函數通過全局唯一業務模塊ID——MODULE_ID_LOGIN來,DDLoginModule*
loginModule = getDDModule(MODULE_ID_LOGIN);[loginModule doLogin];
5.支持插件管理,n(n >=1)個模塊合作來組裝成1個插件,并且支持動態加載/卸載(未實現的目標)
是不感覺DDLogic框架連這種東西也拿出來分享,沒啥技術含量是個程序員都知道用類似的方式來拆分業務?是的你的感覺是對的,但是只對了一半,確實看起來沒啥營養,但是請你再往下看你會發覺這個點才是整個DDLigic框架的精髓,如果說上面講的每個設計點是DDLogic框架的一條條河流的話,那么這里就應該是它們的匯聚地,下面逐個點來分析
1.基于Task的任務調度:每個task的執行都會綁定一個module_id來知道具體是哪個模塊的task在執行,并且通過module_id將任務執行的結果反饋給模塊,這條河流就匯聚到module了。
2.事件的訂閱與發布:每個事件都是通過指定module_id和MKN來訂閱的,等到被訂閱事件發布的時候同樣通過指定module_id和MKN來通知出去,這條河流也匯聚到moudule了
3.pdu通信協議以及拆裝包過程:通過解析pdu協議頭獲取module_id和command_id,然后生成NetworkTask派發到相應的模塊中區,這條河流也匯聚到module了
4.持久化數據以及基于此數據之上的一層數據監聽機制(類似IDE工具調試的 Watch):通過儲格式三元組[module_id,module_item,module_tag],這條河流也匯聚到module了
6 持久化存儲以及基于此數據模型數據監聽機制(類似IDE調試工具的Watch)
DDLogic數據持久化用的是NSCoder可以支持基于業務模塊的數據序列化/反序列化
。基于數據模型的監聽機制(暫且稱作data watch機制)對一個應用程序來說是非常實用的,舉個例子:你的好友管理模塊的數據新增了一個好友,好友列表數據發生add事件,監聽此數據變化的模塊如好友列表控件、消息管理模塊等都會收到相應的通知并作出及時的處理。
這塊將放到《
mac TT開發點點滴滴之四——NSCode與DDlogic的結合》中做深入闡述,敬請期待。