最近因爲利用node
重構某個項目,項目中有一個實時聊天的功能,因而就研究了一下聊天室,在線demo|源碼,歡迎你們反饋。這個聊天室的主要利用到了socket.io
和express
。這個聊天室支持羣聊,私聊,支持發送圖片(PS:你們在體驗時最好開啓兩個瀏覽器,自問自答)。下面就來和你們分享下實現過程:php
HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通訊。css
爲了更好的理解WebSocket
,須要瞭解一下在沒有WebSocket
階段是如何寫聊天室這種實時系統的:
基於http
協議瀏覽器能夠實現單向通訊,只能由瀏覽器發起請求(Request),服務器進行響應(Response),一個請求對應一個響應。因爲服務器不能主動向客戶端推送消息,因而廣泛採用的方式就是輪詢(polling),輪詢實現起來很是簡單,就是定時的利用ajax
向服務器端進行請求。若是服務器有新的數據就返回新的數據,若是沒有數據就返回空響應。用代碼來模擬下就是這個樣子的:前端
// 前端請求代碼 function update (fn) { var xhr = new XMLHttpRequest(); xhr.open("get", "./update.php"); xhr.onreadystatechange = function(){ if(xhr.readyState === 4){ if(xhr.status == 200){ const res = JSON.parse(xhr.response); if (res.flag) { // 進行相應操做 // fn爲接到響應後的處理函數 fn && fn(fn); } } } }; xhr.send(); } function polling () { update(); } setInterval(polling, 2000); // 後臺響應代碼 <?php // 利用隨機數的大小來模擬是否有新數據 if (rand(1, 100) < 35) { echo json_encode(array( "flag" => true, "data" => '有新數據來了' )); } else { echo json_encode(array( "flag" => false )); } ?>
這種定時請求的方式的關鍵在於間隔時間的選取,依據我在上面代碼作的模擬,不多機率能拿到下真正的數據,多半的ajax
請求是無效的,因而又有前輩基於輪詢提出來了Comet(服務器推),這種技術能夠經過長輪詢(long polling)實現(還能夠利用iframe
),長輪詢也是靠ajax
實現客戶端的請求,其流程爲:客戶端發起請求,服務器掛起請求,倘若有新的數據返回,服務器響應客戶端剛纔的請求,客戶端獲得響應後繼續請求服務器。用僞代碼來模擬下長輪詢的過程:node
// 前端利用下面函數進行請求 function longPolling () { update(update); } longpolling(); // 後端代碼作以下更改 <?php // 利用隨機數的大小來模擬是否有新數據 while (true) { if (rand(1, 100) < 5) { echo json_encode(array( "flag" => true, "data" => '有新數據來了' )); break; } } ?>
長輪詢的確減小了請求的次數,可是它也有着很大的問題,那就是耗費服務器的資源。
不管是輪詢仍是長輪詢,還有着一個問題就是http
並非支持長鏈接不少人會說keep-alive
不就是作到了長鏈接嗎?然而並不是如此,keep-alive
是重用一個TCP
鏈接,就是說http 1.1作到了一個TCP
鏈接能夠發送多個http
請求,然而每一個http
請求還須要發送Request Header
,每一個請求的響應還會帶着Response Header
。對於輪詢和長輪詢來講伴隨着真實數據的交換,還有進行的就是大量的http header
的交換。
基於這些問題,WebSocket
被提出,WebSocket
能夠理解爲對http
的一個補丁包,WebSocket
使http
變成了一個真正的長鏈接,握手階段利用http
協議,以後就不會再發起http
請求了。下面來看下WebSocket
握手的過程:git
客戶端的請求頭比通常的http
請求多出來幾個字段:github
Upgrade: websocket,Connection: Upgrade
,利用這兩個字段來告訴服務器,我要將協議升級爲websocket
。web
Sec-WebSocket-Version: 13
,來告訴服務器我想要使用的WebSocket
的版本。ajax
Sec-WebSocket-Key
,其值採用base64編碼的隨機16字節長的字符序列,這個值會在響應頭中迴應。express
Sec-WebSocket-Extensions
,提供了一個客戶端支持的協議擴展列表來供服務器選擇,服務器只能選擇一個,而且會將選擇的擴展寫入響應頭的Sec-WebSocket-Extensions
。json
Sec-WebSocket-Protocol
,與Sec-WebSocket-Extensions
原理類似,用於協商應用子協議。
再來看看響應頭:
Status Code
,值爲101,表示已經升級到WebSocket
協議
Sec-WebSocket-Extensions
告訴客戶端服務器選擇的協議擴展
Sec-WebSocket-Protocol
告訴客戶端服務器選擇的子協議
Sec-WebSocket-Accept
經服務器確認而且加密後的Sec-WebSocket-Key
還有一點值得關注的就是協議頭由http/https
換成了ws/wss
,也標識真http
完成了其使命,接下來的事情由WebSocket
來負責啦!
因爲寫原生的WebSocket
在處理低版本瀏覽器的兼容性上的困難,因此通常在寫實時交互的這種項目時通常會利用到socket.io
。socket.io
並不只僅是WebSocket
,還包含着AJAX long polling
,AJAX multipart streaming
,JSONP Polling
等。socket.io
能夠看作是基於engine.io
的二次開發。經過emit
和on
能夠輕鬆地實現服務器與客戶端之間的雙向通訊,emit
來發布事件,on
來訂閱事件。
下面開始來寫代碼,我利用的構建工具是gulp
,模板語言是jade
,css預處理語言是less
,倘若也須要使用到這些,能夠關注下我所在團隊搭建的一個小的腳手架,先從app.js
開始:
const users = {}, app = express(), server = require("http").createServer(app), io = require("socket.io").listen(server); // 將socket.io綁定到服務器上,使得任何鏈接到服務器的客戶端都具備實時通訊的功能 // 服務器來監聽客戶端 io.on("connection", (socket) => { // socket是返回的鏈接對象,兩端的交互就是經過這個對象 });
須要建立一個對象(users
)來存儲在線用戶,鍵值爲用戶暱稱,爲用戶登陸來訂閱個事件:
socket.on("login", (nickname) => { if (users[nickname] || nickname === "system") { socket.emit("repeat"); } else { socket.nickname = nickname; users[nickname] = { name: nickname, socket: socket, lastSpeakTime: nowSecond() }; socket.emit("loginSuccess"); UsersChange(nickname, true); } }); socket.on("disconnect", () => { if (socket.nickname && users[socket.nickname]) { delete users[socket.nickname]; UsersChange(socket.nickname, false); } }); function UsersChange (nickname, flag) { io.sockets.emit("system", { nickname: nickname, size: Object.keys(users).length, flag: flag }); } function nowSecond () { return Math.floor(new Date() / 1000); }
用戶登陸時須要驗證其暱稱是否含有,倘若函數,則觸發在客戶端的js
代碼中註冊的repeat
事件,反之觸發loginSuccess
事件而且登陸成功後須要向全部的客戶端來廣播,因此利用了io.sockets.emit
。repeat
,loginSuccess
,system
,在src/js/index.js中進行註冊,主要用於頁面的顯示,也就是一些dom操做,因此在這裏沒有什麼好講的。用戶退出,直接調用默認事件disconnect
就好,並將該用戶從用戶對象中移除。
在用戶的狀態上的坑仍是很多的,由於WebSocket
中間過程比較複雜,常常會出現一些異常的狀況,因此須要進行心跳檢測,我採用的方式是服務端定時遍歷用戶列表,倘若用戶最後的發言時間與如今相比超過了5分鐘,就將其視爲掉線,從而避免了"用戶undefined退出羣聊"的這種狀況。
function pong () { const now = nowSecond(); for (let k in users) { if (users[k].lastSpeakTime + MAX_LEAVE_TIME < now) { var socket = users[k].socket; users[k].socket.emit("disconnect"); socket.emit("nouser", "因爲長時間未說話,您已經掉線,請從新刷新頁面"); socket = null; } } } // 心跳檢測 setInterval(pong, PONG_TIME); function UsersChange (nickname, flag) { io.sockets.emit("system", { nickname: nickname, size: Object.keys(users).length, flag: flag }); }
其實socket.io
的使用真的很是簡單,很容易就會上手,因此其他功能再也不一一演示,你們能夠看代碼的實現(寫的比較差,還請見諒),客戶端代碼中大量用到了L
,至關於zepto
的$
,特別須要處理的是在私信和發送圖片的處理上,私信須要處理不一樣消息框,到底把消息添加到那個消息框中,我利用了一個對象來存儲這些信息(cache
),cache
的鍵名爲用戶的暱稱(由於在註冊時判斷了其是否惟一,因此能夠將其視爲惟一的);鍵值爲對象,對象屬性以下圖所示:
具體實現你們仍是到源碼中去看吧!
感謝王哇勇大神的HiChat和小鬍子哥的blogChat
因爲本人水平有限,若有錯誤,歡迎你們指出!