找到問題所在...該來完成ws.io的單元了
這種事情要在腦袋清楚的時候來解決阿...趕稿的時候一定想不出來
由於架構模仿了Socket.IO,問題變得比較複雜。Socket.IO核心在於三個物件的協作,Manager, Namespace, Socket。以Socket.IO首頁的例子:
var io = require('socket.io').listen(80);
這個io,其實就是Manager的instance,這在每個伺服器只會存在一個。然後:
io.sockets.on('connection', ...);
這個sockets其實是一個getter,用來取得預設namespace的Namespace物件。如果不是使用預設的Namespace,會需要使用:
io.of('/namespace').on('connection', ...);
來指派處理這個namespace的事件處理函數。Namespace是與使用到的namespace對應,所以使用到多少namespace就會有多少個實體。
接下來,在on connection事件中,會收到一個socket:
io.sockets.('connection', function(socket) {
......
});
裡面的操作,就都會透過socket物件,他是Socket的instance。這個instance,是每個連線都會產生一個。
原理是這樣啦,但是我在包裝時犯了不少錯誤。關鍵在於處理自定事件的函數,我把它存放在Manager,但是每個事件只會存一個...這樣問題就來了,這些函數都是Closure,在函數裡面使用到socket物件時,這個物件是參考到他自己的closure,但是每個連線其實會有不同的Socket instance,這些函數透過closure取得的應該是不同的socket物件,所以正確的處理方式是,每個Socket的事件處理函數都要存起來,否則使用到的socket物件其實是某一個特定連線的socket物件。
最後正確的實作是,ws的onconnection以及onmessage是在Namespace物件中實作。透過呼叫Socket.on()定義的事件處理函數,實際上是存放在Manager物件,依照namespace/room/Socket.id/event name的規則來存放,這樣在Namespace中就可以透過Manager物件,依照規則呼叫適當的事件處理函數。
對於使用者來說,只要知道事件名稱以及傳遞的資料,而對ws來說,實際傳遞的是一個物件:name屬性代表事件名稱,data屬性存放傳遞的資料。這樣client端的ws.io.js就可以依照同樣的規則剖析出事件以及資料,然後觸發client端的事件。
程式碼如下,首先是Manager.js
var Namespace = require('./Namespace');
var Manager = module.exports = function(server) {
this.server = server;
this.namespaces = {};
this.store = {};
this.rooms = {};
this.handlers = {};
};
Manager.prototype.of = function(ns) {
var namespace = new Namespace(this.server, this, ns);
return namespace;
};
Manager.prototype.__defineGetter__('sockets', function() {
return this.of('/');
});
然後是Namespace.js:
var Socket = require('./Socket');
var wss = require('ws').Server;
var Namespace = module.exports = function(server, manager, ns) {
this.namespace = ns;
this.socket = Socket;
this.wss = new wss({server: server});
this.manager = manager;
this.server = server;
this.manager.handlers[ns] = {};
this.manager.namespaces[this.namespace] = {};
}
Namespace.prototype.on = function(conn, handler) {
var self = this;
if(conn == 'connection') {
this.wss.on('connection', function(ws) {
var id = Math.random()*10000 + '-' + new Date().getTime();
var socket = new Socket(self.server, self.manager, self, id, ws);
if(!!self.manager.namespaces[self.namespace]['all']) {
self.manager.namespaces[self.namespace]['all'][id] = socket;
} else {
self.manager.namespaces[self.namespace]['all'] = {};
self.manager.namespaces[self.namespace]['all'][id] = socket;
}
if(!self.manager.handlers[self.namespace]) {
self.manager.handlers[self.namespace] = {};
}
if(!self.manager.handlers[self.namespace][id]) {
self.manager.handlers[self.namespace][id] = {};
}
handler.call(socket, socket);
ws.on('message', function(data, flags) {
var args = JSON.parse(data);
if(!!self.manager.handlers[self.namespace][id][args.name]) {
var ev = args.name;
delete args.name;
self.manager.handlers[self.namespace][id][ev].call(self, args.data);
ev = null;
} else {
console.log('no handler: ');
}
});
});
}
}
接下來是Socket.js
var Socket = module.exports = function(server, manager, namespace, id, ws) {
this.id = id;
this.server = server;
this.manager = manager;
this.ws = ws;
this.namespace = namespace.namespace;
this.flags = {"allnotme":false,"inroom":false};
};
Socket.prototype.on = function(name, handler) {
this.manager.handlers[this.namespace][this.id][name] = handler;
};
Socket.prototype.emit = function(name, data, opt) {
var room = '';
if(!this.flags.inroom) {
room = 'all';
} else {
room = this.flags.inroom;
}
var targets = [];
if(this.flags['allnotme']) {
for(var i in this.manager.namespaces[this.namespace][room]) {
if(this.flags.allnotme) {
if(i.indexOf(this.id)<0) {
targets.push(this._findSocket(this.namespace, room, i));
}
} else {
targets.push(this._findSocket(this.namespace, room, i));
}
}
} else {
targets.push(this);
}
var self = this;
//data['name'] = name;
targets.forEach(function(socket) {
socket.ws.send(JSON.stringify({name:name,data:data}));
});
this.flags['allnotme'] = false;
this.flags['inroom'] = false;
};
Socket.prototype.__defineGetter__('broadcast', function() {
this.flags['allnotme'] = true;
return this;
});
Socket.prototype._findSocket = function(ns, room, id) {
if(!!this.manager) {
if(!!this.manager.namespaces[ns]) {
if(!!this.manager.namespaces[ns][room]) {
if(!!this.manager.namespaces[ns][room][id]) {
return this.manager.namespaces[ns][room][id];
}
}
}
}
};
Socket.prototype.join = function(room) {
if(!!this.manager.namespaces[this.namespace][room]) {
this.manager.namespaces[this.namespace][room][this.id] = this;
} else {
this.manager.namespaces[this.namespace][room] = {};
this.manager.namespaces[this.namespace][room][this.id] = this;
}
};
Socket.prototype.leave = function(room) {
if(!!this.manager.namespaces[this.namespace][room]) {
if(!!this.manager.namespaces[this.namespace][room][this.id]) {
delete this.manager.namespaces[this.namespace][room][this.id];
}
}
var i = 0;
for(var a in this.manager.namespaces[this.namespace][room]) {
i++
}
if(i===0) {
delete this.manager.namespaces[this.namespace][room];
}
}
Socket.prototype.in = function(room) {
var check = false;
if(!!this.manager.namespaces[this.namespace][room]) {
if(!!this.manager.namespaces[this.namespace][room][this.id]) {
check = true;
}
}
if(check) {
this.flags['inroom'] = room;
}
return this;
}
Socket.prototype.set = function(name, value, cb) {
if(!this.manager.store[this.id]) {
this.manager.store[this.id] = {};
}
this.manager.store[this.id][name] = value;
cb();
};
Socket.prototype.get = function(name, cb) {
if(!!this.manager.store[this.id]) {
if(!!this.manager.store[this.id][name]) {
cb(false, this.manager.store[this.id][name]);
} else {
cb(true);
}
} else {
this.manager.store[this.id] = {};
cb(true);
}
};
Socket.prototype.has = function(name, cb) {
if(!!this.manager.store[this.id]) {
if(!!this.manager.store[this.id][name]) {
cb(false, true);
} else {
cb(true, true);
}
} else {
this.manager.store[this.id] = {};
cb(true, false);
}
};
Client端使用的ws.io.js:
(function(window, undefined) {
var io = window.io = {
connect: function(addr) {
if(addr.indexOf('http')==0) addr = addr.replace('http','ws');
console.log(addr);
return new Socket(new WebSocket(addr));
}
};
function Socket(ws) {
var handlers = {};
var gotbinary = false;
var binaryinfo = {};
//setInterval(function(){console.log(ws.readyState)}, 1000);
this.on = function(name, handler) {
console.log('on', name, handler)
if(!!handlers[name]) {
handlers[name].push(handler);
} else {
handlers[name] = [];
handlers[name].push(handler);
}
};
this.emit = function(name, data, opt) {
console.log('emit', data);
if(opt) {
var type = (!!data.type)? data.type:opt;
ws.send(JSON.stringify({name:'blob',data:{type:type}}));
ws.binaryType = opt.binaryType;
ws.send(data);
} else {
ws.send(JSON.stringify({name:name,data:data}));
}
};
ws.onmessage = function(msg) {
console.log('onmessage', msg);
switch(typeof msg.data) {
case 'object':
break;
case 'string':
console.log(msg);
if(!!msg.data) {
var data = JSON.parse(msg.data);
console.log(data);
switch(data.name) {
case 'blob':
gotbinary = true;
binaryinfo = data.data;
break;
default:
if(!!handlers[data.name]) {
handlers[data.name].forEach(function(fn) {
fn.call(this, data.data);
});
}
break;
}
}
break;
}
}
}
})(window);
把之前寫的chat與塗鴉白板稍微調整後來測試(把require('socket.io')改成require('./ws.io'),然後處理一下載入ws.io.js的方式),終於可以正確運作了。
不過使用ws的目的,是為了要可以傳送blob或ArrayBuffer (typed array)。不過傳送這些資料跟目前的使用方式有一點衝突,因為在傳送blob時或ArrayBuffer時,其實不能夾帶其他的資訊,這樣就無法得知他實際上要觸發哪個事件,一些額外的資訊,例如blob的mime type、檔名等資訊,也無法直接傳送。要把這些跟Socket.IO的使用方式結合,還需要花一些功夫。目前的想法是,透過兩次傳送來解決,然後使用一個內定的event name。先把相關資訊透過這個event來傳送,伺服器收到資訊後,會改變一些內部狀態,等待收到blob或ArrayBuffer後,才用同樣的方式分兩次傳給其他client。這些就留到明天來試試看可不可行。
原本只是想稍微包裝一下ws就好的...