史上最全Web端即時通訊技術原理詳解

IT技術精選文摘發表於2018-05-23

關於IM(InstantMessaging)即時通訊類軟體(如微信,QQ),大多數都是桌面應用程式或者native應用較為流行,而網上關於原生IM或桌面IM軟體類的通訊原理介紹也較多,此處不再贅述。而web端的IM應用,由於瀏覽器的相容性以及其固有的“客戶端請求伺服器處理並響應”的通訊模型,造成了要在瀏覽器中實現一個相容性較好的IM應用,其通訊過程必然是諸多技術的組合,本文的目的就是要詳細探討這些技術並分析其原理和過程。

1.基於web的固有通訊方式

瀏覽器本身作為一個瘦客戶端,不具備直接通過系統呼叫來達到和處於異地的另外一個客戶端瀏覽器通訊的功能。這和我們桌面應用的工作方式是不同的,通常桌面應用通過socket可以和遠端主機上另外一端的一個程式建立TCP連線,從而達到全雙工的即時通訊。瀏覽器從誕生開始一直走的是客戶端請求伺服器,伺服器返回結果的模式,即使發展至今仍然沒有任何改變。所以可以肯定的是,要想實現兩個客戶端的通訊,必然要通過伺服器進行資訊的轉發。例如A要和B通訊,則應該是A先把資訊傳送給IM應用伺服器,伺服器根據A資訊中攜帶的接收者將它再轉發給B,同樣B到A也是這種模式,如下所示:

2.固有通訊方式實現IM應用需要解決的問題

我們認識到基於web實現IM軟體依然要走瀏覽器請求伺服器的模式,這這種方式下,針對IM軟體的開發需要解決如下三個問題:

1)雙全工通訊:即達到瀏覽器拉取(pull)伺服器資料,伺服器推送(push)資料到瀏覽器;

2)低延遲:即瀏覽器A傳送給B的資訊經過伺服器要快速轉發給B,同理B的資訊也要快速交給A,實際上就是要求任何瀏覽器能夠快速請求伺服器的資料,伺服器能夠快速推送資料到瀏覽器;

3)支援跨域:通常客戶端瀏覽器和伺服器都是處於網路的不同位置,瀏覽器本身不允許通過指令碼直接訪問不同域名下的伺服器,即使IP地址相同域名不同也不行,域名相同埠不同也不行,這方面主要是為了安全考慮。

基於以上分析,下面針對這三個問題給出解決方案。

3.全雙工低延遲的解決辦法

解決方案1.客戶端瀏覽器輪詢伺服器(polling)

這是最簡單的一種解決方案,其原理是在客戶端通過Ajax的方式的方式每隔一小段時間就傳送一個請求到伺服器,伺服器返回最新資料,然後客戶端根據獲得的資料來更新介面,這樣就間接實現了即時通訊。優點是簡單,缺點是對伺服器壓力較大,浪費頻寬流量(通常情況下資料都是沒有發生改變的)。

客戶端程式碼如下:

建立一個XHR物件,每2秒就請求伺服器一次獲取伺服器時間並列印出來。

服務端程式碼(Node.js)

結果如下:

解決方案2.長輪詢(long-polling)

在上面的輪詢解決方案中,由於每次都要傳送一個請求,服務端不管資料是否發生變化都傳送資料,請求完成後連線關閉。這中間經過的很多通訊是不必要的,於是又出現了長輪詢(long-polling)方式。這種方式是客戶端傳送一個請求到伺服器,伺服器檢視客戶端請求的資料是否發生了變化(是否有最新資料),如果發生變化則立即響應返回,否則保持這個連線並定期檢查最新資料,直到發生了資料更新或連線超時。同時客戶端連線一旦斷開,則再次發出請求,這樣在相同時間內大大減少了客戶端請求伺服器的次數。程式碼如下:

客戶端:

在XHR物件的readySate為4的時候,表示伺服器已經返回資料,本次連線已斷開,再次請求伺服器建立連線。

服務端程式碼:

在服務端通過生成一個在1到9之間的隨機數來模擬判斷資料是否發生了變化,當隨機數在0到5之間表示資料發生了變化,直接返回,否則保持連線,每隔2秒再檢測。

結果如下:

可以看到返回的時間是沒有規律的,並且單位時間內返回的響應數相比polling方式較少。

