來自蘑菇街的開源IM:TeamTalk
TeamTalk 是蘑菇街開源的一款企業辦公即時通信軟件,最初是為自己內部溝通而做的 IM 工具。
項目框架
麻雀雖小五臟俱全,本項目涉及到多個平臺、多種語言,簡單關系如下圖:
服務端:
- CppServer:TTCppServer工程,包括IM消息服務器、http服務器、文件傳輸服務器、文件存儲服務器、登陸服務器
- Java DB Proxy:TTJavaServer工程,承載著后臺消息存儲、redis等接口
- PHP server:TTPhpServer工程,teamtalk后臺配置頁面
客戶端:
- mac:TTMacClient工程,mac客戶端工程
- iOS:TTIOSClient工程,IOS客戶端工程
- Android:TTAndroidClient工程,android客戶端工程?
- Windows:TTWinClient工程,windows客戶端工程
語言:c++、objective-c、java、php
系統環境:Linux、Windows,Mac, iOS, Android
作為整套系統的組成部分之一,TTServer為TeamTalk 客戶端提供用戶登錄,消息轉發及存儲等基礎服務。TTServer主要包含了以下幾種服務器:
- LoginServer (C++): 登錄服務器,分配一個負載小的MsgServer給客戶端使用
- MsgServer (C++): 消息服務器,提供客戶端大部分信令處理功能,包括私人聊天、群組聊天等
- RouteServer (C++): 路由服務器,為登錄在不同MsgServer的用戶提供消息轉發功能
- FileServer (C++): 文件服務器,提供客戶端之間得文件傳輸服務,支持在線以及離線文件傳輸
- MsfsServer (C++): 圖片存儲服務器,提供頭像,圖片傳輸中的圖片存儲服務
- DBProxy (JAVA): 數據庫代理服務器,提供mysql以及redis的訪問服務,屏蔽其他服務器與mysql與redis的直接交互
當前支持的功能點:
- 私人聊天
- 群組聊天
- 文件傳輸
- 多點登錄
- 組織架構設置.
系統結構圖
- login_server:均衡負載服務器,用來通知客戶端連接到負載最小的msg_server (1臺)。
- msg_server:客戶端連接服務器(N臺)。客戶端通過msg_server登陸,保持長連接。
- route_server:消息中轉服務器(1臺)。
- DBProxy:數據庫服務,操作數據庫(N臺)。
消息收發流程:
- msg_server啟動時,msg_server主動建立到login_server和route_server的長連接。
- 客戶端登陸時,首先通過login_server 獲取負載最小的msg_server。連接到msg_server。登陸成功后,msg_server發消息給route_server,route_server記錄用戶的msg_server。與此同時,msg_server發送消息給login_server,login_server收到后,修改對應msg_server的負載值。
- 客戶端消息發送到msg_server。msg_server判斷接收者是否在本地,是的話,直接轉發給目標客戶端。否的話,轉發給route_server。
- route_server接收到msg_server的消息后,獲取to_id所在的msg_server,將消息轉發給msg_server。msg_server再將消息轉發給目標接收者。
數據庫操作:
- 消息記錄,獲取用戶信息等需要操作數據庫的,由msg_server發送到db_server。db_server操作完后,發送給msg_server。
參考鏈接: http://www.bluefoxah.org/
TeamTalk 之 Mac 客戶端架構分析
項目結構
在軟件架構中,一個項目的目錄結構至關重要,它決定了整個項目的架構風格。通過一個規范的項目結構,我們應該能夠很清楚的定位相應邏輯存放位置,以及能夠沒有歧義的在指定目錄中進行新代碼的撰寫。項目結構便是項目的骨架,如果存在畸形和缺陷,項目的整體面貌就會受到很大影響。我們來看看TeamTalk的項目根結構:
從整個項目結構圖中,我們大致能猜出一些目錄中存放的是什么,以下是這些目錄的主要意圖:
- html:存放著一些HTML相關文件,用于項目中一些用戶界面與HTML進行Hybrid。
- customView:一些公共的自定義視圖,同樣與用戶界面相關。
- Services:封裝了兩個服務,應用更新檢測,和用戶搜索。
- HelpLib:一些公共的幫助庫。
- Category:顧名思義,這里存放的都是現有類的Category。
- Modules:按照功能和業務進行劃分的一系列模塊。
- DDLogic:這里面主要存放著一個模塊化框架。
- teamtalk:這里面是和TeamTalk應用級別相關的東西。
- views:視圖,原本應該是存放應用所有視圖的地方。
- Libraries:第三方庫。
- utilities:一些通用的幫助類和組件。
- 思考與分析
首先,從總體來說,這樣的目錄結構劃分,似乎可以涵蓋到整個項目開發的所有場景,但它存在以下幾個很明顯的問題:
- 命名不夠規范,對于有態度的人來說,看到這樣的目錄結構,可能首先就會將它們的大小寫進行統一,然后單復數進行統一。雖然這可能并不會對最終應用有任何的提升,但我說過,態度決定一切,既然開源了,這樣的規范更應該值得注重。
- 除了大小寫之外,DDLogic也是讓人非常費解的命名,Logic是什么?它是邏輯?那么似乎整個應用的源碼都可以放置到這里了。這里的問題,就跟我們建立了一個h和Common.h一樣,包羅萬象,但這不應該是我們遵從的。命令體現的是抽象能力,它應該是明確的,模棱兩可會導致它在項目的迭代中要么被淘汰,要么膨脹到讓人無法忍受。
- 類別劃分有歧義,HelpLib和Utilities,似乎根本就無法去辨別它們之間的區別,這兩者應該進行合并。并且Helper類本身就不是很好的設計方式,可以通過Category來盡量減少Helper,無法通過Category擴展的,應該按照類的實際行為進行更好的命名和劃分。
- 含有退化的類別,所謂退化的類別,就是項目初期原本的設定,在后續的迭代重構中漸漸失去作用或者演化為另外的形式。這里的Views和Services是很好的例子,這兩個目錄存放在根目錄下非常雞肋,既然已經按模塊化進行劃分,那么Services可以拆分到相應的模塊里;Views也是類似,應該拆分到相應模塊和CustomView中。
- 含有臃腫的類別,這一點也是顯而易見的,之所以臃腫,是因為里面放了不應該放的東西。這里主要體現在Modules這個目錄,我們應該把不屬于模塊實現的東西提取出來,包括數據存儲、系統配置、一些通用組件。這些應該安置到根目錄相應分類中,而明顯層次化的東西,應該提取到單獨庫或目錄中,比如網絡API相關的東西。
- 沒有意義的單獨歸類,這里體現在Html這個目錄,應該和Supporting Files目錄中的資源進行合并,統一歸類為Resources,然后再按照資源的類別進行細分。
項目結構的劃分應該做到有跡可循,也就是說是按照一定的規則進行劃分。這里主要的劃分依據是邏輯模塊化,這樣的方式我還是比較贊同的,雖然有很多細節沒有處理好,但主線還是很好的。
網絡數據處理
在任何需要聯網的應用中,網絡數據處理都是非常重要的,這點在IM中更是毋庸置疑。IM與很多其它應用相比,更具挑戰,它需要處理很多即時消息,并且很多時候需要自己去構建一套通訊機制。
TeamTalk中,主要使用HTTP和TCP進行通訊,我們知道HTTP是基于TCP的更高層協議,而這里的TCP通訊是指用TCP協議發送自定義格式的報文。TeamTalk在HTTP通訊中使用的是RESTful API,并使用JSON格式與服務器進行交換數據;而在TCP這里,主要是通過ProtocolBuffer序列化協議,加上自定義的包頭與服務器進行通信。
HTTP 數據處理
HTTP的數據處理,在TeamTalk中顯得非常簡單,并沒有做過多的設計。主要是使用AFNetworking封裝了一個HTTP模塊:
DDHttpModule.h
typedef void(^SuccessBlock)(NSDictionary *result); typedef void(^FailureBlock)(StatusEntity* error); @interface DDHttpModule : DDModule -(void)httpPostWithUri:(NSString *)uriparams:(NSDictionary *)paramssuccess:(SuccessBlock)successfailure:(FailureBlock)failure; -(void)httpGetWithUri:(NSString *)uriparams:(NSDictionary *)paramssuccess:(SuccessBlock)successfailure:(FailureBlock)failure; @end externDDHttpModule* getDDHttpModule();
這樣一個模塊會被其它模塊進行使用,直接傳遞uri請求服務器,并解析響應,以下是一個使用場景:
DDHttpServer.m
- (void)loginWithUserName:(NSString*)userName password:(NSString*)password success:(void(^)(idrespone))success failure:(void(^)(iderror))failure { DDHttpModule* module = getDDHttpModule(); NSMutableDictionary* dictParams = [NSMutableDictionarydictionary]; ...(省略參數賦值) [[NSURLCachesharedURLCache] removeAllCachedResponses]; [modulehttpPostWithUri:@"user/zlogin/" params:dictParams success:^(NSDictionary *result) { success(result); } failure:^(StatusEntity *error) { failure(error.msg); } ]; }
即便是這樣的一個封裝,在后續的迭代中似乎也慢慢失去了作用,目前大部分所使用到HTTP的代碼里,都是直接使用AFNetworking,那么這樣的一個封裝已經沒有存在的必要了。
TCP 數據處理
在TeamTalk里,針對TCP的數據處理略顯復雜,因為沒有類似AFNetworking這樣的類庫,所以需要自己封裝一套處理機制。大致類圖如下:
通過這樣的一個類圖,我們大致可以推斷出設計者的抽象思維,他把所有網絡操作抽象為API。基于這樣思路,這里有三個最核心的類:
- DDSuperAPI:這個類是對所有Request/Response這種模式網絡的請求進行的抽象,所有遵循這種模式的API都需要繼承這個類。
- DDUnrequestSuperAPI:這個和DDSuperAPI相對應,也就是所有非Request/Response模式的網絡請求,基本上都是服務端推送過來的消息。
- DDAPISchedule:API調度器(應該改名為DDAPIScheduler),顧名思義,是用來調度所有注冊進來的API,這個類主要做了以下幾件事情:
- 通過DDTcpClientManager接收和發送數據包。
- 通過seqNo和數據包標識符(ServiceID和CommandID,這里源碼中CommandID拼寫有誤哦),映射Request和Response,并將服務端的響應派發到正確的API中。
- 管理響應超時,確保每一個Request都會有應答。
基于這樣一個設計,我們來看一個基本的登錄操作序列圖:
所有基于請求響應模式的操作,都是與上圖類似,而服務端推送過來的消息,也是類似,只是沒有了請求的過程。通過我的分析,大家覺得這樣的設計怎么樣?首先從擴展性的角度考慮,每一個API都相對獨立,增加新的API非常容易,所以擴展性還是很不錯的;其次從健壯性的角度考慮,每一個API都由調度器管理,調度器可以對API進行一些容錯處理,API本身也可以做一些容錯處理,這一點也還是可以的;最后從使用者的角度考慮,API對外暴露的接口非常簡單,并且對于異步操作使用Block返回,對于組織代碼還是非常有用的,所以使用者也覺得良好。
那么,這是一個完美的設計了么?我說過,沒有完美的設計,只有符合特定場景的設計。針對這個設計,撇開它一些命名問題,以下是我覺得它不足的地方:
- 子類膨脹,恰恰是為了更好的擴展性,而帶來了這樣的問題,由于一個API最多只能處理兩個協議包(Request,Response),所以協議眾多時,導致API子類泛濫,而所做的基本都是相似事情。TeamTalk這種形式的封裝,本質上是采用了Command模式,這個模式在面向對象的設計中本身就充滿爭議,因為它是封裝行為(面向過程的設計),但也有它適用的場景,比如事務回滾、行為組合、并發執行等,但這里似乎都用不到。所以,我覺得TeamTalk這樣的設計并不是特別合適,或許使用管道設計會更好點。
- 調度器職責不單一,為什么說它的職責不單一呢?因為引起它的變化點不止一處,很顯然的,發送數據不應該納入調度器的職責中。另外DDSuperAPI和DDUnrequestSuperAPI全部由這一個調度器來調度,也是有點別扭的,前者響應分發完后必須要從列表中移除,后者又絕對不能被移除,這樣鮮明的差異性在設計中是不應該存在的,因為它會導致一些使用上的問題。
總體來說,這樣的一個框架還是不錯的,因為它的抽象層次不高,很容易去理解和維護,并且完成了大家的預期,這樣或許就已經足夠了。
本地持久化
本地持久化是個可以有很多設計的地方,但在APP中,進行設計的情況并不是很多,因為APP本身對于持久化的要求沒有MIS高,一般只是做些離線緩存,而在IM中,它還負責存儲歷史消息等結構化數據。TeamTalk對于持久化這塊,也沒有做什么設計,只是依托于FMDB封裝了一個MTDatabaseUtil,這是一個類似于Helper的存在,里面聚集了所有APP會用到的存儲方法。毋庸置疑,這樣的封裝會導致類比較龐大,好在TeamTalk中存儲方法并不多,并且使用了Catagory對方法進行了分類,所以總體感覺也還是可以的。另外,從殘存的目錄結構中可以看出,TeamTalk原本可能是想采用CoreData,但最終放棄了,或許是覺得CoreData整體不夠輕量級吧。
MTDatabaseUtil和API一樣,都只能算是基礎設施(Infrastructure),給高層模塊提供支持,高層模塊會使用這些基礎設施根據業務邏輯進行封裝,可以看一個具的代碼片段:
MTGroupModule.m
- (void)getOriginEntityWithOriginIDsFromRemoteCompletion:(NSArray*)originIDscompletion:(DDGetOriginsInfoCompletion)completion{ ...(省略) DDGroupInfoAPI *api = [[DDGroupInfoAPIalloc] init]; [apirequestWithObject:paramCompletion:^(idresponse, NSError *error) { if (!error) { NSMutableArray* groupInfos = [responseobjectForKey:@"groupList"]; [self addMaintainOriginEntities:groupInfos]; [[MTDatabaseUtilinstance] insertGroups:groupInfos]; completion(groupInfos,error); }else{ DDLog(@"erro:%@",[errordomain]); } }]; }
理想中,只會在業務模塊里依賴持久化操作庫,但從TeamTalk總體使用情況中看,并不是這么理想,很多Controller里面直接對MTDatabaseUtil進行了操作,這樣就削弱了模塊化封裝的意義。顯然,Controller的職責不應該牽扯到數據持久化,這些都應該放置在相應的業務模塊里,統一對外屏蔽這些實現細節。
模塊化設計
模塊化設計是更高層次的抽象和復用,也是業務不斷發展后必然的設計趨勢。在進入目前公司的第二周例會上,我便分享了一個親手設計的模塊化框架,這個框架和TeamTalk模塊化框架有很多類似之處,好壞暫不做對比,我們先看看TeamTalk中的一個模塊化架構。在TeamTalk的DDLogic目錄下,隱藏著一個模塊化的設計,這也是整個項目中模塊設計的基礎構件,以下是這個設計的核心類圖:
- DDModule:最基礎的模塊抽象,所有模塊的基類,包含自己的生命周期方法,并提供一些模塊共有方法。
- DDTcpModule:擁有TCP通訊能力的模塊,監聽網絡數據,子類化模塊可以就此進行業務封裝。
- DDModuleDataManager:按照模塊的粒度進行持久化操作,負責持久化和反持久化所有模塊。
- DDModuleManager:管理所有模塊,負責調用模塊生命周期方法,并對外提供模塊獲取方法。
整個設計還是很簡單明了的,但不知是TeamTalk設計者更換了,還是原設計者變心了,導致這個模塊化設計沒有起到它預期的作用。具體原因就不細究了,但這樣的設計還是值得去推演的,就目前這樣的設計而言,也還是缺少了一些東西:
- DDModule應該通過DDModuleManager注入一些基礎設施,比如數據庫訪問組件、緩存組件、消息組件等。
- DDModule應該有獲取到其它模塊的能力,這里面不應該反依賴與DDModuleManager,可以抽象一個ModuleProvider注入到DDModule中。
- 可通過Objective-C對象的load方法,在模塊實現類中直接注冊模塊到模塊管理器里,這樣會更加內聚。
雖然我覺得有點缺失,但還是很欣慰的看到了這樣的模塊化設計,又讓我想起一些往事,這種心情,就像遇見了一個和初戀很像的人。
UI相關設計
整個UI設計也沒什么特別之處,主要還是采用了xib進行布局,然后連線到相應的Controller中,這里主要的WindowController是DDMainWindowController,它是在登錄窗口消失后出現的,也就是DDLoginWindowController所控制的窗口消失后。
值得一提的是,這里將所有的UI都放置到了相應的業務模塊中,這也是我比較推崇的做法。一個模塊本就應該能夠自成一系,它應該有自己的Model,有自己的View,也有自己的Controller,還可以有自己的Service等。這樣設計下的模塊才會顯得更加內聚,其實設計就是這么簡單,小到類,大到組件都應該遵循內聚的原則。
其它組件
TeamTalk中還使用了一些個第三方組件,具體羅列如下:
- CrashReporter :用于崩潰異常收集。
- Sparkle :用于軟件自動更新。
- Adium :OSX下的一個開源的IM,TeamTalk中使用了其中的一些框架和類。
總結
TeamTalk作為一個敢于開源出來的IM,還是非常值得贊揚的,國內的技術氛圍一直提高不起來,大家似乎都在閉門造車。如果多一些像蘑菇街這樣的開源行為,應該能夠更好的促進圈子里的技術生態。雖然,這篇博文里提出了很多TeamTalk Mac客戶端架構的不足之處,但,設計本身就是如此,根本沒有最好的設計,而,每個設計者的眼光也不相同,或許我說得都不正確也不見得。
所以,只要有顆敢于嘗試設計的心,開放的態度,一切問題都不是問題。
原文地址: http://blog.makeex.com/2015/05/30/the-architecture-of-teamtalk-mac-client/
TT 流程隨筆
細節:
- 如果本地可以自動登錄, 先實現本地登錄,發送事件通知,再請求登錄服務器
- 如果本地不可以登錄(第一次或退出后),直接請求登錄服務器
- 登錄服務器返回消息服務器ip port / 文件服務器
- 鏈接消息服務器(socketThread 通過netty)
- 鏈接成功或失敗都發送事件通知 (可能是在loginactivity 處理,也可能在chatfragment處理,你懂滴)
- 鏈接失敗彈出界面提示
- 鏈接成功 請求登錄消息服務器(發送用戶名 密碼 etc)并且同時開啟 回掉監聽隊列計時器(這個稍后再細看吧~)
- 登錄消息服務器成功或失敗都通過回掉 (回掉函數存儲在packetlistner 中)處理
- 登錄消息服務器失敗 發送總線事件,也可能在兩個位置處理(loginactvity/chatfragment ,你懂得~)
- 消息服務器登錄成功,并解析返回的登錄信息,發送登錄成功的事件總線,事件的訂閱者分為service 和 activity ,activity 中的事件負責ui的更新處理,service中事件處理,消息的進一步獲取 ,與服務器打交道
- 判斷登錄的類型(普通登錄和本地登錄成功后的消息服務器登錄)
- service 收到登錄成功(此指在線登錄成功,本地登錄成功也是一個道理,發送事件更新界面ui和在service中事件觸發進一步的消息獲取(獲取本地庫))的事件通知(按登錄類型有所不同 ,大體一致)后,做如下工作:
- 保存本次的登錄標示到xml
- 初始化數據庫(創建或獲取當前用戶所在數據庫統一操作接口單例)
- 請求聯系列表
- 請求群組列表
- 請求最近會話列表
- 請求未讀消息列表(只是在線登錄狀態)
- 重連管理類的相關設置(廣播的注冊等)
接下來就是對服務端發送消息過來的分析
- 服務端發送消息過來有回調的采用回掉處理
- 服務端沒有回調的,按照commandid處理
消息的處理都是在相關的管理器類實例內完成
該存庫的存庫,該更新內存的,更新內存,然后發送事件總線更新ui 或者通知service中的相關訂閱者,完成業務邏輯的數據相關處理