iT邦幫忙

DAY 7
4

且戰且走HTML5系列 第 7

且戰且走HTML5(7) 進階的Chat應用

管理連線、利用Storage來存放變數其實也是Socket.IO內建的功能。另外,也稍微追蹤一下原始碼,看看要怎麼做出更複雜的功能。
管理連線主要有兩個部分。Socket.IO只要透過設定,就可以讓他斷線後自動連線。瀏覽器端支援許多連線、重新連線的事件,可以用來顯示目前的連線狀態。

另外,把一些設定例如room、id等用變數保存,在單機、單行程時沒問題,但是要使用Cluster或是跨機器時,這些變數就無法共用。Socket.IO內建了Storage引擎,並且做好簡單的介面,讓各種不同的storage有統一的操作方法,這樣要更換時不用改程式,只要透過設定來替換就可以了。

先來看一下伺服器:

var fs = require('fs'),
url = require('url'),
app = require('http').createServer(function(req, res) {
	var filename = '',
	resource = url.parse(req.url).pathname;
	switch(resource) {
		case '/test832a.js':
			console.log(resource);
			filename = __dirname + resource;
			res.setHeader('Content-Type', 'text/plain');
			break;
		default:
			filename = __dirname + '/test832.html';
			res.setHeader('Content-Type', 'text/html');
			break;
	}
	fs.readFile(filename, function(err, data) {
		if(err) {
			res.writeHead(500);
			return res.end('Error reading resource.');
		} else {
			res.writeHead(200);
			res.end(data);
		}
	});
}),
io = require('socket.io').listen(app)
rooms = require('./test832a');
accounts = require('./test832b');

var RedisStore = require('socket.io/lib/stores/redis'),
redis = require('redis'),
pub = redis.createClient(),
sub = redis.createClient(),
client = redis.createClient();

io.configure(function() {
	io.set('transports', ['websocket']);
	io.set('store', new RedisStore({
  		redisPub: pub,
		redisSub: sub,
		redisClient: client
	}));
});

var idmap = {};

io.sockets.on('connection', function(socket) {
	console.dir(socket.id);
	socket.on('setnickname', function(m) {
		if(typeof io.nicknames === 'undefined') {
			io.nicknames = {};
			io.nicknames[m] = {count:0};
			socket.set('nickname', m, function() {
				socket.emit('nicknamesuccess', {nickname:m,id:socket.id});
			})
			idmap[socket.id] = m;
		} else {
			if(typeof io.nicknames[m] === 'undefined') {
				io.nicknames[m] = {count:0};
				socket.set('nickname', m, function() {
					socket.emit('nicknamesuccess', {nickname:m,id:socket.id});
				});
				idmap[socket.id] = m;
			} else {
				io.nicknames[m].count++;
				var t = m + '' + io.nicknames[m].count;
				socket.set('nickname', t, function() {
					socket.emit('nicknamefail', {nickname:t,id:socket.id});
					t = null;
				});
				idmap[socket.id] = t;
			}
		}
	});
	socket.on('join', function(room) {
		if(checkroom(rooms, room)) {
			socket.set('room', room, function() {
				socket.get('nickname', function(err, nickname) {
					if(!err) {
						socket.join(room);
						socket.broadcast.in(room).emit('system', nickname + ' has joined this room.');
						socket.emit('joinroomsuccess', {room:room});
					}
				});
			});
		}
	});
	socket.on('leave', function() {
		socket.has('room', function(error, has) {
			if(has) {				
				socket.get('room', function(error, room) {
					if(!error) {					
						socket.get('nickname', function(error, nickname) {
							if(!error) {
								socket.broadcast.in(room).emit('system', nickname + ' has left this room.');
								socket.leave(room);
							}
						});
					}
				});
			}
		});
	});
	socket.on('post', function(m) {
		socket.has('room', function(error, has) {
			if(has) {
				socket.get('room', function(error, room) {
					socket.broadcast.in(room).emit('msg', m);
				});
			} else {
				socket.emit('warning', 'You should chooese a chat room first.');
			}
		});
	});
	socket.on('disconnect', function() {
		socket.has('room', function(error, has) {
			if(has) {
				socket.get('room', function(error, room) {
					if(!error) {
						socket.get('nickname', function(error, nickname) {
							socket.broadcast.in(room).emit('system', nickname + ' has left this room.');
						});
					}
				});
			}
		});
	});
});

