使用WebSocket構建實時Web應用
隨着時代的進步,傳統的網頁技術已經無法滿足人民羣衆日益增長的物質文化需求(誤)。對於一些特定的需求,如消息推送、在線聊天等,往往受限於BS架構的特性而沒有完美的解決方案。好在現今HTML5標準已日漸成熟,現代瀏覽器大多也實現了對WebSocket 的支持。有了WebSocket,我們就可以構建真正意義上的Web App,實現客戶端到服務端的實時通信。
什麼是WebSocket?
WebSocket是一套基於TCP的協議,可用於在單個TCP連接上進行全雙工通信。雖然設計的時候是爲了在瀏覽器和服務端之間通信,但也可以單獨拿出來在任意的客戶端和服務端之間通信。WebSocket通過在Http頭上加入Upgrade:websocket來進行握手,但之後就和Http沒有任何關係了。
WebSocket握手——客戶端請求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol:chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
WebSocket握手——服務端響應:
HTTP/1.1 101 SwitchingProtocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
注:該例子摘自Wiki—— http://en.wikipedia.org/wiki/WebSocket
什麼時候用WebSocket
並非所有的場景都適合用WebSocket來解決問題。對大多數客戶端向服務端請求內容的需求來說,使用Http的Request-Response方式仍是最好的解決方案。
真正需要WebSocket的主要是一些是實時性要求較高的場景:如消息通知推送,即時通信等。在沒有WebSocket的時代,這類需求往往是通過輪詢或長連接來實現的。輪詢的問題是頻繁的請求會無意義地消耗大量網絡帶寬,而輪詢週期過長又會影響實時性。長連接則充斥了黑科技的味道,違背了HTTP協議的設計初衷,而且會佔用大量的服務器資源。有了WebSocket之後,這類問題終於迎刃而解。
NodeJS方案
雖然很多平臺都對WebSocket提供了支持,但是對於需要同時維持大量客戶端連接的場景來說,基於單進程異步調用的NodeJS是一個非常合適的解決方案。在服務器安裝了NodeJS環境以後,只需要在項目中執行npm install nodejs-websocket,就可以實現對該協議的支持。
首先,讓我們來看一個簡單的例子:
服務端代碼示例(摘自https://www.npmjs.com/package/nodejs-websocket):
var ws =require("nodejs-websocket");
var server =ws.createServer(function (conn) {
console.log("New connection");
conn.on("text", function (str) {
console.log("Received " + str);
conn.sendText(str.toUpperCase() + "!!!");
})
conn.on("close", function (code,reason) {
console.log("Connectionclosed");
})
}).listen(8001);
客戶端(網頁)代碼示例
<html>
<header>
</header>
<body>
<scripttype="text/javascript">
var wsServer ='ws://localhost:8001';
var webSocket = newWebSocket(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 WebSocketserver.");
webSocket.send("hello");
}
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);
}
</script>
</body>
</html>
該例子的執行邏輯如下:
客戶端頁面加載之後,便會同服務端建立WebSocket連接=》服務端收到連接後,便會觸發createServer的回調=》客戶端連接建立成功後,觸發onOpen事件,通過webSocket.send()向服務端發送文本=》服務端收到文本觸發conn.on(“text”)事件,並使用conn.sendText向客戶端推送文本=》客戶端收到該文本,觸發onMessage事件。
從本例可以看出,通過WebSocket,客戶端和服務端可以輕易地實現雙向實時通信。
注:服務端可以通過conn.path對url進行檢查,從而複用同一個端口實現多個接口。
一個簡單的通信框架
由於WebSocket提供的僅是最底層網絡通信的支持,直接在其之上編寫實際應用絕對不是一個好主意。以下的設計對其進行了簡單的封裝,從而將WebSocket的底層實現同業務邏輯進行分離,並實現了簡單的用戶及連接管理。
協議設計
使用JSON作爲消息格式。
服務端發往客戶端的消息:
{
action: “alert”,
message: “hello”
}
其中action是必選項,表明操作的類型,由客戶端註冊的相應的消息處理器來處理。
客戶端發往服務端的消息:
{
action: “register”,
user_id: 123456
}
其中action和user_id是必選項。action表明操作的類型,由服務端註冊的相應的消息處理器來處理。user_id爲識別用戶的唯一id,用於判斷消息來源。
客戶端在建立WebSocket連接後,需要向服務端發送register消息,並附上自己的userId。
服務端設計
模塊 |
ServiceModule |
|
描述 |
封裝WebSocket底層邏輯 |
|
方法 |
startService |
封裝NodeJS-WebSocket中的ws.createServer函數,用於啓動WebSocket監聽 |
sendMessage |
通過指定WebSocket連接發送消息到客戶端 |
|
registerHandler |
註冊消息處理器,格式爲handler(conn, request) |
模塊 |
UserManager |
|
描述 |
維護用戶/WebSocket連接列表 |
|
方法 |
getUsers |
獲取當前活躍用戶 |
sendMessageToUser |
向指定用戶發送消息 |
|
|
|
初始化時,UserManager會在ServiceModule中註冊action爲register的消息處理器。一旦有新用戶註冊,便會觸發該處理器,從而將該用戶及其對應的WebSocket連接加入列表維護。需要向某個用戶發送消息時,則通過該列表查找到對應的WebSocket連接,通過ServiceModule向客戶端發送消息。
注1:如果只是用於實現消息推送,服務端僅處理register消息便足夠了。若需要實現更復雜的邏輯,可以考慮添加一個用戶緯度的ServiceModule,封裝原ServiceModule和UserManager,支持形如handler(userId, request)的消息處理函數。
注2:如果不允許一個用戶建立多個連接,可以在register消息處理器中斷開之前的連接。
客戶端設計
模塊 |
WSAgent |
|
描述 |
封裝WebSocket底層邏輯 |
|
方法 |
sendMessage |
向服務端發送消息 |
registerHandler |
註冊消息處理器,格式爲handler(request) |
|
|
|
客戶端無需維護用戶列表,因此只需單個模塊即可提供所需的封裝。WSAgent初始化時需要輸入服務端url和userId作爲參數,從而同服務端建立WebSocket連接,併發送register消息。收到服務端消息後,則調用相應的消息處理器對其進行處理。
可以改進的點
上述框架僅提供了一個初級的雙向通信解決方案,無法得知消息發送是成功還是失敗。可以根據具體需求進行進一步優化。
對於推送服務來說,只需在此基礎上添加推送相關的消息即可。如果需要推送到達通知,可以爲推送消息指定唯一id。客戶端收到推送消息後,向服務端發送帶有該id的回執。服務端即可更新消息推送狀態。
對於即時通信來說則更爲複雜一些,需要分別在客戶端和服務端跟蹤消息的發送、接收情況。
另外就是需要獲取服務端信息的場景。一種做法是直接在WebSocket上實現類似傳統Http協議的Request-Response機制;還有一種做法對於這種情況通通走Http。前者可自定義協議,且無需重新建立連接,性能更好;後者則更爲通用,實現起來較簡單。