我的網站 歡迎品嚐 edwardesire.comjavascript
下面頁面就是使用Socket.io製做的口袋妖怪遊戲(默認小屏下已隱藏,請切換到大分辨率查看)。左邊是遊戲畫面,右邊是按鍵表和聊天室。畫面達到紅藍版本的水平了。php
前導 ——WebSocket的介紹前端
傳統的Web應用採用的是客戶端發出請求、服務器端響應的工做方式。在這種情景下,瀏覽器做爲Web應用的前端,自身的處理功能是十分有限的。這種方法不能知足某些應用的實時需求(服務器須要主動更新瀏覽器端的數據)。不一樣於服務器端等待HTTP請求,這須要服務器端主動發送數據以給客戶端更新。解決方案有兩類:一類是基於HTTP的Comet推送技術,另外一類是基於套接口(Socket)傳送信息實現消息傳輸。
而目前使用Comet主要有兩種方式,輪詢和iframe流。java
輪詢 polling
瀏覽器週期性的發出請求,若是服務器沒有新數據須要發送就返回以空響應。這種方法問題很大:首先,大量無心義的請求形成網絡壓力;其次,請求週期的限制不能及時地得到最新數據。這種方法很快就被淘汰。git
長輪詢 long polling
長輪詢是在打開一條鏈接之後保持鏈接,等待服務器推送來數據再關閉鏈接。而後瀏覽器再發出新的請求,這能更好地管理請求數量,也能及時地更新數據。AJAX調用XMLHttpRequest對象發出HTTP請求,JS響應處理函數根據服務器返回的數據更新HTML頁面的展現。這個方法必定程度上消除了簡單輪詢的弊端,但服務器壓力也是很大。github
iframe流 iframe streaming
iframe流方式是在頁面中插入一個隱藏的iframe,利用其src屬性在服務器和客戶端之間創建一條長連接,服務器向iframe傳輸數據(一般是HTML,內有負責插入信息的javascript),來實時更新頁面。"iframe是很早就存在的一種 HTML 標記,經過在 HTML 頁面裏嵌入一個隱蔵幀,而後將這個隱蔵幀的 SRC屬性設爲對一個長鏈接的請求,服務器端就能源源不斷地往客戶端輸入數據。」其不足爲:進度條會顯示一直,反應在頁面上就是瀏覽器標籤頁的圖標會不停地轉動。(固然這也是有解決方法的)web
另外一類方法則是基於WebSocket
HTML5提供的Websocket不一樣於上面這些在老的HTML已有框架內的方法,而是在單個TCP鏈接上進行全雙工通信的協議。目前主流瀏覽器都已支持。
1. 初始化過程
不一樣於早期JAVA使用在瀏覽器安裝插件的方法——-Java Applet 套接口:這種方法不足在於Java Applet再收到服務器返回的消息後,沒法經過Javascript去更新HTML頁面的內容。而是經過HTTP創建鏈接(HTTP handshake)。
2. 開始通信
一旦初始鏈接創建,瀏覽器和服務器就打開了一個TCP socket的頻道。在這個頻道內就能進行雙向的數據通訊。數據庫
然而Websocket依然有一些問題。好比瀏覽器兼容性問題(隨着瀏覽器的發展,確定是愈來愈小的),以及網絡中間物(代理服務、防火牆)問題不支持WebSocket,這時Socket.io的出現就是爲了完善WebSocket。express
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(3000);
當客戶端須要鏈接服務器時,它須要先創建一個握手。io.處理鏈接事件,socket 處理斷開鏈接事件。在上面代碼裏,這套握手機制是徹底自動的,咱們能夠經過也能夠io.use()方法來設置這一過程。
客戶端使用js調用socket.io的Client API便可。
<script src="/lib/socket.io/socket.io.js"></script> <script> var socket = io(); socket.on('connect', function() { /* 具體操做 */ }); </script>
Socket.IO還要一些系統事件,包括了鏈接、重連、關閉的事件。咱們也能夠自定義事件,以及監聽方法。
socket.on('customEvent', function(customEventData) { /* 具體操做 */ });
相應地,在對的時間和地方的調用.emit('customEvent', customEventData); 觸發事件就好了。不過,事件是沒法在客戶端之間發送的。
同一個服務器可使用namespaces創造不一樣的Socket鏈接。Socket.IO使用of()來指定不一樣的命名空間。
io.of('/someNamespace').on('connection', function(socket){ socket.on('customEvent', function(customEventData) { /* 具體操做 */ }); }); io.of('/someOtherNamespace').on('connection', function(socket){ socket.on('customEvent', function(customEventData) { /* 具體操做 */ }); });
服務器端則經過在定義Socket對象時傳遞namespace參數。
<script> var someSocket = io('/someNamespace'); someSocket.on('customEvent', function(customEventData) { /* 具體操做 */ }); var someOtherSocket = io('/someOtherNamespace'); someOtherSocket.on('customEvent', function(customEventData) { /* 具體操做 */ }); </script>
在每個namespace中又可使用room來進一步劃分,不過sockets是使用join()、leave()來調用。
//服務器端 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可以正常使用。
var mongoStore = new MongoStore({ db: db.connection.db //經過server.js傳遞參數db到express的配置中 }); app.use(session({ saveUninitialized: true, resave: true, secret: config.sessionSecret, store: mongoStore }));
這樣Session就存到數據庫中來,新建配置文件socketio.js來配置socketio
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); }); }); };
cookieParser首先解析Express的Session,而後讀取sessionId得到數據庫中的session數據,填充到user對象中。若是經過passport來驗證用戶數據是非法的,則跳出Socket.IO的設置,併發出錯誤提示。接下來只須要創建Socket.IO的後端控制器便可完成後端的開發。
配置chat控制器
chat功能的控制器統一監聽和觸發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 }); }); };
肯定監聽事件規則後,將控制器載入到Socket.IO的鏈接事件處理函數中便可。
io.on('connection', function(socket){ console.log('a socket is connected'); require('../app/controllers/chat.server.controller')(io, socket); });
Angular前端設計
咱們先經過創建ng-resource來封裝Socket.IO的方法,再中前端的控制器中調用。
service是懶加載,即只有在請求時才加載。這能夠阻止未驗證用戶調用到service的方法來得到數據,將emit()、on()、removeListenter()一套方法封裝成的更相容的服務方法,減小代碼的重寫。然而ng的數據綁定只有在框架內執行的方法才能實時改變,也就是說第三方事件致使的數據模型的改變是未知的。那麼,咱們在socket中任何事件被觸發時,處理函數對數據的修改可能不會及時地綁定到$scope數據模型上。(這都是抄來的)這裏使用$timeout來強制完成數據的綁定。
angular.module('chat').service('Socket', ['Authentication', '$location', '$timeout', function(Authentication, $location, $timeout){ //首先確認用戶 if(Authentication.user){ this.socket = io(); }else{ $location.path('/'); } //通用監聽方法 this.on = function(eventName, callback){ if(this.socket){ this.socket.on(eventName, function(data){ $timeout(function(){ callback(data); }); }); } }; //通用觸發方法 this.emit = function(eventName, data){ if(this.socket){ this.socket.emit(eventName, data); } }; //通用刪除監聽器的方法 this.removeListener = function(eventName){ if(this.socket){ this.socket.removeListener(eventName); } }; }]);
接着在前端控制器中調用這些方法來處理後端觸發的事件和觸發後端能處理的事件。
//監聽後端發送的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'); });
將ng引入到對應的視圖模板,測試一下便可。
以上就是Socket.IO的上手實戰。先了解Socket.IO的工做機制,再將整個數據通訊的流程走了一遍,在實踐上將Socket.IO與Express、Passport整合到一塊兒完成了Web聊天室的功能,也見識到了Node.JS的小組件大組合的哲學。
References: