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]
這些請求和通常的 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 定義
JavaScript
代碼
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 代碼
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. 生成網絡字節數組的代碼
得到網絡字節數組以后,服務器端生成 16 位安全密鑰的方法如下所示:
C#
代碼
{
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;
}
清單 5. 生成 16 位安全密鑰的代碼
C#
代碼
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 對象的代碼
JavaScript
代碼
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");
};
當用戶按下發送按鈕,客戶端會調用WebSocket對象向服務器發送信息,並且這個消息會廣播給所有的用戶,實現代碼如下所示:
JavaScript
代碼
{
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 也應該提上日程了,否則我們在新一輪的軟件革新的浪潮中只能做壁上觀了。
C# websocket聊天室示例源代碼下載