iT邦幫忙

DAY 25
4

且戰且走HTML5系列 第 25

且戰且走HTML5(25) 解決ws.io模組的問題

找到問題所在...該來完成ws.io的單元了XD

這種事情要在腦袋清楚的時候來解決阿...趕稿的時候一定想不出來Orz
由於架構模仿了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就好的...


上一篇
且戰且走HTML5(24) 完成ws模組的包裝
下一篇
且戰且走HTML5(26) 使用ws.io完成資源共享
系列文
且戰且走HTML530

1 則留言

我要留言

立即登入留言