解決方案3.基於http-stream通訊

上面的long-polling技術為了保持客戶端與服務端的長連線採取的是服務端阻塞(保持響應不返回),客戶端輪詢的方式,在comet技術中,還存在一種基於http-stream流的通訊方式。其原理是讓客戶端在一次請求中保持和服務端連線不斷開,然後服務端源源不斷傳送資料給客戶端,就好比資料流一樣,並不是一次性將資料全部發給客戶端。它與polling方式的區別在於整個通訊過程客戶端只傳送一次請求,然後服務端保持與客戶端的長連線,並利用這個連線在回送資料給客戶端。

這種方案有分為幾種不同的資料流傳輸方式:

3.1基於XHR物件的streaming方式:

這種方式的思想是構造一個XHR物件,通過監聽它的onreadystatechange事件,當它的readyState為3的時候,獲取它的responseText然後進行處理,readyState為3表示資料傳送中,整個通訊過程還沒有結束,所以它還在不斷獲取服務端傳送過來的資料,直到readyState為4的時候才表示資料傳送完畢,一次通訊過程結束。在這個過程中,服務端傳給客戶端的資料是分多次以stream的形式傳送給客戶端,客戶端也是通過stream形式來獲取的,所以稱作http-streaming資料流方式,程式碼如下:

客戶端程式碼:

這裡由於客戶端收到的資料是分段發過來的,所以最好定義一個遊標received,來獲取最新資料而捨棄之前已經接收到的資料,通過這個遊標每次將接收到的最新資料列印出來,並且在通訊結束後列印出整個responseText。

服務端程式碼:

服務端通過計數器count將資料分十次傳送,每次生成一個小於10000的隨機數傳送給客戶端讓它進行處理。

結果如下:

可以看到每次傳過來的資料流都進行了處理,同時列印出了整個最終接收到的完整資料。這種方式間接實現了客戶端請求,服務端及時推送資料給客戶端。

3.2基於iframe的資料流

由於低版本的IE不允許在XHR的readyState為3的時候獲取其responseText屬性,為了達到在IE上使用這個技術,又出現了基於iframe的資料流通訊方式。具體來講,就是在瀏覽器中動態載入一個iframe,讓它的src屬性指向請求的伺服器的URL,實際上就是向伺服器傳送了一個http請求,然後在瀏覽器端建立一個處理資料的函式,在服務端通過iframe與瀏覽器的長連線定時輸出資料給客戶端,但是這個返回的資料並不是一般的資料,而是一個類似於<script type=\"text/javascript\">parent.process('"+randomNum.toString()+"')</script>指令碼執行的方式,瀏覽器接收到這個資料就會將它解析成js程式碼並找到頁面上指定的函式去執行,實際上是服務端間接使用自己的資料間接呼叫了客戶端的程式碼,達到實時更新客戶端的目的。

客戶端程式碼如下:

客戶端為了簡單起見,定義對資料處理就是列印出來

服務端程式碼:

服務端定時傳送隨機數給客戶端,並呼叫客戶端process函式

在IE5中測試結果如下:

可以看到實現在低版本IE中客戶端到伺服器的請求-推送的即時通訊。

3.3 基於htmlfile的資料流通訊

又出現新問題了,在IE中,使用iframe請求服務端,服務端保持通訊連線沒有全部返回之前,瀏覽器title一直處於載入狀態,並且底部也顯示正在載入,這對於一個產品來講使用者體驗是不好的,於是谷歌的天才們又想出了一中hack方式。就是在IE中,動態生成一個htmlfile物件,這個物件ActiveX形式的com元件,它實際上就是一個在記憶體中實現的HTML文件,通過將生成的iframe新增到這個記憶體中的HTMLfile中,並利用iframe的資料流通訊方式達到上面的效果。同時由於HTMLfile物件並不是直接新增到頁面上的,所以並沒有造成瀏覽器顯示正在載入的現象。程式碼如下:

客戶端:

服務端傳送給iframe的是這樣子

這樣就在iframe流的原有方式下避免了瀏覽器的載入狀態。

解決方案4.SSE(服務端推送事件)eventSource

為了解決瀏覽器只能夠單向傳輸資料到服務端,HTML5提供了一種新的技術叫做伺服器推送事件SSE,關於該技術我在之前的文章中有詳細介紹,它能夠實現客戶端請求服務端,然後服務端利用與客戶端建立的這條通訊連線push資料給客戶端,客戶端接收資料並處理的目的。從獨立的角度看,SSE技術提供的是從伺服器單向推送資料給瀏覽器的功能,但是配合瀏覽器主動請求,實際上就實現了客戶端和伺服器的雙向通訊。它的原理是在客戶端構造一個eventSource物件,該物件具有readySate屬性,分別表示如下:

0:正在連線到伺服器;

1:開啟了連線;

2:關閉了連線。

同時eventSource物件會保持與伺服器的長連線,斷開後會自動重連,如果要強制連線可以呼叫它的close方法。

可以它的監聽onmessage事件,服務端遵循SSE資料傳輸的格式給客戶端,客戶端在onmessage事件觸發時就能夠接收到資料,從而進行某種處理,程式碼如下:

客戶端:

服務端:

注意這裡服務端傳送的資料要遵循一定的格式,通常是id:(空格)資料(換行符)data:(空格)資料(兩個換行符),如果不遵循這種格式,實際上客戶端是會觸發error事件的。這裡的id是用來標識每次傳送的資料的id,是強制要加的。

結果如下:

以上就是比較常用的客戶端服務端雙向即時通訊的解決方案,下面再來看如何實現跨域。

4.跨域解決辦法

關於跨域是什麼,限於篇幅所限,這裡不做介紹,網上有很多詳細的文章,這裡只列舉解決辦法:

解決方案4.1基於XHR的COSR(跨域資源共享)

CORS(跨域資源共享)是一種允許瀏覽器指令碼向出於不同域名下伺服器傳送請求的技術,它是在原生XHR請求的基礎上,XHR呼叫open方法時,地址指向一個跨域的地址,在服務端通過設定'Access-Control-Allow-Origin':'*'響應頭部告訴瀏覽器,傳送的資料是一個來自於跨域的並且伺服器允許響應的資料,瀏覽器接收到這個header之後就會繞過平常的跨域限制,從而和平時的XHR通訊沒有區別。該方法的主要好處是在於客戶端程式碼不用修改,服務端只需要新增'Access-Control-Allow-Origin':'*'頭部即可。適用於ff,safari,opera,chrome等非IE瀏覽器。跨域的XHR相比非跨域的XHR有一些限制,這是為了安全所需要的,主要有以下限制:

1.客戶端不能使用setRequestHeader設定自定義頭部;

2.不能傳送和接收cookie;

3.呼叫getAllResponseHeaders()方法總會返回空字串。

以上這些措施都是為了安全考慮,防止常見的跨站點指令碼攻擊(XSS)和跨站點請求偽造(CSRF)。

客戶端程式碼:

服務端程式碼:

注意服務端需要設定頭部Access-Control-Allow-Origin為需要跨域的域名

這裡為了測試在埠8088上監聽請求,然後讓客戶端在80埠上請求服務,結果如下:

解決方案4.2 基於XDR的CORS

對於IE8-10,它是不支援使用原生的XHR物件請求跨域伺服器的,它自己實現了一個XDomainRequest物件,類似於XHR物件,能夠傳送跨域請求,它主要有以下限制:

1.cookie不會隨請求傳送,也不會隨響應返回;

2.只能設定請求頭部資訊中的Content-Type欄位;

3.不能訪問響應頭部資訊;

4.只支援Get和Post請求;

5.只支援IE8-IE10;

客戶端請求程式碼:

服務端程式碼和同上,在IE8中測試結果如下:

解決方案4.3基於JSONP的跨域

這種方式不需要在服務端新增Access-Control-Allow-Origin頭資訊,其原理是利用HTML頁面上script標籤對跨域沒有限制的特點,讓它的src屬性指向服務端請求的地址,其實是通過script標籤傳送了一個http請求,伺服器接收到這個請求之後,返回的資料是自己的資料加上對客戶端JS函式的呼叫,其原理類似於我們上面所說的iframe流的方式,客戶端瀏覽器接收到返回的指令碼呼叫會解析執行,從而達到更新介面的目的。

客戶端程式碼如下:

服務端程式碼:

注意這裡服務端輸出的資料content-type首部要設定為application/javascript,否則某些瀏覽器會將其當做文字解析。

結果如下:

5.websocket

