其實Socket.IO的文件並不完整,雖然不需要完整的說明就可以使用,但是要清楚他的功能跟架構的話,大概要直接追蹤原始碼。
打開socket.io目錄中的package.json可以發現,模組的主檔是index.js,不過這支程式其實直接把lib/socket.io模組載入後就直接輸出了,所以要先進lib目錄看一下。
目錄中有幾個檔案:
其中對於要使用Socket.IO最需要了解的應該是socket.io.js、manager.js、namespace.js、socket.js。
socket.io.js是模組的主檔,程式中比較重要的是listen方法,他最後一行會返回一個Manager物件:
exports.listen = function (server, options, fn) {
......
return new exports.Manager(server, options);
}
......
exports.Manager = require('./manager');
......
所以當使用:
var app = require('http').createServer(...);
var io = require('socket.io').listen(app);
時,取得的io就是一個Manager物件。通常範例程式就接下來使用connection事件,不過這個事件其實是屬於io.sockets物件的事件:
io.sockets.on('connection', function() {
......
});
那麼這個sockets是什麼?追蹤一下會在manager.js中找到:
function Manager (server, options) {
......
this.namespaces = {};
this.sockets = this.of('');
......
}
......
Manager.prototype.of = function (nsp) {
if (this.namespaces[nsp]) {
return this.namespaces[nsp];
}
return this.namespaces[nsp] = new SocketNamespace(this, nsp);
};
簡單地說,對於所有的連線,Socket.IO預設會用一個空字串的NameSpace來管理。SocketNamespace物件定義在namespace.js,來追一下到底connection事件到底是怎樣觸發的:
var Socket = require('./socket')
......
function SocketNamespace (mgr, name) {
......
}
......
SocketNamespace.prototype.socket = function (sid, readable) {
if (!this.sockets[sid]) {
this.sockets[sid] = new Socket(this.manager, sid, this, readable);
}
return this.sockets[sid];
};
......
SocketNamespace.prototype.handlePacket = function (sessid, packet) {
var socket = this.socket(sessid)
......
function connect () {
self.manager.onJoin(sessid, self.name);
self.store.publish('join', sessid, self.name);
// packet echo
socket.packet({ type: 'connect' });
// emit connection event
self.$emit('connection', socket);
};
switch (packet.type) {
case 'connect':
......
if (packet.endpoint == '') {
connect();
} else {
......
this.authorize(handshakeData, function (err, authorized, newData) {
if (authorized) {
......
connect();
} else {
......
}
});
};
};
簡單地說,連線完畢後,傳給connection事件的socket,就定義在socket.js中。最後看一下socket.emit()怎麼運作的:
function Socket (manager, id, nsp, readable) {
this.id = id;
this.namespace = nsp;
this.manager = manager;
......
};
......
Socket.prototype.__defineGetter__('broadcast', function () {
this.flags.broadcast = true;
return this;
});
......
Socket.prototype.to = Socket.prototype.in = function (room) {
this.flags.room = room;
return this;
};
......
Socket.prototype.join = function (name, fn) {
var nsp = this.namespace.name
, name = (nsp + '/') + name;
this.manager.onJoin(this.id, name);
this.manager.store.publish('join', this.id, name);
if (fn) {
this.log.warn('Client#join callback is deprecated');
fn();
}
return this;
};
......
Socket.prototype.leave = function (name, fn) {
var nsp = this.namespace.name
, name = (nsp + '/') + name;
this.manager.onLeave(this.id, name);
this.manager.store.publish('leave', this.id, name);
if (fn) {
this.log.warn('Client#leave callback is deprecated');
fn();
}
return this;
};
......
Socket.prototype.packet = function (packet) {
if (this.flags.broadcast) {
this.log.debug('broadcasting packet');
this.namespace.in(this.flags.room).except(this.id).packet(packet);
} else {
packet.endpoint = this.flags.endpoint;
packet = parser.encodePacket(packet);
this.dispatch(packet, this.flags.volatile);
}
this.setFlags();
return this;
};
......
Socket.prototype.dispatch = function (packet, volatile) {
if (this.manager.transports[this.id] && this.manager.transports[this.id].open) {
this.manager.transports[this.id].onDispatch(packet, volatile);
} else {
if (!volatile) {
this.manager.onClientDispatch(this.id, packet, volatile);
}
this.manager.store.publish('dispatch:' + this.id, packet, volatile);
}
};
......
...略過store操作的部份...
Socket.prototype.emit = function (ev) {
var args = util.toArray(arguments).slice(1)
, lastArg = args[args.length - 1]
, packet = {
type: 'event'
, name: ev
};
if ('function' == typeof lastArg) {
packet.id = ++this.ackPackets;
packet.ack = lastArg.length ? 'data' : true;
this.acks[packet.id] = lastArg;
args = args.slice(0, args.length - 1);
}
packet.args = args;
return this.packet(packet);
};
當我們呼叫emit,例如socket.emit('post', message),資訊會包裝到packet物件,包括事件的名稱以及要傳遞的資訊,然後呼叫this.packet()處理。packet會接著送到this.dispatch,然後傳送給transport物件,傳送給適當的端點。
另外,可以看到當呼叫join()/leave()來加入、離開聊天室,實際上是在Manager物件上操作。最後,會在Manager.roomClients中存放room與user的對應,同時會在Manager.rooms存放user與room的對應。
至於broadcast的操作,或是呼叫in(room)/to(room),都會設定flags,這樣在this.packet就會依照設定的flag,調整送出packet的對象。
有了這樣的理解,就可以任意操作各個連線的socket。例如可以從socket.manager.rooms['/'+room_name]取得所有在這個room裡面的連線ID清單。另外,透過socket.namespace.clients(room_name)可以取得在這個room裡面所有連線的Socket物件清單。透過Socket物件,就可以傳送訊息給任意ID。
Socket.IO實際上在瀏覽器上是透過Socket.IO-Client這個Javascript Library來與Socket.IO伺服器連線。上面看到的連線ID,或是程式裡面看到的sessionid等等,都是這個Library在與Socket.IO伺服器handshake時由伺服器產生然後傳送給瀏覽器,瀏覽器在傳送資料時,也同時會傳送這個sessionid。這個handshake的過程,其實並不是透過WebSocket,而是透過http。另外,連線時雖然可以指定URI,看起來跟使用一般的WebSocket差不多,實際上跟Socket.IO請求的網址是:ws:/[host]:[port]/socket.io/[socket.io protocol version]/WebSocket/[session id]。那URI跑到哪裡去了?其實是隱藏在每個次傳送的資料裡面。
就目前的Socket.IO來說,他實際上是跟WebSocket不太一樣的,只是操作起來很像。實際上,他是制定好了一套溝通的規則,而實際在傳輸上,並不需要只使用WebSocket,所以在不支援WebSocket的樓懶器上也可以執行。不過這樣一來,Socket.IO就無法像WebSocket一樣,傳送Binary資料了。
就到這裡把WebSocket、Chat跟Socket.IO告一段落,明天開始來玩玩Canvas。