当前位置: 首页 > 工具软件 > SAEA.Socket > 使用案例 >

socket.io之Server类

丰智
2023-12-01
var http = require('http');
var read = require('fs').readFileSync;
var path = require('path');
var exists = require('fs').existsSync;
//引擎
var engine = require('engine.io');
var clientVersion = require('socket.io-client/package.json').version;
var Client = require('./client');
var Emitter = require('events').EventEmitter;
var Namespace = require('./namespace');
//适配器
var Adapter = require('socket.io-adapter');
//解析器
var parser = require('socket.io-parser');
var debug = require('debug')('socket.io:server');
var url = require('url');

/**
 * Module exports.
 */

module.exports = Server;


//客户端源码字符串
var clientSource = undefined;
var clientSourceMap = undefined;


/* 服务构造函数,用来创建socket服务器,构造函数等于Server.listen函数
   第一个参数是http模块中的服务对象,第二个参数为选项
   options:
          path:捕获的路径名称,默认为/socket.io
          serveClient:是否提供客户端文件
          adapter:适配器,默认采用socket.io-adapter
          origins: 允许的请求来源,默认为*:*
          parser:解析器,默认采用socket.io-parser
*/
function Server(srv, opts){
  //保证作为构造函数调用
  if (!(this instanceof Server)) return new Server(srv, opts);
  //第一个参数为对象时,说明只传入了一个opts参数
  if ('object' == typeof srv && srv instanceof Object && !srv.listen) {
    opts = srv;
    srv = null;
  }
  opts = opts || {};
  //命名空间,一个name对应一个namespace对象
  this.nsps = {};
  //设置服务路径,默认为socket.io,即连接的url
  this.path(opts.path || '/socket.io');
  //如果为true,读取客户端js文件源码并存储
  this.serveClient(false !== opts.serveClient);
  //socketio解析器
  this.parser = opts.parser || parser;
  //编码器
  this.encoder = new this.parser.Encoder();
  //设置socketio适配器
  this.adapter(opts.adapter || Adapter);
  //请求的允许源,默认为*:*,代表任意域名、任意端口
  this.origins(opts.origins || '*:*');
  //生成根命名空间对象为sockets属性
  this.sockets = this.of('/');
  //如果传入了http服务器对象,在其上监听
  if (srv) this.attach(srv, opts);
}


/*请求验证函数,检查请求源是否允许,如果允许执行回调fn函数
  回调函数第二个参数为请求是否被允许
*/
Server.prototype.checkRequest = function(req, fn) {
  //获取头信息中的请求来源
  var origin = req.headers.origin || req.headers.referer;

  // file:// URLs produce a null Origin which can't be authorized via echo-back
  if ('null' == origin || null == origin) origin = '*';

  //当_origins为一个函数
  if (!!origin&& typeof(this._origins) == 'function') return this._origins(origin, fn);
  //如果包含了默认源*:*,直接执行回调
  if (this._origins.indexOf('*:*') !== -1) return fn(null, true);
  if (origin) {
    try {
      //解析请求来源url
      var parts = url.parse(origin);
      //https协议默认端口为443,http默认端口为80
      var defaultPort = 'https:' == parts.protocol ? 443 : 80;
      parts.port = parts.port != null
        ? parts.port
        : defaultPort;
      //允许的源是否包含了请求源
      var ok =
        ~this._origins.indexOf(parts.hostname + ':' + parts.port) ||
        //查找指定域名
        ~this._origins.indexOf(parts.hostname + ':*') ||
        //查找指定端口
        ~this._origins.indexOf('*:' + parts.port);
        //执行回调
      return fn(null, !!ok);
    } catch (ex) {
    }
  }
  fn(null, false);
};

//根据配置,读取源码并存储
Server.prototype.serveClient = function(v){
  if (!arguments.length) return this._serveClient;
  //设置是否提供客户端js文件
  this._serveClient = v;
  //解析文件路径方法
  var resolvePath = function(file){
    var filepath = path.resolve(__dirname, './../../', file);
    if (exists(filepath)) {
      return filepath;
    }
    return require.resolve(file);
  };
  if (v && !clientSource) {
    //读取客户端socketio库源码
    clientSource = read(resolvePath( 'socket.io-client/dist/socket.io.js'), 'utf-8');
    try {
      //读取map文件
      clientSourceMap = read(resolvePath( 'socket.io-client/dist/socket.io.js.map'), 'utf-8');
    } catch(err) {
      debug('could not load sourcemap file');
    }
  }
  return this;
};

