1. 程式人生 > >蘑菇街的開源IM:TeamTalk

蘑菇街的開源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臺)。

訊息收發流程:

  1. msg_server啟動時,msg_server主動建立到login_server和route_server的長連線。
  2. 客戶端登陸時,首先通過login_server 獲取負載最小的msg_server。連線到msg_server。登陸成功後,msg_server發訊息給route_server,route_server記錄使用者的msg_server。與此同時,msg_server傳送訊息給login_server,login_server收到後,修改對應msg_server的負載值。
  3. 客戶端訊息傳送到msg_server。msg_server判斷接收者是否在本地,是的話,直接轉發給目標客戶端。否的話,轉發給route_server。
  4. route_server接收到msg_server的訊息後,獲取to_id所在的msg_server,將訊息轉發給msg_server。msg_server再將訊息轉發給目標接收者。

資料庫操作:

  • 訊息記錄,獲取使用者資訊等需要操作資料庫的,由msg_server傳送到db_server。db_server操作完後,傳送給msg_server。

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中還使用了一些個第三方元件,具體羅列如下:

  • Adium :OSX下的一個開源的IM,TeamTalk中使用了其中的一些框架和類。

總結

TeamTalk作為一個敢於開源出來的IM,還是非常值得讚揚的,國內的技術氛圍一直提高不起來,大家似乎都在閉門造車。如果多一些像蘑菇街這樣的開源行為,應該能夠更好的促進圈子裡的技術生態。雖然,這篇博文裡提出了很多TeamTalk Mac客戶端架構的不足之處,但,設計本身就是如此,根本沒有最好的設計,而,每個設計者的眼光也不相同,或許我說得都不正確也不見得。

所以,只要有顆敢於嘗試設計的心,開放的態度,一切問題都不是問題。

TT 流程隨筆

細節:

  • 如果本地可以自動登入, 先實現本地登入,傳送事件通知,再請求登入伺服器
  • 如果本地不可以登入(第一次或退出後),直接請求登入伺服器
  • 登入伺服器返回訊息伺服器ip port / 檔案伺服器
  • 連結訊息伺服器(socketThread 通過netty)
  • 連結成功或失敗都發送事件通知 (可能是在loginactivity 處理,也可能在chatfragment處理,你懂滴)
  • 連結失敗彈出介面提示
  • 連結成功 請求登入訊息伺服器(傳送使用者名稱 密碼 etc)並且同時開啟 回掉監聽佇列計時器(這個稍後再細看吧~)
  • 登入訊息伺服器成功或失敗都通過回掉 (回掉函式儲存在packetlistner 中)處理
  • 登入訊息伺服器失敗 傳送匯流排事件,也可能在兩個位置處理(loginactvity/chatfragment ,你懂得~)
  • 訊息伺服器登入成功,並解析返回的登入資訊,傳送登入成功的事件匯流排,事件的訂閱者分為service 和 activity ,activity 中的事件負責ui的更新處理,service中事件處理,訊息的進一步獲取 ,與伺服器打交道
  • 判斷登入的型別(普通登入和本地登入成功後的訊息伺服器登入)
  • service 收到登入成功(此指線上登入成功,本地登入成功也是一個道理,傳送事件更新介面ui和在service中事件觸發進一步的訊息獲取(獲取本地庫))的事件通知(按登入型別有所不同 ,大體一致)後,做如下工作:
    • 儲存本次的登入標示到xml
    • 初始化資料庫(建立或獲取當前使用者所在資料庫統一操作介面單例)
    • 請求聯絡列表
    • 請求群組列表
    • 請求最近會話列表
    • 請求未讀訊息列表(只是在線登入狀態)
    • 重連管理類的相關設定(廣播的註冊等)

接下來就是對服務端傳送訊息過來的分析

  • 服務端傳送訊息過來有回撥的採用回掉處理
  • 服務端沒有回撥的,按照commandid處理

訊息的處理都是在相關的管理器類例項內完成

該存庫的存庫,該更新記憶體的,更新記憶體,然後傳送事件匯流排更新ui  或者通知service中的相關訂閱者,完成業務邏輯的資料相關處理

相關網址: