使用 HTML5 WebSocket 構建實時 Web 應用
本文主要介紹了 HTML5 WebSocket 的原理以及它給實時 Web 開發帶來的革命性的創新,并通過一個 WebSocket 服務器和客戶端的案例來充分展示 WebSocket 的強大和易用。
作為下一代的 Web 標準,HTML5 擁有許多引人注目的新特性,如 Canvas、本地存儲、多媒體編程接口、WebSocket 等等。這其中有“Web 的 TCP ”之稱的 WebSocket 格外吸引開發人員的注意。WebSocket 的出現使得瀏覽器提供對 Socket 的支持成為可能,從而在瀏覽器和服務器之間提供了一個基于 TCP 連接的雙向通道。Web 開發人員可以非常方便地使用 WebSocket 構建實時 web 應用,開發人員的手中從此又多了一柄神兵利器。本文首先介紹 HTML5 WebSocket 的基本概念以及這個規范試圖解決的問題,然后介紹 WebSocket 的基本原理和編程接口。接下來會通過一個簡單案例來示范怎樣實現一個 WebSocket 應用,并且展示 WebSocket 如何在功能強大和編程簡單易用上達到的完美統一。最后介紹了目前主流瀏覽器對 WebSocket 支持的狀況、局限性以及未來的展望。
實時 Web 應用的窘境
Web 應用的信息交互過程通常是客戶端通過瀏覽器發出一個請求,服務器端接收和審核完請求后進行處理并返回結果給客戶端,然后客戶端瀏覽器將信息呈現出來,這種 機制對于信息變化不是特別頻繁的應用尚能相安無事,但是對于那些實時要求比較高的應用來說,比如說在線游戲、在線證券、設備監控、新聞在線播報、RSS 訂閱推送等等,當客戶端瀏覽器準備呈現這些信息的時候,這些信息在服務器端可能已經過時了。所以保持客戶端和服務器端的信息同步是實時 Web 應用的關鍵要素,對 Web 開發人員來說也是一個難題。在 WebSocket 規范出來之前,開發人員想實現這些實時的 Web 應用,不得不采用一些折衷的方案,其中最常用的就是輪詢 (Polling) 和 Comet 技術,而 Comet 技術實際上是輪詢技術的改進,又可細分為兩種實現方式,一種是長輪詢機制,一種稱為流技術。下面我們簡單介紹一下這幾種技術:
輪詢:
這 是最早的一種實現實時 Web 應用的方案。客戶端以一定的時間間隔向服務端發出請求,以頻繁請求的方式來保持客戶端和服務器端的同步。這種同步方案的最大問題是,當客戶端以固定頻率向 服務器發起請求的時候,服務器端的數據可能并沒有更新,這樣會帶來很多無謂的網絡傳輸,所以這是一種非常低效的實時方案。
長輪詢:
長 輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網絡傳輸。當服務器端沒有數據更新的時候,連接會保持一段時間周期直到數據或狀態改變或者時間過期, 通過這種機制來減少無效的客戶端和服務器間的交互。當然,如果服務端的數據變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的性能的提高。
流:
流 技術方案通常就是在客戶端的頁面使用一個隱藏的窗口向服務端發出一個長連接的請求。服務器端接到這個請求后作出回應并不斷更新連接狀態以保證客戶端和服務 器端的連接不過期。通過這種機制可以將服務器端的信息源源不斷地推向客戶端。這種機制在用戶體驗上有一點問題,需要針對不同的瀏覽器設計不同的方案來改進 用戶體驗,同時這種機制在并發比較大的情況下,對服務器端的資源是一個極大的考驗。
綜合這幾種方案,您會發現這些目前我們所使用的所謂的實 時技術并不是真正的實時技術,它們只是在用 Ajax 方式來模擬實時的效果,在每次客戶端和服務器端交互的時候都是一次 HTTP 的請求和應答的過程,而每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭信息,這就增加了每次傳輸的數據量,而且這些方案中客戶端和服務器端的編程實現都比較復雜,在實際的應用中,為了模擬比較真實的實時效果,開發人員往往 需要構造兩個 HTTP 連接來模擬客戶端和服務器之間的雙向通訊,一個連接用來處理客戶端到服務器端的數據傳輸,一個連接用來處理服務器端到客戶端的數據傳輸,這不可避免地增加 了編程實現的復雜度,也增加了服務器端的負載,制約了應用系統的擴展性。
WebSocket 的拯救
HTML5 WebSocket 設計出來的目的就是要取代輪詢和 Comet 技術,使客戶端瀏覽器具備像 C/S 架構下桌面系統的實時通訊能力。 瀏覽器通過 JavaScript 向服務器發出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務器端就可以通過 TCP 連接直接交換數據。因為 WebSocket 連接本質上就是一個 TCP 連接,所以在數據傳輸的穩定性和數據傳輸量的大小方面,和輪詢以及 Comet 技術比較,具有很大的性能優勢。Websocket.org 網站對傳統的輪詢方式和 WebSocket 調用方式作了一個詳細的測試和比較,將一個簡單的 Web 應用分別用輪詢方式和 WebSocket 方式來實現,在這里引用一下他們的測試結果圖:
圖 1. 輪詢和 WebSocket 實現方式的網絡負載對比圖
通過這張圖可以清楚的看出,在流量和負載增大的情況下,WebSocket 方案相比傳統的 Ajax 輪詢方案有極大的性能優勢。這也是為什么我們認為 WebSocket 是未來實時 Web 應用的首選方案的原因。
WebSocket 規范
WebSocket 協議本質上是一個基于 TCP 的協議。為了建立一個 WebSocket 連接,客戶端瀏覽器首先要向服務器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息,其中附加頭信息”Upgrade: WebSocket”表 明這是一個申請協議升級的 HTTP 請求,服務器端解析這些附加的頭信息然后產生應答信息返回給客戶端,客戶端和服務器端的 WebSocket 連接就建立起來了,雙方就可以通過這個連接通道自由的傳遞信息,并且這個連接會持續存在直到客戶端或者服務器端的某一方主動的關閉連接。
下面我們來詳細介紹一下 WebSocket 規范,由于這個規范目前還是處于草案階段,版本的變化比較快,我們選擇 draft-hixie-thewebsocketprotocol-76版本來描述 WebSocket 協議。因為這個版本目前在一些主流的瀏覽器上比如 Chrome,、FireFox、Opera 上都得到比較好的支持,您如果參照的是新一些的版本話,內容可能會略有差別。
一個典型的 WebSocket 發起請求和得到響應的例子看起來如下:
清單 1. WebSocket 握手協議
客戶端到服務端: GET /demo HTTP/1.1 Host: example.com Connection: Upgrade Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 Upgrade: WebSocket Sec-WebSocket-Key1: 4@1 46546xW%0l 1 5 Origin: http://example.com [8-byte security key]服務端到客戶端:HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade WebSocket-Origin: http://example.com WebSocket-Location: ws://example.com/demo [16-byte hash response]</pre>
這些請求和通常的 HTTP 請求很相似,但是其中有些內容是和 WebSocket 協議密切相關的。我們需要簡單介紹一下這些請求和應答信息,”Upgrade:WebSocket”表示這是一個特殊的 HTTP 請求,請求的目的就是要將客戶端和服務器端的通訊協議從 HTTP 協議升級到 WebSocket 協議。從客戶端到服務器端請求的信息里包含有”Sec-WebSocket-Key1”、“Sec-WebSocket-Key2”和”[8-byte securitykey]”這樣的頭信息。這是客戶端瀏覽器需要向服務器端提供的握手信息,服務器端解析這些頭信息,并在握手的過程中依據這些信息生成一 個 16 位的安全密鑰并返回給客戶端,以表明服務器端獲取了客戶端的請求,同意創建 WebSocket 連接。一旦連接建立,客戶端和服務器端就可以通過這個通道雙向傳輸數據了。
在實際的開發過程中,為了使用 WebSocket 接口構建 Web 應用,我們首先需要構建一個實現了 WebSocket 規范的服務器,服務器端的實現不受平臺和開發語言的限制,只需要遵從 WebSocket 規范即可,目前已經出現了一些比較成熟的 WebSocket 服務器端實現,比如:
Kaazing WebSocket Gateway — 一個 Java 實現的 WebSocket Server
mod_pywebsocket — 一個 Python 實現的 WebSocket Server
Netty —一個 Java 實現的網絡框架其中包括了對 WebSocket 的支持
node.js —一個 Server 端的 JavaScript 框架提供了對 WebSocket 的支持
如果以上的 WebSocket 服務端實現還不能滿足您的業務需求的話,開發人員完全可以根據 WebSocket 規范自己實現一個服務器。在“WebSocket 實戰”這一節,我們將使用 Microsoft .NET 平臺上的 C# 語言來打造一個簡單的 WebSocket 服務器,繼而構建一個簡單的實時聊天系統。
WebSocket JavaScript 接口
上 一節介紹了 WebSocket 規范,其中主要介紹了 WebSocket 的握手協議。握手協議通常是我們在構建 WebSocket 服務器端的實現和提供瀏覽器的 WebSocket 支持時需要考慮的問題,而針對 Web 開發人員的 WebSocket JavaScript 客戶端接口是非常簡單的,以下是 WebSocket JavaScript 接口的定義:
清單 2. WebSocket JavaScript 定義
[Constructor(in DOMString url, in optional DOMString protocol)] interface WebSocket { readonly attribute DOMString URL; // ready state const unsigned short CONNECTING = 0; const unsigned short OPEN = 1; const unsigned short CLOSED = 2; readonly attribute unsigned short readyState; readonly attribute unsigned long bufferedAmount; //networking attribute Function onopen; attribute Function onmessage; attribute Function onclose; boolean send(in DOMString data); void close(); }; WebSocket implements EventTarget;
其中 URL 屬性代表 WebSocket 服務器的網絡地址,協議通常是”ws”,send 方法就是發送數據到服務器端,close 方法就是關閉連接。除了這些方法,還有一些很重要的事件:onopen,onmessage,onerror 以及 onclose。我們借用 Nettuts 網站上的一張圖來形象的展示一下 WebSocket 接口:
圖 2. WebSocket JavaScript 接口
下面是一段簡單的 JavaScript 代碼展示了怎樣建立 WebSocket 連接和獲取數據:
清單 3. 建立 WebSocket 連接的實例 JavaScript 代碼
var wsServer = 'ws://localhost:8888/Demo'; var websocket = new WebSocket(wsServer); websocket.onopen = function (evt) { onOpen(evt) }; websocket.onclose = function (evt) { onClose(evt) }; websocket.onmessage = function (evt) { onMessage(evt) }; websocket.onerror = function (evt) { onError(evt) }; function onOpen(evt) { console.log("Connected to WebSocket server."); } function onClose(evt) { console.log("Disconnected"); } function onMessage(evt) { console.log('Retrieved data from server: ' + evt.data); } function onError(evt) { console.log('Error occured: ' + evt.data); }
瀏覽器支持
下面是主流瀏覽器對 HTML5 WebSocket 的支持情況:
瀏覽器 | 支持情況 |
---|---|
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported in version 5+ |
WebSocket 實戰
這 一節里我們用一個案例來演示怎么使用 WebSocket 構建一個實時的 Web 應用。這是一個簡單的實時多人聊天系統,包括客戶端和服務端的實現。客戶端通過瀏覽器向聊天服務器發起請求,服務器端解析客戶端發出的握手請求并產生應答 信息返回給客戶端,從而在客戶端和服務器之間建立連接通道。服務器支持廣播功能,每個聊天用戶發送的信息會實時的發送給所有的用戶,當用戶退出聊天室時, 服務器端需要清理相應用戶的連接信息,避免資源的泄漏。以下我們分別從服務器端和客戶端來演示這個 Web 聊天系統的實現,在實現方式上我們采用了 C# 語言來實現 WebSocket 服務器,而客戶端是一個運行在瀏覽器里的 HTML 文件。
WebSocket 服務器端實現
這 個聊天服務器的實現和基于套接字的網絡應用程序非常類似,首先是服務器端要啟動一個套接字監聽來自客戶端的連接請求,關鍵的區別在于 WebSocket 服務器需要解析客戶端的 WebSocket 握手信息,并根據 WebSocket 規范的要求產生相應的應答信息。一旦 WebSocket 連接通道建立以后,客戶端和服務器端的交互就和普通的套接字網絡應用程序是一樣的了。所以在下面的關于 WebSocket 服務器端實現的描述中,我們主要闡述 WebSocket 服務器怎樣處理 WebSocket 握手信息,至于 WebSocket 監聽端口的建立,套接字信息流的讀取和寫入,都是一些常用的套接字編程的方式,我們就不多做解釋了,您可以自行參閱本文的附件源代碼文件。
在描述 WebSocket 規范時提到,一個典型的 WebSocket Upgrade 信息如下所示:
GET /demo HTTP/1.1 Host: example.com Connection: Upgrade Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 Upgrade: WebSocket Sec-WebSocket-Key1: 4@1 46546xW%0l 1 5 Origin: http://example.com [8-byte security key]
其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 這幾個頭信息是 WebSocket 服務器用來生成應答信息的來源,依據 draft-hixie-thewebsocketprotocol-76 草案的定義,WebSocket 服務器基于以下的算法來產生正確的應答信息:
逐個字符讀取 Sec-WebSocket-Key1 頭信息中的值,將數值型字符連接到一起放到一個臨時字符串里,同時統計所有空格的數量;
將在第 1 步里生成的數字字符串轉換成一個整型數字,然后除以第 1 步里統計出來的空格數量,將得到的浮點數轉換成整數型;
將第 2 步里生成的整型值轉換為符合網絡傳輸的網絡字節數組;
對 Sec-WebSocket-Key2 頭信息同樣進行第 1 到第 3 步的操作,得到另外一個網絡字節數組;
將 [8-byte security key] 和在第 3,第 4 步里生成的網絡字節數組合并成一個 16 字節的數組;
對第 5 步生成的字節數組使用 MD5 算法生成一個哈希值,這個哈希值就作為安全密鑰返回給客戶端,以表明服務器端獲取了客戶端的請求,同意創建 WebSocket 連接
至 此,客戶端和服務器的 WebSocket 握手就完成了,WebSocket 通道也建立起來了。下面首先介紹一下服務器端實現是如何根據用戶傳遞的握手信息來生成網絡字節數組的。.NET 平臺提供了很方便的對字符串,數值以及數組操作的函數,所以生成字節數組的方法還是非常簡單明了的,代碼如下:
清單 4. 生成網絡字節數組的代碼
private byte[] BuildServerPartialKey(string clientKey) { string partialServerKey = ""; byte[] currentKey; int spacesNum = 0; char[] keyChars = clientKey.ToCharArray(); foreach (char currentChar in keyChars) { if (char.IsDigit(currentChar)) partialServerKey += currentChar; if (char.IsWhiteSpace(currentChar)) spacesNum++; } try { currentKey = BitConverter.GetBytes((int)(Int64.Parse(partialServerKey) / spacesNum)); if (BitConverter.IsLittleEndian) Array.Reverse(currentKey); } catch { if (currentKey!= null) Array.Clear(currentKey, 0, currentKey.Length); } return currentKey; }
得到網絡字節數組以后,服務器端生成 16 位安全密鑰的方法如下所示:
清單 5. 生成 16 位安全密鑰的代碼
private byte[] BuildCompleteServerKey(byte[] serverKey1, byte[] serverKey2, byte[] last8Bytes) { byte[] concatenatedKeys = new byte[16]; Array.Copy(serverKey1, 0, concatenatedKeys, 0, 4); Array.Copy(serverKey2, 0, concatenatedKeys, 4, 4); Array.Copy(last8Bytes, 0, concatenatedKeys, 8, 8); System.Security.Cryptography.MD5 MD5Service = System.Security.Cryptography.MD5.Create(); return MD5Service.ComputeHash(concatenatedKeys); }
整個實現是非常簡單明了的,就是將生成的網絡字節數組和客戶端提交的頭信息里的 [8-byte security key] 合并成一個 16 位字節數組并用 MD5 算法加密,然后將生成的安全密鑰作為應答信息返回給客戶端,雙方的 WebSocekt 連接通道就建立起來了。實現了 WebSocket 握手信息的處理邏輯,一個具有基本功能的 WebSocket 服務器就完成了。整個 WebSocket 服務器由兩個核心類構成,一個是 WebSocketServer,另外一個是 SocketConnection,出于篇幅的考慮,我們不介紹每個類的屬性和方法了,文章的附件會給出詳細的源代碼,有興趣的讀者可以參考。
服務器剛啟動時的畫面如下:
圖 3. WebSocket 服務器剛啟動的畫面
客戶端可以依據這個信息填寫聊天服務器的連接地址,當有客戶端連接到聊天服務器上時,服務器會打印出客戶端和服務器的握手信息,每個客戶的聊天信息也會顯示在服務器的界面上,運行中的聊天服務器的界面如下:
圖 4. 有客戶端連接到 WebSocket 服務器的
以上我們簡單描述了實現一個 WebSocket 服務器的最基本的要素,下一節我們會描述客戶端的實現。
客戶端實現
客 戶端的實現相對于服務器端的實現來說要簡單得多了,我們只需要發揮想象去設計 HTML 用戶界面,然后呼叫 WebSocket JavaScript 接口來和 WebSocket 服務器端來交互就可以了。當然別忘了使用一個支持 HTML5 和 WebSocket 的瀏覽器,在筆者寫這篇文章的時候使用的瀏覽器是 Firefox。客戶端的頁面結構是非常簡潔的,初始運行界面如下:
圖 5. 聊天室客戶端初始頁面
當 頁面初次加載的時候,首先會檢測當前的瀏覽器是否支持 WebSocket 并給出相應的提示信息。用戶按下連接按鈕時,頁面會初始化一個到聊天服務器的 WebSocekt 連接,初始化成功以后,頁面會加載對應的 WebSocket 事件處理函數,客戶端 JavaScript 代碼如下所示:
清單 6. 初始化客戶端 WebSocket 對象的代碼
function ToggleConnectionClicked() { if (SocketCreated && (ws.readyState == 0 || ws.readyState == 1)) { ws.close(); } else { Log("準備連接到聊天服務器 ..."); try { ws = new WebSocket("ws://" + document.getElementById("Connection").value); SocketCreated = true; } catch (ex) { Log(ex, "ERROR"); return; } document.getElementById("ToggleConnection").innerHTML = "斷開"; ws.onopen = WSonOpen; ws.onmessage = WSonMessage; ws.onclose = WSonClose; ws.onerror = WSonError; } };function WSonOpen() { Log("連接已經建立。", "OK"); $("#SendDataContainer").show("slow"); };
function WSonMessage(event) { Log(event.data); };
function WSonClose() { Log("連接關閉。", "ERROR"); document.getElementById("ToggleConnection").innerHTML = "連接"; $("#SendDataContainer").hide("slow"); };
function WSonError() { Log("WebSocket錯誤。", "ERROR"); };</pre>
當用戶按下發送按鈕,客戶端會調用WebSocket對象向服務器發送信息,并且這個消息會廣播給所有的用戶,實現代碼如下所示:
function SendDataClicked() { if (document.getElementById("DataToSend").value != "") { ws.send(document.getElementById("txtName").value + "說 :\"" + document.getElementById("DataToSend").value + "\""); document.getElementById("DataToSend").value = ""; } };如果有多個用戶登錄到聊天服務器,客戶端頁面的運行效果如下所示:
圖 6. 聊天客戶端運行頁面
至 此我們已經完成了一個完整的 WebSocket 客戶端實現,用戶可以體驗一下這個聊天室的簡單和快捷,完全不用考慮頁面的刷新和繁瑣的 Ajax 調用,享受桌面程序的用戶體驗。WebSocket 的強大和易用可見一斑,您完全可以在這個基礎上加入更多的功能,設計更加漂亮的用戶界面,切身體驗 WebSocket 的震撼力。完整的客戶端代碼請參閱附件提供的源代碼。
WebSocket 的局限性
WebSocket 的優點已經列舉得很多了,但是作為一個正在演變中的 Web 規范,我們也要看到目前用 Websocket 構建應用程序的一些風險。首先,WebSocket 規范目前還處于草案階段,也就是它的規范和 API 還是有變動的可能,另外的一個風險就是微軟的 IE 作為占市場份額最大的瀏覽器,和其他的主流瀏覽器相比,對 HTML5 的支持是比較差的,這是我們在構建企業級的 Web 應用的時候必須要考慮的一個問題。
總結
本 文介紹了 HTML5 WebSocket 的橫空出世以及它嘗試解決的的問題,然后介紹了 WebSocket 規范和 WebSocket 接口,以及和傳統的實時技術相比在性能上的優勢,并且演示了怎樣使用 WebSocket 構建一個實時的 Web 應用,最后我們介紹了當前的主流瀏覽器對 HTML5 的支持情況和 WebSocket 的局限性。不過,我們應該看到,盡管 HTML5 WebSocket 目前還有一些局限性,但是已經是大勢所趨,微軟也明確表達了未來對 HTML5 的支持,而且這些支持我們可以在 Windows 8 和 IE10 里看到,我們也在各種移動設備,平板電腦上看到了 HTML5 和 WebSocket 的身影。WebSocket 將會成為未來開發實時 Web 應用的生力軍應該是毫無懸念的了,作為 Web 開發人員,關注 HTML5,關注 WebSocket 也應該提上日程了,否則我們在新一輪的軟件革新的浪潮中只能做壁上觀了。
原文地址:http://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/