//旧的设置,为了向后兼容
var oldSettings = {
  "transports": "transports",
  "heartbeat timeout": "pingTimeout",
  "heartbeat interval": "pingInterval",
  "destroy buffer size": "maxHttpBufferSize"
};
//向后兼容,为服务器对象设置属性
Server.prototype.set = function(key, val){
  //如果键为authorization,val为认证鉴权函数
  if ('authorization' == key && val) {
    //为服务器设置中间件函数
    this.use(function(socket, next) {
      //对请求进行认证
      val(socket.request, function(err, authorized) {
        //如果有错误或者认证未通过,则抛出异常
        if (err) return next(new Error(err));
        if (!authorized) return next(new Error('Not authorized'));
        next();
      });
    });
  } 
  //设置允许源
  else if ('origins' == key && val) {
    this.origins(val);
  } 
  //如果键为resource,则设置为服务路径
  else if ('resource' == key) {
    this.path(val);
  } 
  //如果设置存在于引擎服务对象上,设置到引擎上
  else if (oldSettings[key] && this.eio[oldSettings[key]]) {

    this.eio[oldSettings[key]] = val;
  } else {
    console.error('Option %s is not valid. Please refer to the README.', key);
  }
  return this;
};

//设置服务路径为_path属性,即请求url
Server.prototype.path = function(v){
  if (!arguments.length) return this._path;
  //将/转换为空字符串
  this._path = v.replace(/\/$/, '');
  return this;
};

//设置房间适配器构造函数为_adapter属性
Server.prototype.adapter = function(v){
  if (!arguments.length) return this._adapter;
  this._adapter = v;
  //遍历命名空间名称
  for (var i in this.nsps) {
    //对每个命名空间初始化适配器
    if (this.nsps.hasOwnProperty(i)) {
      //为每个命名空间初始化适配器,设置适配器实例到命名空间属性
      this.nsps[i].initAdapter();
    }
  }
  return this;
};

//设置请求的允许源为_origins属性
Server.prototype.origins = function(v){
  if (!arguments.length) return this._origins;

  this._origins = v;
  return this;
};


//listen方法,将socketio服务器连接到一个http服务器
Server.prototype.listen =
Server.prototype.attach = function(srv, opts){
  if ('function' == typeof srv) {
    var msg = 'You are trying to attach socket.io to an express ' +
    'request handler function. Please pass a http.Server instance.';
    throw new Error(msg);
  }
  
  //如果第一个参数为数字,则为端口号
  if (Number(srv) == srv) {
    srv = Number(srv);
  }

  //如果传递的是端口,根据端口创建服务器
  if ('number' == typeof srv) {
    debug('creating http server and binding to %d', srv);
    var port = srv;
    //创建http服务器,默认请求处理函数为404未找到
    srv = http.Server(function(req, res){
      res.writeHead(404);
      res.end();
    });
    srv.listen(port);
  }
  opts = opts || {};
  //服务路径
  opts.path = opts.path || this.path();
  //请求源验证函数
  opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this);

  //如果根命名空间上的中间件函数多于1个
  if (this.sockets.fns.length > 0) {
    //初始化引擎后返回,选项中没有初始化数据包
    this.initEngine(srv, opts);
    return this;
  }

  var self = this;
  //连接数据包
  var connectPacket = { type: parser.CONNECT, nsp: '/' };
  //编码数据包
  this.encoder.encode(connectPacket, function (encodedPacket){
    //设置选项中的初始化数据包
    opts.initialPacket = encodedPacket;
    //初始化引擎,选项中有初始化数据包
    self.initEngine(srv, opts);
  });
  return this;
};

//初始化引擎
Server.prototype.initEngine = function(srv, opts){
  debug('creating engine.io instance with opts %j', opts);
  //引擎服务对象,使用http服务实例来初始化,会在其上设置一些请求处理函数
  this.eio = engine.attach(srv, opts);

  //如果提供客户端js文件,则设置相应的请求处理函数
  if (this._serveClient) this.attachServe(srv);

  //http服务实例属性
  this.httpServer = srv;

  // bind to engine events
  this.bind(this.eio);
};

//客户端js文件服务
Server.prototype.attachServe = function(srv){
  debug('attaching client serving req handler');
  //在socketio服务路径下,获取客户端js'文件的url
  var url = this._path + '/socket.io.js';
  var urlMap = this._path + '/socket.io.js.map';
  //复制请求处理函数数组
  var evs = srv.listeners('request').slice(0);
  var self = this;
  //移除所有请求处理函数
  srv.removeAllListeners('request');
  //为http请求设置监听器
  srv.on('request', function(req, res) {
    //获取.map.js文件
    if (0 === req.url.indexOf(urlMap)) {
      self.serveMap(req, res);
    } 
    //获取.js文件
    else if (0 === req.url.indexOf(url)) {
      self.serve(req, res);
    } else {
      //遍历之前的监听器函数,并且调用,即保证静态文件请求最先处理
      for (var i = 0; i < evs.length; i++) {
        evs[i].call(srv, req, res);
      }
    }
  });
};


