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文件的服务等等