app.listen(80);

function checkroom(rooms, room) {
	var ret = false;
	for(var i in rooms) {
		console.log(i);
		if(room==i && rooms[i] == 'public') {
			ret = true;
		} 
	}
	return ret;
}

在io.configure()中的動作就是在進行設定。在這之前先使用redis模組取得client,然後依照Socket.IO的範例作設定就可以。另外,也把傳輸設定為只使用WebSocket。

不過,由於Storage會使用Callback的非同步操作語法,程式看起來會比較複雜XD

另外看一下瀏覽器:

<?DOCTYPE html>

<meta charset='utf-8'>

<style>
.container {
	font-size: 12px;
	border-radius: 10px;
	border: solid 1px #336699;
	padding: 15px 15px 15px 15px;
	line-height: 20px;
	width: 400px;

}
.disabled {
	color: gray;
}
.enabled {
	color: true;
}
div #status {
	background-color: #99CCFF;
	color: gray;
	font-size: 12ps;
	width: 100%;
	text-align: center;
	vertical-align: bottom;
}
</style>
<script src='/socket.io/socket.io.js'></script>
<script src='http://code.jquery.com/jquery-1.8.2.min.js'></script>
<script>
$(document).ready(function() {
	var nickname = '';
	var room = '';
	var rooms = {};
	var tries = 0;
	var id = '';
// connection control start	
	var socket = io.connect('ws://localhost', {
		transports: ['websocket'],
		"try multiple transports": false,
		reconnect: true
	});// configure connection settings
	socket.on('connect', function() {
		tries = 0;
		$('#status').html('Server Connected.');
	});
	socket.on('connecting', function() {
		$('#status').html('Connecting...');
	});
	socket.on('connect_failed', function() {
		$('#status').html('Connect Failed.');
	});
	socket.on('reconnect', function() {
		tries = 0;
		$('#status').html('Server Reconnected.');
	});
	socket.on('reconnecting', function() {
		tries++;
		$('#status').html('Try to reconnect '+tries+' times...');
	});
	socket.on('reconnect_failed', function() {
		$('#status').html('Reconnect Failed.');
	});
// connection control end	
	$('#form1').submit(function(e) {
		e.preventDefault();
		socket.emit('setnickname', $('#nickname').val());
	});
	socket.on('nicknamesuccess', function(m) {
		nickname = m.nickname;
		id=m.id;
		$('#nickname').prop('disabled', true);
		$('#sendnickname').prop('disabled', true);
		$('#rooms').prop('disabled', false);
		$('#msg').prop('disabled', false);
		$('#send').prop('disabled', false);
		$('#msglabel').prop('className', 'enabled');
		$('#rooms').focus();
	});
	socket.on('nicknamefail', function(m) {
		alert('Nickname conflict. Your nickname will be changed to "'+m+'"');
		nickname = m.nickname;
		id = m.id;
		$('#nickname').val(m);
		$('#nickname').prop('disabled', true);
		$('#sendnickname').prop('disabled', true);
		$('#rooms').prop('disabled', false);
		$('#msg').prop('disabled', false);
		$('#send').prop('disabled', false);
		$('#msglabel').prop('className', 'enabled');
		$('#rooms').focus();
	});
	$('#form2').submit(function(e) {
		e.preventDefault();
		var m = $('#msg').val();
		socket.emit('post', {nickname: nickname, msg: m});
		$('#msg').val('');
		updateMsg({nickname:nickname,msg:m});
	})
	socket.on('msg', function(m) {
		updateMsg(m);
	});
	$('#rooms').bind('change', function() {
		var value = $(this).val();
		if(value!=='') {
			socket.emit('leave');
			socket.emit('join', value);
			$('#msg').focus();
		}
	});
	socket.on('joinroomsuccess', function(m) {
		if(room !== '') {
			rooms[room] = $('#panel').val();
			room = m.room;
			if(typeof rooms[room] !== 'undefined') {
				$('#panel').val(rooms[room]+'\n[old messages before you left this room...]');
			} else {
				$('#panel').val('');
			}
		} else {
			room = m.room;
		}
	});
	socket.on('member', function(m) {
		var members = $('#members');
		members.html('');
		var option = document.createElement('option');
		option.value = '';
		option.innerHTML = 'All';
		console.log(m);
		members.append(option);
		for(var i in m) {
			if(i.indexOf(id)<0) {
				option = document.createElement('option');
				option.value = i;
				option.innerHTML = m[i];
				members.append(option);
			}
		}
		option = null;
	});
	socket.on('system', function(m) {
		updateMsg({nickname:'SYSTEM', msg:m});
	});
	socket.on('warning', function(m) {
		alert(m);
	});
	define('/test832a.js', function(conf) {
		var sel = $('#rooms');
		var opt = document.createElement('option');
		opt.value = '';
		opt.innerHTML = '';
		sel.append(opt);
		for(var i in conf) {
			var opt = document.createElement('option');
			opt.value = i;
			opt.innerHTML = i;
			sel.append(opt);
		}
		opt = null;
	});
	$('#nickname').focus();

	function updateMsg(msg) {
		var ta = $("#panel");
		var t = new Date();
		var s = t.getHours() + ':' + t.getMinutes() + ':' + t.getSeconds();
		var m = '[ ' + msg.nickname + ' (' + s + ')]: ' + msg.msg;
		ta.val(ta.val()+'\n'+m);
		setTimeout(function(){
			ta.scrollTop(ta[0].scrollHeight - ta.innerHeight());
		},10);
	}
	function define(url, cb) {
		$.ajax({
			url:url
		})
		.done(function(data) {
			var f = new Function('var module={},exports=null;\n'+data+'\nif(typeof module.exports !== "undefined") {\nreturn module.exports;\n}\nif(null != exports) {\n return exports;\n}');
			var exports = f();
			if(typeof exports !== 'undefined') {
				cb(exports);
				return;
			}
		});
	}
});
</script>


