實時Web與WebSocket實踐

引言:實時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的:

將千字節的數據降爲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)也無意中打破了對反向代理和網關的兼容性。爲了更好更成功地使用WebSocket,這裏給出一些步驟:

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

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

  • Node.js

─node-Websocket-server(http://github.com/miksago/node-websocket-server) 
─Socket.IO(http://socket.io

  • Ruby

─EventMachine(http://github.com/igrigorik/em-websocket) 
─Cramp(https://github.com/lifo/cramp) 
─Sunshowers(http://rainbows.rubyforge.org/sunshowers/

  • Python

─Twisted(http://github.com/rlotun/txWebSocket) 
─Apachemodule(http://code.google.com/p/pywebsocket

  • PHP

─php-Websocket(http://github.com/nicokaiser/php-websocket

  • Java

─Jetty(http://www.eclipse.org/jetty

  • GoogleGo

─native(http://code.google.com/p/go

  本文選自《基於MVC的JavaScript Web富應用開發》,點此鏈接可在博文視點官網查看。 
                     圖片描述

  想及時獲得更多精彩文章,可在微信中搜索“博文視點”或者掃描下方二維碼並關注。
                        圖片描述

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