摘要: 即時Web越來越被重視,Google、Facebook等大公司也逐漸開始提供即時性服務。即時Web將是未來最熱門的話題之一。本文選自《基於MVC的JavaScript Web富應用開發》。
為什麼即時Web這麼重要?我們生活在一個即時(real-time)的世界中,因此Web的最終最自然的狀態也應當是即時的。使用者需要即時的溝通、資料和搜尋。我們對互連網資訊即時性的要求也越來越高,如果資訊或訊息延時幾分鐘後才更新,簡直讓人無法忍受。現在很多大公司(如Google、Facebook和Twitter)已經開始關注即時Web,並提供了即時性服務。即時Web將是未來最熱門的話題之一。
即時Web的發展曆史
傳統的Web是基於HTTP的請求/響應模型的:用戶端請求一個新頁面,伺服器將內容發送到用戶端,用戶端再請求另外一個頁面時又要重新發送請求。後來有人提出了AJAX,AJAX使得頁面的體驗更加“動態”,可以在後台發起到伺服器的請求。但是,如果伺服器有更多資料需要推送到用戶端,在頁面載入完成後是無法實現直接將資料從伺服器發送給用戶端的。即時資料無法被“推送”給用戶端。
為瞭解決這個問題,有人提出了很多解決方案。最簡單(暴力)的方案是用輪詢:每隔一段時間都會向伺服器請求新資料。這讓使用者感覺應用是即時的。實際上這會造成延時和效能問題,因為伺服器每秒都要處理大量的串連請求,每次請求都會有TCP三向交握並附帶HTTP的頭資訊。儘管現在很多應用仍在使用輪詢,但這並不是最理想的解決方案。
後來隨著Comet技術的提出,又出現了很多更進階的解決方案。這些技術方案包括永久幀(forever frame)、XHR流(xhr-multipart)、htmlfile,以及長輪詢。長輪詢是指,用戶端發起一個到伺服器的XHR串連,這個串連永不關閉,對用戶端來說串連始終是掛起狀態。當伺服器有新資料時,就會及時地將響應發送給用戶端,接著再將串連關閉。然後重複整個過程,通過這種方式就實現了“伺服器推”(server push)。
Comet技術是非標準的hack技術,正因為此,瀏覽器端的相容性就成了問題。首先,效能問題無法解決,向伺服器發起的每個串連都帶有完整的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的:
將KB的資料降為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也非常簡單、直接:
varsupported=("WebSocket"inwindow);if(supported)alert("WebSocketsaresupported");
長遠來看,瀏覽器的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("Newdata-",msg);};socket.onopen=function(){ socket.send("Why,hellothere").};發送和接收的訊息只支援字串格式。但在字串和JSON資料之間可以很輕鬆地相互轉換,這樣就可以建立你自己的協議:varrpc={ test:function(arg1,arg2){/*...*/}};socket.onmessage=function(data){ //解析JSON varmsg=JSON.parse(data); //調用RPC函數 rpc[msg.method].apply(rpc,msg.args);};
這段代碼中,我們建立了一個遠端程序呼叫(remoteprocedurecall,RPC)指令碼,伺服器可以發送一些簡單的JSON來調用用戶端的函數,就像下面這行代碼:
{"method":"test","args":[1,2]}
注意,這裡的調用是限制在rpc對象裡的。這樣做的原因主要是出於安全考慮,如果允許在用戶端執行任意JavaScript代碼,駭客就會利用這個漏洞。可以調用close()函數來關閉這個串連:
varsocket=newWebSocket("ws://localhost:8000/server");
你肯定注意到了我們在執行個體化一個WebSocket的時候使用了WebSocket特有的協議首碼ws://,而不是http://。WebSocket同樣支援加密的串連,這需要使用以wss://為協議首碼的TLS。預設情況下WebSocket使用80連接埠建立非加密的串連,使用443連接埠建立加密的串連。你可以通過給URL帶上自訂連接埠來覆蓋預設配置。要記住,並不是所有的連接埠都可以被用戶端使用,一些非常規的連接埠很容易被防火牆攔截。
說到現在,你或許會想,“我還不能在項目中使用WebSocket,因為標準還未成型,而且IE不支援WebSocket”。這樣的想法並沒有錯,幸運的是,我們有解決方案。Web-socket-js是一個基於AdobeFlash實現的WebSocket。用這個庫就可以在不支援WebSocket的瀏覽器中做優雅降級。畢竟幾乎所有的瀏覽器都安裝了Flash外掛程式。基於Flash實現的SocketAPI和HTML5標準規範完全一樣,因此當WebSocket的瀏覽器安全色性更好的時候,只需簡單地將庫移除即可,而不必對代碼做任何修改。
儘管用戶端的API非常簡潔、直接,但在伺服器端情況就不同了。WebSocket協議包含兩個互不相容的草案協議:草案75和草案76。伺服器需要通過檢測用戶端使用的串連握手類型來判斷使用哪個草案協議。
WebSocket首先向伺服器發起一個HTTP“升級”(upgrade)請求。如果你的伺服器支援WebSocket,則會執行WebSocket握手並初始化一個串連。“升級”請求中包含了原始域(請求所發出的網域名稱)的資訊。用戶端可以和任意網域名稱建立WebSocket串連,只有伺服器才會決定哪些用戶端可以和它建立串連,常用做法是將允許串連的網域名稱做成白名單。
在WebSocket的設計之初,設計者們希望只要初始串連使用了常用的連接埠和HTTP頭欄位,就可以和防火牆和代理軟體和諧相處。然而理想是豐滿的,現實是骨感的。有些代理軟體對WebSocket的“升級”請求的頭資訊做了修改,打破了協議規則。事實上,協議草案的最近一次更新(版本76)也無意中打破了對反向 Proxy和網關的相容性。為了更好更成功地使用WebSocket,這裡給出一些步驟:
- 使用安全的WebSocket串連(wss)。代理軟體不會對加密的串連胡亂篡改,此外你所發送的資料都是加密後的,不容易被他人竊取。
- 在WebSocket伺服器前面使用TCP負載平衡器,而不要使用HTTP負載平衡器,除非某個HTTP負載平衡器大肆宣揚自己支援WebSocket。
- 不要假設瀏覽器支援WebSocket,雖然瀏覽器支援WebSocket只是時間問題。誠然,如果串連無法快速建立,則迅速優雅降級使用Comet和輪詢的方式來處理。
那麼,如何選擇伺服器端的解決方案呢?幸運的是,在很多語言中都實現了對WebSocket的支援,比如Ruby、Python和Java。要再次確認每個實現是否支援最新的76版協議草案,因為這個協議是被大多數用戶端所支援的。
─node-Websocket-server(http://github.com/miksago/node-websocket-server)
─Socket.IO(http://socket.io)
─EventMachine(http://github.com/igrigorik/em-websocket)
─Cramp(https://github.com/lifo/cramp)
─Sunshowers(http://rainbows.rubyforge.org/sunshowers/)
─Twisted(http://github.com/rlotun/txWebSocket)
─Apachemodule(http://code.google.com/p/pywebsocket)
─php-Websocket(http://github.com/nicokaiser/php-websocket)
─Jetty(http://www.eclipse.org/jetty)
─native(http://code.google.com/p/go)
本文選自《基於MVC的JavaScript Web富應用開發》,點此連結可在博文視點官網查看。
想及時獲得更多精彩文章,可在中搜尋“博文視點”或者掃描下方二維碼並關注。