<div class="container">
	<textarea cols='54' rows='24' id='panel' readonly></textarea><br>
	<form id='form1' name='form1'>
		<label id='nicknamelabel' class='enabled'>Your Nickname: </label><input type='text' size='20' id='nickname'><input type='submit' value='send' id='sendnickname'>
	</form>
	<label id="roomlabel">Rooms: </label><select id='rooms' disabled></select><label>Members: </label><select id="members"></select>
	<form id='form2' name='form2'>
		<label id='msglabel' class='disabled'>Message: </label><input type='text' size='54' id='msg' disabled><input type='submit' value='send' id='send' disabled>
	</form>
	<div id='status'></div>
</div>

增加的部份,主要是在顯示連線狀況。在伺服器端使用不同的Storage,對於前端並沒有影響。

仔細追蹤一下Socket.IO的原始碼,可以發現他會為每個連線建立一個獨特的id(socket.id),然後用他來做控制。另外一個重點是,在socket.manager.rooms以及socket.manager.roomClients中,有client->room與room->client的對應資料,透過這些組合,其實就可以取得各個連線的socket物件,透過他來對個人傳送訊息。這些...明天再戰吧。(本來想要今天弄完,但是來不及了XD

另外,透過namespace(其實就像是URI),他可以用同一個應用程式,透過不同的URI路徑提供不同的服務。不過,其實內部的管理是跟room差不多。(仔細追蹤會發現,room的名稱會被偷偷加上'/'prefix,就像URI路徑。


上一篇
且戰且走HTML5(6) 多人協同運作
下一篇
且戰且走HTML5(8) Socket.IO的架構與連線管理機制
系列文
且戰且走HTML530

尚未有邦友留言

立即登入留言