iT邦幫忙

DAY 8
4

且戰且走HTML5系列 第 8

且戰且走HTML5(8) Socket.IO的架構與連線管理機制

其實Socket.IO的文件並不完整,雖然不需要完整的說明就可以使用,但是要清楚他的功能跟架構的話,大概要直接追蹤原始碼。
打開socket.io目錄中的package.json可以發現,模組的主檔是index.js,不過這支程式其實直接把lib/socket.io模組載入後就直接輸出了,所以要先進lib目錄看一下。

目錄中有幾個檔案:

  1. logger.js
  2. manager.js
  3. namespace.js
  4. parser.js
  5. socket.io.js
  6. socket.js
  7. static.js
    8 store.js
  8. transport.js
  9. util.js

其中對於要使用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。


上一篇
且戰且走HTML5(7) 進階的Chat應用
下一篇
且戰且走HTML5(9) 應用的主軸:Canvas
系列文
且戰且走HTML530

尚未有邦友留言

立即登入留言