應用 HTML5 的 WebSocket 實現 BiDirection 數據交換
HTML5 是新一代的 Web 標準。雖然 HTML5 標準還沒有最終確定但是它已然成為主流,各大廠商都開始提前實現其草案中的功能。HTML5 提供了很多新特征,比如 Canvas、離線存儲、多線程、視頻標簽等等。其中一個很重要的特征是 WebSocket,它提供了一種雙向全雙工的服務器和客戶端通信的功能。WebSocket 現對于以前的技術實現方案來說,有著本質的不同。它是原生的支持雙向通信的 B/S 應用協議,有著多種優勢。
WebSocket 的優勢:
- 它可以實現真正的實時數據通信。眾所周知,B/S 模式下應用的是 HTTP 協議,是無狀態的,所以不能保持持續的鏈接。數據交換是通過客戶端提交一個 Request 到服務器端,然后服務器端返回一個 Response 到客戶端來實現的。而 WebSocket 是通過 HTTP 協議的初始握手階段然后升級到 Web Socket 協議以支持實時數據通信。
- WebSocket 可以支持服務器主動向客戶端推送數據。一旦服務器和客戶端通過 WebSocket 建立起鏈接,服務器便可以主動的向客戶端推送數據,而不像普通的 web 傳輸方式需要先由客戶端發送 Request 才能返回數據,從而增強了服務器的能力。
- WebSocket 協議設計了更為輕量級的 Header,除了首次建立鏈接的時候需要發送頭部和普通 web 鏈接類似的數據之外,建立 WebSocket 鏈接后,相互溝通的 Header 就會異常的簡潔,大大減少了冗余的數據傳輸。
WebSocket 提供了更為強大的通信能力和更為簡潔的數據傳輸平臺,能更為方便的完成 Web 開發中的雙向通信功能。
一般情況下,通過瀏覽器訪問一個網頁,需要瀏覽器發送一個 HTTP Request,服務器接收到瀏覽器的請求,返回相應的消息。在一些數據更新比較頻繁的應用里,頁面的數據要想得到最新的結果需要重新刷新頁面,但這樣會 產生大量的冗余數據在服務器和客戶端傳輸,另外由于頁面是同步處理的,所以在頁面加載完畢之前是不能繼續操作的。這樣會阻塞用戶的動作,顯然不是一個好的 解決方案。
隨著技術發展,后來出現了新的技術方案,即 Ajax 技術。Ajax 全稱是 Asynchronous JavaScript and XML,即異步 JavaScript 和 XML。它的核心是 XMLHttpRequest 技術,通過 XMLHttpRequest 對象可以向服務器提交異步請求,服務器可以將數據以異步方式返回給客戶端,通過 Javascript 局部的更新頁面,不會阻塞當前用戶操作,這樣的解決方案相對于前面的來說用戶體驗提高了很多。這樣可以通過 JavaScript 使客戶端不斷地向服務器發送異步請求,并用返回的數據刷新局部頁面來達到“實時”的數據更新。
但是這樣的方案也有問題,因為有些情況下,服務器端的數據更新間隔我們是不能預知的,通常我們都是在客戶端設定一定的時間間隔去服務端請求數 據,即所謂的輪詢技術。但是如果服務端數據的更新間隔小于我們設定的頻率,那么就會有一些數據取不到。而如果服務端數據的更新間隔大于我們設定的頻率,那 么就會有冗余的數據傳輸。
鑒于這些缺點,有一些改進的方案應運而生,Comet 就是其中的代表。
Comet 技術被稱為(long-polling)長輪詢技術,它改變了服務器和客戶端的交互方式的。首先由客戶端發出請求,服務器接收到請求后并不一定立即返回,而是等到有數據更新時才返回或者直到連接超時。這樣就不會出現冗余的數據請求。
圖 1. Comet 技術工作方式

