• <source id="sqj45"><optgroup id="sqj45"></optgroup></source>
  • <rt id="sqj45"></rt>
    <rt id="sqj45"></rt><rt id="sqj45"></rt>
  • <source id="sqj45"><nav id="sqj45"></nav></source><rt id="sqj45"></rt><rt id="sqj45"></rt><tt id="sqj45"><noscript id="sqj45"></noscript></tt>
    專業 靠譜 的軟件外包伙伴

    您的位置:首頁 > 新聞動態 > 大型同城交易網站IM軟件開發架構分析!

    大型同城交易網站IM軟件開發架構分析!

    2017-08-25 13:03:32

    對于 58 同城 App 這樣以信息展示及交易為主體的平臺而言,App 內的 IM 即時消息功能,相比電話和短信,在促成商品/服務交易上更有著舉足輕重的地位。也正因如此,自 1.0 版本開始,便一直致力于自研 IM 系統。在自研過程中,我們發現如何降低 IM 系統層次和頁面間的耦合,減少 IM 系統的復雜性,是降低技術成本提高研發效率的關鍵。

    為此,本文將主要從兩個方面闡述 58 同城 iOS 客戶端 IM 系統架構的變遷過程。一是 IM 系統如何解除對數據庫和 Socket 接口的依賴;二是 IM 聊天頁面從傳統的 MVC 模式走向面向協議的新型架構。希望給具有相似業務場景的開發者提供一些借鑒。

    老版本 IM 系統遇到的問題

    58 App 在項目早期就自研了 IM 系統,但只實現了文本消息、圖片消息、音頻等基本類型。雖然業務需求場景簡單,卻還是遇到了如下問題。

    數據擴展性差

    數據格式使用的是 Google ProtocolBuffer(以下簡稱 PB),是因為這種數據格式相比 XML 和 JSON 相同的數據形式,體積更小,解析更加迅速。但 PB 是用 C++實現的,使用起來相對繁瑣。需要對不同的消息類型編寫不同的 PB 數據結構,每種 PB 結構還需要單獨的數據解析方法。由于 58 業務的發展,這種數據協議增加了系統的復雜性。

    代碼封裝性差,研發成本高

    在數據發送前,為了安全,還需要將待傳輸的數據通過特定的加密算法進行加密,再利用 AsyncSocket 做數據傳輸。相對應的,每接到一種消息類型,就需要解密,將 PB 格式轉換成對象模型。這種方案,每次新增消息類型時都比較痛苦,要寫加密算法,寫 PB 模型解析器。這樣不僅代碼的擴展性很差,開發難度也比較大。

    代碼耦合性強

    每次如果有新增消息類型,要在 DB 層寫個接口對新消息的數據解析并存儲。同樣,在 Socket 傳輸層也要新增收發接口與之對應。這種設計方式,開發過程中耦合性很大。

    代碼可讀性差

    App 內只有一種消息類型,叫 WBMessagModel。在消息類型判斷上,是通過 WBMessageModel 里的特定字段來進行區分的,比如根據 mtype、isOnlineTip、m_msgtype 等字段判斷,方式相對混亂。

    為了解決上面的問題,打造一款低耦合、可擴展性強的 IM 系統,我們決定重構。

    新版本的 IM 系統

    框架演進

    老的 IM 系統由于代碼耦合性嚴重,一旦遇到問題難于追查。并且擴展性差,每個版本的需求研發,都從底層修改到業務層,影響研發進度。結合之前 IM 開發過程中遇到的問題,新的 IM 系統亟需解決如下問題。

    • 簡化調用流程

    業務開發過程中做到與“底層 DB+數據加密+數據加密+數據傳輸”的分離,通過調用底層接口就可以做到收發、存儲消息。

    • 設計低耦合的中間層接口

    中間層接口要做到承上啟下對接,業務層和底層接口無任何耦合。如果做到這些,以后在 IM 底層升級甚至更換時,只需調整業務接口與底層接口的重新對接,讓頂層的業務無感知,做到無感知的迭代。

    • 設計單一職能的模型和接口

    在具體業務層處理上,要做到模型分離,設計統一。模型上,將之前的只有一個 IM 模型根據各自的類型拆分。接口上,通過底層、中間層業務層的結構劃分,每層接口各司其職。

    • 可擴展性強

    利用面向協議方式抽象和組織代碼,做到按照協議新增消息。利用 UITableView 的類別做到現有及新增的消息類型 Cell 能夠自動計算高度。通過這種業務上的設計方式,能夠快速定位問題。如有新增的消息類型,只需關注新增的消息模型和與之對應的消息界面即可,完全無需關注視圖的填充時機以及如何計算視圖的高度等。確定了這些設計原則,才能保證在業務研發過程中做到快速迭代,進而滿足日益增長的用戶需求。

    基于上面的目標,重構后的 IM 整體架構圖 1 所示。

     

     

     

    圖 1 新版 IM 架構設計

     

    新的 IM 系統整體架構包含底層、接口服務層、業務層三個部分。底層主要進行數據收發、存儲等相關處理,并抽象出通用底層接口,與接口服務層交互。接口服務層主要負責合理地將底層的數據傳遞到業務層,同樣,業務層的數據能夠通過接口服務層傳遞給底層。清晰明了的接口服務層不僅可以讓業務層處理數據變得更簡單,還能極大地降低業務層和底層的耦合。業務層主要針對具體需求場景,如何合理使用數據進行視圖的展示?;谶@樣的設計,下面詳細介紹一下各個層次之間的具體實現。

    設計調用流程簡潔的底層接口

    新的 IM 底層采用了全新的設計思路,如圖 2 所示。在底層,為了數據的可擴展性,放棄了之前 PB 的數據協議,而是采用傳統的 JSON 格式作為 Socket 端數據的收發協議。

     

     

     

    圖 2 底層架構設計

     

    在消息模型上,摒棄了之前只有一種消息模型的策略,而是根據消息類型劃分出文本消息模型、圖片消息模型等基本消息模型。

    58 App 將 DB 和 Socket 的內部處理封裝成 SDK,對外只暴露 IMClient 底層接口。頂層所有消息相關的事件都是和底層 IMClient 的接口交互,內部流程完全不用關心。這樣業務層完全感知不到數據是如何收發和存儲的,極大地簡化了接入和使用成本。

    但是讀者也許會有疑問,IM SDK 里內置了如此多的類型消息,那以后有新增 SDK 里沒有的消息類型該怎么辦?為了解決這個問題,58 App 采用了一種和 iOS 自定義對象歸解檔相似的策略——任意定義一種新的消息,只要它繼承自基礎的消息類型,并遵循 IMMessageCoding 協議。這個協議里定義了 encode 和 decode 方法,其中,encode 方法用于將新類型消息里的數據存儲到數據庫中(當然,這個過程并不需要上層開發者關注,他們只需在這個函數里返回待存儲的數據即可);decode 方法用于將數據庫中的數據恢復成相應的消息模型?,F在,我們有了消息類型的定義方式,又如何使用呢?為了讓底層能夠感知到自定義的消息類型,需要在統一接口層 IMClient 初始化之后,立即注冊給它,注冊后 IM 底層就知道當前的消息類型,并且明白如何存儲和恢復數據?;谶@種設計方式,目前 58 App 的 IM 底層可以任意擴展其他消息類型,而底層的代碼完全不用修改。

    底層代碼不僅有良好的擴展性,并且在設計時還為一些基礎的場景提供了很多協議。這些協議都是可動態定制或移除的。例如,當聯系人列表發生變化時,需要修改聯系人頭像,就可以訂制底層IMClientConversationListUpdateDelegate協議。使用時,業務方通過注冊協議addUpdateConversationListDelegate:,當監聽到聯系人更新回掉后,執行頭像更新操作。當不需要時,可通過removeUpdateConversationListDelegate:方式,解除監聽。類似的場景還有消息接收協議、在線狀態變化協議等。通過這種方式,就可靈活配置業務代碼對 IM 的某些狀態變化的監聽。

    目前,通過對底層代碼的抽象,提供頂層接口與內部數據處理分離,且很多 IM 服務都可定制化實現,由此就做到了和具體業務無耦合。通過這樣的底層設計,完全可以作為基礎的 IM SDK,給其他 App 使用,快速集成 IM 功能。

    設計低耦合、職責單一的中間層接口

    為了業務層和底層能夠通信,并且互不耦合,我們創建了中間接口層用以承上啟下。根據實際的業務場景,中間接口層分了三種情況,即為登錄相關的接口、消息收發相關的接口以及消息查詢相關接口,分別和底層統一接口對接。通過業務場景的劃分,開發過程中可以快速定位相關業務對應的模塊。對于底層提供的消息模型,并沒有直接使用,究其原因是底層的消息模型完全不關心視圖展示屬性,比如行高、重用標識等屬性(下節會詳細介紹)。而 MVVM 中 VM 部分屬性需要和視圖關聯,因此將底層的消息模型轉換成了聊天 Cell 直接可用的消息模型。通過這樣的業務接口劃分和消息模型的轉換,即使之后底層統一接口或消息模型發生變化,只要做好中間接口的重新對接和消息模型的重新轉換,頂層業務就完全感知不到下面的變化。

    設計可擴展性強的業務層

    由于老的 IM 系統項目是早期搭建的,處理的業務場景簡單,擴展性不足。例如所有消息都使用同一個數據模型,就會造成隨著業務場景的擴展,模型的代碼體積越來越大,使用時好多屬性冗余不堪。在設計上,老架構使用了 MVC 設計模式,由于在聊天場景下,VC 要處理的聊天視圖類型較多,VC 內部十分臃腫。因為之前架構的局限性,這就對新的 IM 業務架構提出了要求,怎樣設計出低耦合、擴展性強的業務層?接下來介紹一下具體的實現方案。

    • 拆分 IM 消息模型:明確了上面的問題,現在 58 App 把之前只有一個消息模型,拆分成了文本、圖片、語音、提醒、音頻、視頻等消息模型,它們統一繼承基類消息的模型,基類消息模型存儲了 IM 所需的必要數據,如聊天用戶的信息、消息發送的狀態等。

    • 使用 MVVM 架構:為了降低 VC 和各個聊天視圖之間的耦合。VC 管理各種消息模型,消息模型中存儲視圖展示時需要的數據。在消息視圖和消息模型之間,實現了雙向數據綁定。實現的方式是在聊天視圖里存儲與之對應的消息模型,這樣當聊天視圖變化并需要消息模型做數據更新時,直接對消息模型賦值即可。當聊天視圖要根據消息模型屬性變化而變化時,則通過 KVO 的方式實現這一功能。例如在 IM 場景中,我們發送一條消息,消息模型中的發送狀態是發送中,當發送狀態變化時(如發送成功或失?。?,聊天視圖就可以根據改變后的值進行更新;

    • 使用面向協議組織 IM 模型和視圖:通過面向協議的方式,組織 IM 模型和視圖,可以增強 IM 消息模型和視圖的擴展性。下文會結合具體的技術細節,闡述面向協議的設計在 58 IM 系統中的重要作用。

    技術細節

    聊天列表頁技術細節

    由于 IM 模塊的特點,伴隨著業務需求的發展,IM 的類型會越來越多。為了避免在研發過程中每次都要花費很多精力計算 UITableView 中 Cell 的高度,為此我們在 App 內利用 XIB 創建不同的 Cell,并使用 AutoLayout 的方式給 Cell 中的視圖布局。當然,你也可以通過手寫代碼的方式,然后利用 AutoLayout 布局。而 App 在 IM 中利用 XIB 布局,目的是為了讓視圖的布局更直觀地展示,以及更好地讓視圖部分和 VC 分離。當 Cell 中所有布局合理完成后,就可以通過調用系統的 systemLayoutSizeFittingSize:方法,獲得 Cell 的高度?;谶@種思路,58 App 內部給 UITabelView 增加了自動計算 Cell 高度的能力,代碼如下:

    #import <uikit uikit.h="">
    #import "WBAutoCalculateTableViewDelegate.h"
    
    @interface  NSObject (WBAutoCalculateTableView)
    @property (nonatomic,assign) CGFloat kid_height;
    @end
    
    @interface UITableView(WBAutoCalculateTableView)
    - (CGFloat)heightForRowWithReuseIdentifier:(NSString *indentifiercellEntity:(NSObject *)cellEntity;
    @end</uikit>

    首先我們給 NSObject 增加了類別,并在類別里添加了 kid_height 屬性,目的是在計算完 Cell 的高度后,將其緩存好。這樣下次重新加載 UITableView 時,就直接返回緩存過的高度。

    其次,我們給 UITableView 添加了類別。利用heightForRowWithReuseIndentifier: cellEntity:這個 API,在傳入當前消息 Cell 的重用標識和當前的消息模型后,就返回當前 Cell 的高度。而調用者完全不用關心高度計算細節,計算完成后,立即將高度利用 NSObject 的類別屬性緩存在消息模型中。

    為了解決不同類型的消息 Cell 填充數據方式不一致的問題,我們引入了如下協議:

    #import <foundation foundation.h="">
    
    @protocol  WBCellConfigDelegate<nsobject>
    @required
    - (void)setModel:(id)cellEntity;
    @end</nsobject></foundation>

    如此,讓 UITableView 中所有的消息 Cell 都遵循此協議,此協議規范了不同的消息 Cell 之間填充數據的統一性。不同的消息 Cell 使用不同類型的消息模型, 但卻可以使用相同的填充規范。

    @protocol WBAutoCalculateCellViewModelProtocol <nsobject>
    @required
      - (NSString *)cellReuseIndentifier;
     - (void)registerCellForTableView:(UITableView *)tableView;
    @optional
      - (CGFloat)cellHeight;
    @end</nsobject>

    為了解決消息視圖在即將展示時,還要根據當前的消息類型,去判斷該使用哪種視圖的模板,58 App 采用讓每個消息模型遵循上面的協議,每個消息模型都存儲與之對應的重用標識。因為 Cell 的注冊方式有多種,如通過類注冊或 Nib 注冊,這里設計成靈活的接口,注冊 Cell 方式完全交由開發者決定。

    下面的可選協議,在此還要著重在介紹一下- (CGFloat)cellHeight。這個協議是這樣的,雖然大部分場景能夠自動計算某個 Cell 的高度,但有些消息類型的高度是固定的,根本無需計算。為了解決這個問題,我們給消息模型增加了可選的 cellHeight 協議,如果消息模型實現這個協議,則 Cell 的高度就不自動計算了,通過此方法的返回值決定。

    做項目有時就像搭積木一樣,通過上面的介紹,我們已經有了很多小的解決方案,就像有了很多積木零件,如何將這些方案組織在一起,下面到了將這些“積木”組裝到一起的時候了。因為我們是通過 UITableView 組織和管理聊天頁面視圖的,而tableView:heightForRowAtIndexPath:是其重要的代理方法,目前實現如下:

    #pragma mark  UITableViewDelegate
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
      CGFloat cellHeight = 0;
      id <wbautocalculatecellviewmodelprotocol>cellEntity = self.viewModel.dataSource[indexPath.row];
      //向 tableview 中注冊 cell 通過 cellindentifier
      if ([cellEntity conformsToProtocol:@protocol(WBAutoCalculateCellViewModelProtocol)]) {
         if(!self.tableViewRegisters[[cellEntity cellReuseIndentifier]]) {
           [cellEntity registerCellForTableView:tableView];
           self.tableViewRegisters[[cellEntity cellReuseIndentifier]] = @(1);
            }
        }
    
        if ([cellEntity respondsToSelector:@selector(cellHeight)]) {
            cellHeight = [cellEntity cellHeight];
        }else{
            cellHeight = [tableView heightForRowWithReuseIdentifier:[cellEntitycellReuseIndentifier] cellEntity:(NSObject *)cellEntity];
        }
        return cellHeight;
    }</wbautocalculatecellviewmodelprotocol>

    在這個方法中,我們看到了每個cellEntity(消息模型),都遵循了上面介紹的 WBAutoCalculateCellViewModelProtocol。在此方法里,讓每個消息模型去注冊自己的 Cell 類型,然后計算 Cell 的高度,如果消息模型有 cellHeight 方法,則通過此方法計算高度,否則通過上面提到的自動算高的方式,返回 Cell 的高度。

    在 Cell 的展示處理上,UITableView 的數據源方法tableview:cellForRowAtIndexPath:是核心的方法,目前實現如下:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
        id <wbautocalculatecellviewmodelprotocol>model = self.viewModel.dataSource[indexPath.row];
        NSString *cellIndentifier = [model cellReuseIndentifier];
    
        UITableViewCell<wbcellconfigdelegate> *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentifier];
        if (cell &&[cellconformsToProtocol:@protocol(WBCellConfigDelegate)]) {
            [cell setModel:model];
        }
        if (!cell) {
            cell =  (UITableViewCell<wbcellconfigdelegate> *)[[UITableViewCell alloc]init];
        }
        return cell;
    }</wbcellconfigdelegate></wbcellconfigdelegate></wbautocalculatecellviewmodelprotocol>

    通過消息模型找出重用標識。因為已經在tableView:heightForRowAtIndexPath:時注冊過了 Cell,所有通過重用隊列一定能返回該消息類型下的 Cell。而消息 Cell 都遵循WBCellConfigDelegate協議,使得數據在填充時具有統一的方式。  通過面向協議的設計方式,我們在 VC 里 tableView 的代理和數據源方法就變得如此簡單。而且以后如果在擴充新的消息類型時,繼續遵循相應的協議,VC 里的代碼是一行都不用修改的,開發人員只要關和注新增的消息模型和視圖即可。

     

     

     

    圖 3 承上啟下的業務中間層設計

     

    處理離線 Voip 消息的技術細節

    實際開發過程中,我們遇到了一個問題,當 B 不在線時,B 的聊天對象可能向 B 發起音視頻消息,服務器為了信令消息的完備性,會建立一個隊列,將所有向 B 發消息的信令記錄下來。過了一段時間,當 B 登錄時,Server 會把 B 離線期間所有的通話信令發過來。由于剛開始設計時沒有考慮到這一點,造成一個問題就是當 B 啟動時,A 發送了一個視頻消息過來時,B 接受到第一個視頻信令是離線期間的視頻消息信令(如果有)。這就造成了 B 嘗試連接一個早已不存在的視頻通道,而讓 A-B 視頻聊天連接不上??蛻舳藶榱艘仓С诌@種信令序列,利用條件鎖技術有序地處理視頻連接信令,如圖 4 所示。

     

     

     

    圖 4 通話信令序列設計

     

    具體解決方案如下:

    • 首先,我們創建一個 Concurrent Queue,當有信令信號傳給客戶端時,就放在 Concurrent Queue 里執行;

    • 為了保證 Voip 信令能有序執行,我們引入了條件鎖 NSCondition, 并行隊列在處理 Voip 信號時,先獲取條件鎖,獲取完畢后,我們將 isAvLockActive Bool 變量標記為 YES,然后對信號進行初步處理,初步處理完畢后 Unlock 條件鎖;

    • 由于 Unlock 了條件鎖,隊列里其他的 Voip 信令就有了處理的機會。處理時,檢測 isAvLockActive 狀態,如果為 YES,說明此前有 Voip 信令還沒有處理完畢,則執行條件鎖的 wait 方法;

    當某個 Voip 信號事件完全處理完畢后,會觸發條件鎖 Signal, 這時,隊列里其他等待條件鎖的信號就可以得到處理。這時我們又返回步驟 2,直至隊列里沒有待處理的 Voip 信號。

    總結

    這次 IM 系統重構,通過底層接口分離使得 IM SDK 耦合性降低,利用面向協議設計方式使得聊天頁面可擴展性增強,所以短時間內 App 內部擴展了富文本、圖片、地理位置、簡歷、卡片等類型消息。希望通過 58 App IM 的重構歷程,能給設計或改造優化 IM 模塊的開發者提供一些參考。未來,我們會在如何提高頁面性能和降低用戶流量上進一步調優,繼續完善 IM 的各個細節。

     

    關于:中科研拓

    深圳市中科研拓科技有限公司專注提供軟件+硬件結合系統解決方案定制開發服務,其中包括:軟件外包、軟件開發、軟件定制、硬件開發、硬件定制、智能硬件開發、物聯網項目等開發外包服務,通過IT技術實現創造客戶和社會的價值,成為優秀的軟件公司,通過客戶需求導向、開放式創新、卓越運營管理等戰略的實施,全面打造公司的核心競爭力。優秀軟件外包公司、軟件開發公司,聯系電話400-0316-532,郵箱sales@zhongkerd.com,網址www.ruige-europe.com


      上一篇   [返回首頁] [打印] [返回上頁]   下一篇
    香蕉97超级碰碰碰免费公开
  • <source id="sqj45"><optgroup id="sqj45"></optgroup></source>
  • <rt id="sqj45"></rt>
    <rt id="sqj45"></rt><rt id="sqj45"></rt>
  • <source id="sqj45"><nav id="sqj45"></nav></source><rt id="sqj45"></rt><rt id="sqj45"></rt><tt id="sqj45"><noscript id="sqj45"></noscript></tt>