轉自:http://www.cnblogs.com/edwardstudy/p/4358202.html
下面頁面就是使用Socket.io制作的口袋妖怪遊戲(預設小屏下已隐藏,請切換到大分辨率檢視)。左邊是遊戲畫面,右邊是按鍵表和聊天室。畫面達到紅藍版本的水準了。
-
前導 ——WebSocket的介紹
傳統的Web應用采用的是用戶端送出請求、伺服器端響應的工作方式。在這種情景下,浏覽器作為Web應用的前端,自身的處理功能是十分有限的。這種方法不能滿足某些應用的實時需求(伺服器需要主動更新浏覽器端的資料)。不同于伺服器端等待HTTP請求,這需要伺服器端主動發送資料以給用戶端更新。解決方案有兩類:一類是基于HTTP的Comet推送技術,另一類是基于套接口(Socket)傳送資訊實作消息傳輸。
而目前使用Comet主要有兩種方式,輪詢和iframe流。
-
輪詢 polling
浏覽器周期性的送出請求,如果伺服器沒有新資料需要發送就傳回以空響應。這種方法問題很大:首先,大量無意義的請求造成網絡壓力;其次,請求周期的限制不能及時地獲得最新資料。這種方法很快就被淘汰。
-
長輪詢 long polling
長輪詢是在打開一條連接配接以後保持連接配接,等待伺服器推送來資料再關閉連接配接。然後浏覽器再發出新的請求,這能更好地管理請求數量,也能及時地更新資料。AJAX調用XMLHttpRequest對象發出HTTP請求,JS響應處理函數根據伺服器傳回的資料更新HTML頁面的展示。這個方法一定程度上消除了簡單輪詢的弊端,但伺服器壓力也是很大。
-
iframe流 iframe streaming
iframe流方式是在頁面中插入一個隐藏的iframe,利用其src屬性在伺服器和用戶端之間建立一條長連結,伺服器向iframe傳輸資料(通常是HTML,内有負責插入資訊的javascript),來實時更新頁面。"iframe是很早就存在的一種 HTML 标記,通過在 HTML 頁面裡嵌入一個隐蔵幀,然後将這個隐蔵幀的 SRC屬性設為對一個長連接配接的請求,伺服器端就能源源不斷地往用戶端輸入資料。”其不足為:進度條會顯示一直,反應在頁面上就是浏覽器标簽頁的圖示會不停地轉動。(當然這也是有解決方法的)
另一類方法則是基于WebSocket
HTML5提供的Websocket不同于上面這些在老的HTML已有架構内的方法,而是在單個TCP連接配接上進行全雙工通訊的協定。目前主流浏覽器都已支援。
1. 初始化過程
不同于早期JAVA使用在浏覽器安裝插件的方法——-Java Applet 套接口:這種方法不足在于Java Applet再收到伺服器傳回的消息後,無法通過Javascript去更新HTML頁面的内容。而是通過HTTP建立連接配接(HTTP handshake)。
2. 開始通訊
一旦初始連接配接建立,浏覽器和伺服器就打開了一個TCP socket的頻道。在這個頻道内就能進行雙向的資料通信。
然而Websocket依然有一些問題。比如浏覽器相容性問題(随着浏覽器的發展,肯定是越來越小的),以及網絡中間物(代理服務、防火牆)問題不支援WebSocket,這時Socket.io的出現就是為了完善WebSocket。
-
-
Socket.IO
Guillermo Rauch在2010年開發第一版時,目的很明确地指向Node.js實時應用。在幾次版本更新後,重新定義和封裝核心功能而分化出一個基礎子產品 Engine.io——力求建立更穩定的工具。Engine.IO有着更穩定的連接配接品質。使得Socket.IO在先打開一個長輪詢,再在将連接配接推至WebSocket頻道繼續通信。
在使用Node的http子產品建立伺服器同時還要Express應用,因為這個伺服器對象需要同時充當Express服務和Socket.io服務。(如下)
var app = require('express')(); //Express服務 var server = require('http').Server(app); //原生Http服務 var io = require('socket.io')(server); //Socket.io服務 io.on('connection', function(socket){ /* 具體操作 */ }); server.listen(
當用戶端需要連接配接伺服器時,它需要先建立一個握手。io.處理連接配接事件,socket 處理斷開連接配接事件。在上面代碼裡,這套握手機制是完全自動的,我們可以通過也可以io.use()方法來設定這一過程。
用戶端使用js調用socket.io的Client API即可。
Socket.IO還要一些系統事件,包括了連接配接、重連、關閉的事件。我們也可以自定義事件,以及監聽方法。
socket.on('customEvent', function(customEventData) { /* 具體操作 */ });
相應地,在對的時間和地方的調用.emit('customEvent', customEventData); 觸發事件就行了。不過,事件是無法在用戶端之間發送的。
同一個伺服器可以使用namespaces創造不同的Socket連接配接。Socket.IO使用of()來指定不同的命名空間。
伺服器端則通過在定義Socket對象時傳遞namespace參數。
在每一個namespace中又可以使用room來進一步劃分,不過sockets是使用join()、leave()來調用。<script> var someSocket = io('/someNamespace'); someSocket.on('customEvent', function(customEventData) { /* 具體操作 */ }); var someOtherSocket = io('/someOtherNamespace'); someOtherSocket.on('customEvent', function(customEventData) { /* 具體操作 */ }); </script>
//伺服器端 io.on('event', function(eventData){ //監聽join事件 socket.on('join', function(roomData){ socket.join(roomData.roomName); }); //監聽leave事件 socket.on('leave', function(roomData){ socket.leave(roomData.roomName); }); }); //浏覽器端 io.on('connection', function(socket){ //在此room下觸發事件 io. in('someRoom') .emit('customEvent', customEventData); });
下面通過《MEAN Web Development》書中的例子來實際操作一下。
-
配置Socket.io伺服器
首先安裝安裝Socket.IO、connect-mongo、cookie-parser依賴我們先将依賴報引入,然後定義伺服器對象。
var http = require('http'); var socketio = require('socket.io'); //... var app = express(); var server = http.createServer(app); var io = socketio.listen(server);
-
配置Socket.io Session
為了是Socket.io seesion 和Express session一起工作,我們必須讓他們資訊共享。Express Session 預設是存儲在記憶體,我們需要把它存在mongoDB以便Socket.io能擷取。使用connect-mongo來控制session資訊的存儲,以及使用以前用到過的cookie-parse來解析session cookie資訊。
先來修改express.js檔案以便connect-mongo能夠正常使用。
這樣Session就存到資料庫中來,建立配置檔案socketio.js來配置socketiovar mongoStore = new MongoStore({ db: db.connection.db //通過server.js傳遞參數db到express的配置中 }); app.use(session({ saveUninitialized: true, resave: true, secret: config.sessionSecret, store: mongoStore }));
cookieParser首先解析Express的Session,然後讀取sessionId獲得資料庫中的session資料,填充到user對象中。如果通過passport來驗證使用者資料是非法的,則跳出Socket.IO的設定,并發出錯誤提示。接下來隻需要建立Socket.IO的後端控制器即可完成後端的開發。var config = require('./config'), cookieParser = require('cookie-parser'), passport = require('passport'); /** * @description * @param {HTTP object} server 帶socket服務的http服務 * @param {Socket.io Object} io 監聽server的Socket服務 * @param {MongoStore Object} mongoStore mongoDB的存儲 * * */ module.exports = function(server, io, mongoStore){ io.use(function(socket, next){ //解析請求socket.request cookieParser(config.sessionSecret)(socket.request, {}, function(err){ //獲得sessionId var sessionId = socket.request.signedCookies['connect.sid']; //獲得資料庫中的session資料 mongoStore.get(sessionId, function(err, session){ socket.request.session = session; //填充 socket.request.user對象 passport.initialize()(socket.request, {}, function(){ passport.session()(socket.request, {}, function(){ if(socket.request.user){ next(null, true); }else{ next(new Error('User is not authenticated'), false); } }); }); }); }); io.on('connection', function(socket){ console.log('a socket is connected'); require('../app/controllers/chat.server.controller')(io, socket); }); }); };
-
配置chat控制器
chat功能的控制器統一監聽和觸發Socket.IO事件來進行資料通信。通過事件處理的回調函數來控制資料格式的建立和分發。
确定監聽事件規則後,将控制器載入到Socket.IO的連接配接事件處理函數中即可。module.exports = function(io, socket){ //觸發chatMessage事件,提示使用者已連接配接 io.emit('chatMessage', { type: 'status', text: 'connected', created: Date.now(), username: socket.request.user.username }); //監聽chatMessage事件,獲得使用者的消息 socket.on('chatMessage', function(message){ message.type = 'message'; message.created = Date.now(); messsage.username = socket.request.user.username; //觸發事件并發送資料。 io.emit('chatMessage', message); }); //監聽斷開連接配接事件 socket.on('disconnect', function(message){ //觸發事件并發送資料。 io.emit('chatMessage', { type: 'status', text: 'disconnected', created: Date.now(), username: socket.request.user.username }); }); };
-
Angular前端設計
我們先通過建立ng-resource來封裝Socket.IO的方法,再中前端的控制器中調用。
service是懶加載,即隻有在請求時才加載。這可以阻止未驗證使用者調用到service的方法來獲得資料,将emit()、on()、removeListenter()一套方法封裝成的更相容的服務方法,減少代碼的重寫。然而ng的資料綁定隻有在架構内執行的方法才能實時改變,也就是說第三方事件導緻的資料模型的改變是未知的。那麼,我們在socket中任何事件被觸發時,處理函數對資料的修改可能不會及時地綁定到$scope資料模型上。(這都是抄來的)這裡使用$timeout來強制完成資料的綁定。
接着在前端控制器中調用這些方法來處理後端觸發的事件和觸發後端能處理的事件。
将ng引入到對應的視圖模闆,測試一下即可。//監聽後端發送的chatMessage事件 Socket.on('chatMessage', function(message){ $scope.messages.push(message); }); //監聽後端發送的chatMessage事件 $scope.sendMessage = function(){ var message = { text: this.messageText }; //監聽後端發送的chatMessage事件 Socket.emit('chatMessage', message); //及時清空ng-model this.messageText = ''; }; //監聽$destroy,當controller執行個體被摧毀删除 監聽器 $scope.$on('$destroy', function(){ Socket.removeListener('chatMessage'); });
以上就是Socket.IO的上手實戰。先了解Socket.IO的工作機制,再将整個資料通信的流程走了一遍,在實踐上将Socket.IO與Express、Passport整合到一起完成了Web聊天室的功能,也見識到了Node.JS的小元件大組合的哲學。
References:
- 推送技術
- 小程源碼