通常,為了模擬基于半雙工 HTTP 上的全雙工通信,目前的許多解決方案都使用了兩個連接:一個下行連接,一個上行連接。一方面,維護和協調這兩個連 接需要大量的系統開銷,并增加了復雜性。另一方面,還給網絡負載帶來了很大壓力。
下面是一次 Ajax 請求的傳輸數據:
清單 1. HTTP 請求
var worker = new Worker(dedicated.js'); Host:www.demo.com User-AgentMozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0 Accept:*/* Accept-Language:en-us,en;q=0.5 Accept-Encoding:gzip, deflate Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection:keep-alive Referer:http://www.demo.com/news/12365-demo-Chrome Cookie:_application_cookie_id_=1302165217141933; __utma=185941238.840487749.1311923297.1315056086.1315210256.36; __utmz=185941238.1315210256.36.42.utmcsr=search| utmccn=(organic)|utmcmd=organic|utmctr=websocket%20%C0%FD%D7%D3; __utmc=432143214.765498335.268654276.1565587542.4468744325.36; lzstat_uv=37479629961521195708|2366120@796778; ldsetat_ac=54325299615432195708|235432320@74321778; _application3_session_=BAh7BjoPc2Vzc2lvbl9pZCIlMmM2ZTIyYjhmMmQ3 ZTUyNDI2NTRlNTc1YzZjOGYwOWY%3D-- fb0c80e3bb59c54f4a5080652a6e1f0addccf4e0; __utmc=185941238 |
而 WebSocket 與上述方法不同,它首先通過客戶端和服務器在初始握手階段從 HTTP 協議升級到 WebSocket 協議。握手分為兩個階段,首先是客戶端請求:
清單 2. WebSocket 請求
worker.onmessage = function (event) { ... }; GET /demo HTTP/1.1 Host: example.com Connection: Upgrade Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 Sec-WebSocket-Protocol: sample Upgrade: WebSocket Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5 Origin: http://example.com ^n:ds[4U |
然后是服務器響應:
清單 3. 服務器響應
HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: http://example.com Sec-WebSocket-Location: ws://example.com/demo Sec-WebSocket-Protocol: sample 8jKS'y:G*Co,Wxa- |
在這個請求串中,“Sec-WebSocket-Key1”, “Sec-WebSocket-Key2”和最后的“^n:ds[4U”都是隨機的,服務器端會用這些數據來構造出一個 16 字節大小的應答結果。
把第一個 Key 中的數字除以第一個 Key 的空白字符的數量,而第二個 Key 也是如此。然后把這兩個結果與請求最后的 8 字節字符串連接起來成為一個字符串,服務器應答正文(“8jKS ’ y:G*Co,Wxa-”)即這個字符串的 MD5 sum。
在建立起 WebSocket 連接之后,服務器和客戶端的通信消息頭就變的非常簡潔了,整個消息頭只有僅僅兩個字節,以“0x00 ″開頭以” 0xFF”結尾,中間傳輸的數據采用 UTF-8 格式。
假設以下場景,如果我們以每秒一次的頻率去服務器拉取數據的話,無論每次傳輸的數據有多少,圖 2 中的 HTTP 協議頭都必須要傳送,這在用戶量不太大的情況下還不是太糟糕,但是在多用戶的場景下,便會給網絡帶來的巨大的負載。不同的應用傳輸的頭信息不盡相同,以上 面提到的傳輸數據為例,假設一次數據請求傳輸過程中數據頭大小為 800Byte,我們來看一下在不同的用戶量的條件下,冗余數據的增長情況。
圖 2. 數據傳輸量對比

由圖 2 看以看出,在用戶規模大的情況下,情況會變得的愈加惡劣,即使我們把輪詢技術改成 comet 技術,也只能減少小部分重復數據的輪詢數據,而且 comet 技術還會給服務器帶來額外的消耗。所以,使用 WebSocket 技術可以讓我們的在大規模的應用場景下減少大量的冗余數據帶寬消耗,而且用戶規模越大,它所帶來的優勢就越明顯。
WebSocket 為瀏覽器端提供了簡潔的操作接口,用戶可以方便實現與服務器的信息溝通。首先我們先看一下 WebSocket 接口的定義:
清單 4. WebSocket API
[Constructor(DOMString url, optional DOMString protocols), Constructor(DOMString url, optional DOMString[] protocols)] interface WebSocket : EventTarget { readonly attribute DOMString url; // ready state const unsigned short CONNECTING = 0; const unsigned short OPEN = 1; const unsigned short CLOSING = 2; const unsigned short CLOSED = 3; readonly attribute unsigned short readyState; readonly attribute unsigned long bufferedAmount; // networking attribute Function onopen; attribute Function onerror; attribute Function onclose; readonly attribute DOMString extensions; readonly attribute DOMString protocol; void close([Clamp] optional unsigned short code, optional DOMString reason); // messaging attribute Function onmessage; attribute DOMString binaryType; void send(DOMString data); void send(ArrayBuffer data); void send(Blob data); }; |
可以看到,WebSocket API 接口非常簡潔。
構造函數 WebSocket(url, protocols) 有兩個參數,參數 url 指定了要連接的 URL 地址。參數 protocols 是可選參數,可以為字符串或字符串數組,指定了連接子協議。連接過程的狀態保存在 readyState 屬性中:CONNECTING、OPEN、CLOSING 和 CLOSED,分別代表了連接過程中的正在連接狀態、已連接狀態、正在關閉狀態和連接已關閉狀態。通過 send 方法可以通過建立起來的連接傳輸數據。其參數可以為 String、Blob 對象或 ArrayBuffer 對象。close 方法會把 readyState 狀態設為設為 CLOSING 從而觸發連接關閉事件。另外接口還定義了相應的事件處理器:onopen、onerror、onmessage 和 onclose 來響應服務器的事件。
下面簡單介紹一下介紹如何利用 WebSocket 提供的 API 向 Server 發送信息和接收 Server 的消息。
我們可以通過下面一個簡單的語句來創建一個 Socket 實例:
清單 5. 創建 WebSocket 實例
var socket = new WebSocket('ws://localhost:8080'); |
需要注意的是,這個里的 url 不是以 http 開始的,而是 ws 開始的,因為我們使用的是 WebSocket 協議而不是普通的 HTTP 協議。
然后,我們需要定義 socket 對象的相應事件來對其作出處理
清單 6. 定義 Socket 打開時的回調方法
socket.onopen = function(event) { // 向服務器發送消息 socket.send('Hello Server!'); } |
清單 7. 定義消息接收回調方法
socket.onmessage = function(event) { alert(“Got a message from server!”); }; |
onmessage 事件提供了一個 data 屬性,它可以包含消息的 Body 部分,通過 event.data 可以取到。消息的 Body 部分必須是一個字符串,可以進行序列化 / 反序列化操作,以便傳遞更多的數據。
清單 8. Socket 關閉回調方法
socket.onclose = function(event) { console.log('Client notified socket has closed',event); }; |
清單 9. 關閉 Socket 方法
socket.close() ; |
WebSocket API 的設計比較精煉,只需要簡單地定義幾個回調方法就可以輕易操縱 WebSocket 數據傳輸,這給我們帶來的一定的便利性。
由于現在的 HTML5 的標準還沒有完成,目前只有一部分瀏覽器針對 HTML5 進行了支持,而且都沒有完全實現 HTML5 的新功能的支持。為了應用 HTML5 的 WebSocket 功能,我們需要檢測瀏覽器是否對其進行支持。
清單 10. 檢測瀏覽器支持狀態
if (!window.WebSocket) { alert("WebSocket not supported by this browser!"); } |
WebSocket 適用于需要實時通信的場景,例如實時信息監控,即時消息傳遞,網頁游戲,股票信息推送等等,下面我們來看一個基于 WebSocket 的在線聊天室的例子。
清單 11. Server 端程序設計
public class WSChatRoom extends WebSocketServlet{ private final List clients; //構造存放所有已連接的客戶端 public WSChatRoomServlet() { this.clients = new ArrayList(); } public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { return new WebSocketChat(); } //嵌套類 class ChatSocket implements WebSocket.OnTextMessage { WebSocket.Connection connection; //當連接建立的時候將此ChatSocket對象添加進當前連接客戶端列表 public void onOpen(WebSocket.Connection con) { this.connection = con; WSChatRoom.this.clients.add(this); } //處理客戶端發送的消息 public void onMessage(String data){ //循環當前連接的所有客戶端 for (ChatWebSocket member : WSChatRoomServlet.this.clients) { try { //向所有客戶端發送信息 clients.connection.sendMessage(data); }catch (IOException e) { Logger.error(e); } } } //關閉連接的時候從集合中刪除該客戶客戶端 public void onClose(int code, String message) { WebSocketChatServlet.this._members.remove(this); } } } |
這段程序采用 Java 語言,實現了服務器端的處理邏輯。測試服務器采用的 Jetty 7.4.5,程序中 WebSocket 相關的類引用自 jetty-websocket-7.4.5.v20110725.jar,這是 Jetty 服務器自帶的包。
程序比較簡單,首先通過 doWebSocketConnect 方法返回一個 ChatWebSocket 對象,然后通過實現 onMessage 方法并用循環逐一將收到的消息逐一發送到各個客戶端。最后通過 onOpen 和 onClose 來處理與每一客戶端建立連接和關閉連接的事件。
由于 HTML5 的標準還沒有最終確定,所以現在各個廠商對服務器端的實現都不盡相同,而且只有一部分廠商對 HTML5 中的 WebSocket 進行了支持,例如 Jetty 7.0 以上的版本,resin 4.0.2 以上版本以及 pywebsocket 等等。不過隨著時間的推移,會有越來越多的服務器支持 WebSocket 協議。
清單 12. 客戶端
<script type="text/javascript"> if (!window.WebSocket) { alert("此瀏覽器不支持WebSocket!"); } var username = ''; var socket = null; //初始化用戶界面 function open() { getObject('status').value = '開啟'; getObject('joinDiv').className = 'hidden'; getObject('joined').className = ''; getObject('words').focus(); send(username, '進入聊天室!'); } //建立WebSocket連接并綁定相應的事件處理方法 function join(name) { username = name; socket = new WebSocket('ws://localhost:8080/ws/'); socket.onmessage = getMsg; socket.onopen = open; socket.onclose = close; } //處理服務器返回的數據 function getMsg(event) { if (event.data) { var c = event.data.indexOf(':'); var from = event.data.substring(0, c).replace('<', '<').replace( '>', '>'); var text = event.data.substring(c + 1).replace('<', '<') .replace('>', '>'); var chat = getObject('chat'); var spanFrom = document.createElement('span'); spanFrom.className = 'from'; spanFrom.innerHTML = from ; var spanText = document.createElement('span'); spanText.className = 'text'; spanText.innerHTML = text; var lineBreak = document.createElement('br'); chat.appendChild(spanFrom); chat.appendChild(spanText); chat.appendChild(lineBreak); chat.scrollTop = chat.scrollHeight - chat.clientHeight; } } //連接關閉后的處理 function close(event) { socket = null; getObject('joinDiv').className = ''; getObject('joined').className = 'hidden'; getObject('username').focus(); getObject('chat').innerHTML = ''; getObject('status').value = '關閉'; } //向服務器發送數據 function send(user, message) { user = user.replace(':', '_'); if (socket) { socket.send(user + ':' + message); } } function chat(text) { if (text != null && text.length > 0) { send(username, text); } } function getObject() { return document.getElementById(arguments[0]); } function getValue() { return document.getElementById(arguments[0]).value; } </script> </head> <div id="chat"></div> <div id="input"> <div id="joinDiv"> 昵稱: <input id="username" type="text"> <input id="joinButton" class="button" type="submit" name="joinButton" value="進入聊天室"> </div> <div id="joined" class="hidden"> 聊天: <input id="words" type="text"> <input id="sendButton" class="button" type="submit" name="sendButton" value="發送"> </div> <div id="statusDiv"> WebSocket狀態:<input id="status" type="text" readonly="true" value="未開啟"> </div> </div> <script type="text/javascript"> getObject('joinButton').onclick = function(event) { join(getValue('username')); return false; }; getObject('sendButton').onclick = function(event) { chat(getValue('words')); getObject('words').value = ''; return false; }; </script> |
操作 WebSocket 的客戶端代碼也很簡單。核心代碼只有以下幾點:
首先,我們需要在客戶端創建一個 WebSocket 對象 socket = new WebSocket('ws://localhost:8080/ws/');。這樣就會請求與服務器建立 WebSocket 連接。
當打開連接時調用的方法:function open(),并將其綁定到 onopen 事件。當連接建立起來后這個方法會被調用,我們在此方法中完成界面的初始化操作。
關閉連接時調用的方法:function close(event) 并將其綁定到 onclose 事件。這是連接關閉時的回調方法,在此方法中可以放置一些連接關閉后的處理工作。
通過 send 方法向服務器發送消息:function send(user, message)。這是最重要的接口方法之一,用來完成向服務器發送數據的功能。
接收服務器發送來的消息:function getMsg(event),并將其綁定到 onmessage 事件。當服務器返回數據時便可以做相應的處理。
我們可以看到,應用 WebSocket 比以前的 Ajax 方案更加方便簡潔,只需要簡單的處理相應的回調方法即可完成相應的通信功能。
由于目前不是所有的瀏覽器都支持 WebSocket,所以對于不支持 WebSocket 的瀏覽器我們需要提供相應的替代方案。Dojo 1.6 為我們完成了這一工作,它提供了簡潔的調用接口,并能自動的檢測瀏覽器對 WebSocket 的支持能力來調用不同的實現技術。
在 Dojo 1.6 中提供了一個基于 WebSocket API 開發的可以進行實時通信的 Dojo socket API, 利用 HTML5 中 WebSocket 提供的一種支持全雙工通信的對象,可以非常方便地實時地將消息從服務端直接發送到客戶端。
與直接的 WebSocket 調用類似,您可以通過如下語句與服務器建立 WebSocket 連接:
清單 13. 建立 Dojo WebSocket 連接
var socket = dojox.socket( "ws://localhost:8080/ws" ) ; |
或者
清單 14. 建立 Dojo WebSocket 連接
var socket = dojox.socket ( { url:" ws://localhost:8080/ws " , headers: { "Accept" : "application/json" , "Content-Type" : "application/json" } } ) ; |
Dojo WebSocket 可以自動判斷瀏覽器是否支持 WebSocket,以選擇是應用 WebSocket 模式還是轉換成 HTTP/long-polling 模式。Dojo WebSocket 模塊屏蔽了各種不同的 long-polling 不同的實現方式,為其提供了統一的接口。
可以通過 socket.connect() 或者 socket.on() 方法注冊處理函數。這兩個方法可以通用,socket.on() 方法其實是 socket.connect() 方法的別名。
清單 15. 連接建立回調方法
socket.connect( "open" , function ( event) { alert(“Socket connected”); } ) ; |
當然,我們也可以像以前一樣直接定義回調方法:
清單 16. 回調方法定義
socket.onopen=function{ alert(“Socket connected”); } |
清單 17. 接收服務器消息處理方法
socket.connect ( "message" , function ( event) { var data = event.data ; alert(data); } ) ; |
清單 18. 連接關閉
socket.connect ( "close" , function ( event) { alert(“Socket closed”); } ) ; |
我們同樣可以利用 socket 的 send() 方法發送消息給服務器,調用 socket 的 close() 方法關閉連接。
另外,Dojo 框架還提供了一個 Reconnect 模塊,它給 dojox.socket 進行了增強,使其可以實現自動重連的功能。當 WebSocket 連接關閉時,它會自動重新連接服務器,可以容易的通過下面的語句將一個普通的 socket 增強為帶有自動連接功能的 socket。
清單 19. Reconnect
socket = dojox.socket.Reconnect(socket); |
我們可以把上面的例子改成用 Dojo WebSocket 的 Reconnect 方式,將
清單 20. WebSocket
socket =new WebSocket('ws://localhost:8080/ws/'); |
改成
清單 21. Reconnect WebSocket
socket = dojox.socket('ws://localhost:8080/ws'); socket = dojox.socket.Reconnect(socket); |
另外把 function close(event) 方法去掉,因為程序已經可以自動重新建立連接。這時聊天室程序便實現了自動重連功能,經過測試,此程序可以保持一直在線而不是像原先的程序一樣過一段時間無操作就會斷開連接。
HTML5 的 WebSocket 機制為新一代的 Web 開發提供了良好的數據通信基礎,利用它可以實現全雙工的信息交換,而且服務器可以主動的向 Server 推送數據。利用 WebSocket 提供的方便的接口可以方便的實現服務器和客戶端的實時數據傳輸。它相對于用 Ajax 輪詢技術或 Comet“服務器推”技術實現的“實時”通信方式來說,它利用了精簡的協議設計和頭定義提供了原生的 biDirection 數據通信和更為節省的數據傳送量,減輕了網絡負載。
文章出處: IBM developerWorks