管理連線、利用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的非同步操作語法,程式看起來會比較複雜
另外看一下瀏覽器:
<?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物件,透過他來對個人傳送訊息。這些...明天再戰吧。(本來想要今天弄完,但是來不及了)
另外,透過namespace(其實就像是URI),他可以用同一個應用程式,透過不同的URI路徑提供不同的服務。不過,其實內部的管理是跟room差不多。(仔細追蹤會發現,room的名稱會被偷偷加上'/'prefix,就像URI路徑。