在上面的這些解決方案中,都是利用瀏覽器單向請求伺服器或者伺服器單向推送資料到瀏覽器這些技術組合在一起而形成的hack技術,在HTML5中,為了加強web的功能,提供了websocket技術,它不僅是一種web通訊方式,也是一種應用層協議。它提供了瀏覽器和伺服器之間原生的雙全工跨域通訊,通過瀏覽器和伺服器之間建立websocket連線(實際上是TCP連線),在同一時刻能夠實現客戶端到伺服器和伺服器到客戶端的資料傳送。關於該技術的原理,我之前一篇文章中做了比較詳細的介紹,此處就不在贅述了,直接給出程式碼。在看程式碼之前,需要先了解websocket整個工作過程:

首先是客戶端new 一個websocket物件,該物件會傳送一個http請求到服務端,服務端發現這是個webscoket請求,會同意協議轉換,傳送回客戶端一個101狀態碼的response,以上過程稱之為一次握手,經過這次握手之後,客戶端就和服務端建立了一條TCP連線,在該連線上,服務端和客戶端就可以進行雙向通訊了。這時的雙向通訊在應用層走的就是ws或者wss協議了,和http就沒有關係了。所謂的ws協議,就是要求客戶端和服務端遵循某種格式傳送資料包文(幀),然後對方才能夠理解。關於ws協議要求的資料格式官網指定如下:

其中比較重要的是FIN欄位,它佔用1位,表示這是一個資料幀的結束標誌,同時也下一個資料幀的開始標誌。opcode欄位,它佔用4位,當為1時,表示傳遞的是text幀,2表示二進位制資料幀,8表示需要結束此次通訊(就是客戶端或者服務端哪個傳送給對方這個欄位,就表示對方要關閉連線了)。9表示傳送的是一個ping資料。mask佔用1位,為1表示masking-key欄位可用,masking-key欄位是用來對客戶端傳送來的資料做unmask操作的。它佔用0到4個位元組。Payload欄位表示實際傳送的資料,可以是字元資料也可以是二進位制資料。

所以不管是客戶端和服務端向對方傳送訊息,都必須將資料組裝成上面的幀格式來傳送。首先來看服務端程式碼:

這裡借用了次碳酸鈷的程式碼:

服務端通過監聽data事件來獲取客戶端傳送來的資料,如果是握手請求,則傳送http 101響應,否則解析得到的資料並列印出來,然後判斷是不是斷開連線的請求(Opcode為8),如果是則斷開連線,否則將接收到的資料組裝成幀再傳送給客戶端。

客戶端程式碼:

客戶端建立一個websocket物件,在onopen時間觸發之後(握手成功後),給頁面上的button指定一個事件,用來傳送頁面input當中的資訊,服務端接收到資訊列印出來,並組裝成幀返回給日客戶端,客戶端再append到頁面上。

結果如下:

瀏覽器:

服務端輸出結果:

從上面可以看出,websocket在支援它的瀏覽器上確實提供了一種全雙工跨域的通訊方案,所以在各以上各種方案中,我們的首選無疑是websocket。

6.總結

上面論述了這麼多對於IM應用開發所涉及到的通訊方式,在實際開發中,我們通常使用的是一些別人寫好的實時通訊的庫,比如socket.io,sockjs,他們的原理就是將上面(還有一些其他的如基於Flash的push)的一些技術進行了在客戶端和服務端的封裝,然後給開發者一個統一呼叫的介面。這個介面在支援websocket的環境下使用websocket,在不支援它的時候啟用上面所講的一些hack技術。從實際來講,單獨使用上面所講的任何一種技術(websocket除外)達不到我們在文章開頭提出的低延時,雙全工,跨域的全部要求,只有把他們組合起來才能夠很好地工作,所以通常情況下,這些庫都是在不同的瀏覽器上採用各種不同的組合來實現實時通訊的。下面是sockjs在不同瀏覽器下面採取的不同組合方式:

從圖上可以看出,對於現代瀏覽器(IE10+,chrome14+,Firefox10+,Safari5+以及Opera12+)都是能夠很好的支援websocket的,其餘低版本瀏覽器通常使用基於XHR(XDR)的polling(streaming)或者是基於iframe的的polling(streaming),對於IE6,7來講,它不僅不支援XDR跨域,也不支援XHR跨域,所以只能夠採取jsonp-polling的方式。

公眾號推薦:

相關文章