Socket.io之Socket類
阿新 • • 發佈:2019-01-25
var Emitter = require('events').EventEmitter; var parser = require('socket.io-parser'); var url = require('url'); var debug = require('debug')('socket.io:socket'); module.exports = exports = Socket; //事件名陣列 exports.events = [ //錯誤事件 'error', //連線事件 'connect', //斷開連線事件 'disconnect', //正在斷開 'disconnecting', //新監聽器 'newListener', //移除監聽器 'removeListener' ]; var flags = [ 'json', 'volatile', 'broadcast' ]; var emit = Emitter.prototype.emit; //頂層Socket建構函式,對應一個客戶端對指定名稱空間的連線 function Socket(nsp, client, query){ //名稱空間物件 this.nsp = nsp; //服務物件 this.server = nsp.server; //名稱空間介面卡 this.adapter = this.nsp.adapter; //id,如果不是根名稱空間,則為空間名稱加上#加上客戶端id,否則為客戶端id this.id = nsp.name !== '/' ? nsp.name + '#' + client.id : client.id; //客戶端 this.client = client; //客戶端連線,底層socket this.conn = client.conn; //已加入的房間名雜湊,索引為房間名,值也是房間名 this.rooms = {}; //資料包id,與應答回撥函式的雜湊 this.acks = {}; //是否已連線 this.connected = true; //是否已斷開連線 this.disconnected = false; //構建握手,返回一個握手http請求的資訊,這個http請求就是最初建立連線的請求 this.handshake = this.buildHandshake(query); //中介軟體? this.fns = []; //標誌物件,屬性名為標誌名,屬性值為true與false this.flags = {}; //要被廣播的房間名陣列 this._rooms = []; } //繼承事件發射器 Socket.prototype.__proto__ = Emitter.prototype; //遍歷標誌 flags.forEach(function(flag){ //定義獲取標誌的get方法 Object.defineProperty(Socket.prototype, flag, { get: function() { //指定標誌物件中對應位為true,即獲取一次屬性,就變為true this.flags[flag] = true; return this; } }); }); //獲取請求物件,從底層連線中獲取請求物件 Object.defineProperty(Socket.prototype, 'request', { get: function() { return this.conn.request; } }); //構建握手,query為建構函式引數 Socket.prototype.buildHandshake = function(query){ var self = this; //構建查詢字串 function buildQuery(){ //請求的查詢物件 var requestQuery = url.parse(self.request.url, true).query //將指定查詢物件與請求查詢物件合併 return Object.assign({}, query, requestQuery); } //返回握手的http請求資訊 return { //頭資訊 headers: this.request.headers, //日期 time: (new Date) + '', //請求ip address: this.conn.remoteAddress, //請求來源是否存在 xdomain: !!this.request.headers.origin, //是否是安全請求 secure: !!this.request.connection.encrypted, //釋出日期 issued: +(new Date), //請求url url: this.request.url, //查詢物件 query: buildQuery() }; }; //發射事件方法 Socket.prototype.emit = function(ev){ //如果事件名引數包含在指定陣列中 if (~exports.events.indexOf(ev)) { //呼叫發射方法 emit.apply(this, arguments); return this; } //複製引數陣列? var args = Array.prototype.slice.call(arguments); //構建資料包 var packet = { //型別為事件 type: parser.EVENT, //資料為事件型別 data: args }; //最後一個引數為函式 if (typeof args[args.length - 1] === 'function') { if (this._rooms.length || this.flags.broadcast) { throw new Error('Callbacks are not supported when broadcasting'); } debug('emitting packet with ack id %d', this.nsp.ids); this.acks[this.nsp.ids] = args.pop(); packet.id = this.nsp.ids++; } //獲取要被廣播的房間陣列,注意要被廣播的房間名與已加入的房間是兩個概念 var rooms = this._rooms.slice(0); var flags = Object.assign({}, this.flags); //重置陣列,為什麼要重置?廣播一次就要重置? this._rooms = []; this.flags = {}; //如果存在加入的房間或者標誌位廣播,廣播就是想同一名稱空間下的其他客戶端的Socket連線寫資料包 if (rooms.length || flags.broadcast) { //使用介面卡進行廣播,對指定名稱房間裡的所有連線廣播資料包,本連線除外 this.adapter.broadcast(packet, { except: [this.id], rooms: rooms, flags: flags }); } //不廣播的話, else { this.packet(packet, flags); } return this; }; //新增一個要被廣播的房間 Socket.prototype.to = Socket.prototype.in = function(name){ //新增房間名到陣列 if (!~this._rooms.indexOf(name)) this._rooms.push(name); return this; }; //傳送message事件 Socket.prototype.send = Socket.prototype.write = function(){ //複製引數 var args = Array.prototype.slice.call(arguments); //引數陣列前新增message元素 args.unshift('message'); //發射message事件,會進行廣播或者使用底層Socket寫資料包 this.emit.apply(this, args); return this; }; //寫出資料包 Socket.prototype.packet = function(packet, opts){ //資料包設定名稱空間名稱 packet.nsp = this.nsp.name; opts = opts || {}; //是否壓縮 opts.compress = false !== opts.compress; //使用客戶端傳送資料包 this.client.packet(packet, opts); }; //加入多個房間 Socket.prototype.join = function(rooms, fn){ debug('joining room %s', rooms); var self = this; if (!Array.isArray(rooms)) { rooms = [rooms]; } //過濾房間名,只選出其中還未加入的房間 rooms = rooms.filter(function (room) { return !self.rooms.hasOwnProperty(room); }); if (!rooms.length) { fn && fn(null); return this; } //將this.id加入指定房間名陣列對應的房間中去,房間沒有物件,只有一個名稱,相當於名稱空間下的名稱空間 //但是,房間與Socket可以使多對多關係,一個Socket可以加入多個房間,一個房間可以包含多個Socket的id this.adapter.addAll(this.id, rooms, function(err){ if (err) return fn && fn(err); debug('joined room %s', rooms); //遍歷房間名,設定到屬性 rooms.forEach(function (room) { self.rooms[room] = room; }); fn && fn(null); }); return this; }; //離開指定房間 Socket.prototype.leave = function(room, fn){ debug('leave room %s', room); var self = this; //使用介面卡刪除指定房間內的id this.adapter.del(this.id, room, function(err){ if (err) return fn && fn(err); debug('left room %s', room); //刪除自身的房間儲存 delete self.rooms[room]; fn && fn(null); }); return this; }; //離開所有房間 Socket.prototype.leaveAll = function(){ this.adapter.delAll(this.id); this.rooms = {}; }; //連接回調 Socket.prototype.onconnect = function(){ debug('socket connected - writing packet'); //將這個socket加入名稱空間下已連線物件 this.nsp.connected[this.id] = this; //id作為房間名稱,加入指定房間,也就是每個Socket預設會加入自身id標識的房間 this.join(this.id); //如果是根名稱空間且沒有中介軟體 var skip = this.nsp.name === '/' && this.nsp.fns.length === 0; if (skip) { //則資料包已經在初始化握手中傳送過了 //因為每個客戶端連線建立之初都要連線到根名稱空間,那個時候,對應的Socket已經發送過連線資料包了 debug('packet already sent in initial handshake'); } else { //其他名稱空間下要傳送 this.packet({ type: parser.CONNECT }); } }; //每個資料包的回撥 Socket.prototype.onpacket = function(packet){ debug('got packet %j', packet); switch (packet.type) { //事件型別、二進位制事件型別資料包 case parser.EVENT: this.onevent(packet); break; case parser.BINARY_EVENT: this.onevent(packet); break; //應答型別、二進位制應答型別資料包 case parser.ACK: this.onack(packet); break; case parser.BINARY_ACK: this.onack(packet); break; //斷開連線資料包,呼叫斷開連接回調 case parser.DISCONNECT: this.ondisconnect(); break; //錯誤資料包 case parser.ERROR: this.onerror(new Error(packet.data)); } }; //事件資料包回撥 Socket.prototype.onevent = function(packet){ //事件型別資料包資料就是事件型別 var args = packet.data || []; debug('emitting event %j', args); //如果資料包有id,代表與應答有關係 if (null != packet.id) { debug('attaching ack callback to event'); //新增一個應答回撥到引數 args.push(this.ack(packet.id)); } //分發指定型別事件 this.dispatch(args); }; //根據資料包id返回應答回撥函式 Socket.prototype.ack = function(id){ var self = this; var sent = false; //應答回撥函式,傳送應答資料包,與請求資料包有相同id return function(){ // prevent double callbacks if (sent) return; //複製引數 var args = Array.prototype.slice.call(arguments); debug('sending ack %j', args); //傳送資料包,指定id、型別為應答 self.packet({ id: id, type: parser.ACK, data: args }); sent = true; }; }; //應答資料包回撥,整個過程為提問->設定回撥->回答->根據回答資料呼叫回撥 Socket.prototype.onack = function(packet){ //根據資料包id獲取應答回撥 var ack = this.acks[packet.id]; if ('function' == typeof ack) { debug('calling ack %s with %j', packet.id, packet.data); //呼叫應答回撥函式 ack.apply(this, packet.data); delete this.acks[packet.id]; } else { debug('bad ack %s', packet.id); } }; //斷開連線 Socket.prototype.ondisconnect = function(){ debug('got disconnect packet'); this.onclose('client namespace disconnect'); }; Socket.prototype.onerror = function(err){ if (this.listeners('error').length) { this.emit('error', err); } else { console.error('Missing error handler on `socket`.'); console.error(err.stack); } }; //關閉回撥 Socket.prototype.onclose = function(reason){ if (!this.connected) return this; debug('closing socket - reason %s', reason); //發射正在關閉事件 this.emit('disconnecting', reason); //離開所有房間 this.leaveAll(); //離開名稱空間 this.nsp.remove(this); //離開客戶端 this.client.remove(this); this.connected = false; this.disconnected = true; //從名稱空間已連線Socket中刪除 delete this.nsp.connected[this.id]; this.emit('disconnect', reason); }; //傳送錯誤資料包 Socket.prototype.error = function(err){ this.packet({ type: parser.ERROR, data: err }); }; //斷開連線,引數指定是否斷開客戶端連線 Socket.prototype.disconnect = function(close){ if (!this.connected) return this; if (close) { this.client.disconnect(); } else { this.packet({ type: parser.DISCONNECT }); this.onclose('server namespace disconnect'); } return this; }; //設定壓縮標誌 Socket.prototype.compress = function(compress){ this.flags.compress = compress; return this; }; //分發進入的事件型別資料包到監聽器 Socket.prototype.dispatch = function(event){ debug('dispatching an event %j', event); var self = this; //執行完中介軟體之後的回撥,中介軟體錯誤為引數 function dispatchSocket(err) { process.nextTick(function(){ //如果有錯誤傳送錯誤資料包 if (err) { return self.error(err.data || err.message); } //發射指定事件 emit.apply(self, event); }); } // this.run(event, dispatchSocket); }; //設定中介軟體函式? Socket.prototype.use = function(fn){ this.fns.push(fn); return this; }; //對進入事件資料包執行中介軟體函式 Socket.prototype.run = function(event, fn){ var fns = this.fns.slice(0); if (!fns.length) return fn(null); //遞迴執行中介軟體函式 function run(i){ //函式引數為事件名 fns[i](event, function(err){ if (err) return fn(err); if (!fns[i + 1]) return fn(null); run(i + 1); }); } //從下標為0開始執行 run(0); };
可以看到,Socket可以加入多個房間中去,而每個房間中有哪些Socketid,則在名稱空間物件的adapter中進行管理,房間其實就是名稱空間下的一層名稱空間,只不過與Socket為多對多的關係,不像在同一個名稱空間下Socket與Client是一對一關係,在同一個Client下,Socket與Namespace是一對一關係
這裡可以看到一個數據流:
onpacket:接收到一個數據包
->onevent:接收到一個事件型別或二進位制事件型別資料包
->dispatch:分發資料包
->dispatchSocket:分發回撥函式
->emit:觸發指定事件
->this.adapter.broadcast進行廣播或者packet向客戶端寫入資料包