客戶端軟件的結構思考(一)

文章中有些思路還是可以借鑑下。不過感覺目前公司項目中應用的通信類結構比文章中介紹的思路要強多了。


https://blog.csdn.net/analogous_love/article/details/78395024

關於這個標題的內容我思考了很多年,也求索了很多年,每次遇到一份新的質量看起來不錯客戶端軟件的源碼時,我總是忍不住地去學習和研究,以期能解決我的困惑,希望能達到我心中「完美」方案的樣子。但是直到今天,我仍然沒找到所謂的「完美」的答案,但是在這個過程中,因爲借鑑、融合和吸納了許多其他客戶端軟件的設計思想和技巧,我在做pc軟件整體結構設計時越來越得心應手。這個系列文章將是我的成長的心路歷程,故事很長,有太多前程往事,如果你準備好聽我說一說,那咱們就開始吧。當然,這是這個系列的第一篇。

      注意:下面的軟件內容是關於pc端軟件的,但是不侷限於pc端,移動端也類似。


一、困惑我的問題

       互聯網從業多年,設計過很多pc端產品,讓這個產品性能卓越、界面流暢、用戶體驗好一直是我追求的目標。然而,從接觸Windows程序設計以來,我一直被這樣一些問題困惑着:

1.  對UI流暢性的追求

       軟件UI流暢性直接影響到軟件的用戶體驗。Windows程序的消息機制原理決定着主線程必然是UI線程(當然你的程序如果沒有GUI那就另當別論了),那麼決定一款軟件的界面的流暢性很大程度上取決於這個主線程中對Windows消息處理的時耗,對一個消息處理的時耗越長,界面將越卡頓,反之,可能會越流暢。(注意,我這裏只是說「可能會越流暢」,因爲除了這裏討論的因素外,還有其他因素決定者Windows UI的流暢性,所以這裏討論的因素只是UI流暢性的必要因素之一。)所以,爲了達到這個目標,一般比較耗時的操作,例如網絡收發數據、磁盤讀寫較大文件,我都會開啓新的工作線程來處理這些耗時的操作。在這種認知和實踐過程中,我走了很多彎路,有段時間,我甚至認爲所有的非UI邏輯都應該搬到工作線程裏面去,這樣才能最大程度地保證UI在用戶操作時響應的及時性。這種想法其實有點極端了,毫不委婉地說這種想法也是錯誤的,理由有以下兩點:

a. 只要UI線程在處理事件時不阻塞,不做那些明顯耗時(人能感知)的操作,不一定要把非UI邏輯移到工作線程裏面去;因爲現代計算機在執行這些代碼時耗相對於人類所能感知的基本上是可以忽略不計的;反過來,因爲計算機執行這些消息處理代碼非常快,所以消息隊列大多數情況下是空的(沒有消息),有大量的空閒時間(Idle time)。所以即使你利用這些空閒時間,也不會對界面流暢性有任何影響。反過來說,如果不用,卻是「暴殄天物」,大大的浪費了。爲什麼這麼說呢?因爲理由b:

b.將非UI邏輯移到工作線程,不僅要開啓新的線程,而且新線程在邏輯處理完之後通知UI線程更新界面(線程之間通信),這樣的步驟明顯地加大了實際的項目代碼的複雜度和開發週期,寫出來的代碼從結構上來說更加複雜、更容易出錯。

      所以我的觀點是,讓UI流暢,只要不是長時間阻塞UI線程,即使是千行萬行代碼,放到主線程的消息處理函數中執行又何妨。而filezilla的CAsyncSocketEx類代碼就是這麼做的(filezilla項目下載地址https://github.com/baloonwj/filezilla,電驢客戶端也用了CAsyncSocketEx這個類https://github.com/baloonwj/easyMule))。我這裏簡化一下細枝末節,抽出主幹框架:

[cpp]  view plain  copy
  1. //Processes event notifications sent by the sockets or the layers  
  2. static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)  
  3. {  
  4.     if (message>=WM_SOCKETEX_NOTIFY)  
  5.     {  
  6.         //此處省略195行代碼  
  7.     }  
  8.     else if (message == WM_USER) //Notification event sent by a layer  
  9.     {  
  10.         //此處省略138行代碼  
  11.     }  
  12.     else if (message == WM_USER+1)  
  13.     {  
  14.         //此處省略40行代碼  
  15.     }  
  16.     else if (message == WM_USER + 2)  
  17.     {  
  18.         //此處省略23行代碼  
  19.     }  
  20.     else if (message == WM_TIMER)  
  21.     {  
  22.         //次數省略21行代碼  
  23.     }  
  24.     return DefWindowProc(hWnd, message, wParam, lParam);  
  25. }  


以上面的代碼爲例,省略195行那個地方的是收發socket數據的,因爲產生這個窗口消息之前,已經檢測了相應的socket是否可讀可寫了。所以不會執行的時候不會阻塞,雖然代碼兩比較大,因爲不會阻塞或者很耗時,所以filezilla的界面還是非常流暢的。

       當然上面的代碼有Windows平臺特有的特點,Windows平臺專門提供了一個WSAAsyncSelect函數來將網絡收發數據的通知與Windows窗口關聯起來,甚至提供了專門用於處理非UI邏輯的窗口類型(HWND_MESSAGE,術語叫Message-Only Windows:https://msdn.microsoft.com/en-us/library/windows/desktop/ms632599(v=vs.85).aspx#message_only)。


2. pc端網絡通信層的設計

        可能有人會覺得這個問題有什麼好令你苦惱的呢?請聽我說。我理想中的pc端網絡通信層應該是專注於網絡通信細節,不涉及任何業務細節。其他模塊只要調用它封裝好的接口就可以了,但是呢,實際上做到這一點很難。舉幾個例子:

a. 在通信協議不明確的的情況下,解包操作到底應該屬於網絡層本身還是其他層呢?我說的通信協議不明確,指的是數據包無法按統一格式來解包,比如逐個解析字段,但是字段數量和類型因協議類型不同而發生變化。再比如重連機制,重連一般不僅是操作系用API connec()t函數的重連,還有業務上的重連(比如重新發送登錄數據包請求),那必然需要記錄用戶名、密碼等信息,但是這樣的信息卻是不屬於網絡層。即使封裝成Relogin()這樣的函數,網絡層在涉及到重連時,很多內部狀態也依賴於UI層。

b.心跳包的產生到底應該由網絡層本身產生還是其他層,比如UI層。心跳包一般和斷線重連、維持連接存活有關,而網絡狀態一般需要反映在UI界面上,甚至用戶可能需要加以干預的(比如用戶主動下線或者用戶點擊界面上重連或者重試按鈕)。用戶的干預,可能會影響心跳包的發送(例如用戶主動下線,就不要發送心跳包),從這個意義上說心跳包似乎不單屬於網絡層。

c.我曾經存在這樣一個認識,假如現在有一個socket與服務器保持連接,這個socket上不僅有主動發包應答,還有服務器隨時給你推送的數據。因爲socket是全雙工的,收和發我一般都會單獨開一個線程來操作,這樣收數據和發數據就會獨立開,在一定的情況下互不影響。如果某一方(收或者發)比較頻繁,也不會影響對方。但是,因爲同時存在收發兩個線程,邏輯處理時就會非常複雜了。舉個例子,如果收發數據任一個的過程中出錯(不一定是網絡出錯,可能是邏輯上認爲不正確),到底要不要關閉socket,如果關閉了socket,那麼在更新m_bConnected這樣表示網絡狀態的字段時,可能會涉及到兩個線程同時操作這個字段,那麼勢必要加鎖保護這個變量,有鎖的話,勢必對性能有影響。這還不是大問題,假設這個軟件需要定時重連,那到底是放在收線程重連還是發線程重連呢?重連過程中,另外一個線程需不需要停止或者暫停呢?而這樣的設計導致重連邏輯非常複雜,因爲:如果如果是用戶主動下線或者被踢下線,那麼就不該重連。綜合上面,在設計代碼結構和邏輯時,變得非常複雜。所以,回過頭來認真地想一想,對於客戶端軟件真的有必要做成開啓兩個線程、收發分離嗎?如果我們開啓兩個線程真能帶來效率上的提升,那我們這樣做也是可以考慮的。但是對於大多數pc端軟件來說,就算你這麼做,我覺得帶來的效率提升也是上層界面或者用戶無法察覺的。既然用戶無法察覺,那就不會帶來用戶體驗的提升。然而這種結構,在網絡通信層代碼結構設計上卻非常麻煩。那不如收發數據都放在一個線程中,這樣不僅沒有了收發兩個線程同時讀寫狀態標誌需要加鎖帶來性能損失的問題,而且代碼結構也簡單許多。

3.工作線程與UI層如何通信的問題

        爲了避免在非UI線程直接操作UI元素,目前Windows上的主流做法是工作線程通過PostMessage()攜帶堆內存數據給UI線程指定的元素髮消息。當然在Windows上從來沒有規定不能在非UI線程直接操作界面元素,有的時候迫不得已,我們還得這麼做(當然這是一種不好的使用方案,應該儘量避免)。舉個迫不得已的例子,比如一款即時通訊軟件聊天中的窗口抖動效果,實現原理是幾個MoveWindow函數調用之間,嵌入幾個Sleep函數,代碼實例如下:

[cpp]  view plain  copy
  1. RECT rtWindow;  
  2. ::GetWindowRect(hwnd, &rtWindow);  
  3. long x = rtWindow.left;  
  4. long y = rtWindow.top;  
  5. long cxWidth = rtWindow.right - rtWindow.left;  
  6. long cyHeight = rtWindow.bottom - rtWindow.top;  
  7. const long nOffset = 9;  
  8. const long SLEEP_INTERAL = 60;  
  9.   
  10. for (long i = 0; i <= 2; ++i)  
  11. {  
  12.     ::MoveWindow(hwnd, x + nOffset, y - nOffset, cxWidth, cyHeight, FALSE);  
  13.     ::Sleep(SLEEP_INTERAL);  
  14.     ::MoveWindow(hwnd, x - nOffset, y - nOffset, cxWidth, cyHeight, FALSE);  
  15.     ::Sleep(SLEEP_INTERAL);  
  16.     ::MoveWindow(hwnd, x - nOffset, y + nOffset, cxWidth, cyHeight, FALSE);  
  17.     ::Sleep(SLEEP_INTERAL);  
  18.     ::MoveWindow(hwnd, x + nOffset, y + nOffset, cxWidth, cyHeight, FALSE);  
  19.     ::Sleep(SLEEP_INTERAL);  
  20.     ::MoveWindow(hwnd, x, y, cxWidth, cyHeight, FALSE);  
  21.     ::Sleep(SLEEP_INTERAL);  
  22. }  

        如果這段代碼放在主線程裏面,由於Sleep的使用會導致消息隊列中其他消息不能及時處理,導致界面卡頓,尤其是用戶在頻繁地抖動窗口時(假設允許用戶頻繁地發送窗口抖動)。所以Teamtalk這款蘑菇街開源的即時通訊軟件裏面(pc端源碼,源碼下載: https://github.com/baloonwj/TeamTalk )是開啓單獨一個線程來調用上面的代碼。但是隨着技術的發展,後來的操作系統比如安卓就未必允許這麼做了,安卓操作系統是一直不允許在非UI線程直接操作UI元素,即使是一個小小的toast提示。所以,我們只能老老實實地給UI線程PostMessage(),但是這樣也存在問題——如果PostMessage時需要攜帶複雜類型數據,那麼我們必須使用堆內存,以確保消息到達UI線程並處理的時候,這塊數據的內存仍然存在。這樣做存在兩個問題:第一,很容易造成內存泄漏,因爲工作線程不斷new出內存,UI線程不斷delete,尤其是在多箇中間窗口中轉時,萬一哪一個中間步驟處理後不繼續往下拋消息,這塊內存就不會被釋放了。我的意思是工作線程A new出數據通過PostMessage發給窗口A,窗口A處理或者不處理再PostMessage給窗口B,B同樣處理或不處理再給窗口C,等等。這個過程非常容易造成內存泄漏,比如窗口B被關閉了。爲了做到萬無一失,我們也許會在各個窗口的事件處理函數裏面出現大量半途delete的代碼,非常分散,很難管理。我曾經嘗試着用C++11 std::shared_ptr或std::unique_ptr這樣的智能指針去解決,但是這些智能指針是否在多個線程之間工作的很好,顯然是一個問題。當然你可以採用內存池技術,但是讓大多數人設計一個不出問題且性能不錯的內存池還是有點難度的。這是這種方法存在的缺點之一。缺點之二是,頻繁的new和delete將產生大量的內存碎片。雖然客戶端軟件一般不用考慮這個問題,但是我有時候還是會表示憂慮的。

      既然這種傳堆內存的方法不好,另外一種可以使用全局對象,這個全局對象有大量getter和setter方法。但是在每次getter和setter時,由於涉及到多個線程操作,還是同樣得加上鎖,這又迴歸到上面提的鎖帶來的性能降低問題。

      上面的討論,讓我想起倉頡嘉措的詩:「自恐多情損梵行,入山又恐失傾城。世間安得兩全法,不負如來不負卿?」如何二者都兼顧呢?真是個頭痛的問題。那到底該如何解決呢?我目前的做法是自己定義一個智能指針對象(實現技術是引用計數),PostMessage需要new出來並傳遞的對象都繼承自這個智能指針對象,這樣就能做到自釋放了。因爲是自己定義的智能指針,所以我們可以自己加上額外的代碼保證多線程之間的操作的原子性。例如下面的代碼可以參考一下:

[cpp]  view plain  copy
  1. class TTPUBAPI atomic_count  
  2. {  
  3. public:  
  4.     explicit atomic_count( long v = 0) ;  
  5.     long increment() ;  
  6.     long decrement() ;  
  7.     long value() const ;      
  8. private:  
  9.   
  10.     atomic_count( atomic_count const & );  
  11.     atomic_count & operator=( atomic_count const & );  
  12.   
  13.     long volatile value_;  
  14. };  
[cpp]  view plain  copy
  1. atomic_count::atomic_count( long v )  
  2. {  
  3.     value_ = v ;  
  4. }  
  5.   
  6. long atomic_count::increment()  
  7. {  
  8.     return InterlockedIncrement( &value_ );  
  9. }  
  10.   
  11. long atomic_count::decrement()  
  12. {  
  13.     return InterlockedDecrement( &value_ );  
  14. }  
  15.   
  16. long atomic_count::value() const  
  17. {  
  18.     return static_cast<long const volatile &>( value_ );  
  19. }  

[cpp]  view plain  copy
  1. class TTPUBAPI safe_object : public tt::atomic_count {  
  2. public:  
  3.     virtual ~safe_object() {}  
  4. } ;  
  5.   
  6. class TTPUBAPI safe_object_ref {  
  7. private:  
  8.     safe_object * object_ ;  
  9.     bool auto_free_ ;  
  10. public:  
  11.     safe_object_ref() ;  
  12.     safe_object_ref(safe_object * object , bool auto_free = true) ;  
  13.     safe_object_ref(const safe_object_ref &ref) ;  
  14.     virtual ~safe_object_ref() ;  
  15.   
  16.     safe_object * get() const;  
  17.   
  18.     bool get_auto_free() const ;  
  19.   
  20.     void attach(safe_object *object , bool auto_free = true) ;  
  21.     void detach() ;  
  22.   
  23.     safe_object_ref& operator=(const safe_object_ref& ref) ;  
  24.     bool operator==(const safe_object_ref& ref) ;  
  25.   
  26.     safe_object * operator->() const ;  
  27.   
  28.     bool check_valid() const ;  
  29. } ;  

[cpp]  view plain  copy
  1. safe_object_ref::safe_object_ref()  
  2. {  
  3.     object_ = NULL ;  
  4.     auto_free_ = true ;  
  5. }  
  6.   
  7. safe_object_ref::safe_object_ref(safe_object * object , bool auto_free)  
  8. {  
  9.     object_ = NULL ;  
  10.     attach(object , auto_free) ;  
  11. }  
  12.   
  13. safe_object_ref::safe_object_ref(const safe_object_ref &ref)  
  14. {  
  15.     object_ = NULL ;  
  16.     attach(ref.object_ , ref.auto_free_) ;  
  17. }  
  18.   
  19. safe_object_ref& safe_object_ref::operator=(const safe_object_ref& ref)  
  20. {  
  21.     attach(ref.object_ , ref.auto_free_) ;  
  22.     return (*this) ;  
  23. }  
  24.   
  25. safe_object_ref::~safe_object_ref()  
  26. {  
  27.     detach() ;  
  28. }  
  29.   
  30. safe_object * safe_object_ref::get() const  
  31. {  
  32.     return object_ ;  
  33. }  
  34.   
  35. bool safe_object_ref::get_auto_free() const  
  36. {  
  37.     return auto_free_ ;   
  38. }  
  39.   
  40. void safe_object_ref::attach(safe_object *object , bool auto_free)   
  41. {  
  42.     if(object != NULL)  
  43.     {  
  44.         object->increment() ;  
  45.     }  
  46.   
  47.     detach() ;  
  48.     object_ = object ;  
  49.     auto_free_ = auto_free ;  
  50. }  
  51.   
  52. void safe_object_ref::detach()   
  53. {  
  54.     if(object_ != NULL)  
  55.     {  
  56.         long val = object_->decrement() ;  
  57.         if(val == 0 && auto_free_ == true)  
  58.         {  
  59.             delete object_ ;  
  60.         }  
  61.         object_ = NULL ;      
  62.     }  
  63. }  
  64.   
  65. safe_object * safe_object_ref::operator->() const  
  66. {  
  67.     return object_ ;  
  68. }  
  69.   
  70. bool safe_object_ref::check_valid() const  
  71. {  
  72.     return (object_ != NULL) ;  
  73. }  
  74.   
  75. bool safe_object_ref::operator==(const safe_object_ref& ref)  
  76. {  
  77.     return (object_ == ref.object_) ;  
  78. }  


二、案例分析

       上面討論的一些疑惑以及解決辦法只是一些具體而微的東西,下面我們來實際討論一個pc端軟件的架構,先看我個人的一款即時通訊軟件的pc端(flamingo: 源碼下載地址:https://github.com/baloonwj/flamingo,關於flamingo的介紹,您可以參考這篇文章: http://blog.csdn.net/analogous_love/article/details/69481542 ),這個軟件的基礎功能和qq一樣,可以進行單聊和羣聊,當然也可以自定義用戶信息,工程代碼結構如下:



這個軟件的框架結構圖如下:


理論上說只要有UI層和網絡層就夠了,但是爲了保證UI界面的流暢和網絡通信層單純高效地收發網絡數據,加了一箇中間層,即數據加工層。從上往下看,UI層產生調用網絡請求或者數據處理請求,如果這些數據需要進行加工,而加工過程比較耗時,那麼無論放在UI層還是網絡層都不合適;從下往上看,網絡上收到數據以後,將這些數據解包後,必須加工成界面層需要的格式,這些數據加工工作也放在數據加工層。我們現在來詳細介紹一下每一層如何實現的:

1. 數據加工層

該層實際上是一組線程組成的,每一個線程都是一個從自己的任務隊列中取任務執行,這些任務處理完之後產生網絡數據包,或者直接放到網絡層的發送隊列中去,或者直接調用網絡層接口函數將數據發出去。

其中SendMsgThread會將自己的隊列中任務加工成網絡數據格式放到網絡層的發送緩衝區中去,RecvMsgThread會自己任務隊列中的數據加工成界面層需要的樣子,然後PostMessage給界面。而FileTaskThread、ImageTaskThread分別處理即時通訊聊天中與文件和圖片相關的任務,根據任務類型,或者發送出去,或者顯示到界面上去。每個任務隊列的結構都是差不多的(這裏以SendMsgThread爲例):

[cpp]  view plain  copy
  1. //處理任務的線程函數  
  2. void CSendMsgThread::Run()  
  3. {  
  4.     while (!m_bStop)  
  5.     {  
  6.         CNetData* lpMsg;  
  7.         {  
  8.             std::unique_lock<std::mutex> guard(m_mtItems);  
  9.             while (m_listItems.empty())  
  10.             {  
  11.                 if (m_bStop)  
  12.                     return;  
  13.   
  14.                 m_cvItems.wait(guard);  
  15.             }  
  16.   
  17.             lpMsg = m_listItems.front();  
  18.             m_listItems.pop_front();  
  19.         }  
  20.   
  21.         HandleItem(lpMsg);  
  22.     }  
  23. }  

[cpp]  view plain  copy
  1. //供UI層調用的、產生新任務的接口函數  
  2. void CSendMsgThread::AddItem(CNetData* pItem)  
  3. {  
  4.     std::lock_guard<std::mutex> guard(m_mtItems);  
  5.     m_listItems.push_back(pItem);  
  6.     m_cvItems.notify_one();  
  7. }  


每個任務處理好之後,會產生界面需要的數據,然後new出堆對象,用PostMessage攜帶發給UI層,這裏以創建羣組成功的代碼爲例:

[cpp]  view plain  copy
  1. BOOL CRecvMsgThread::HandleCreateNewGroupResult(const std::string& strMsg)  
  2. {  
  3.     //{"code":0, "msg": "ok", "groupid": 12345678, "groupname": "我的羣名稱"}  
  4.     Json::Reader JsonReader;  
  5.     Json::Value JsonRoot;  
  6.     if (!JsonReader.parse(strMsg, JsonRoot))  
  7.         return FALSE;  
  8.   
  9.     if (!JsonRoot["code"].isInt() || !JsonRoot["groupid"].isInt() || !JsonRoot["groupname"].isString())  
  10.         return FALSE;  
  11.   
  12.     CCreateNewGroupResult* pResult = new CCreateNewGroupResult();  
  13.     pResult->m_uAccountID = JsonRoot["groupid"].asInt();  
  14.     strcpy_s(pResult->m_szGroupName, ARRAYSIZE(pResult->m_szGroupName), JsonRoot["groupname"].asCString());  
  15.   
  16.      
  17.     //發給主線程  
  18.     ::PostMessage(m_lpUserMgr->m_hProxyWnd, FMG_MSG_CREATE_NEW_GROUP_RESULT, 0, (LPARAM)pResult);  
  19.   
  20.     return TRUE;  
  21. }  

[cpp]  view plain  copy
  1. //具體每個任務的處理過程  
  2. void CSendMsgThread::HandleItem(CNetData* pNetData)  
  3. {  
  4.     if(pNetData == NULL)  
  5.         return;  
  6.   
  7.     switch(pNetData->m_uType)  
  8.     {  
  9.     case NET_DATA_REGISTER:  
  10.         HandleRegister((const CRegisterRequest*)pNetData);  
  11.         break;  
  12.   
  13.     case NET_DATA_LOGIN:  
  14.         HandleLogon((const CLoginRequest*)pNetData);  
  15.         break;  
  16.   
  17.     case NET_DATA_USER_BASIC_INFO:  
  18.         HandleUserBasicInfo((const CUserBasicInfoRequest*)pNetData);  
  19.         break;  
  20.         case msg_type_creategroup:  
  21.             HandleCreateNewGroupResult(data);  
  22.             break;  
  23.               
  24.     //創建羣  
  25.     case msg_type_creategroup:  
  26.         HandleCreateNewGroupResult(data);  
  27.         break;  
  28.   
  29.     //類似代碼省略  
  30.   
  31.     default:  
  32. #ifdef _DEBUG  
  33.         ::MessageBox(::GetForegroundWindow(), _T("Be cautious! Unhandled data type in send queen."), _T("Warning"), MB_OK|MB_ICONERROR);  
  34. #else  
  35.         LOG_WARNING("Be cautious! Unhandled data type in send queen.");  
  36. #endif  
  37.     }  
  38.   
  39.     m_seq++;  
  40.       
  41.     delete pNetData;      
  42. }  


2. 網絡層

網絡層就是按照我上面介紹的同一個socket收發分開成兩個線程。

[cpp]  view plain  copy
  1. //網絡層發送數據的線程函數  
  2. void CIUSocket::SendThreadProc()  
  3. {  
  4.     LOG_INFO("Recv data thread start...");  
  5.       
  6.     while (!m_bStop)  
  7.     {  
  8.         std::unique_lock<std::mutex> guard(m_mtSendBuf);  
  9.         while (m_strSendBuf.empty())  
  10.         {  
  11.             if (m_bStop)  
  12.                 return;  
  13.   
  14.             m_cvSendBuf.wait(guard);  
  15.         }  
  16.          
  17.         if (!Send())  
  18.         {  
  19.             //進行重連,如果連接不上,則向客戶報告錯誤  
  20.         }  
  21.     }  
  22.   
  23.     LOG_INFO("Recv data thread finish...");  
  24. }  
  25.   
  26.   
  27. //供數據加工層調用的、產生網絡數據包的接口函數  
  28. void CIUSocket::Send(const std::string& strBuffer)  
  29. {   
  30.     std::lock_guard<std::mutex> guard(m_mtSendBuf);  
  31.     //插入包頭  
  32.     int32_t length = (int32_t)strBuffer.length();  
  33.     msg header = { length };  
  34.     m_strSendBuf.append((const char*)&header, sizeof(header));  
  35.     m_strSendBuf.append(strBuffer.c_str(), length);  
  36.     m_cvSendBuf.notify_one();  
  37. }  

[cpp]  view plain  copy
  1. //接收數據的網絡線程  
  2. void CIUSocket::RecvThreadProc()  
  3. {  
  4.     LOG_INFO("Recv data thread start...");  
  5.       
  6.     int nRet;  
  7.     //上網方式   
  8.     DWORD   dwFlags;                
  9.     BOOL    bAlive;  
  10.     while (!m_bStop)  
  11.     {  
  12.         //檢測到數據則收數據  
  13.         nRet = CheckReceivedData();  
  14.         //出錯  
  15.         if (nRet == -1)  
  16.         {              
  17.             m_pRecvMsgThread->NotifyNetError();  
  18.         }  
  19.         //無數據  
  20.         else if (nRet == 0)  
  21.         {                
  22.             bAlive = ::IsNetworkAlive(&dwFlags);        //是否在線      
  23.             if (!bAlive && ::GetLastError() == 0)  
  24.             {  
  25.                 //網絡已經斷開  
  26.                 m_pRecvMsgThread->NotifyNetError();  
  27.                 LOG_ERROR("net error, exit recv and send thread...");  
  28.                 Uninit();  
  29.                 break;  
  30.             }  
  31.               
  32.             long nLastDataTime = 0;  
  33.             {  
  34.                 std::lock_guard<std::mutex> guard(m_mutexLastDataTime);  
  35.                 nLastDataTime = m_nLastDataTime;  
  36.             }  
  37.   
  38.             if (m_nHeartbeatInterval > 0)  
  39.             {  
  40.                 if (time(NULL) - nLastDataTime >= m_nHeartbeatInterval)  
  41.                     SendHeartbeatPackage();  
  42.             }  
  43.         }  
  44.         //有數據  
  45.         else if (nRet == 1)  
  46.         {  
  47.             if (!Recv())  
  48.             {  
  49.                 m_pRecvMsgThread->NotifyNetError();  
  50.                 continue;  
  51.             }  
  52.               
  53.             //解包,並將得到的業務數據交給RecvMsgThread的任務隊列   
  54.             DecodePackages();  
  55.         }// end if  
  56.     }// end while-loop  
  57.   
  58.     LOG_INFO("Recv data thread finish...");  
  59. }  
  60.   
  61. bool CIUSocket::DecodePackages()  
  62. {  
  63.     //一定要放在一個循環裏面解包,因爲可能一片數據中有多個包,  
  64.     //對於數據收不全,這個地方我糾結了好久T_T  
  65.     while (true)  
  66.     {  
  67.         //接收緩衝區不夠一個包頭大小  
  68.         if (m_strRecvBuf.length() <= sizeof(msg))  
  69.             break;  
  70.   
  71.         msg header;  
  72.         memcpy_s(&header, sizeof(msg), m_strRecvBuf.data(), sizeof(msg));  
  73.         //防止包頭定義的數據是一些錯亂的數據,這裏最大限制每個包大小爲10M  
  74.         if (header.packagesize >= MAX_PACKAGE_SIZE || header.packagesize <= 0)  
  75.         {  
  76.             LOG_ERROR("Recv a strange packagesize in header, packagesize=%d", header.packagesize);  
  77.             m_strRecvBuf.clear();  
  78.             return false;  
  79.         }  
  80.   
  81.         //接收緩衝區不夠一個整包大小(包頭+包體)  
  82.         if (m_strRecvBuf.length() < sizeof(msg) + header.packagesize)  
  83.             break;  
  84.   
  85.         //去除包頭信息  
  86.         m_strRecvBuf.erase(0, sizeof(msg));  
  87.         std::string strBody;  
  88.         strBody.append(m_strRecvBuf.c_str(), header.packagesize);  
  89.         //去除包體信息  
  90.         m_strRecvBuf.erase(0, header.packagesize);  
  91.   
  92.         m_pRecvMsgThread->AddMsgData(strBody);  
  93.     }  
  94.   
  95.     return true;  
  96. }  



3. UI層

UI層實際上包含兩部分,一部分是包含各種程序的界面,還有一個所謂的代理窗口,數據加工層將數據先發到這個窗口上,然後可能在這裏進一步處理一下界面的邏輯,然後再發給目標窗口。當然有些數據只是原封不動地轉發:

[cpp]  view plain  copy
  1. LRESULT CALLBACK CFlamingoClient::ProxyWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)  
  2. {  
  3.     CFlamingoClient* lpFMGClient = (CFlamingoClient*)::GetWindowLong(hWnd, GWL_USERDATA);  
  4.     if (NULL == lpFMGClient)  
  5.         return ::DefWindowProc(hWnd, message, wParam, lParam);  
  6.   
  7.     if (message < FMG_MSG_FIRST || message > FMG_MSG_LAST)  
  8.         return ::DefWindowProc(hWnd, message, wParam, lParam);  
  9.   
  10.     switch (message)  
  11.     {  
  12.     //網絡錯誤  
  13.     case FMG_MSG_NET_ERROR:  
  14.         ::PostMessage(lpFMGClient->m_UserMgr.m_hCallBackWnd, FMG_MSG_NET_ERROR, 0, 0);  
  15.         break;  
  16.   
  17.     case FMG_MSG_HEARTBEAT:  
  18.         lpFMGClient->OnHeartbeatResult(message, wParam, lParam);  
  19.         break;  
  20.   
  21.     case FMG_MSG_NETWORK_STATUS_CHANGE:  
  22.         lpFMGClient->OnNetworkStatusChange(message, wParam, lParam);  
  23.         break;  
  24.       
  25.     case FMG_MSG_REGISTER:              // 註冊結果  
  26.         lpFMGClient->OnRegisterResult(message, wParam, lParam);  
  27.         break;  
  28.     case FMG_MSG_LOGIN_RESULT:          // 登錄返回消息  
  29.         lpFMGClient->OnLoginResult(message, wParam, lParam);  
  30.         break;  
  31.     case FMG_MSG_LOGOUT_RESULT:         // 註銷返回消息  
  32.     case FMG_MSG_UPDATE_BUDDY_HEADPIC:  // 更新好友頭像  
  33.         //::MessageBox(NULL, _T("Change headpic"), _T("Change head"), MB_OK);  
  34.     case FMG_MSG_UPDATE_GMEMBER_HEADPIC:    // 更新羣成員頭像  
  35.     case FMG_MSG_UPDATE_GROUP_HEADPIC:  // 更新羣頭像  
  36.         ::SendMessage(lpFMGClient->m_UserMgr.m_hCallBackWnd, message, wParam, lParam);  
  37.         break;  
  38.     case FMG_MSG_UPDATE_USER_BASIC_INFO:    //收到用戶的基本信息  
  39.         lpFMGClient->OnUpdateUserBasicInfo(message, wParam, lParam);  
  40.         break;  
  41.   
  42.     case FMG_MSG_UPDATE_GROUP_BASIC_INFO:  
  43.         lpFMGClient->OnUpdateGroupBasicInfo(message, wParam, lParam);  
  44.         break;  
  45.   
  46.     case FMG_MSG_MODIFY_USER_INFO:              //修改個人信息結果  
  47.         lpFMGClient->OnModifyInfoResult(message, wParam, lParam);  
  48.         break;  
  49.     case FMG_MSG_RECV_USER_STATUS_CHANGE_DATA:  
  50.         lpFMGClient->OnRecvUserStatusChangeData(message, wParam, lParam);  
  51.         break;  
  52.       
  53.     case FMG_MSG_USER_STATUS_CHANGE:  
  54.         lpFMGClient->OnUserStatusChange(message, wParam, lParam);  
  55.         break;  
  56.   
  57.     case FMG_MSG_UPLOAD_USER_THUMB:  
  58.         lpFMGClient->OnSendConfirmMessage(message, wParam, lParam);  
  59.         break;  
  60.   
  61.     case FMG_MSG_UPDATE_USER_CHAT_MSG_ID:  
  62.         lpFMGClient->OnUpdateChatMsgID(message, wParam, lParam);  
  63.         break;    
  64.     case FMG_MSG_FINDFREIND:  
  65.         lpFMGClient->OnFindFriend(message, wParam, lParam);  
  66.         break;  
  67.   
  68.     case FMG_MSG_DELETEFRIEND:  
  69.         lpFMGClient->OnDeleteFriendResult(message, wParam, lParam);  
  70.         break;  
  71.   
  72.     case FMG_MSG_RECVADDFRIENDREQUSET:  
  73.         lpFMGClient->OnRecvAddFriendRequest(message, wParam, lParam);  
  74.         break;  
  75.   
  76.     case FMG_MSG_CUSTOMFACE_AVAILABLE:  
  77.         lpFMGClient->OnBuddyCustomFaceAvailable(message, wParam, lParam);  
  78.         break;  
  79.   
  80.     case FMG_MSG_MODIFY_PASSWORD_RESULT:  
  81.         lpFMGClient->OnModifyPasswordResult(message, wParam, lParam);  
  82.         break;  
  83.   
  84.     case FMG_MSG_CREATE_NEW_GROUP_RESULT:  
  85.         lpFMGClient->OnCreateNewGroupResult(message, wParam, lParam);  
  86.         break;  
  87.   
  88.     case FMG_MSG_UPDATE_BUDDY_LIST:             //更新好友列表  
  89.         lpFMGClient->OnUpdateBuddyList(message, wParam, lParam);  
  90.         break;  
  91.     case FMG_MSG_UPDATE_GROUP_LIST:     // 更新羣列表消息  
  92.         lpFMGClient->OnUpdateGroupList(message, wParam, lParam);  
  93.         break;  
  94.     case FMG_MSG_UPDATE_RECENT_LIST:        // 更新最近聯繫人列表消息  
  95.         lpFMGClient->OnUpdateRecentList(message, wParam, lParam);  
  96.         break;  
  97.     case FMG_MSG_BUDDY_MSG:             // 好友消息  
  98.         lpFMGClient->OnBuddyMsg(message, wParam, lParam);  
  99.         break;  
  100.     case FMG_MSG_GROUP_MSG:             // 羣消息  
  101.         lpFMGClient->OnGroupMsg(message, wParam, lParam);  
  102.         break;  
  103.     case FMG_MSG_SESS_MSG:              // 臨時會話消息  
  104.         lpFMGClient->OnSessMsg(message, wParam, lParam);  
  105.         break;  
  106.     case FMG_MSG_STATUS_CHANGE_MSG:     // 好友狀態改變消息  
  107.         lpFMGClient->OnStatusChangeMsg(message, wParam, lParam);  
  108.         break;  
  109.     case FMG_MSG_SELF_STATUS_CHANGE:    //自己的狀態發生改變,例如被踢下線消息  
  110.         lpFMGClient->OnKickMsg(message, wParam, lParam);  
  111.         break;  
  112.     case FMG_MSG_SCREENSHOT:    //截屏消息  
  113.         lpFMGClient->OnScreenshotMsg(message, wParam, lParam);          
  114.         break;  
  115.     case FMG_MSG_SYS_GROUP_MSG:         // 羣系統消息  
  116.         lpFMGClient->OnSysGroupMsg(message, wParam, lParam);  
  117.         break;  
  118.     case FMG_MSG_UPDATE_BUDDY_NUMBER:   // 更新好友號碼  
  119.         lpFMGClient->OnUpdateBuddyNumber(message, wParam, lParam);  
  120.         break;  
  121.     case FMG_MSG_UPDATE_GMEMBER_NUMBER: // 更新羣成員號碼  
  122.         lpFMGClient->OnUpdateGMemberNumber(message, wParam, lParam);  
  123.         break;  
  124.     case FMG_MSG_UPDATE_GROUP_NUMBER:   // 更新羣號碼  
  125.         lpFMGClient->OnUpdateGroupNumber(message, wParam, lParam);  
  126.         break;  
  127.     case FMG_MSG_UPDATE_BUDDY_SIGN:     // 更新好友個性簽名  
  128.         lpFMGClient->OnUpdateBuddySign(message, wParam, lParam);  
  129.         break;  
  130.     case FMG_MSG_UPDATE_GMEMBER_SIGN:   // 更新羣成員個性簽名  
  131.         lpFMGClient->OnUpdateGMemberSign(message, wParam, lParam);  
  132.         break;  
  133.     case FMG_MSG_UPDATE_BUDDY_INFO:     // 更新用戶信息  
  134.         lpFMGClient->OnUpdateBuddyInfo(message, wParam, lParam);  
  135.         break;  
  136.     case FMG_MSG_UPDATE_GMEMBER_INFO:   // 更新羣成員信息  
  137.         lpFMGClient->OnUpdateGMemberInfo(message, wParam, lParam);  
  138.         break;  
  139.     case FMG_MSG_UPDATE_GROUP_INFO:     // 更新羣信息  
  140.         lpFMGClient->OnUpdateGroupInfo(message, wParam, lParam);  
  141.         break;  
  142.     case FMG_MSG_UPDATE_C2CMSGSIG:      // 更新臨時會話信令  
  143.         //lpFMGClient->OnUpdateC2CMsgSig(message, wParam, lParam);  
  144.         break;  
  145.     case FMG_MSG_CHANGE_STATUS_RESULT:  // 改變在線狀態返回消息  
  146.         lpFMGClient->OnChangeStatusResult(message, wParam, lParam);  
  147.         break;  
  148.     case FMG_MSG_TARGET_INFO_CHANGE:        //有用戶信息發生改變:  
  149.         lpFMGClient->OnTargetInfoChange(message, wParam, lParam);  
  150.         break;  
  151.   
  152.     case FMG_MSG_INTERNAL_GETBUDDYDATA:  
  153.         lpFMGClient->OnInternal_GetBuddyData(message, wParam, lParam);  
  154.         break;  
  155.     case FMG_MSG_INTERNAL_GETGROUPDATA:  
  156.         lpFMGClient->OnInternal_GetGroupData(message, wParam, lParam);  
  157.         break;  
  158.     case FMG_MSG_INTERNAL_GETGMEMBERDATA:  
  159.         lpFMGClient->OnInternal_GetGMemberData(message, wParam, lParam);  
  160.         break;  
  161.     case FMG_MSG_INTERNAL_GROUPID2CODE:  
  162.         return lpFMGClient->OnInternal_GroupId2Code(message, wParam, lParam);  
  163.         break;  
  164.   
  165.     default:  
  166.         return ::DefWindowProc(hWnd, message, wParam, lParam);  
  167.     }  
  168.     return 0;  
  169. }  

這裏的代理窗口其實就是上文中說的HWND_MESSAGE窗口。之所以在UI層創建一個窗口進行中轉一下的原因是,給所有消息有一個集中處理的機會,方便因數據變化帶來的UI邏輯的統一處理(說白了,就是將界面邏輯的代碼集中在這個模塊這裏)。

     UI層與數據加工層通信、往數據加工層的各個線程加工隊列中產生數據,是通過一個專門的類提供的各種接口:

[cpp]  view plain  copy
  1. class CFlamingoClient  
  2. {  
  3. public:  
  4.     static CFlamingoClient& GetInstance();  
  5.   
  6. public:  
  7.     CFlamingoClient(void);  
  8.     ~CFlamingoClient(void);  
  9.   
  10. public:  
  11.     bool InitProxyWnd();                                        // 初始化代理窗口  
  12.     bool InitNetThreads();                                      // 初始化網絡線程  
  13.     void Uninit();                                              // 反初始化客戶端  
  14.   
  15.     void SetServer(PCTSTR pszServer);  
  16.     void SetFileServer(PCTSTR pszServer);  
  17.     void SetImgServer(PCTSTR pszServer);  
  18.     void SetPort(short port);  
  19.     void SetFilePort(short port);  
  20.     void SetImgPort(short port);  
  21.   
  22.     void SetUser(LPCTSTR lpUserAccount, LPCTSTR lpUserPwd);     // 設置UTalk號碼和密碼  
  23.     void SetLoginStatus(long nStatus);                          // 設置登錄狀態  
  24.     void SetCallBackWnd(HWND hCallBackWnd);                     // 設置回調窗口句柄  
  25.     void SetRegisterWindow(HWND hwndRegister);                  // 設置註冊結果的反饋窗口  
  26.     void SetModifyPasswordWindow(HWND hwndModifyPassword);      // 設置修改密碼結果反饋窗口  
  27.     void SetCreateNewGroupWindow(HWND hwndCreateNewGroup);      // 設置創建羣組結果反饋窗口  
  28.     void SetFindFriendWindow(HWND hwndFindFriend);              // 設置查找用戶結果反饋窗口  
  29.   
  30.     void StartCheckNetworkStatusTask();                           
  31.     //void StartGetUserInfoTask(long nType);                    //獲取好友  
  32.     void StartHeartbeatTask();  
  33.   
  34.     void Register(PCTSTR pszAccountName, PCTSTR pszNickName, PCTSTR pszPassword);  
  35.     void Login(int nStatus = STATUS_ONLINE);                    // 登錄  
  36.     BOOL Logout();                                              // 註銷  
  37.     void CancelLogin();                                         // 取消登錄  
  38.     void GetFriendList();                                       // 獲取好友列表  
  39.     void GetGroupMembers(int32_t groupid);                      // 獲取羣成員  
  40.     void ChangeStatus(int32_t nNewStatus);                      // 更改自己的登錄狀態          
  41.   
  42.     BOOL FindFriend(PCTSTR pszAccountName, long nType, HWND hReflectionWnd);// 查找好友  
  43.     BOOL AddFriend(UINT uAccountToAdd);  
  44.     void ResponseAddFriendApply(UINT uAccountID, UINT uCmd);    //迴應加好友請求任務  
  45.     BOOL DeleteFriend(UINT uAccountID);                         // 刪除好友  
  46.     BOOL UpdateLogonUserInfo(PCTSTR pszNickName,   
  47.                              PCTSTR pszSignature,  
  48.                              UINT uGender,  
  49.                              long nBirthday,  
  50.                              PCTSTR pszAddress,  
  51.                              PCTSTR pszPhone,  
  52.                              PCTSTR pszMail,  
  53.                              UINT uSysFaceID,  
  54.                              PCTSTR pszCustomFacePath,  
  55.                              BOOL bUseCustomThumb);  
  56.   
  57.     void SendHeartbeatMessage();  
  58.     void ModifyPassword(PCTSTR pszOldPassword, PCTSTR pszNewPassword);  
  59.     void CreateNewGroup(PCTSTR pszGroupName);  
  60.     void ChangeStatus(long nStatus);                            // 改變在線狀態  
  61.     void UpdateBuddyList();                                     // 更新好友列表  
  62.     void UpdateGroupList();                                     // 更新羣列表  
  63.     void UpdateRecentList();                                    // 更新最近聯繫人列表  
  64.     void UpdateBuddyInfo(UINT nUTalkUin);                       // 更新好友信息  
  65.     void UpdateGroupMemberInfo(UINT nGroupCode, UINT nUTalkUin);// 更新羣成員信息  
  66.     void UpdateGroupInfo(UINT nGroupCode);                      // 更新羣信息  
  67.     void UpdateBuddyNum(UINT nUTalkUin);                        // 更新好友號碼  
  68.     void UpdateGroupMemberNum(UINT nGroupCode, UINT nUTalkUin); // 更新羣成員號碼  
  69.     void UpdateGroupMemberNum(UINT nGroupCode, std::vector<UINT>* arrUTalkUin);   // 更新羣成員號碼  
  70.     void UpdateGroupNum(UINT nGroupCode);                                           // 更新羣號碼  
  71.     void UpdateBuddySign(UINT nUTalkUin);                                           // 更新好友個性簽名  
  72.     void UpdateGroupMemberSign(UINT nGroupCode, UINT nUTalkUin);                    // 更新羣成員個性簽名  
  73.     void ModifyUTalkSign(LPCTSTR lpSign);                                           // 修改UTalk個性簽名  
  74.     void UpdateBuddyHeadPic(UINT nUTalkUin, UINT nUTalkNum);                        // 更新好友頭像  
  75.     void UpdateGroupMemberHeadPic(UINT nGroupCode, UINT nUTalkUin, UINT nUTalkNum); // 更新羣成員頭像  
  76.     void UpdateGroupHeadPic(UINT nGroupCode, UINT nGroupNum);                       // 更新羣頭像  
  77.     void UpdateGroupFaceSignal();                                                   // 更新羣表情信令  
  78.   
  79.   
  80.     BOOL SendBuddyMsg(UINT nFromUin, const tstring& strFromNickName, UINT nToUin, const tstring& strToNickName, time_t nTime, const tstring& strChatMsg, HWND hwndFrom = NULL);// 發送好友消息  
  81.     BOOL SendGroupMsg(UINT nGroupId, time_t nTime, LPCTSTR lpMsg, HWND hwndFrom);   // 發送羣消息  
  82.     BOOL SendSessMsg(UINT nGroupId, UINT nToUin, time_t nTime, LPCTSTR lpMsg);      // 發送臨時會話消息  
  83.     BOOL SendMultiChatMsg(const std::set<UINT> setAccountID, time_t nTime, LPCTSTR lpMsg, HWND hwndFrom=NULL);//羣發消息  
  84.   
  85.     BOOL IsOffline();                                           // 是否離線狀態  
  86.   
  87.   
  88.     long GetStatus();                                           // 獲取在線狀態  
  89.     BOOL GetVerifyCodePic(const BYTE*& lpData, DWORD& dwSize);  // 獲取驗證碼圖片  
  90.     void SetBuddyListAvailable(BOOL bAvailable);  
  91.     BOOL IsBuddyListAvailable();  
  92.   
  93.     CBuddyInfo* GetUserInfo(UINT uAccountID=0);         // 獲取用戶信息  
  94.     CBuddyList* GetBuddyList();                     // 獲取好友列表  
  95.     CGroupList* GetGroupList();                     // 獲取羣列表  
  96.     CRecentList* GetRecentList();                       // 獲取最近聯繫人列表  
  97.     CMessageList* GetMessageList();                 // 獲取消息列表  
  98.     CMessageLogger* GetMsgLogger();                 // 獲取消息記錄管理器  
  99.   
  100.     tstring GetUserFolder();                            // 獲取用戶文件夾存放路徑  
  101.   
  102.     tstring GetPersonalFolder(UINT nUserNum = 0);       // 獲取個人文件夾存放路徑  
  103.     tstring GetChatPicFolder(UINT nUserNum = 0);        // 獲取聊天圖片存放路徑  
  104.   
  105.     tstring GetUserHeadPicFullName(UINT nUserNum = 0);  // 獲取用戶頭像圖片全路徑文件名  
  106.     tstring GetBuddyHeadPicFullName(UINT nUTalkNum);    // 獲取好友頭像圖片全路徑文件名  
  107.     tstring GetGroupHeadPicFullName(UINT nGroupNum);    // 獲取羣頭像圖片全路徑文件名  
  108.     tstring GetSessHeadPicFullName(UINT nUTalkNum);     // 獲取羣成員頭像圖片全路徑文件名  
  109.     tstring GetChatPicFullName(LPCTSTR lpszFileName);   // 獲取聊天圖片全路徑文件名  
  110.     tstring GetMsgLogFullName(UINT nUserNum = 0);       // 獲取消息記錄全路徑文件名  
  111.   
  112.     BOOL IsNeedUpdateBuddyHeadPic(UINT nUTalkNum);      // 判斷是否需要更新好友頭像  
  113.     BOOL IsNeedUpdateGroupHeadPic(UINT nGroupNum);      // 判斷是否需要更新羣頭像  
  114.     BOOL IsNeedUpdateSessHeadPic(UINT nUTalkNum);       // 判斷是否需要更新羣成員頭像  
  115.   
  116.     void RequestServerTime();                           // 獲取服務器時間  
  117.     time_t GetCurrentTime();                            // 獲取當前時間(以服務器時間爲基準)  
  118.     void LoadUserConfig();                              // 加載用戶設置信息  
  119.     void SaveUserConfig();                              // 保存用戶設置信息  
  120.   
  121.     void GoOnline();                                      
  122.     void GoOffline();                                   // 掉線或者下線  
  123.   
  124.     long ParseBuddyStatus(long nFlag);                  // 解析用戶在線狀態  
  125.     void CacheBuddyStatus();                            // 緩存用戶在線狀態  
  126.     BOOL SetBuddyStatus(UINT uAccountID, long nStatus);  
  127.     BOOL SetBuddyClientType(UINT uAccountID, long nNewClientType);  
  128.   
  129. private:  
  130.     void OnHeartbeatResult(UINT message, WPARAM wParam, LPARAM lParam);  
  131.     void OnNetworkStatusChange(UINT message, WPARAM wParam, LPARAM lParam);  
  132.     void OnRegisterResult(UINT message, WPARAM wParam, LPARAM lParam);  
  133.     void OnLoginResult(UINT message, WPARAM wParam, LPARAM lParam);  
  134.     void OnUpdateUserBasicInfo(UINT message, WPARAM wParam, LPARAM lParam);  
  135.     void OnUpdateGroupBasicInfo(UINT message, WPARAM wParam, LPARAM lParam);  
  136.     void OnModifyInfoResult(UINT message, WPARAM wParam, LPARAM lParam);  
  137.     void OnRecvUserStatusChangeData(UINT message, WPARAM wParam, LPARAM lParam);  
  138.     void OnRecvAddFriendRequest(UINT message, WPARAM wParam, LPARAM lParam);  
  139.     void OnUserStatusChange(UINT message, WPARAM wParam, LPARAM lParam);  
  140.     void OnSendConfirmMessage(UINT message, WPARAM wParam, LPARAM lParam);  
  141.     void OnUpdateChatMsgID(UINT message, WPARAM wParam, LPARAM lParam);  
  142.     void OnFindFriend(UINT message, WPARAM wParam, LPARAM lParam);  
  143.     void OnBuddyCustomFaceAvailable(UINT message, WPARAM wParam, LPARAM lParam);  
  144.     void OnModifyPasswordResult(UINT message, WPARAM wParam, LPARAM lParam);  
  145.     void OnCreateNewGroupResult(UINT message, WPARAM wParam, LPARAM lParam);      
  146.     void OnDeleteFriendResult(UINT message, WPARAM wParam, LPARAM lParam);  
  147.     void OnUpdateBuddyList(UINT message, WPARAM wParam, LPARAM lParam);  
  148.     void OnUpdateGroupList(UINT message, WPARAM wParam, LPARAM lParam);  
  149.     void OnUpdateRecentList(UINT message, WPARAM wParam, LPARAM lParam);  
  150.     void OnBuddyMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  151.     void OnGroupMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  152.     void OnSessMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  153.     void OnSysGroupMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  154.     void OnStatusChangeMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  155.     void OnKickMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  156.     void OnScreenshotMsg(UINT message, WPARAM wParam, LPARAM lParam);  
  157.     void OnUpdateBuddyNumber(UINT message, WPARAM wParam, LPARAM lParam);  
  158.     void OnUpdateGMemberNumber(UINT message, WPARAM wParam, LPARAM lParam);  
  159.     void OnUpdateGroupNumber(UINT message, WPARAM wParam, LPARAM lParam);  
  160.     void OnUpdateBuddySign(UINT message, WPARAM wParam, LPARAM lParam);  
  161.     void OnUpdateGMemberSign(UINT message, WPARAM wParam, LPARAM lParam);  
  162.     void OnUpdateBuddyInfo(UINT message, WPARAM wParam, LPARAM lParam);  
  163.     void OnUpdateGMemberInfo(UINT message, WPARAM wParam, LPARAM lParam);  
  164.     void OnUpdateGroupInfo(UINT message, WPARAM wParam, LPARAM lParam);  
  165.     //void OnUpdateC2CMsgSig(UINT message, WPARAM wParam, LPARAM lParam);  
  166.     void OnChangeStatusResult(UINT message, WPARAM wParam, LPARAM lParam);  
  167.     void OnTargetInfoChange(UINT message, WPARAM wParam, LPARAM lParam);  
  168.   
  169.     void OnInternal_GetBuddyData(UINT message, WPARAM wParam, LPARAM lParam);  
  170.     void OnInternal_GetGroupData(UINT message, WPARAM wParam, LPARAM lParam);  
  171.     void OnInternal_GetGMemberData(UINT message, WPARAM wParam, LPARAM lParam);  
  172.     UINT OnInternal_GroupId2Code(UINT message, WPARAM wParam, LPARAM lParam);  
  173.   
  174.     BOOL CreateProxyWnd();      // 創建代理窗口  
  175.     BOOL DestroyProxyWnd();     // 銷燬代理窗口  
  176.     static LRESULT CALLBACK ProxyWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);  
  177.   
  178. public:  
  179.     CUserMgr                        m_UserMgr;  
  180.     CCheckNetworkStatusTask         m_CheckNetworkStatusTask;  
  181.   
  182.     CSendMsgThread                  m_SendMsgThread;  
  183.     CRecvMsgThread                  m_RecvMsgThread;  
  184.     CFileTaskThread                 m_FileTask;  
  185.     CImageTaskThread                m_ImageTask;  
  186.       
  187.     CUserConfig                     m_UserConfig;  
  188.   
  189.     std::vector<AddFriendInfo*>       m_aryAddFriendInfo;  
  190.   
  191. private:  
  192.     time_t                          m_ServerTime;           //服務器時間  
  193.     DWORD                           m_StartTime;            //開始計時的時間  
  194.   
  195.     BOOL                            m_bNetworkAvailable;    //網絡是否可用  
  196.   
  197.     HWND                            m_hwndRegister;         //註冊窗口  
  198.     HWND                            m_hwndFindFriend;       //查找好友窗口  
  199.     HWND                            m_hwndModifyPassword;   //修改密碼窗口  
  200.     HWND                            m_hwndCreateNewGroup;   //創建羣組窗口  
  201.   
  202.     BOOL                            m_bBuddyIDsAvailable;   //用戶好友ID是否可用  
  203.     BOOL                            m_bBuddyListAvailable;  //用戶好友列表信息是否可用  
  204.   
  205.     std::map<UINTlong>          m_mapUserStatusCache;   //好友在線狀態緩存:key是賬戶ID,value是狀態碼  
  206.     std::map<UINTUINT>          m_mapAddFriendCache;    //加好友操作緩存: key是賬戶ID,value是操作碼  
  207.   
  208.     long                            m_nGroupCount;  
  209.     BOOL                            m_bGroupInfoAvailable;  
  210.     BOOL                            m_bGroupMemberInfoAvailable;  
  211. };  

舉個具體的例子,以刪除好友爲例:

[cpp]  view plain  copy
  1. // 刪除好友  
  2. BOOL CFlamingoClient::DeleteFriend(UINT uAccountID)  
  3. {  
  4.     //TODO: 先判斷是否離線  
  5.     COperateFriendRequest* pRequest = new COperateFriendRequest();  
  6.     pRequest->m_uCmd = Delete;  
  7.     pRequest->m_uAccountID = uAccountID;  
  8.       
  9.     m_SendMsgThread.AddItem(pRequest);  
  10.   
  11.     return TRUE;  
  12. }  


實際上也是產生一個任務丟到SendMsgThread所屬的消息隊列中去。

我的即時通訊軟flamingo pc端的基本框架大致介紹完畢了。這種架構,基本上是目前大多數客戶端軟件的通用結構(包括安卓,上文中介紹的filezilla是將利用Windows API WSAAsyncSelect將UI層與網絡層合併了),只不過不同的軟件在細節上可能做的比flamingo好。那麼我們來看下flamingo pc端的這種設計在細節上有哪些地方需要優化的呢?

1. 缺點一: 請求是無狀態無反饋的。比如某個請求,加到SendMsgThread中之後,如果處理失敗(可能在數據加工層或者網絡層沒發出去),對應的產生這個請求的UI元素無法得到任何反饋,所以也就沒法重試。解決辦法是,UI元素記錄一下自己發送的請求,然後開啓一個定時器,在一定時間後,該請求無應答,則重試。

2. 缺點二: 網絡層收發不必分開,原因上文也具體分析過了。這種分開,導致我現在給flamingo增加斷線重連機制非常麻煩。因爲涉及到用戶主動下線、掉線和被同名用戶在另外機器上登錄踢下線。這也是今後flamingo要重點優化的一個地方。

3. 缺點三:有些數據可以抽象出一個公共的模塊變量出來,允許在多個線程(UI線程和數據加工層)中讀寫(當然需要加鎖),這樣可以減少一部分通過PostMessage攜帶的、new出來的對象。(安卓中這種方法就很舒服了,因爲Java中不需要自己做內存回收)。其實這種方法是結合上文中討論的兩個方法的綜合運用。

4. 缺點四:缺少定時器,缺點一我介紹了UI層需要的定時器,其實網絡層也需要一個定時器,可用於網絡層心跳包的發送、或者自動重連。所以定時器是pc端軟件中非常重要的一個模塊。


三、pc端軟件中很有意思的設計技巧

當然,客戶端軟件還涉及到一些具體的細節設計技巧。這裏介紹幾個:

1. 放入任務隊列中、new出來的任務對象是使用它的父模塊刪除,還是自我刪除,所以有的客戶端軟件給每個這樣的任務提供一個自我刪除的接口,裏面delete this。看看teamtalk pc版就是這麼做的:

[cpp]  view plain  copy
  1. struct MODULE_API IHttpOperation : public ICallbackOpertaion  
  2. {  
  3. public:  
  4.     IHttpOperation(IOperationDelegate& callback)  
  5.         :ICallbackOpertaion(callback)  
  6.     {  
  7.   
  8.     }  
  9.     inline void cancel() { m_bIsCancel = TRUE; }  
  10.     inline BOOL isCanceled() const { return m_bIsCancel; }  
  11.   
  12.     virtual void release() = 0;  
  13.   
  14. private:  
  15.     BOOL        m_bIsCancel = FALSE;  
  16. };  

注意release()和cancel()接口,release()接口一般就是自我釋放資源和刪除自己,cancel()允許取消任務的執行。


2. 同步的、且帶超時時間的網絡通信接口該怎麼設計

一個系統在啓動之前,或者在登錄成功之前,其實一般不需要啓動上面所說數據加工層和網絡層。所以登錄成功之前,尤其是有登錄界面的客戶端,都可以使用同步的登錄接口,因爲登錄之前服務器一般不會有推送的數據給你,一般你發生的什麼請求,收到的數據就是該請求的應答。具體做法如下:

a. 用戶點擊了登錄按鈕之後,開啓一個新的線程(開啓新線程是爲了防止網絡通信阻塞界面)

b. 新線程函數中,調用網絡接口API發送數據和接收應答。當然涉及的socket,你仍然需要設置成非阻塞的,這樣,你可以在網絡接口中不必等待或者調用select函數等待socket可寫或者可讀一段你規定的時間。

c. 這樣的接口設計,你可以先連接服務器,再檢測socket是否可寫,如果可寫,發送數據,接着檢測socket是否可讀,如果可讀,收取一個包頭數據大小,接着根據包頭數據中指定的包體大小收取一個包體大小,然後解包數據,最後判斷是否是正確的應答。

d. 需要注意的是,新開啓的工作線程得到結果以後或者一定時間內超時,向界面元素反饋信息,也是通過PostMessage返回給界面的。

我以flamingo裏面的登錄過程爲例:

[cpp]  view plain  copy
  1. // 「登錄」按鈕  
  2. void CLoginDlg::OnBtn_Login(UINT uNotifyCode, int nID, CWindow wndCtl)  
  3. {     
  4.     if (m_cboUid.IsDefaultText())  
  5.     {  
  6.         MessageBox(_T("請輸入賬號!"), _T("提示"), MB_OK|MB_ICONINFORMATION);  
  7.         m_cboUid.SetFocus();  
  8.         return;  
  9.     }  
  10.   
  11.     if (m_edtPwd.IsDefaultText())  
  12.     {  
  13.         MessageBox(_T("請輸入密碼!"), _T("提示"), MB_OK|MB_ICONINFORMATION);  
  14.         m_edtPwd.SetFocus();  
  15.         return;  
  16.     }  
  17.   
  18.     m_cboUid.GetWindowText(m_stAccountInfo.szUser, ARRAYSIZE(m_stAccountInfo.szUser));  
  19.     m_edtPwd.GetWindowText(m_stAccountInfo.szPwd, ARRAYSIZE(m_stAccountInfo.szPwd));  
  20.     m_stAccountInfo.bRememberPwd = (m_btnRememberPwd.GetCheck() == BST_CHECKED);  
  21.     m_stAccountInfo.bAutoLogin = (m_btnAutoLogin.GetCheck() == BST_CHECKED);  
  22.   
  23.     // 記錄當前用戶信息  
  24.     m_lpFMGClient->m_UserMgr.m_UserInfo.m_strAccount = m_stAccountInfo.szUser;  
  25.       
  26.     //開啓線程  
  27.     HANDLE hLoginThread = (HANDLE)::_beginthreadex(NULL, 0, LoginThreadProc, this, 0, NULL);  
  28.     if (hLoginThread != NULL)  
  29.         ::CloseHandle(hLoginThread);  
  30.   
  31.     EndDialog(IDOK);  
  32. }  

[cpp]  view plain  copy
  1. UINT CLoginDlg::LoginThreadProc(void* pParam)  
  2. {  
  3.     CLoginDlg* pLoginDlg = (CLoginDlg*)pParam;  
  4.     if (pLoginDlg == NULL)  
  5.         return 0;  
  6.   
  7.     char szUser[64] = { 0 };  
  8.     EncodeUtil::UnicodeToUtf8(pLoginDlg->m_stAccountInfo.szUser, szUser, ARRAYSIZE(szUser));  
  9.     char szPassword[64] = { 0 };  
  10.     EncodeUtil::UnicodeToUtf8(pLoginDlg->m_stAccountInfo.szPwd, szPassword, ARRAYSIZE(szPassword));  
  11.   
  12.     std::string strReturnData;  
  13.     //調用網絡接口,超時時間設置爲3秒  
  14.     bool bRet = CIUSocket::GetInstance().Login(szUser, szPassword, 1, 1, 3000, strReturnData);  
  15.     int nRet = LOGIN_FAILED;  
  16.     CLoginResult* pLoginResult = new CLoginResult();  
  17.     pLoginResult->m_LoginResultCode = LOGIN_FAILED;  
  18.     if (bRet)  
  19.     {  
  20.         //{"code": 0, "msg": "ok", "userid": 8}  
  21.         Json::Reader JsonReader;  
  22.         Json::Value JsonRoot;  
  23.         if (JsonReader.parse(strReturnData, JsonRoot) && !JsonRoot["code"].isNull() && JsonRoot["code"].isInt())  
  24.         {  
  25.             int nRetCode = JsonRoot["code"].asInt();  
  26.   
  27.             if (nRetCode == 0)  
  28.             {  
  29.                 if (!JsonRoot["userid"].isInt() || !JsonRoot["username"].isString() || !JsonRoot["nickname"].isString() ||  
  30.                     !JsonRoot["facetype"].isInt() || !JsonRoot["gender"].isInt() || !JsonRoot["birthday"].isInt() ||  
  31.                     !JsonRoot["signature"].isString() || !JsonRoot["address"].isString() ||  
  32.                     !JsonRoot["customface"].isString() || !JsonRoot["phonenumber"].isString() ||  
  33.                     !JsonRoot["mail"].isString())  
  34.                 {  
  35.                     LOG_ERROR(_T("login failed, login response json is invalid, json=%s"), strReturnData.c_str());  
  36.                     pLoginResult->m_LoginResultCode = LOGIN_FAILED;  
  37.                 }  
  38.                 else  
  39.                 {  
  40.                     pLoginResult->m_LoginResultCode = 0;  
  41.                     pLoginResult->m_uAccountID = JsonRoot["userid"].asInt();  
  42.                     strcpy_s(pLoginResult->m_szAccountName, ARRAYSIZE(pLoginResult->m_szAccountName), JsonRoot["username"].asCString());  
  43.                     strcpy_s(pLoginResult->m_szNickName, ARRAYSIZE(pLoginResult->m_szNickName), JsonRoot["nickname"].asCString());  
  44.                     //pLoginResult->m_nStatus = JsonRoot["status"].asInt();  
  45.                     pLoginResult->m_nFace = JsonRoot["facetype"].asInt();  
  46.                     pLoginResult->m_nGender = JsonRoot["gender"].asInt();  
  47.                     pLoginResult->m_nBirthday = JsonRoot["birthday"].asInt();  
  48.                     strcpy_s(pLoginResult->m_szSignature, ARRAYSIZE(pLoginResult->m_szSignature), JsonRoot["signature"].asCString());  
  49.                     strcpy_s(pLoginResult->m_szAddress, ARRAYSIZE(pLoginResult->m_szAddress), JsonRoot["address"].asCString());  
  50.                     strcpy_s(pLoginResult->m_szCustomFace, ARRAYSIZE(pLoginResult->m_szCustomFace), JsonRoot["customface"].asCString());  
  51.                     strcpy_s(pLoginResult->m_szPhoneNumber, ARRAYSIZE(pLoginResult->m_szPhoneNumber), JsonRoot["phonenumber"].asCString());  
  52.                     strcpy_s(pLoginResult->m_szMail, ARRAYSIZE(pLoginResult->m_szMail), JsonRoot["mail"].asCString());  
  53.                 }  
  54.             }  
  55.             else if (nRetCode == 102)  
  56.                 pLoginResult->m_LoginResultCode = LOGIN_UNREGISTERED;  
  57.             else if (nRetCode == 103)  
  58.                 pLoginResult->m_LoginResultCode = LOGIN_PASSWORD_ERROR;  
  59.             else  
  60.                 pLoginResult->m_LoginResultCode = LOGIN_FAILED;  
  61.         }      
  62.     }  
  63.     //m_lpUserMgr爲野指針  
  64.     ::PostMessage(pLoginDlg->m_lpFMGClient->m_UserMgr.m_hProxyWnd, FMG_MSG_LOGIN_RESULT, 0, (LPARAM)pLoginResult);  
  65.   
  66.     return 1;  
  67. }  

[cpp]  view plain  copy
  1. bool CIUSocket::Login(const char* pszUser, const char* pszPassword, int nClientType, int nOnlineStatus, int nTimeout, std::string& strReturnData)  
  2. {  
  3.     if (!Connect())  
  4.         return false;  
  5.       
  6.     char szLoginInfo[256] = { 0 };  
  7.     sprintf_s(szLoginInfo,  
  8.         ARRAYSIZE(szLoginInfo),  
  9.         "{\"username\": \"%s\", \"password\": \"%s\", \"clienttype\": %d, \"status\": %d}",  
  10.         pszUser,  
  11.         pszPassword,  
  12.         nClientType,  
  13.         nOnlineStatus);  
  14.   
  15.     std::string outbuf;  
  16.     BinaryWriteStream writeStream(&outbuf);  
  17.     writeStream.WriteInt32(msg_type_login);  
  18.     writeStream.WriteInt32(0);  
  19.     //std::string data = szLoginInfo;  
  20.     writeStream.WriteCString(szLoginInfo, strlen(szLoginInfo));  
  21.     writeStream.Flush();  
  22.   
  23.     LOG_INFO("Request logon: Account=%s, Password=*****, Status=%d, LoginType=%d.", pszUser, pszPassword, nOnlineStatus, nClientType);  
  24.   
  25.     int32_t length = (int32_t)outbuf.length();  
  26.     msg header = { length };  
  27.     std::string strSendBuf;  
  28.     strSendBuf.append((const char*)&header, sizeof(header));  
  29.     strSendBuf.append(outbuf.c_str(), length);  
  30.       
  31.     //超時時間設置爲3秒  
  32.     if (!SendData(strSendBuf.c_str(), strSendBuf.length(), nTimeout))  
  33.         return false;  
  34.   
  35.     memset(&header, 0, sizeof(header));  
  36.     if (!RecvData((char*)&header, sizeof(header), nTimeout))  
  37.         return false;  
  38.       
  39.     if (header.packagesize <= 0)  
  40.         return false;  
  41.   
  42.     CMiniBuffer minBuff(header.packagesize);  
  43.     if (!RecvData(minBuff, header.packagesize, nTimeout))  
  44.     {  
  45.         return false;  
  46.     }  
  47.   
  48.     BinaryReadStream readStream(minBuff, header.packagesize);  
  49.     int32_t cmd;  
  50.     if (!readStream.ReadInt32(cmd))  
  51.         return false;  
  52.   
  53.     int32_t seq;  
  54.     if (!readStream.ReadInt32(seq))  
  55.         return false;  
  56.   
  57.     size_t datalength;  
  58.     if (!readStream.ReadString(&strReturnData, 0, datalength))  
  59.     {  
  60.         return false;  
  61.     }  
  62.   
  63.     return true;  
  64. }  

[cpp]  view plain  copy
  1. bool CIUSocket::SendData(const char* pBuffer, int nBuffSize, int nTimeout)  
  2. {  
  3.     //TODO:這個地方可以先加個select判斷下socket是否可寫  
  4.   
  5.     int64_t nStartTime = time(NULL);  
  6.       
  7.     int nSentBytes = 0;  
  8.     int nRet = 0;  
  9.     while (true)  
  10.     {  
  11.         nRet = ::send(m_hSocket, pBuffer, nBuffSize, 0);  
  12.         if (nRet == SOCKET_ERROR)  
  13.         {  
  14.             //對方tcp窗口太小暫時發佈出去,同時沒有超時,則繼續等待  
  15.             if (::WSAGetLastError() == WSAEWOULDBLOCK && time(NULL) - nStartTime < nTimeout)  
  16.             {  
  17.                 continue;  
  18.             }  
  19.             else  
  20.                 return false;  
  21.         }  
  22.         else if (nRet < 1)  
  23.         {  
  24.             //一旦出現錯誤就立刻關閉Socket  
  25.             LOG_ERROR("Send data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);  
  26.             Close();  
  27.             return false;  
  28.         }  
  29.   
  30.         nSentBytes += nRet;  
  31.         if (nSentBytes >= nBuffSize)  
  32.             break;  
  33.   
  34.         pBuffer += nRet;  
  35.         nBuffSize -= nRet;  
  36.   
  37.         ::Sleep(1);  
  38.     }  
  39.       
  40.     return true;  
  41. }  

[cpp]  view plain  copy
  1. bool CIUSocket::RecvData(char* pszBuff, int nBufferSize, int nTimeout)  
  2. {  
  3.     int64_t nStartTime = time(NULL);  
  4.       
  5.     fd_set writeset;  
  6.     FD_ZERO(&writeset);  
  7.     FD_SET(m_hSocket, &writeset);  
  8.   
  9.     timeval timeout;  
  10.     timeout.tv_sec = nTimeout;  
  11.     timeout.tv_usec = 0;  
  12.   
  13.     int nRet = ::select(m_hSocket + 1, NULL, &writeset, NULL, &timeout);  
  14.     if (nRet != 1)  
  15.     {  
  16.         Close();  
  17.         return false;  
  18.     }  
  19.       
  20.     int nRecvBytes = 0;  
  21.     int nBytesToRecv = nBufferSize;  
  22.     while (true)  
  23.     {  
  24.         nRet = ::recv(m_hSocket, pszBuff, nBytesToRecv, 0);  
  25.         if (nRet == SOCKET_ERROR)               //一旦出現錯誤就立刻關閉Socket  
  26.         {  
  27.             if (::WSAGetLastError() == WSAEWOULDBLOCK && time(NULL) - nStartTime < nTimeout)  
  28.                 continue;  
  29.             else  
  30.             {  
  31.                LOG_ERROR("Recv data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);  
  32.                 Close();  
  33.                 return false;  
  34.             }  
  35.         }  
  36.         else if (nRet < 1)  
  37.         {  
  38.             LOG_ERROR("Recv data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);  
  39.             Close();  
  40.             return false;  
  41.         }  
  42.   
  43.         nRecvBytes += nRet;  
  44.         if (nRecvBytes >= nBufferSize)  
  45.             break;  
  46.   
  47.         pszBuff += nRet;  
  48.         nBytesToRecv -= nRet;  
  49.   
  50.         ::Sleep(1);  
  51.     }  
  52.       
  53.     return true;  
  54. }  

3. 不少pc客戶端軟件,會設計一個接口,這個接口含有所有可以處理收到的網絡數據的方法,也就是說所有繼承自這個接口的子類都可以處理收到的網絡數據,這些子類會在程序初始化的時候,被加入到網絡層或者數據加工層的一個接受者集合中去。這樣的話,網絡層在收到數據後,遍歷這個對象,然後利用C++多態,分別調用各個改寫的方法去處理具體的數據。舉個代碼例子:

[cpp]  view plain  copy
  1. interface IMessageRevcer  
  2. {  
  3. virtual CString GetRecverName() = 0;  
  4. virtual void OnDataPackRecv(GWBasePack *pPack);  
  5. virtual
相關文章
相關標籤/搜索