深入瞭解HTTP和Socket在實時性Web上的實踐

注:本文很多內容直接來自參考資料,如果需要更詳細的瞭解,建議閱讀本文後繼續閱讀參考資料

實時的Web

在我們的大部分網站中,我們都採用了傳統的HTTP請求來和服務端進行通信,包括資源文件的下載,異步數據的請求。這樣的策略已經能滿足大部分網站的需求,因爲它並不需要保證站點上的數據有多麼實時。但是,也總有很多情況下,我們需要去保證數據的實時性,比如股票價格,微博刷新,以及交通狀況等。僅僅寄希望於用戶的鈦合金F5肯定是不負責任的,我們需要去考慮如何在我們的站點中也能提供實時的數據。

8IIli 深入瞭解HTTP和Socket在實時性Web上的實踐

基於HTTP的實現

瀏覽器作爲 Web 應用的前臺,自身的處理功能比較有限。瀏覽器的發展需要客戶端升級軟件,同時由於客戶端瀏覽器軟件的多樣性,在某種意義上,也影響了瀏覽器新技術的推廣。在 Web 應用中,瀏覽器的主要工作是發送請求、解析服務器返回的信息以不同的風格顯示。AJAX 是瀏覽器技術發展的成果,通過在瀏覽器端發送異步請求,提高了單用戶操作的響應性。但 Web 本質上是一個多用戶的系統,對任何用戶來說,可以認爲服務器是另外一個用戶。現有 AJAX 技術的發展並不能解決在一個多用戶的 Web 應用中,將更新的信息實時傳送給客戶端,從而用戶可能在“過時”的信息下進行操作。而 AJAX 的應用又使後臺數據更新更加頻繁成爲可能。

現在網站中大部分實時數據的實現,還是依賴於輪詢和其他一些服務端推送的數據。最主要的原因是早期的瀏覽器只提供的傳統HTTP請求的支持,所幸 HTML5規範中涉及到了新的請求機制, webSockets API 。各大瀏覽器也在最新的版本中對其進行了一些實踐,兼容性如下:

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Version -76 support Obsolete} 6 4.0 (2.0) Not supported 11.00 (disabled) 5.0.1
Protocol version 7 support Not supported 6.0 (6.0) 
Moz
Not supported Not supported Not supported
Protocol version 10 support 14 7.0 (7.0) 
Moz
HTML5 Labs ? ?
RFC 6455 Support (IETF Draft 17) 16 11.0 (11.0) 10 12.10 ?
Feature Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
Version -76 support Obsolete} ? ? ? ? ?
Protocol version 7 support ? ? ? ? ?
Protocol version 8 support (IETF draft 10) ? 7.0 (7.0) ? ? ?
RFC 6455 Support (IETF Draft 17) 16 11.0 (11.0) ? 12.10 ?

其實我們還有一個選擇就是通過使用Flash的XMLSocket來實現實時的數據交互。但是問題很明顯:客戶端必須安裝flash;XMLSocket沒有HTTP隧道功能,無法自動穿過防火牆;因爲是使用套接口,需要設置一個通信端口,防火牆、代理服務器也可能對非 HTTP 通道端口進行限制。

Java Applet也是一個實現方式,但是有諸多的限制,有興趣的同學可以自行了解一下。

我們先從傳統的實現方式說起。

Comet推送

“服務器推送”是一種很早就存在的技術,在非瀏覽器平臺上,通常都是通過客戶端的套接字接口或者是服務端的遠程調用實現。由於瀏覽器本身帶來的限制,並沒有一個非常完善的方案去實現並應用到產品中。以前,可以通過AJAX,或者Iframe嵌入文檔的ActiveX組件中解決IE上的加載問題。後來Alex Russell(Dojo Toolkit 的項目 Lead)將基於HTTP長連接,無需在安裝插件的服務端推送技術稱爲“Comet”。

Comet實現了一種從服務端異步推送數據到客戶端的機制,雖然它是基於HTTP長連接的一種實現,但是HTTP規範本身並不提倡這種工作模式。下面介紹兩種Comet的具體實踐。

長輪詢(long-polling)

