實時Web的發展與實踐

MariTabor 7年前發布 | 35K 次閱讀 WebSocket WebSocket 開發

為什么實時Web這么重要?我們生活在一個實時(real-time)的世界中,因此Web的最終最自然的狀態也應當是實時的。用戶需要實時的溝通、數據和搜索。我們對互聯網信息實時性的要求也越來越高,如果信息或消息延時幾分鐘后才更新,簡直讓人無法忍受。現在很多大公司(如Google、非死book和推ter)已經開始關注實時Web,并提供了實時性服務。實時Web將是未來最熱門的話題之一。

一、實時Web的發展歷史

傳統的Web是基于HTTP的請求/響應模型的:客戶端請求一個新頁面,服務器將內容發送到客戶端,客戶端再請求另外一個頁面時又要重新發送請求。后來有人提出了AJAX,AJAX使得頁面的體驗更加“動態”,可以在后臺發起到服務器的請求。但是,如果服務器有更多數據需要推送到客戶端,在頁面加載完成后是無法實現直接將數據從服務器發送給客戶端的。實時數據無法被“推送”給客戶端。

為了解決這個問題,有人提出了很多解決方案。最簡單(暴力)的方案是用輪詢:每隔一段時間都會向服務器請求新數據。這讓用戶感覺應用是實時的。實際上這會造成延時和性能問題,因為服務器每秒都要處理大量的連接請求,每次請求都會有TCP三次握手并附帶HTTP的頭信息。盡管現在很多應用仍在使用輪詢,但這并不是最理想的解決方案。

后來隨著Comet技術的提出,又出現了很多更高級的解決方案。這些技術方案包括永久幀(forever frame)、XHR流(xhr-multipart)、htmlfile,以及長輪詢。長輪詢是指,客

戶端發起一個到服務器的XHR連接,這個連接永不關閉,對客戶端來說連接始終是掛起狀態。當服務器有新數據時,就會及時地將響應發送給客戶端,接著再將連接關閉。然后重復整個過程,通過這種方式就實現了“服務器推”(server push)。

Comet技術是非標準的hack技術,正因為此,瀏覽器端的兼容性就成了問題。首先,性

98 能問題無法解決,向服務器發起的每個連接都帶有完整的HTTP頭信息,如果你的應用需要很低的延時,這將是一個棘手的問題。當然不是說Comet本身有問題,因為還沒有其他替代方案前Comet是我們的唯一選擇。

瀏覽器插件(如Flash)和Java同樣被用于實現服務器推。它們可以基于TCP直接和服務器建立socket連接,這種連接非常適合將實時數據推給客戶端。問題是并不是所有的瀏覽器都安裝了這些插件,而且它們常常被防火墻攔截,特別是在公司網絡中。

現在HTML5規范為我們準備了一個替代方案。但這個規范稍微有些超前,很多瀏覽器都還不支持,特別是IE,對于現在很多開發者來說幫助不大,鑒于大部分瀏覽器還未實現HTML5的WebSocket,現行最好的辦法仍然是使用Comet。

二、WebSocket

