Web領域的實時推送技術,也被稱作Realtime技術。這種技術要達到的目的是讓用戶不需要刷新瀏覽器就可以獲得實時更新。它有着廣泛的應用場景,比如在線聊天室、在線客服系統、評論系統、WebIM等。
WebSocket簡介
談到Web實時推送,就不得不說WebSocket。在WebSocket出現之前,很多網站為了實現實時推送技術,通常采用的方案是輪詢 (Polling)和Comet技術,Comet又可細分為兩種實現方式,一種是長輪詢機制,一種稱為流技術,這兩種方式實際上是對輪詢技術的改進,這些 方案帶來很明顯的缺點,需要由瀏覽器對服務器發出HTTP request,大量消耗服務器帶寬和資源。面對這種狀況,HTML5定義了WebSocket協議,能更好的節省服務器資源和帶寬並實現真正意義上的實 時推送。
WebSocket協議本質上是一個基於TCP的協議,它由通信協議和編程API組成,WebSocket能夠在瀏覽器和服務器之間建立雙向連接, 以基於事件的方式,賦予瀏覽器實時通信能力。既然是雙向通信,就意味着服務器端和客戶端可以同時發送並響應請求,而不再像HTTP的請求和響應。
為了建立一個WebSocket連接,客戶端瀏覽器首先要向服務器發起一個HTTP請求,這個請求和通常的HTTP請求不同,包含了一些附加頭信 息,其中附加頭信息”Upgrade: WebSocket”表明這是一個申請協議升級的HTTP請求,服務器端解析這些附加的頭信息然后產生應答信息返回給客戶端,客戶端和服務器端的 WebSocket連接就建立起來了,雙方就可以通過這個連接通道自由的傳遞信息,並且這個連接會持續存在直到客戶端或者服務器端的某一方主動的關閉連 接。
一個典型WebSocket客戶端請求頭:
前面講到WebSocket是HTML5中新增的一種通信協議,這意味着一部分老版本瀏覽器(主要是IE10以下版本)並不具備這個功能, 通過百度統計的公開數據顯示,IE8 目前仍以33%的市場份額占據榜首,好在chrome瀏覽器市場份額逐年上升,現在以超過26%的市場份額位居第二,同時微軟前不久宣布停止對IE6的技 術支持並提示用戶更新到新版本瀏覽器,這個曾經讓無數前端工程師為之頭疼的瀏覽器有望退出歷史舞台,再加上幾乎所有的智能手機瀏覽器都支持HTML5,所 以使得WebSocket的實戰意義大增,但是無論如何,我們實際的項目中,仍然要考慮低版本瀏覽器的兼容方案:在支持WebSocket的瀏覽器中采用 新技術,而在不支持WebSocket的瀏覽器里啟用Comet來接收發送消息。
WebSocket實戰
本文將以多人在線聊天應用作為實例場景,我們先來確定這個聊天應用的基本需求。
需求分析
1、兼容不支持WebSocket的低版本瀏覽器。
2、允許客戶端有相同的用戶名。
3、進入聊天室后可以看到當前在線的用戶和在線人數。
4、用戶上線或退出,所有在線的客戶端應該實時更新。
5、用戶發送消息,所有客戶端實時收取。
在實際的開發過程中,為了使用WebSocket接口構建Web應用,我們首先需要構建一個實現了 WebSocket規范的服務端,服務端的實現不受平台和開發語言的限制,只需要遵從WebSocket規范即可,目前已經出現了一些比較成熟的 WebSocket服務端實現,比如本文使用的Node.js+Socket.IO。為什么選用這個方案呢?先來簡單介紹下他們兩。
Node.js
Node.js采用C++語言編寫而成,它不是Javascript應用,而是一個Javascript的運行環境,據Node.js創始人 Ryan Dahl回憶,他最初希望采用Ruby來寫Node.js,但是后來發現Ruby虛擬機的性能不能滿足他的要求,后來他嘗試采用V8引擎,所以選擇了 C++語言。
Node.js支持的系統包括*nux、Windows,這意味着程序員可以編寫系統級或者服務器端的Javascript代碼,交給 Node.js來解釋執行。Node.js的Web開發框架Express,可以幫助程序員快速建立web站點,從2009年誕生至今,Node.js的 成長的速度有目共睹,其發展前景獲得了技術社區的充分肯定。
Socket.IO
Socket.IO是一個開源的WebSocket庫,它通過Node.js實現WebSocket服務端,同時也提供客戶端JS庫。Socket.IO支持以事件為基礎的實時雙向通訊,它可以工作在任何平台、瀏覽器或移動設備。
Socket.IO支持4種協議:WebSocket、htmlfile、xhr-polling、jsonp-polling,它會自動根據瀏覽 器選擇適合的通訊方式,從而讓開發者可以聚焦到功能的實現而不是平台的兼容性,同時Socket.IO具有不錯的穩定性和性能。
編碼實現
本文一開始的的插圖就是效果演示圖:可以點擊這里查看在線演示,整個開發過程非常簡單,下面簡單記錄了開發步驟:
安裝Node.js
根據自己的操作系統,去Node.js官網下載安裝即可。如果成功安裝。在命令行輸入node -v
和npm -v
應該能看到相應的版本號。
node -v v0.10.26 npm -v 1.4.6
搭建WebSocket服務端
這個環節我們盡可能的考慮真實生產環境,把WebSocket后端服務搭建成一個線上可以用域名訪問的服務,如果你是在本地開發環境,可以換成本地ip地址,或者使用一個虛擬域名指向本地ip。
先進入到你的工作目錄,比如 /workspace/wwwroot/plhwin/realtime.plhwin.com
,新建一個名為 package.json
的文件,內容如下:
{ "name": "realtime-server", "version": "0.0.1", "description": "my first realtime server", "dependencies": {} }
接下來使用npm
命令安裝express
和socket.io
npm install --save express npm install --save socket.io
安裝成功后,應該可以看到工作目錄下生成了一個名為node_modules
的文件夾,里面分別是express
和socket.io
,接下來可以開始編寫服務端的代碼了,新建一個文件:index.js
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.send('<h1>Welcome Realtime Server</h1>'); }); http.listen(3000, function(){ console.log('listening on *:3000'); });
命令行運行node index.js
,如果一切順利,你應該會看到返回的listening on *:3000
字樣,這說明服務已經成功搭建了。此時瀏覽器中打開http://localhost:3000
應該可以看到正常的歡迎頁面。
如果你想要讓服務運行在線上服務器,並且可以通過域名訪問的話,可以使用Nginx做代理,在nginx.conf中添加如下配置,然后將域名(比如:realtime.plhwin.com)解析到服務器IP即可。
server { listen 80; server_name realtime.plhwin.com; location / { proxy_pass http://127.0.0.1:3000; } }
完成以上步驟,http://realtime.plhwin.com:3000
的后端服務就正常搭建了。
服務端代碼實現
前面講到的index.js
運行在服務端,之前的代碼只是一個簡單的WebServer歡迎內容,讓我們把WebSocket服務端完整的實現代碼加入進去,整個服務端就可以處理客戶端的請求了。完整的index.js
代碼如下:
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.send('<h1>Welcome Realtime Server</h1>'); }); //在線用戶 var onlineUsers = {}; //當前在線人數 var onlineCount = 0; io.on('connection', function(socket){ console.log('a user connected'); //監聽新用戶加入 socket.on('login', function(obj){ //將新加入用戶的唯一標識當作socket的名稱,后面退出的時候會用到 socket.name = obj.userid; //檢查在線列表,如果不在里面就加入 if(!onlineUsers.hasOwnProperty(obj.userid)) { onlineUsers[obj.userid] = obj.username; //在線人數+1 onlineCount++; } //向所有客戶端廣播用戶加入 io.emit('login', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj}); console.log(obj.username+'加入了聊天室'); }); //監聽用戶退出 socket.on('disconnect', function(){ //將退出的用戶從在線列表中刪除 if(onlineUsers.hasOwnProperty(socket.name)) { //退出用戶的信息 var obj = {userid:socket.name, username:onlineUsers[socket.name]}; //刪除 delete onlineUsers[socket.name]; //在線人數-1 onlineCount--; //向所有客戶端廣播用戶退出 io.emit('logout', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj}); console.log(obj.username+'退出了聊天室'); } }); //監聽用戶發布聊天內容 socket.on('message', function(obj){ //向所有客戶端廣播發布的消息 io.emit('message', obj); console.log(obj.username+'說:'+obj.content); }); }); http.listen(3000, function(){ console.log('listening on *:3000'); });
客戶端代碼實現
進入客戶端工作目錄/workspace/wwwroot/plhwin/demo.plhwin.com/chat
,新建一個index.html
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="format-detection" content="telephone=no"/> <meta name="format-detection" content="email=no"/> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" name="viewport"> <title>多人聊天室</title> <link rel="stylesheet" type="text/css" href="./style.css" /> <!--[if lt IE 8]><script src="./json3.min.js"></script><![endif]--> <script src="http://realtime.plhwin.com:3000/socket.io/socket.io.js"></script> </head> <body> <div id="loginbox"> <div style="width:260px;margin:200px auto;"> 請先輸入你在聊天室的昵稱 <br/> <br/> <input type="text" style="width:180px;" placeholder="請輸入用戶名" id="username" name="username" /> <input type="button" style="width:50px;" value="提交" onclick="CHAT.usernameSubmit();"/> </div> </div> <div id="chatbox" style="display:none;"> <div style="background:#3d3d3d;height: 28px; width: 100%;font-size:12px;"> <div style="line-height: 28px;color:#fff;"> <span style="text-align:left;margin-left:10px;">Websocket多人聊天室</span> <span style="float:right; margin-right:10px;"><span id="showusername"></span> | <a href="javascript:;" onclick="CHAT.logout()" style="color:#fff;">退出</a></span> </div> </div> <div id="doc"> <div id="chat"> <div id="message" class="message"> <div id="onlinecount" style="background:#EFEFF4; font-size:12px; margin-top:10px; margin-left:10px; color:#666;"> </div> </div> <div class="input-box"> <div class="input"> <input type="text" maxlength="140" placeholder="請輸入聊天內容,按Ctrl提交" id="content" name="content"> </div> <div class="action"> <button type="button" id="mjr_send" onclick="CHAT.submit();">提交</button> </div> </div> </div> </div> </div> <script type="text/javascript" src="./client.js"></script> </body> </html>
上面的html內容本身沒有什么好說的,我們主要看看里面的4個文件請求:
1、realtime.plhwin.com:3000/socket.io/socket.io.js
2、style.css
3、json3.min.js
4、client.js
第1個JS是Socket.IO提供的客戶端JS文件,在前面安裝服務端的步驟中,當npm安裝完socket.io並搭建起WebServer后,這個JS文件就可以正常訪問了。
第2個style.css文件沒什么好說的,就是樣式文件而已。
第3個JS只在IE8以下版本的IE瀏覽器中加載,目的是讓這些低版本的IE瀏覽器也能處理json,這是一個開源的JS,詳見:http://bestiejs.github.io/json3/
第4個client.js
是完整的客戶端的業務邏輯實現代碼,它的內容如下:
(function () { var d = document, w = window, p = parseInt, dd = d.documentElement, db = d.body, dc = d.compatMode == 'CSS1Compat', dx = dc ? dd: db, ec = encodeURIComponent; w.CHAT = { msgObj:d.getElementById("message"), screenheight:w.innerHeight ? w.innerHeight : dx.clientHeight, username:null, userid:null, socket:null, //讓瀏覽器滾動條保持在最低部 scrollToBottom:function(){ w.scrollTo(0, this.msgObj.clientHeight); }, //退出,本例只是一個簡單的刷新 logout:function(){ //this.socket.disconnect(); location.reload(); }, //提交聊天消息內容 submit:function(){ var content = d.getElementById("content").value; if(content != ''){ var obj = { userid: this.userid, username: this.username, content: content }; this.socket.emit('message', obj); d.getElementById("content").value = ''; } return false; }, genUid:function(){ return new Date().getTime()+""+Math.floor(Math.random()*899+100); }, //更新系統消息,本例中在用戶加入、退出的時候調用 updateSysMsg:function(o, action){ //當前在線用戶列表 var onlineUsers = o.onlineUsers; //當前在線人數 var onlineCount = o.onlineCount; //新加入用戶的信息 var user = o.user; //更新在線人數 var userhtml = ''; var separator = ''; for(key in onlineUsers) { if(onlineUsers.hasOwnProperty(key)){ userhtml += separator+onlineUsers[key]; separator = '、'; } } d.getElementById("onlinecount").innerHTML = '當前共有 '+onlineCount+' 人在線,在線列表:'+userhtml; //添加系統消息 var html = ''; html += '<div class="msg-system">'; html += user.username; html += (action == 'login') ? ' 加入了聊天室' : ' 退出了聊天室'; html += '</div>'; var section = d.createElement('section'); section.className = 'system J-mjrlinkWrap J-cutMsg'; section.innerHTML = html; this.msgObj.appendChild(section); this.scrollToBottom(); }, //第一個界面用戶提交用戶名 usernameSubmit:function(){ var username = d.getElementById("username").value; if(username != ""){ d.getElementById("username").value = ''; d.getElementById("loginbox").style.display = 'none'; d.getElementById("chatbox").style.display = 'block'; this.init(username); } return false; }, init:function(username){ /* 客戶端根據時間和隨機數生成uid,這樣使得聊天室用戶名稱可以重復。 實際項目中,如果是需要用戶登錄,那么直接采用用戶的uid來做標識就可以 */ this.userid = this.genUid(); this.username = username; d.getElementById("showusername").innerHTML = this.username; this.msgObj.style.minHeight = (this.screenheight - db.clientHeight + this.msgObj.clientHeight) + "px"; this.scrollToBottom(); //連接websocket后端服務器 this.socket = io.connect('ws://realtime.plhwin.com:3000'); //告訴服務器端有用戶登錄 this.socket.emit('login', {userid:this.userid, username:this.username}); //監聽新用戶登錄 this.socket.on('login', function(o){ CHAT.updateSysMsg(o, 'login'); }); //監聽用戶退出 this.socket.on('logout', function(o){ CHAT.updateSysMsg(o, 'logout'); }); //監聽消息發送 this.socket.on('message', function(obj){ var isme = (obj.userid == CHAT.userid) ? true : false; var contentDiv = '<div>'+obj.content+'</div>'; var usernameDiv = '<span>'+obj.username+'</span>'; var section = d.createElement('section'); if(isme){ section.className = 'user'; section.innerHTML = contentDiv + usernameDiv; } else { section.className = 'service'; section.innerHTML = usernameDiv + contentDiv; } CHAT.msgObj.appendChild(section); CHAT.scrollToBottom(); }); } }; //通過“回車”提交用戶名 d.getElementById("username").onkeydown = function(e) { e = e || event; if (e.keyCode === 13) { CHAT.usernameSubmit(); } }; //通過“回車”提交信息 d.getElementById("content").onkeydown = function(e) { e = e || event; if (e.keyCode === 13) { CHAT.submit(); } }; })();
至此所有的編碼開發工作全部完成了,在瀏覽器中打開http://demo.plhwin.com/chat/就可以看到效果了。
上面所有的客戶端和服務端的代碼可以從Github上獲得,地址:https://github.com/plhwin/nodejs-socketio-chat
git clone https://github.com/plhwin/nodejs-socketio-chat.git
下載本地后有兩個文件夾 client
和 server
,client
文件夾是客戶端源碼,可以放在Nginx/Apache的WebServer中,也可以放在Node.js的WebServer中。后面的server
文件夾里的代碼是websocket服務端代碼,放在Node.js環境中,使用npm安裝完 express
和 socket.io
后,node index.js
啟動后端服務就可以了。
本例只是一個簡單的Demo,留下2個有關項目擴展的思考:
1、假設是一個在線客服系統,里面有許多的公司使用你的服務,每個公司自己的用戶可以通過一個專屬URL地址進入該公司的聊天室,聊天是一對一的,每個公司可以新建多個客服人員,每個客服人員可以同時和客戶端的多個用戶聊天。
2、又假設是一個在線WebIM系統,實現類似微信,qq的功能,客戶端可以看到好友在線狀態,在線列表,添加好友,刪除好友,新建群組等,消息的發送除了支持基本的文字外,還能支持表情、圖片和文件。
來自:http://www.plhwin.com/2014/05/28/nodejs-socketio/