//获取socket.io.js文件的处理函数
Server.prototype.serve = function(req, res){
  var expectedEtag = '"' + clientVersion + '"';
  var etag = req.headers['if-none-match'];
  if (etag) {
    if (expectedEtag == etag) {
      debug('serve client 304');
      res.writeHead(304);
      res.end();
      return;
    }
  }
  debug('serve client source');
  res.setHeader('Content-Type', 'application/javascript');
  res.setHeader('ETag', expectedEtag);
  res.writeHead(200);
  //写入socket.io.js文件字符串
  res.end(clientSource);
};

//获取socket.io.js.map文件的处理函数
Server.prototype.serveMap = function(req, res){
  var expectedEtag = '"' + clientVersion + '"';
  var etag = req.headers['if-none-match'];
  if (etag) {
    if (expectedEtag == etag) {
      debug('serve client 304');
      res.writeHead(304);
      res.end();
      return;
    }
  }
  debug('serve client sourcemap');
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('ETag', expectedEtag);
  res.writeHead(200);
  res.end(clientSourceMap);
};

//绑定服务器实例与引擎实例
Server.prototype.bind = function(engine){
  //设置引擎实例
  this.engine = engine;
  //底层服务上的连接事件,会调用顶层服务上的回调函数,参数为底层socket对象
  this.engine.on('connection', this.onconnection.bind(this));
  return this;
};

//连接回调,参数为engine.io包下面的Socket类
Server.prototype.onconnection = function(conn){
  debug('incoming connection with id %s', conn.id);
  //创建客户端,参数为服务对象、底层Socket对象
  var client = new Client(this, conn);
  //客户端连接到根命名空间
  client.connect('/');
  return this;
};

//查找指定名称命名空间
Server.prototype.of = function(name, fn){
  //命名空间必须以/开头
  if (String(name)[0] !== '/') name = '/' + name;

  //查找获取命名空间对象
  var nsp = this.nsps[name];
  if (!nsp) {
    debug('initializing namespace %s', name);
    //不存在要初始化
    nsp = new Namespace(this, name);
    //存储到服务
    this.nsps[name] = nsp;
  }
  //命名空间添加回调
  if (fn) nsp.on('connect', fn);
  return nsp;
};

//关闭服务器
Server.prototype.close = function(fn){
  //遍历根命名空间下所有Socket连接,并关闭
  for (var id in this.nsps['/'].sockets) {
    if (this.nsps['/'].sockets.hasOwnProperty(id)) {
      this.nsps['/'].sockets[id].onclose();
    }
  }
  //关闭引擎
  this.engine.close();
  //关闭http服务器
  if (this.httpServer) {
    this.httpServer.close(fn);
  } else {
    fn && fn();
  }
};

//所有的Emitter原型上的函数数组
var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){
  return typeof Emitter.prototype[key] === 'function';
});

//连接这些个方法
emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress']).forEach(function(fn){
  //在服务对象上设置这些函数
  Server.prototype[fn] = function(){
    //内部调用根命名空间对象的同名函数
    return this.sockets[fn].apply(this.sockets, arguments);
  };
});

//遍历['json','volatile','local']标志
Namespace.flags.forEach(function(flag){
  //将标志定义在服务对象原型上
  Object.defineProperty(Server.prototype, flag, {
    //getter
    get: function() {
      this.sockets.flags = this.sockets.flags || {};
      //设置根命名空间对象上的指定标志为true
      this.sockets.flags[flag] = true;
      return this;
    }
  });
});

/**
 * BC with `io.listen`
 */
//在类上定义一个listen函数,等于构造函数
Server.listen = Server;


 

可以看到服务对象上只要有以下属性:

1.nsps:命名空间对象的存储,属性名为空间名称,属性名为命名空间对象,默认值生成了一个根命名空间

2._path:访问路径,即跟在域名:端口后的上下文路径,貌似下一阶路径会作为命名空间,默认为socket.io

3._serveClient:是否提供客户端socket.io.js文件的服务

4.parser:解析器

5.encoder:编码器,用于对数据包编码,二进制或者base64

6._adapter:适配器类,用于在命名空间内分配房间,默认使用socket.io-adapter包中Adapter类

7._origins:允许请求源,用于验证请求是否允许处理,默认为*:*即处理所有请求

8.sockets:根命名空间对象,每个客户端默认连接到根命名空间

9.eio:engine.io包中的服务对象,用于发送数据包等,等同于engine属性

10.httpServer:http包中服务对象,一开始会采用http来处理,如果支持websocket协议,经过协议升级会使用websocket来传输数据,如果不支持,使用http轮询来模拟

 

可以看到,服务对象与底层引擎的连接在于引擎的connection事件,每当底层引擎上触发连接事件时,调用服务类的onconnection函数,传递一个底层的Socket连接,构造一个客户端对象,并且连接到根命名空间

 

其他的就是代理了对根命名空间对象上一些标志、属性的访问,提供了客户端js文件的服务等等

 类似资料: