用長輪詢實現Chat並遷移到Azure測試

本文轉自博客園:http://www.cnblogs.com/indream/p/3187540.html

公司的OA從零開始進行開發,繼簡單的單點登陸、角色與權限、消息中間件之後,輪到在線即時通信的模塊需要我獨立去完成。這三週除了逛網店見愛*看動漫接兼職,基本上都花在這上面了。簡單地說就是用MVC4基於長輪詢實現(僞)即時通信,利用BootMetro搭建即時聊天系統,同時跨域組件化之後今晚移植到了Azure上方便週末進行第一次迭代的公網測試,地址在http://indreamchat.cloudapp.net/。有興趣的朋友可以上去送測試數據,剝離了認證登陸,簡單地僞裝了一個...一個...怎麼說,反正能用就好了...

一大早要坐客車回家,所以現在睡也不是不睡也不是,就分享一下實現方式。由於已經回到租房了,所以代碼不在手上,寫得如何,有待指正。

最後,它長這個樣子:

 

首先介紹下用戶情況。

系統的用戶除了1/4的內網用戶外,基本上都是全國各地辦事處的外網用戶,而且出差尤多,特別是海外,這是網絡狀況。

也就是我們的用戶遍佈世界各地,有不同的網絡狀況,而且世界上大部分可以想得到的設備都可能會是接入端,所以一開始就有較高兼容需求。

 

回到即時通信,其實只是其難點而已,總體來說是一個Chat和消息推送模塊,允許各個子系統按照需求用羣組會話組織管理用戶,並推送系統消息,同時允許用戶間的通信交流,並且滿足移植性,使得不同子系統能直接引用。

下文先交代架構,然後最後交流下關鍵技術吧。

那麼直接用文字說下架構吧,從上到下是從底層(數據庫)到頂層(UI):

  • SQL Server 2012
  • Data Access / Entity Framework 5——數據鏈路以及數據緩存,利用EF實現
  • ChatManager + ListenQueue——會話的管理對象,監聽列表提供監聽服務和持有監聽對象,在新消息和相關監聽存在的時候通過callback推送消息
  • CometManager——長輪詢的管理對象,負責向MVC提供輪詢服務,同時向上層監聽消息,可以視爲一個服務的Adapter
  • Service / MVC 4——提供UI和JSONP API的(工程意義上的)UI/Service層
  • ChatDataManager.js——與Service進行數據通信並維護本地數據對象的管理對象
  • chatUiEngine.js——利用ChatDataManager.js向UI提供動態UI服務的引擎
  • UI——提供了配置和界面容器後,只執行了一個chatUiEngine.Start()方法啓動的頁面

以下逐層詳細說明:

SQL Server 2012

Data Access / Entity Framework 5

即時通信有一個很特別的地方,就是對集中的數據進行頻繁讀寫。你可以站在數據的角度來看,基本上所有用戶都在訪問並且添加最近最新的那羣數據,所以作爲數據鏈路層的數據對象,只需要將盡量新的數據緩存起來即可。另外可以保證的是先寫後讀(發了消息其他人才可以看見),寫完馬上要(發完了消息其他人馬上要讀取),基本上就是個對寫方法加了信號量(同步鎖)的數據棧。

那麼,是用帶有緩存的ORM就十分合適,比如Entity Framework。

從業務方面考慮,這是個頻繁修改需求的項目,有Model First的Entity Framework是個不錯的選擇。

至於爲什麼用那麼新,其實只是用Nuget更新的,不過還是很喜歡它的Convert To Enum功能。只是用EF的話要十分小心數據庫的結構和Model並不完全同步,比如1 to 1/0在生成數據庫再另外生成Data Model的時候會變成1 to *,因爲只有一個外鍵約束。

ChatManager + ListenQueue

這是會話管理與消息管理的核心,會話管理其實也就是增刪查改的問題,主要功能實現在於消息部分,也就是ListenQueue。

ListenQueue是一個監聽隊列,可以添加監聽,由ChatManager作爲其Fascade,對外提供監聽和停止監聽服務。

業務實現的方法就是,向ChatManager提出監聽某用戶/會話的最新信息,在ChatManager有最新信息的時候通過Callback將有最新消息的消息返回給監聽者(怎麼說着那麼彆扭呢),由監聽者決定是獲取新消息還是執行什麼業務。

所以,這一層實現了消息的發送獲取管理、會話的管理、監聽列表的管理,而它們各自有業務相關。

在這裏,得感慨一下delegate閉包的強大和便利。

CometManager

一個監聽實現者和一個長輪詢服務者,通過長輪詢實現監聽到最新消息後即時推送回客戶端。長輪詢是怎麼回事呢?應該可以搜到不少資源,放在後面講吧。

在這裏不是用ChatManager直接提供輪詢服務是因爲需要擴充性,將來必然有其他形式的客戶端和其連接方式需要獲取最新消息,比如Web Socket、WCF、Hessian。到時候這些方式的接收者只要實現符合delegate約束的監聽方法,即可將消息以自己的通信方式發送回自己所服務的客戶端了。

Service / MVC 4

這就是爲瀏覽器提供最終頁面和數據的項目層面上的UI層。選用MVC 4是因爲其可以同時提供輕量級的跨域Web API的JSONP服務,如何實現JSONP後面簡述吧。

這一層主要的任務就是界面和數據服務,並沒有什麼特別的。當然,依賴注入由這裏啓動,我用的是StructureMap2.6.4。

這裏使用JSONP服務的一個目的就是爲了能讓其他系統跨域調用Chat。

ChatDataManager.js

這個在站點可以直接看到,沒有做編碼,所以可以從頁面源代碼處看到源代碼。

這是利用jQuery.Ajax與上一層進行數據交流以及本地數據管理的管理對象。它主要的功能就是獲取數據、將數據格式化並持久化、同步更新數據,在數據更新時用回調通知監聽對象,讓其對數據更新作出反應。

用大寫字母開頭而不用JS常用的命名法就是不希望一般用戶直接使用。

chatUiEngine.js

利用ChatDataManager所持久化的數據和數據更變讓界面持續工作的“引擎”,從一個Start(settings)方法開始啓動。

它啓動後的第一步就是啓動ChatDataManager.js,然後用獲取到的數據構建Chat的整個頁面界面,然後一直維持界面運轉。比如在有新內容的時候刷新或者更改界面,用戶操作時控制界面作出反應,用戶發送消息時將消息通過ChatDataManager.js推送回服務器,等等。

將所有UI操作的方法封裝成API的目的就是讓其他系統可以通過調用兩個JS而在自己系統打開Chat,並且使用;而將數據與UI的持久化控制分成兩層,是爲了讓客戶端在有需要的時候獲取部分數據,而不需交互。

UI

UI所作的就是提供容器(顯示Chat以及相關內容的地方)和配置(告訴chatUiEngine.js有什麼具體UI需求)。

這裏嘗試展示下打開chatUiEngine.js的方法(不大懂插入代碼...):

 啓動chatUiEngine.js

這些是在Html中提供給chatUiEngine.js的容器,chatUiEngine.js利用它們生成合適的界面元素,將數據渲染上去後展示到容器中,而容器在上面的配置中進行描述。

 Html模板

剩下的就是UI中大致的容器了,用一個簡單的table搭建出來,然後chatUiEngine就會將界面元素動態導入。

 貼了也沒什麼意義的代碼

UI部分已經儘量簡化了,目的就是希望對原有的系統可以實現無痛人流植入,儘量少造成更改,同時可以讓它們實現自己特殊的界面需求。

當然,至此只是我打了半個星期醬油,敲了兩個星期多一點代碼的第一次迭代的發佈,所以必定很不完善。另外代碼只在上週末重構過一次,這周測試和需求頻繁發生也造成了新的代碼亂搞基冗餘,近期需要再次重構。

 

 

 

 

下面就分享下一些技術理解吧:

關於長輪詢Long-Polling

詳細的許多內容應該挺容易搜索到得,我也是從找到@dudu的謀篇博文開始知道MVC是具體怎麼實現的,就用我的方式和實現方法籠統地分享下吧。

首先是原理。

原理很簡單,HTTP是個異步轉同步的協議,客戶端發送了一個請求後,保持了與服務端的一條TCP連接,然後服務端通過這條連接將網頁以及相關內容發送回客戶端。而原來的Web只允許這一種通信方式,也是出於安全方面考慮(現在有Web Socket了)。

那服務端有消息要馬上推送給客戶端要怎麼辦呢?所以有了長輪詢。

服務端將那條連接Hold住,直到有消息了再將數據通過那條連接返回給用戶,然後用戶再繼續請求新的連接,然後服務端繼續Hold住......

體現在我的開發過程裏面就是,一開始我想用自旋鎖鎖住那條連接的線程的(沒那麼神祕就是一直While(true) {sleep();}而已),後來發現了MVC可以通過實現AsyncController,然後用AsyncManager來實現異步返回,從而節約了CPU資源。

然後效果就來了:

用戶請求連接,然後等啊等,等啊等,等到我有新消息了,然後就斷線了(返回了結果),然後發現,唉媽呀(粵語Diu,英語Oh, f**k),斷了,有新消息了。然後主動去請求了新消息,這時EF就把剛剛存進去新鮮滾熱辣的最新消息返回給該用戶。用戶拿到後在繼續請求連接,然後等啊等,等啊等,等啊等,等啊等,等啊等,按後就斷線了,唉媽呀,......

然後,實時通信就這麼實現了,雖然我覺得是很聰明,但是卻很噁心的技術...

JSONP

一般的Web不許跨域請求消息,但是有一個例外,就是引用文件,比如圖片、JS文件、CSS,所以就可以把所需要跨域請求的東西通過文件,動態地引用進本頁面。

而JSONP就是用這種方式實現用JSON通信的。

實現起來並不神奇,就是客戶度先新建一個function,比如叫做callback1234(),並且把方法名同時和請求一起發送回服務端,然後服務端把數據準備好後,包裝成callback1234([數據內容]);,並打包進一個.js文件,發送回客戶端。客戶端收到那個文件後將其添加進引用,然後因爲callback1234是本地已有的一個function,所以就執行了callback1234(data),以此將數據推進了你已經定義好了的代碼的深淵......

移植到Windows Azure

就是Microsoft的公有云,一開始並沒有這個打算。不過爲了方便回家能測試,同時上個月正好去了Azure廣州的Live to Code(喫喝玩樂,還發了篇博客,就懶得翻出來了),拿了一個還有兩天到期的試用賬號,所以今晚...呃...好吧,剛纔就掛上去了。

掛上去還算比較簡單,首先在Azure建立自己的數據庫,然後用SQL Server Management Studio連接上,並執行了EF Model First生成的SQL代碼就把數據庫在上面生成了。

在這裏,我做錯的就是用DB First生成了Azure用的那個Container,導致1 to 1/0的約束變成了1 to *,煩了我半天。原來直接吧connetionString改了就可以了,不用新建的...

其他代碼都是從原有項目複製黏貼上去的,唯一修改的地方就是Web層的Global文件已經失效了,因爲不是用IIS啓動的,不會被執行。所以添加了一個WebRole(其實是自動添加的),用上面的OnStart()等方法代替了Application_Start()等方法,僅此而已。

在Visual Studio 2012裏,右鍵創建的那個Windows Azure項目,點擊publish,然後第一次下一步下一步下一步地設置好,比如用多少個CPU、多少個實例等等,然後就會推送到Azure了。推送完成後馬上可以通過自己設置的二級域名打開網站。

我第一次用,不大熟悉,用了cloudapp.com的一級域名,另外建了個windowsazure.com的站點。不過,暫時來說,能掛上去能跑就是好事了,我也太累趕着下班了(其實還不是通宵沒睡)。

最後再說一下http://indreamchat.cloudapp.net/進入這個站點哦,賬號快過期,時間有限!時間有限哦!!!

最最後,關於360瀏覽器和IE6

最最後,作爲一個要涉足前端,並且涉足兼容的開發人員,允許我再一次表達對360垃圾瀏覽器最深刻最深沉最深入的鄙視,以及對IE6最悲痛最悲劇最悲哀的嘆息。

(通宵腦子已經很不清醒了,寫得怎樣就怎樣吧,回頭再補救...)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章