var parser = require('socket.io-parser');
var debug = require('debug')('socket.io:client');
var url = require('url');
module.exports = Client;
//客户端类,conn参数为engine.io包下面的Socket对象
//代表一个客户端对服务器的连接
function Client(server, conn){
//服务对象
this.server = server;
//engine.io包下面的Socket对象,底层连接对象,如果支持,经过协议升级,可以采用websocket传输数据包
this.conn = conn;
//编码器
this.encoder = server.encoder;
//解码器
this.decoder = new server.parser.Decoder();
//sid
this.id = conn.id;
//请求对象
this.request = conn.request;
//在底层socket连接中设置监听器
this.setup();
//客户端内顶层socket缓存,属性名为id,属性值为socket
this.sockets = {};
//客户端内对应各个命名空间的socket的缓存
//属性名为命名空间名称,属性值为socket,也就是一个客户端只能有一个加入指定命名空间的socket
this.nsps = {};
//命名空间连接缓存,只有/命名空间存在时,才会连接缓存中的其他命名空间
this.connectBuffer = [];
}
//设置事件监听器
Client.prototype.setup = function(){
//监听函数上下文对象为Client
this.onclose = this.onclose.bind(this);
this.ondata = this.ondata.bind(this);
this.onerror = this.onerror.bind(this);
this.ondecoded = this.ondecoded.bind(this);
//解码监听器设置到解码器上
this.decoder.on('decoded', this.ondecoded);
//在engine.io包下面的Socket对象上设置监听器
this.conn.on('data', this.ondata);
this.conn.on('error', this.onerror);
this.conn.on('close', this.onclose);
};
//连接方法,name为命名空间名称,每个客户端建立时会自动连接到根命名空间
Client.prototype.connect = function(name, query){
debug('connecting to namespace %s', name);
//命名空间对象
var nsp = this.server.nsps[name];
//命名空间对象不存在
if (!nsp) {
//发送错误数据包,无效命名空间
this.packet({ type: parser.ERROR, nsp: name, data : 'Invalid namespace'});
return;
}
//要连接的不是根命名空间,且客户端内没有对根命名空间的Socket连接时,暂时不连接其他的命名空间,先缓存他们
//要保证根命名空间第一个被连接
if ('/' != name && !this.nsps['/']) {
//加入连接缓存,暂时不连接
this.connectBuffer.push(name);
return;
}
var self = this;
//在命名空间内加入客户端对象、查询对象
//返回顶层socket对象,一个客户端对象对应多个顶层socket对象
var socket = nsp.add(this, query, function(){
//缓存顶层socket
//因为一个客户端可能对应了与不同命名空间连接的顶层socket
//表明在一个客户端内,对一个命名空间只有一个Socket连接
self.sockets[socket.id] = socket;
self.nsps[nsp.name] = socket;
//如果是根命名空间,且连接缓存存在,遍历连接在根命名空间之前连接的命名空间
//实际上很难发生,因为在服务器的连接事件中,每个Client对象建立之初,就会先连接根命名空间
if ('/' == nsp.name && self.connectBuffer.length > 0) {
//遍历连接缓存连接它们,保证/命名空间第一个连接
self.connectBuffer.forEach(self.connect, self);
//清空缓存
self.connectBuffer = [];
}
});
};
//断开客户端连接
Client.prototype.disconnect = function(){
//遍历所有客户端对所有命名空间的Socket连接,断开他们
for (var id in this.sockets) {
if (this.sockets.hasOwnProperty(id)) {
this.sockets[id].disconnect();
}
}
//清空
this.sockets = {};
this.close();
};
//移除一个Socket
Client.prototype.remove = function(socket){
if (this.sockets.hasOwnProperty(socket.id)) {
//从两个缓存中删除socket
var nsp = this.sockets[socket.id].nsp.name;
delete this.sockets[socket.id];
delete this.nsps[nsp];
} else {
debug('ignoring remove for %s', socket.id);
}
};
//关闭客户端
Client.prototype.close = function(){
//顶层socket状态我open
if ('open' == this.conn.readyState) {
debug('forcing transport close');
//关闭底层socket
this.conn.close();
//调用关闭回调
this.onclose('forced server close');
}
};
//写入数据包数组
Client.prototype.packet = function(packet, opts){
opts = opts || {};
var self = this;
//向底层socket写入编码数据包数组的函数
function writeToEngine(encodedPackets) {
//底层socket不可写,直接返回
if (opts.volatile && !self.conn.transport.writable) return;
//遍历已编码数据包
for (var i = 0; i < encodedPackets.length; i++) {
//使用底层Socket写入数据包,选项为压缩与否
self.conn.write(encodedPackets[i], { compress: opts.compress });
}
}
//底层socket状态为open
if ('open' == this.conn.readyState) {
debug('writing packet %j', packet);
//如果没有预编码,貌似只有广播会编码
if (!opts.preEncoded) {
//对数据包编码,并发送
this.encoder.encode(packet, writeToEngine); // encode, then write results to engine
} else {
//已经预编码,直接发送
writeToEngine(packet);
}
} else {
debug('ignoring packet write %j', packet);
}
};
//有传输数据进入时回调
Client.prototype.ondata = function(data){
try {
//添加数据到解码器
this.decoder.add(data);
} catch(e) {
this.onerror(e);
}
};
//当数据包被完全解码时调用
Client.prototype.ondecoded = function(packet) {
//如果是连接数据包,代表要连接到一个命名空间
if (parser.CONNECT == packet.type) {
//解析出数据包命名空间中的命名空间名称,并连接它
this.connect(url.parse(packet.nsp).pathname, url.parse(packet.nsp, true).query);
}
//不是连接数据包,那么数据包肯定对应了一个已存在命名空间
else {
//根据命名空间名称获取顶层socket
var socket = this.nsps[packet.nsp];
if (socket) {
process.nextTick(function() {
//调用与数据包指定命名空间对应的Socket连接上的数据包回调
socket.onpacket(packet);
});
} else {
debug('no socket for namespace %s', packet.nsp);
}
}
};
//错误回调
Client.prototype.onerror = function(err){
//遍历所有Socket,调用每个Socket的错误回调
for (var id in this.sockets) {
if (this.sockets.hasOwnProperty(id)) {
this.sockets[id].onerror(err);
}
}
//出现错误关闭连接
this.conn.close();
};
//关闭回调
Client.prototype.onclose = function(reason){
debug('client close with reason %s', reason);
//销毁方法,移除事件监听器
this.destroy();
//调用所有Socket上的关闭回调
for (var id in this.sockets) {
if (this.sockets.hasOwnProperty(id)) {
this.sockets[id].onclose(reason);
}
}
this.sockets = {};
//销毁解码器
this.decoder.destroy();
};
//销毁方法,在底层socket上移除各种监听器
Client.prototype.destroy = function(){
this.conn.removeListener('data', this.ondata);
this.conn.removeListener('error', this.onerror);
this.conn.removeListener('close', this.onclose);
this.decoder.removeListener('decoded', this.ondecoded);
};
在Client类上,有以下属性:
1.conn:底层Socket连接,对应了客户端与服务器的连接通道,用来发送数据包
2.encoder:编码器,数据包发送之前要进行编码,base64或者二进制
3.decoder:解码器,接收到的数据包要先加入解码器,解码完毕后触发回调,根据数据包类型进行处理
4.sockets:Socket对象的哈希,索引为id
5.nsps:Socket对象的哈希,索引为命名空间名称,明确了一个客户端对一个命名空间只能有一个Socket连接
6.connectBuffer:命名空间连接缓存,在根命名空间连接之前,其他所有命名空间都只能被缓存,当根命名空间连接之后,在回调函数中连接缓存的命名空间
另外就是对数据包的处理,所有数据包必须先解码,解码成功后,在回调函数中根据类型分发
parser.CONNECT:连接数据包,可以在pakcet.nsp属性中解析出命名空间名称,然后连接指定命名空间
其他类型:根据packet.nsp(即命名空间名称)获取对应Socket对象,将数据包发送给Socket上的回调,可见数据包是属于指定命名空间的