iT邦幫忙

DAY 5
8

且戰且走HTML5系列 第 5

且戰且走HTML5(5) 更深入Socket.IO

除了使用簡單,Socket.IO更內建了一些Chat必須的邏輯,可以減少一些開發的工作。
如果一個Chat只能支援一個聊天室,那功能就太簡單了。今天來看一下,要怎樣利用Socket.IO的內建功能,做出可選擇的不同聊天室。同時添加進入/離開聊天室的通知。

Socket.IO除了基本的訊息傳輸跟廣播,還可以直接建立虛擬的聊天室。使用socket.join('room')就可以加入一個room,使用socket.leave('room')就可以離開。另外,加入某個room後,就可以透過socket.broadcast.in('room').emit送出群播事件給同一個room的其他用戶。如果沒有內建這樣的機制,就需要自己在伺服器端管理連線,雖然要實作也不很困難,不過總是比較麻煩。

修改過的版本,加入了額外的事件:

  1. leave:收到瀏覽器變更room的請求,會先呼叫socket.leave()離開之前的room,並且對原先的room送出離開的通知
  2. join:呼叫完socket.leave之後,使用join來離開room,並對新的room送出加入的通知
  3. disconnect:這是Socket.IO內建的事件,會在使用者斷線時觸發。可以用它來送出離開room的通知
  4. joinroomsuccess:成功加入room後在瀏覽器觸發的事件,會更新聊天訊息的顯示
  5. warn:觸發時,瀏覽器會跳出alert,顯示警告訊息
  6. system:用來顯示系統訊息,包含使用者加入和離開的通知

先來看看伺服器端的程式變成怎樣:

 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 '/test831a.js':
 			console.log(resource);
 			filename = __dirname + resource;
 			res.setHeader('Content-Type', 'text/plain');
 			break;
 		default:
 			filename = __dirname + '/test831.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);
 		}
 	});
 }),
 nicknames = {},
 io = require('socket.io').listen(app)
 rooms = require('./test831a');
 
 io.sockets.on('connection', function(socket) {
 	socket.on('setnickname', function(m) {
 		if(typeof nicknames[m] === 'undefined') {
 			nicknames[m] = {count: 0};
 			socket.emit('nicknamesuccess', m);
 			socket['nickname'] = m;
 		} else {
 			nicknames[m].count++;
 			var t = m + '' + nicknames[m].count;
 			socket.emit('nicknamefail', t);
 			socket['nickname'] = t;
 		}
 	});
 // 2
 	socket.on('join', function(m) {
 		if(checkroom(rooms, m)) {
 			socket.join(m);
 			socket['room'] = m;
 			socket.broadcast.in(socket['room']).emit('system', socket['nickname'] + ' has joined this room.');// 6
 			socket.emit('joinroomsuccess', {room:m});// 4
 		}
 	});
 // 1
 	socket.on('leave', function() {
 		if(typeof socket['room'] !== 'undefined') {
 			socket.broadcast.in(socket['room']).emit('system', socket['nickname'] + ' has left this room.');// 6
 			socket.leave(socket['room']);
 			delete socket['room'];
 		}
 	});
 	socket.on('post', function(m) {
 		if(typeof socket['room'] !== 'undefined') {
 			socket.broadcast.in(socket['room']).emit('msg', m);
 		} else {
 			socket.emit('warning', 'You should choose a chat room first.');// 5
 		}
 	});
 // 3
 	socket.on('disconnect', function() {
 		if(typeof socket['room'] !== 'undefined') {
 			socket.broadcast.in(socket['room']).emit('system', socket['nickname'] + ' has left this room.');// 6
 		}
 	});
 });
 
 app.listen(80);
 
 function checkroom(rooms, room) {
 	var ret = false;
 	for(var i in rooms) {
 		if(room==i && rooms[i] == 'public') {
 			ret = true;
 		} 
 	}
 	return ret;
 }

為了讓「可使用」的room可以管理,目前用一個簡單的module(test831a.js)取得列表來做檢查。另外,在瀏覽器端也用共這個模組,來產生room的選單,不過不想引入像require.js這樣的額外東西,所以先寫一個簡單的define函數來撐著。

test831a.js很簡單,只是定義了幾個room:

exports = module.exports = {
	room1: 'public',
	room2: 'public',
	room3: 'public'
};

瀏覽器端其他的調整,主要還是為了支援可以隨意換聊天室以及額外的事件處理。介面只是多了一個聊天室選單,其實沒有差很多。(不過即使是這樣,還是要比伺服器多不少程式...)

 <?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;
 }
 </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 socket = io.connect();
 	var nickname = '';
 	var room = '';
 	var rooms = {};
 	$('#form1').submit(function(e) {
 		e.preventDefault();
 		socket.emit('setnickname', $('#nickname').val());
 	});
 	socket.on('nicknamesuccess', function(m) {
 		nickname = 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();
 	});
 	socket.on('nicknamefail', function(m) {
 		alert('Nickname conflict. Your nickname will be changed to "'+m+'"');
 		nickname = m;
 		$('#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');// 1
 			socket.emit('join', value);// 2
 			$('#msg').focus();
 		}
 	});
 // 4
 	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;
 		}
 	});
 // 6 
 	socket.on('system', function(m) {
 		updateMsg({nickname:'SYSTEM', msg:m});
 	});
 // 5 
	socket.on('warning', function(m) {
 		alert(m);
 	});
 	define('/test831a.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>
 	<select id='rooms' disabled></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>
 
 

執行畫面如下...
由左而右依次是輸入nickname、已輸入nickname焦點跳至room選單、已選擇要進入的room:

開始聊天,最左邊瀏覽器的使用者剛開room1到room2:

由於我最後想要整合出的功能,是比較接近視訊會議系統、協同分享等應用,所以Chat功能並不需要很複雜,就先在這裡打住。

另外要提一下,Socket.IO雖然好用又好上手,但是目前的版本還是有些缺點:

  1. RFC6455裡面,除了傳送UTF-8字串,還可以支援傳送binary資料,但是Socket.IO目前還不支援。其實Socket.IO如果要真正使用WebSocket來做傳輸的話(他還可以透過XMLHttp、Flash等來做,以跟舊瀏覽器相容),底層是用另一個WebSocket模組,叫做ws,而這個模組其實已經支援傳輸Binary資料
  2. 如果要傳送大量資料,可能會需要使用到bufferedAmount屬性,來確認資料是否完全送出。這個部分也沒有直接支援。

基於這些理由,在特定使用情境下,可能就無法使用Socket.IO。這是目前版本還存在的問題。

阿,修改一下XD
(介紹過WebSocket之後,明天開始挑戰Canvas,不過目標其實還是跟WebSocket整合,做出多人協同的應用。)<==這一段拿掉。

回頭看了一下之前草擬的大綱,明天應該是先停下來稍微思考一下多人協同運作應用的實現,後天則是更深入一些Socket.IO的東西,包含他怎麼控制/管理連線、怎麼利用storage、怎麼對使用者進行驗證等,當做是WebSocket議題的結束。如果有時間,再看看是不是有辦法透過簡單的方式,讓他支援Binary資料傳輸。(不過我想是沒時間)


上一篇
且戰且走HTML5(4) 基本的Chat應用
下一篇
且戰且走HTML5(6) 多人協同運作
系列文
且戰且走HTML530

1 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-13 21:52:54

沙發

除了趕快抄下來,還能說什麼呢~~~

筆記筆記

我要留言

立即登入留言