个人网站 欢迎品尝edwardesire.com
下面页面就是使用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(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来强
新闻热点
疑难解答