除了使用簡單,Socket.IO更內建了一些Chat必須的邏輯,可以減少一些開發的工作。
如果一個Chat只能支援一個聊天室,那功能就太簡單了。今天來看一下,要怎樣利用Socket.IO的內建功能,做出可選擇的不同聊天室。同時添加進入/離開聊天室的通知。
Socket.IO除了基本的訊息傳輸跟廣播,還可以直接建立虛擬的聊天室。使用socket.join('room')就可以加入一個room,使用socket.leave('room')就可以離開。另外,加入某個room後,就可以透過socket.broadcast.in('room').emit送出群播事件給同一個room的其他用戶。如果沒有內建這樣的機制,就需要自己在伺服器端管理連線,雖然要實作也不很困難,不過總是比較麻煩。
修改過的版本,加入了額外的事件:
先來看看伺服器端的程式變成怎樣:
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雖然好用又好上手,但是目前的版本還是有些缺點:
基於這些理由,在特定使用情境下,可能就無法使用Socket.IO。這是目前版本還存在的問題。
阿,修改一下
(介紹過WebSocket之後,明天開始挑戰Canvas,不過目標其實還是跟WebSocket整合,做出多人協同的應用。)<==這一段拿掉。
回頭看了一下之前草擬的大綱,明天應該是先停下來稍微思考一下多人協同運作應用的實現,後天則是更深入一些Socket.IO的東西,包含他怎麼控制/管理連線、怎麼利用storage、怎麼對使用者進行驗證等,當做是WebSocket議題的結束。如果有時間,再看看是不是有辦法透過簡單的方式,讓他支援Binary資料傳輸。(不過我想是沒時間)