个人网站 欢迎品尝 edwardesire.com

下面页面就是使用Socket.io制作的口袋妖怪游戏(默认小屏下已隐藏,请切换到大分辨率查看)。左边是游戏画面,右边是按键表和聊天室。画面达到红蓝版本的水平了。


  1. 前导 ——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。

  2. 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》书中的例子来实际操作一下。

  1. 配置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);
  2. 配置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的后端控制器即可完成后端的开发。

  3. 配置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);
    });
  4. 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:

  1. 推送技术
  2. 小程源码

Socket.io:有点意思的更多相关文章

  1. 在web浏览器上显示室内温度(nodeJs+arduino+socket.io)

    上次的nodejs操作arduino入门篇中实现了如何连接arduino.这次我们来实现通过arduino测量室内温度并在浏览器上显示出来. [所需材料] 硬件:LM35温度传感器,arduino u ...

  2. Node学习笔记(三):基于socket.io web版你画我猜(二)

    上一篇基础实现的功能是客户端canvas作图,导出dataURL从而实现图片信息推送,下面具体讲下服务端的配置及客户端的配置同步 首先先画一个流程图,讲下大概思路 <canvas id=&quo ...

  3. node.js+socket.io配置详解

    由于我是在win7的环境下,在这里就以win7系统为例进行讲解了. 首先需要在nodejs官网下载最新版的node.js,下载完毕直接安装即可,安装成功后在cmd命令行中执行node指令,如下结果就说 ...

  4. 使用Node.js+Socket.IO搭建WebSocket实时应用

    Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新.它有着广泛的应用场景,比如在线聊天室.在线客服系统.评论系统.WebIM等. W ...

  5. socket.io简单入门(一.实现简单的图表推送)

    引子:随着nodejs蓬勃发展,虽然主要业务系统因为架构健壮性不会选择nodejs座位应用服务器.但是大量的内部系统却可以使用nodejs试水,大量的前端开发人员转入全堆开发也是一个因素. 研究本例主 ...

  6. websocket与socket.io

    什么是Websocket? Websocket是一个独立于http的实时通信协议,最初是在HTML5中被引用进来的,在HTML5规范中作为浏览器与服务器的核心通信技术被嵌入到浏览器中.WebSocke ...

  7. socket.io安装部署

    需要node.js环境 创建package.json npm init 下载相关依赖 npm install --save express@4.10.2npm会在当前目录下载所需要的依赖到node_m ...

  8. Node.js、Express、Socket.io 入门

    前言 周末断断续续的写了第一个socket.io Demo.初次接触socket.io是从其官网看到的,看着get started做了一遍,根据官网的Demo能提供简单的服务端和客户端通讯. 这个De ...

  9. socket.io,io=Manager(source, opts)

    原文:http://www.cnblogs.com/xiezhengcai/p/3968067.html 当我们在使用 var socket = io("ws://103.31.201.15 ...

  10. socket.io,命名空间

    原文:http://www.cnblogs.com/xiezhengcai/p/3966263.html 命名空间 在api部分我们说io.connect('ws://103.31.201.154:5 ...

随机推荐

  1. Windows下Java File对象创建文件夹时的一个"坑"

    import java.io.File; import java.io.IOException; public class DirCreate { public static void main(St ...

  2. opencv 画延长线

    hough变换可以让我们检测到直线,这在前面已有详解,对于车道检测,我们需要其到图像边界的延长线一遍之后数据帧分析. 以下代码帮助我们在opencv中画延长线,本来想用虚线表示延长线的,无奈参数调不好 ...

  3. JBPM4 常用表结构

    JBPM4 常用表结构 第一部分:表结构说明 Jbpm4 共有18张表,如下,其中红色的表为经常使用的表   一:资源库与运行时表结构 1.  JBPM4_DEPLOYMENT 流程定义表 2.  J ...

  4. PHP5.4连接sqlserver

    1.下载微软的php连接驱动:SQLSRV30.EXE(5.4对应,后面的native client要用2012)/SQLSRV20.EXE(5.3对应,native client要用2008)/SQ ...

  5. Linux 线程属性函数总结

    1.初始化一个线程对象的属性 int pthread_attr_init(pthread_attr_t *attr); 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性 ...

  6. Android权限安全(3)权限的分级和自定义权限

    Android的不同权限分级 Normal 一般apk都可以用, Dangerous 一般apk都可以用,但有提示 SignatureOrSystem 特定的private key签名的或系统的apk ...

  7. android的helloworld工程目录学习

    android的helloworld工程目录学习 Android工程的主要目录有src.gen.Android X.X.bin.res等文件夹. 1.     Src文件夹 Src文件夹包含java源 ...

  8. oracle portlist.ini

    Enterprise Manager Database Control URL - (orcl) :https://redhat4.7:1158/em [root@redhat4 install]# ...

  9. chrome浏览器无法设置打开特定网页

    最近chrome浏览器更新后,发现以前设置的启动浏览器“重上次停下的地方继续”功能消失了. 当我点击设置网页时,会出现如上提示. 后来有同事给了如下一个连接,里面说到这个是公司的超级管理员搞的,他定义 ...

  10. MSSQL大全

    一.基础 1.说明:创建数据库CREATE DATABASE database-name 2.说明:删除数据库drop database dbname3.说明:备份sql server--- 创建 备 ...