長輪詢,從字面的意思,我們就可以揣測到具體的實現方式:

客戶端按照一個固定的時間定期向服務器發送請求,通常這個時間間隔的長度受到服務端的更新頻率和客戶端處理更新數據時間的影響。

服務端的處理方式就是:如果有更新數據,則返回更新數據,如果沒有更新數據,則阻塞請求。當客戶端處理接收的數據、重新建立連接時,服務器端可能有新的數據到達;這些信息會被服務器端保存直到客戶端重新建立連接,客戶端會一次把當前服務器端所有的信息取回。

2x0kZ 深入瞭解HTTP和Socket在實時性Web上的實踐

基於Iframe的方式

iframe可以在頁面中嵌套一個子頁面,通過將iframe的src指向一個長連接的請求地址,服務端就能不斷往客戶端傳輸數據。具體的實現方式就是iframe服務端不直接輸出數據到頁面,而是通過腳本執行的方式,比如輸出

“<script type=”text/javascript”>js_func(“data from server ”)</script>”

這樣形式的代碼可以直接被瀏覽器執行,從而把返回的數據傳遞給客戶端。

Iframe解決方案的問題在於,IE和FF下的進度欄都會顯示加載沒有完成,並且IE上方的圖標會不停轉動。這個問題可以通過使用 new ActiveXObject(“htmlfile”) 來解決,有興趣的同學可以自行了解下。

S6Dlq 深入瞭解HTTP和Socket在實時性Web上的實踐

長連接只會在通信出現錯誤的時候關閉連接(一些防火牆經常會丟棄過長的連接),所以我們還需要有一個超時和出錯重連的機制。

流(streaming)AJAX解決方案

streaming ajax是一種通過ajax實現的長連接維持機制。可以理解爲是長輪詢的一個分支,緣由是因爲Firefox提供了對Streaming AJAX的支持,主要目的就是在數據傳輸過程中對返回的數據進行讀取,並且不會關閉連接。

具體處理方式可能是

<span class="keyword" style="font-weight:bold">var</span> dv = document.getElementById(<span class="string" style="color:#dd1144;">'dvRst'</span>), 
  xhr = <span class="keyword" style="font-weight:bold">new</span> XMLHttpRequest();
xhr.onreadystatechange = <span class="function"><span class="keyword" style="font-weight:bold">function</span> <span class="params">()</span> {</span>
  dvRst.innerHTML += <span class="string" style="color:#dd1144;">"AJAX.readyState:"</span> + xhr.readyState + <span class="string" style="color:#dd1144;">"<br/>"</span>;
  <span class="keyword" style="font-weight:bold">if</span> (xhr.readyState == <span class="number" style="color:#09999;">3</span>) {
    dvRst.innerHTML += xhr.responseText + <span class="string" style="color:#dd1144;">'<br/>'</span>; 
  }
  <span class="keyword" style="font-weight:bold">if</span> (xhr.readyState == <span class="number" style="color:#09999;">4</span>) {
    clearInterval(timer);
  }
}
xhr.open(<span class="string" style="color:#dd1144;">"get"</span>, <span class="string" style="color:#dd1144;">"StreamingAJAX.ashx"</span>, <span class="literal">true</span>);
xhr.send(<span class="literal">null</span>);