WebSocket( http://dev.w3.org/html5/websockets )是HTML5規范( http://www.w3.org/TR/html5 )的一部分,提供了基于TCP的雙向的、全雙工的socket連接。這意味著服務器可以直接將數據推送給客戶端,而不需要開發者求助于長輪詢或插件來實現,這是一個很大的進步。盡管有一些瀏覽器實現了WebSocket,但由于一些安全問題沒有解決,因此協議( http://goo.gl/F7lvW )仍然在修訂之中。然而這不會阻礙我們的腳步,這些安全問題屬于技術性問題,會很快被修復,WebSocket很快就會成為最終規范。與此同時,對于那些不支持WebSocket的瀏覽器,可以降級使用笨方法來實現,比如Comet或輪詢。

和之前的服務器推的技術相比,WebSocket有著巨大的優勢,因為WebSocket是全雙工的,而不是基于HTTP的,一旦建立連接就不會斷掉。Comet所面對的現實問題就是HTTP的體積太大,每個請求都帶有完整的HTTP頭信息。而且包含很多沒有用的TCP握手,因為HTTP是比TCP更高層次的網絡協議。

使用WebSocket時,一旦服務器和客戶端之間完成握手,信息就可以暢通無阻地隨意往來于兩端,而不用附加那些無用的HTTP頭信息。這極大地降低了帶寬的占用,提高了性能。因為連接一直處于活動狀態,服務器一旦有新數據要更新時就可以立即發送給客戶端(不需要客戶端先請求,服務器再響應了)。另外,連接是雙工的,因此客戶端同樣可以發送數據給服務器,當然也不需要附帶多余的HTTP頭。

下面這段話出自Google的Ian Hickson,HTML5規范小組負責人,它是這樣描述WebSocket的:

將千字節的數據降為2字節……并將延時從150毫秒降為50毫秒,這種優化跨越了不止一個量級,實際上僅這兩點優化就足以讓Google確信WebSocket會給產品帶來非一般的用戶體驗。

現在我們來看一下都有哪些瀏覽器支持WebSocket:

  • Chrome >= 4
  • Safari >= 5
  • iOS >= 4.2
  • Firefox >= 4*
  • Opera >= 11*

盡管Firefox和Opera也都實現了WebSocket,但考慮到WebSocket仍然存在安全隱患,默認并沒有啟用它。但這不是什么大問題,或許本書出版時WebSocket的安全問題就已經解決了。同時你也可以在那些對WebSocket支持不好的瀏覽器中進行降級處理,使用諸如Comet和Flash的笨方法。

檢測瀏覽器是否支持WebSocket也非常簡單、直接:

var supported = ("WebSocket" in window);
if (supported) alert("WebSockets are supported");

長遠來看,瀏覽器的WebSocket API非常清晰且合乎邏輯。可以使用WebSocket類來實例化一個新的套接字(socket),這需要傳入服務器的端地址,在這個例子中是ws://example.com:

var socket = new WebSocket("ws://example.com");

然后我們需要給這個套接字添加事件監聽 :

// 建立連接
socket.onopen = function(){ /* ... */ }

// 通過連接發送了一些新數據
socket.onmessage = function(data){ /* ... */ }

// 關閉連接
socket.onclose = function(){ /* ... */ }

當服務器發送一些數據時,就會觸發onmessage事件,同樣,客戶端也可以調用send()

函數將數據傳回服務器。很明顯,我們應當在連接建立且觸發了onopen事件之后調用它:

socket.onmessage = function(msg){
console.log("New data - ", msg);
};

socket.onopen = function(){
socket.send("Why, hello there").
};

發送和接收的消息只支持字符串格式。但在字符串和JSON數據之間可以很輕松地相互轉換,這樣就可以創建你自己的協議:

var rpc = {
test: function(arg1, arg2) { /* ... */ }
};

socket.onmessage = function(data){
// 解析 JSON
var msg = JSON.parse(data);

// 調用 RPC 函數
rpc[msg.method].apply(rpc, msg.args);
};

這段代碼中,我們創建了一個遠程過程調用(remote procedure call,RPC)腳本,服務器可以發送一些簡單的JSON來調用客戶端的函數,就像下面這行代碼:

{"method": "test", "args": [1, 2]}

注意,這里的調用是限制在rpc對象里的。這樣做的原因主要是出于安全考慮,如果允許在客戶端執行任意JavaScript代碼,黑客就會利用這個漏洞。可以調用close()函數來關閉這個連接:

var socket = new WebSocket("ws://localhost:8000/server");

你肯定注意到了我們在實例化一個WebSocket的時候使用了WebSocket特有的協議前綴ws://,而不是http://。WebSocket同樣支持加密的連接,這需要使用以wss://為協議前綴的TLS。默認情況下WebSocket使用80端口建立非加密的連接,使用443端口建立加密的連接。你可以通過給URL帶上自定義端口來覆蓋默認配置。要記住,并不是所有的端口都可以被客戶端使用,一些非常規的端口很容易被防火墻攔截。

說到現在,你或許會想,“我還不能在項目中使用WebSocket,因為標準還未成型,而且IE不支持WebSocket”。這樣的想法并沒有錯,幸運的是,我們有解決方案。Web-socket-js( https://github.com/gimite/web-socket-js )是一個基于AdobeFlash實現的WebSocket。用這個庫就可以在不支持WebSocket的瀏覽器中做優雅降級。畢竟幾乎所有的瀏覽器都安裝了Flash插件。基于Flash實現的SocketAPI和HTML5標準規范完全一樣,因此當WebSocket的瀏覽器兼容性更好的時候,只需簡單地將庫移除即可,而不必對代碼做任何修改。

盡管客戶端的API非常簡潔、直接,但在服務器端情況就不同了。WebSocket協議包含兩個互不兼容的草案協議:草案75( http://goo.gl/cgSjp )和草案76( http://goo.gl/2u78y )。服務器需要通過檢測客戶端使用的連接握手類型來判斷使用哪個草案協議。

WebSocket首先向服務器發起一個HTTP“升級”(upgrade)請求。如果你的服務器支持WebSocket,則會執行WebSocket握手并初始化一個連接。“升級”請求中包含了原始域(請求所發出的域名)的信息。客戶端可以和任意域名建立WebSocket連接,只有服務器才會決定哪些客戶端可以和它建立連接,常用做法是將允許連接的域名做成白名單。

在WebSocket的設計之初,設計者們希望只要初始連接使用了常用的端口和HTTP頭字段,就可以和防火墻和代理軟件和諧相處。然而理想是豐滿的,現實是骨感的。有些代理軟件對WebSocket的“升級”請求的頭信息做了修改,打破了協議規則。事實上,協議草案的最近一次更新(版本76)也無意中打破了對反向代理和網關的兼容性。為了更好更成功地使用WebSocket,這里給出一些步驟:

  • 使用安全的WebSocket連接(wss)。代理軟件不會對加密的連接胡亂篡改,此外你所發送的數據都是加密后的,不容易被他人竊取。
  • 在WebSocket服務器前面使用TCP負載均衡器,而不要使用HTTP負載均衡器,除非某個HTTP負載均衡器大肆宣揚自己支持WebSocket。
  • 不要假設瀏覽器支持WebSocket,雖然瀏覽器支持WebSocket只是時間問題。誠然,如果連接無法快速建立,則迅速優雅降級使用Comet和輪詢的方式來處理。

那么,如何選擇服務器端的解決方案呢?幸運的是,在很多語言中都實現了對WebSocket的支持,比如Ruby、Python和Java。要再次確認每個實現是否支持最新的76版協議草案,因為這個協議是被大多數客戶端所支持的。

三、Node.js和Socket.IO

在上面的名單中,Node.js( http://nodejs.org )是一名新成員,也是當下最受關注的新技術。Node.js是基于事件驅動的JavaScript服務器,采用了Google的V8引擎( http://code.google.com/p/v8 )。正因為此,Node.js速度非常快,也可以解決服務器高并發連接數的資源消耗問題,和WebSocket服務器一樣。

Socket.IO( http://socket.io/ )是一個Node.js庫,實現了WebSocket。最讓人感興趣的不止于此,來看一段官網上的宣傳文字:

Socket.IO的目標是在每個瀏覽器和移動設備中構建實時APP,這縮小了多種傳輸機制之間的差異。

如果環境支持WebSocket,那么Socket.IO就會嘗試使用WebSocket,若有必要也會降級使用其他的傳輸方式。這里列出了所支持的傳輸方式,非常全面,因此WebSocket.IO可以做到更好的瀏覽器兼容:

  • WebSocket
  • Adobe Flash Socket
  • ActiveX HTMLFile (IE)
  • 基于 multipart 編碼發送 XHR(XHR with multipart encoding)
  • 基于長輪詢的XHR
  • JSONP 輪詢(用于跨域的場景)

Socket.IO 的瀏覽器支持非常全面。“服務器推”的實現是眾所周知的難題,但Socket.IO團隊為你解決了這些煩惱,Socket.IO保證了它能兼容大多數瀏覽器,瀏覽器支持情況如下:

  • Safari >= 4
  • Chrome >= 5
  • IE >= 6
  • iOS
  • Firefox >= 3
  • Opera >= 10.61

盡管在服務器端實現的Socket.IO最初是基于Node.js的,現在也有用其他語言實現的版本了,比如Ruby(Rack)( http://github.com/markjeee/Socket.IQ-rack ),Python(Tornado)

https://github.com/MrJoes/tornadio ),Java( http://code.google.com/p/socketio-java )和

GoogleGo( http://github.com/madari/go-socket.io )。

來看一下它的API,寫法非常簡單、直接,客戶端的API和WebSocket的API看起來很像:

var socket = new io.Socket();

socket.on("connect", function(){
socket.send('hi!');
});

socket.on("message", function(data){
alert(data);
});

socket.on("disconnect", function(){});

在后臺Socket.IO會選擇使用最佳的傳輸方式。正如在readme文件中所描述的,“你可以使用Socket.IO在任何地方構建實時APP”。

如果你想尋求比Socket.IO更高級的解決方案,可以關注一下Juggernaut( http://github.com/maccman/juggernaut ),它就是基于Socket.IO實現的。Juggernaut包含一個信道接口(channelinterface):客戶端可以訂閱信道監聽,服務器端可以向信道發布消息,即所謂的訂閱/發布( http://en.wikipedia.org/wiki/PubSub )模式。這個庫可以針對不同的客戶端和實現環境作靈活擴展,比如基于TLS等。

如果你需要虛擬主機中的解決方案,可以參考Pusher( http://pusherapp.com/ )。Pusher可以讓你從繁雜的服務器管理事務中抽身出來,使你能將注意力集中在有意義的部分:Web應用的開發。客戶端的實現非常簡單,只需將JavaScript文件引入頁面中并訂閱信道監聽即可。當有消息發布的時候,僅僅是發送一個HTTP請求到RESTAPI( http://pusherapp.com/docs )。

四、實時架構

將數據從服務器推送給客戶端的理論看起來有點紙上談兵,如何將理論和JavaScript應用的開發實踐相結合呢?如果你的應用正確地劃分出了模型,那么應用實時架構將會非常簡單。接下來我們給出在應用中構建實時架構的每個步驟,這里大量用到了訂閱/發布模式。首先需要了解的是將更新通知到客戶端的整個過程。

實時架構是基于事件驅動的(event-driven)。事件往往是由用戶交互觸發的:用戶修改了數據記錄,事件就會傳播給系統,直到數據推送給已經建立連接的客戶端并更新數據。要想為你的應用構建實時架構,則需要考慮兩件事:

  • 哪個模型需要是實時的?
  • 當模型實例發生改變時,需要通知哪些用戶?

實際情況往往是當模型發生改變時,你希望給所有建立連接的客戶端發送通知。這種情況更多發生在網站首頁需要實時提供活動的數據源的場景中,比如,每個客戶端都能看到相同的信息。然而更多的應用場景是,要想針對不同的用戶群發送不同的數據源,你需要根據不同類型的數據源有針對性地給用戶推送更新。

我們來看一個聊天室的場景:

1.用戶在聊天室中發送了一個新消息。

2.客戶端向服務器發送一條AJAX請求,并創建一條Chat記錄。

3.在Chat模型上觸發了“保存”的回調,調用我們的方法來更新客戶端數據。

4.查找聊天室中所有和這個Chat記錄有關的用戶,我們需要給這些用戶發送更新通知。

5.用一條更新來描述發生了什么事情(創建Chat記錄),將這個更新推送給相關的用戶。

這個過程的細節和你選用的服務器環境有關,然而,如果你使用Rails,Holla( http://github.com/maccman/holla )是一個非常不錯的例子。當創建了Message記錄時,JuggernautObserver會更新相關的客戶端。

現在就引入了另外一個問題:如何向特定用戶發送通知?最佳方法是使用發布/訂閱模式:客戶端訂閱某個特定的信道,服務器向這個信道發布消息。每個用戶訂閱唯一的信道,信道包含一個ID,可能是用戶在數據庫中存放的ID。然后,服務器只需向這個唯一的信道發布消息即可,這樣就可以做到將通知發送給特定的用戶。

例如,某個用戶可以訂閱下面這個信道:

/observer/0765F0ED-96E6-476D-B82D-8EBDA33F4EC4

這里的隨機字符串是當前登錄用戶唯一的標識。要想將通知發送給這個特定用戶,服務器只需向同一個信道發布消息即可。

你可能很想知道發布/訂閱模式在信息傳輸過程(WebSocket或Comet)中是怎樣工作的。幸運的是,已經有很多可用的解決方案,比如Juggernaut和Pusher,之前都有提到過。發布/訂閱是最常見的抽象,處于WebSocket的最高層,不管你選用什么服務或庫,它們的API都非常相似。

一旦服務器將通知推送給客戶端,你將體會到MVC架構帶來的美感。讓我們回過頭來看剛才的聊天室的例子。發送給客戶端的通知格式看起來像這樣:

{
"klass":"Chat", "type": "create", "id": "3",
"record": {"body": "New chat"}
}

它包含一個被更改的模型、更新類型和其他相關屬性。使用它可以讓客戶端在本地創建新的Chat記錄。由于客戶端的模型已經綁定了UI,因此用戶界面會根據新的聊天記錄自動更新。

最讓人吃驚之處在于這個過程并不和特定的Chat模型相關,如果我們想創建另一個實時模型,只需添加另外一個服務器觀察者,確保服務器更新時客戶端會隨之更新即可。現在我們的后臺和客戶端模型綁定在一起。任何后臺模型的更改都會自動傳播給相關的客戶端,并更新UI。使用這種架構搭建的應用就是真正的實時應用。一個用戶和應用產生的任何交互即刻被廣播給其他的用戶。

五、感知速度

速度是UI設計最重要也是最易忽略的問題,速度對用戶體驗(UX)的影響非常大,并直接影響網站的收益。很多大公司一直都在研究、調查速度和網站收益之間的關系:

  • Amazon

    頁面加載時間每增加100毫秒,就會造成1%的銷售損失(來源:GregLinden,Amazon)。

  • Google

    頁面加載時間每增加500毫秒,就會造成20%的流量損失(來源:Marrissa Mayer,Google)。

  • Yahoo!

    頁面加載時間每增加400毫秒,在頁面加載完成之前就單擊“后退”按鈕的人會增加5%~9%(來源:Nicole Sullivan, Yahoo!)。

“感知速度”(perceived speed)和真實的速度同等重要,因為感知速度關系到用戶的感官體驗。因此,關鍵是要讓用戶“感覺”到你的應用很快,盡管實際的速度可能并不快,而這正是JavaScript應用帶給我們的最大好處:盡管某一時刻在后臺會有很多請求不會及時響應,但UI不會被阻塞。

讓我們再次回過頭來討論剛才聊天室的場景。用戶發送了新的消息,觸發了一個AJAX請求。我們可以等待這個請求在網絡中走一個來回之后,將響應結果更新到聊天記錄中。然而,從發起請求的時刻開始,到獲得響應并更新至聊天記錄,會有幾秒鐘的延時。這會讓應用看起來很慢,肯定會造成用戶體驗上的損失。

既然如此,為什么不直接在本地創建一個新記錄呢?只需將消息立即添加至聊天記錄中即可。用戶會感知到這個消息被立即發送出去了,他們不知道(甚至不關心)這個消息是否被分發給了聊天室中的所有人。只有這種清澈、流暢的產品體驗,才會讓用戶倍感愉悅。

除了交互設計的小竅門之外,Web應用中最耗時的部分是新數據的加載。最明智的做法是在用戶請求數據之前預測用戶的行為并預加載數據,這一點非常重要。預加載的數據被緩存在內存中,如果隨后用戶需要這個數據,就不必再發起到服務器的請求了。應用在啟動伊始就應當預加載常用的數據。應用加載時的略微延時或許可忍,而加載完成后糟糕的交互體驗斷不可忍。

當用戶和你的應用產生交互時,你需要適時給用戶一些反饋,通常使用一些可視化的進

度指示來給出反饋。用行業術語來講就是“期望管理”(expectationmanagment)——要讓用戶知道當前項目的狀態和估計完成時間。“期望管理”同樣適用于用戶體驗領域,適時地給用戶一些反饋,告知用戶發生了什么事情,會讓用戶更有耐心等待程序的運行。當用戶等待新數據的加載時最好給出信息提示或一張旋轉的小圖片。如果在上傳文件,則給出上傳進度條及估計完成時間。這些都屬于感知速度的范疇,可有效地提升產品的用戶體驗。

書籍介紹

在琳瑯滿目的Web富客戶端應用實現方式中,JavaScript在其中巧妙地穿針引線,扮演著”黏合劑”的作用。JavaScript與各種瀏覽器插件技術(Silverlight、ActiveX、Flash、Applet)均擁有互操作能力,無論這種插件技術是主流的、還是生僻的,是傳統的、還是現代的。JavaScript是唯一不需安裝任何插件,便被各大主流Web瀏覽器支持的動態腳本,可謂擁有天然的跨平臺性。未來之RIA,必是以JavaScript為核心!

 

來自:http://www.infoq.com/cn/articles/JavaScript-Web

 

 本文由用戶 MariTabor 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!