Socket.io之Client类

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上的回调,可见数据包是属于指定命名空间的



你可能感兴趣的:(socket.io)