Comet的一些注意問題

  • 由於在HTTP 1.1規範中規定,客戶端不應該與服務器建立超過兩個的HTTP連接,這也是爲什麼IE6/7在最多隻能並行連接兩個HTTP 1.1的同域請求。而Iframe的長連接會一直保持,導致佔用了兩個連接中的一個,那麼如果有兩個及以上的iframe保持長連接,就會導致其他同域請求的阻塞,可以考慮讓多個iframe公用一個長連接。
  • Web服務器會爲每一個連接創建一個線程,這對服務端來說需要維護大量併發的長連接。HTTP 1.1與1.0規範也有一個很大的不同:1.0規範在服務器處理完每個GET/POST請求後會關閉套接字接口連接;而1.1規範下服務器會保持這個連接,在處理兩個請求的間隔時間裏,這個連接會處於空閒狀態。一些服務端在處理空閒連接的時候會把連接分配到的線程資源返回給線程池,如果頻繁地進行請求,而且Comet機制下連接的事件會比較長,會導致服務端的效率降低,甚至阻塞新的連接處理。
  • 使用長連接的時候,存在一個問題,客戶端的網頁已經準備關閉了,但是服務端還處於阻塞狀態,客戶端需要通知服務端關閉數據連接。所以通常我們在使用iframe進行長連接時,還需要有一個額外的請求來告訴服務端客戶端的狀態,及時釋放客戶端的資源。
  • 服務端需要做一些異常狀況湖綜合超時處理,及時釋放被客戶端佔用的資源。
  • 客戶端需要有一個合理的機制來處理服務端返回的數據,包括輪詢時間等等。頻繁地刷新頁面在用戶體驗也是一個巨大的考驗,在保證數據實時性的同時,我們還要能保證頁面的性能。

基於Socket的實現

好吧,上面贅述了很多關於HTTP上的web實時機制的實現。接下來應該說說重點了,就是Socket的實現方式,以及優勢。

在網絡上查找socket相關資料的時候,經常會把socket和電話插座拿來對比,不僅僅是因爲socket的英文原義是“插座”,更重要的是,電話機發送信號和對方從電話機接收信號的過程,相當於socket發送和接收數據的過程。對於通話雙方來說,通信設施的細節不重要,重要的是兩端都有電話機,也就是都支持socket這樣的連接方式。

webSocket機制

websocket是一個基於TCP連接的全雙工通信方式,服務端和客戶端可以相互推送數據。

我們可以看看websocket是如何保持客戶端和服務端通信的。

這裏需要注意一點,websocket在連接的時候有一個握手階段,但是這和TCP的三次握手又是不一樣的。TCP的三次握手是爲了保證連接可靠,當TCP三次握手成功的時候,websocket的握手階段才真正開始。TCP三次握手傳送的是TCP報文,而websocket的握手傳送的是HTTP報文,這個是不太一樣的地方。

握手開始的時候,我們需要現發送一個HTTP 1.1的請求頭部:(下面的例子來自websocket的 TFC6455 文檔)

<span class="request" style="font-weight:bold">GET <span class="string" style="color:#dd1144;">/chat</span> HTTP/1.1</span>
<span class="attribute" style="color:#08080;">Host</span>: <span class="string" style="color:#dd1144;">server.example.com</span>
<span class="attribute" style="color:#08080;">Upgrade</span>: <span class="string" style="color:#dd1144;">websocket</span>
<span class="attribute" style="color:#08080;">Connection</span>: <span class="string" style="color:#dd1144;">Upgrade</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Key</span>: <span class="string" style="color:#dd1144;">dGhlIHNhbXBsZSBub25jZQ==</span>
<span class="attribute" style="color:#08080;">Origin</span>: <span class="string" style="color:#dd1144;">http://example.com</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Protocol</span>: <span class="string" style="color:#dd1144;">chat, superchat</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Version</span>: <span class="string" style="color:#dd1144;">13</span>

服務端返回的成功握手請求頭部如下:

<span class="status" style="font-weight:bold">HTTP/1.1 <span class="number" style="color:#09999;">101</span> Switching Protocols</span>
<span class="attribute" style="color:#08080;">Upgrade</span>: <span class="string" style="color:#dd1144;">websocket</span>
<span class="attribute" style="color:#08080;">Connection</span>: <span class="string" style="color:#dd1144;">Upgrade</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Accept</span>: <span class="string" style="color:#dd1144;">s3pPLMBiTxaQ9kYGzzhZRbK+xOo=</span>
<span class="attribute" style="color:#08080;">Sec-WebSocket-Protocol</span>: <span class="string" style="color:#dd1144;">chat</span>

Upgrade:WebSocket表示這是一個特殊的 HTTP 請求,請求的目的就是要將客戶端和服務器端的通訊協議從 HTTP 協議升級到 WebSocket 協議。

一旦連接成功後,就可以在全雙工的模式下在客戶端和服務端之間來回傳送WebSocket消息。這就意味着,在同一時間、任何方向,都可以雙向發送基於文本的消息。每個消息已0×00字節開頭,以0xff結尾(這樣就可以解決TCP協議中的黏包問題,在TCP協議中,會存在兩個緩衝區來存放發送的數據或者接收的數據,如果沒有明顯的分隔符,服務端無法正確識別命令),中間數據的編碼是UTF-8。

P5Ff9 深入瞭解HTTP和Socket在實時性Web上的實踐

關於如何使用WebSocket也不贅述,主要還是說說WebSocket帶來了什麼。

WebSocket的優勢

傳輸速度收到的影響很多,我們可以從多個角度對HTTP和WebSocket進行比較。

從純粹的字節數角度考慮

HTTP:每一次數據傳輸都需要有一個HTTP頭部,頭部的大小不一,可能只有幾百B,也可能有幾千B。

Xin3J 深入瞭解HTTP和Socket在實時性Web上的實踐

WebSocket只有在進行連接的時候需要發送一個HTTP請求,之後就再也不需要發送紛繁的HTTP頭部信息,光從字節數上就減少了很多。而在關閉WebSocket的過程中,也不需要像建立握手的時候那麼繁雜,只需要傳送一個特定的字節碼0×8的關閉幀就行,服務端收到之後,需要響應一個關閉幀到客戶端。

從請求數的角度考慮

正常情況下,如果我們要請求多個數據,就多發多次HTTP請求,整個過程包括建立連接,關閉連接,特別是建立連接的時間在整個傳輸時間中還佔據了比較大的比重。HTTP長連接的劣勢也在上面有描述過。

s1jMq 深入瞭解HTTP和Socket在實時性Web上的實踐

WebSocket可以一直保持連接,通過Socket通道傳輸數據,節省掉了建立連接需要耗費的時間。

從服務器併發數的角度考慮

服務端要同時維持大量連接處於打開狀態,就需要能以低性能開銷接收高併發數據的架構。此類架構通常是圍繞線程或所謂的非阻塞 IO 而設計的。這就與傳統服務器圍繞 HTTP 請求/響應循環的設計不同。這個時候,我們就會想到nodejs,使用事件機制和異步IO對請求進行處理,提高了服務器的併發能力,並且減少了線程切換帶來的開銷。

Java曾引入一個新的I/O API,其被稱爲非阻塞式的I/O。這一API使用一個選擇器來避免每次有新的HTTP連接在服務器端建立時都要綁定一個線程的做法,當有數據到來時,就會有一個事件被接收,接着某個線程就被分配來處理該請求。因此,這種做法被稱爲每個請求一個線程(thread-per-request)模式。其允許web服務器,比如說WebSphere和Jetty等,使用固定數量的線程來容納並處理越來越多的用戶連接。在相同硬件配置的情況下,在這一模式下運行的web服務器的伸縮性要比運行在每個連接一個線程(thread-per-connection)模型下的好得多。

每個連接一個線程模式通常會有一個更好的響應時間,因爲所有的線程都已啓動、準備好且是等待中,但在連接的數目過高時,其會停止提供服務。在每個請求一個線程模式中,線程被用來爲到達的請求提供服務,連接則是通過一個NIO選擇器來處理。響應時間可能會較慢一些,但線程會回收再用,因此該方案在大容量連接方面有着更好的伸縮性。

而WebSocket對於服務端的優勢就在於Socket減少了數據傳輸和處理的成本,使得這些異步的IO機制能夠充分地揚長避短。

換句話說,WebSocket帶來的併發能力提升,不僅僅因爲傳輸機制本身,服務端一樣需要做調整來適應新的機制,這樣才能充分發揮WebSocket的優勢。

Socket.io

Socket.IO是一個JavaScript端的框架, 提供了一個簡單類似WebSocket的API,實現異步接收和發送服務端數據。Socket.io支持WebSocket,Flash Sockets,長輪詢,流,持久幀(iframe)和JSONP輪詢。具體使用哪個方案取決與瀏覽器的兼容性,儘可能使用最優方案解決。

具體可以上 Socket.io官網 看詳細介紹。

發佈了11 篇原創文章 · 獲贊 30 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章