Socket.io 的架構

socket.io 是 node js 的一個 framework,它可以幫助我們建立聊天室這種推播功能的系統,這篇文章我們不會說明它如何使用,而是要理解 socket.io 這個套件的架構組成。

socket.io 主要由以下幾個東東構成的 :

  • engine.io、engino.io-client
  • socket.io-parser
  • socket.io-adapter
  • socket.io-client
  • socket.io-protocol

接下來我們將一個一個說明它們是做啥用的,並且最後會在進行一個總結。

engine.io

engine.io是一個實際執行 socket.io 通訊層級的 libary,嚴格說起來,socket.io 的核心就是engine.io,所有的建立連線、傳輸資訊實際上都是由它來做,並且根據前端傳送回來的資訊,來決定使用什麼傳輸方式。

目前 engine.io 所提供的溝通方式有以下幾種 :

  • polling-jsonp
  • polling-xhr
  • pollin
  • websocket

上面有提到,socket.io 本身不提供連線功能,而是在 engine.io 才提供,所以事實上,如果你沒有一定要使用到 socket.io 的功能,而只是要連線到 http server 或是監聽 port 的話,只要用 engine.io 就夠了,這邊有個重點要記得 socket.io 是個 framework 而 engine.io 只是個 libary,只要分的出這兩個差別,你就可以自由的選你要的使用。

var engine = require('engine.io');
var server = engine.listen(80);

server.on('connection', function(socket){
  socket.send('utf 8 string');
  socket.send(new Buffer([0, 1, 2, 3, 4, 5])); // binary data
});

engino.io-client

engine.io-clientsocket.io-client的核心,所有關鍵的連線、選擇傳輸方式,都是在這裡面執行。

我們這裡來看看,它是從那決定要用那種的傳輸方式(websocket、polling)。

engine.io-client下面這段程式碼(程式碼傳送門)中 ,這段onOpen是在 socket 要與 server 進行連線時,會先執行的事件,其中,就是由this.probe(this.upgrades[i])這個方法來決定要用什麼方式(websocket、polling)來進行傳輸。

Socket.prototype.onOpen = function () {
  debug('socket open');
  this.readyState = 'open';
  Socket.priorWebsocketSuccess = 'websocket' === this.transport.name;
  this.emit('open');
  this.flush();

  // we check for `readyState` in case an `open`
  // listener already closed the socket
  if ('open' === this.readyState && this.upgrade && this.transport.pause) {
    debug('starting upgrade probes');
    for (var i = 0, l = this.upgrades.length; i < l; i++) {
      this.probe(this.upgrades[i]);
    }
  }
};

然後在後端 server ,也就是engine.io裡根據前端傳送回來的url參數的Transport來決定要用什麼來進行傳輸 :

url 範例 : 

/engine.io/default/?transport=polling

engine.io的下面這段程式碼裡,check用來驗證傳進來的參數是否合法,來決定這個transport參數是否合法,然後再來將 http 升級為 websocket 協義。

server.on('upgrade', function (req, socket, head) {
      if (check(req)) {
        self.handleUpgrade(req, socket, head);
      } else if (false !== options.destroyUpgrade) {
        // default node behavior is to disconnect when no handlers
        // but by adding a handler, we prevent that
        // and if no eio thing handles the upgrade
        // then the socket needs to die!
        setTimeout(function () {
          if (socket.writable && socket.bytesWritten <= 0) {
            return socket.end();
          }
        }, destroyUpgradeTimeout);
      }
    });

socket.io-parser

在 socket.io 的世界中,有一個東西叫做 packet,它是所有溝通的基礎包,事實上它也就是 socket.io 的協議,有一個東西叫socket.io-protocol(傳送門),它裡面有定議好,你這個 packet 要長什麼樣子。

下面為最簡單的 packet 包 :

{
    type: 2,
    data: [{
        word: "hello mark"
    },{
        word: "hello gg"
    }]
}

其中2所代表的為這個 packet 是要用來處理event事件,像要傳送到前端的事件也是用這個代號,這個數字會由 socket.io-protocol 裡定義好。

然後每當我們要傳送 packet 到前端時,都會先使用socket.io-parser`將 packet 給 encode ,而致於為什麼要 encode 後在傳呢 ? 主要原因,可能是希望儘可能的將要傳送出去的 packet 縮小已節省傳輸成本。

像官方所提供的socket.io-parser會將上面的 packet 包 encode 成如下數據 :

"2[{"word":"hello mark"},{"word":"hello gg"}]"

它的確比原本要傳輸的資料還少點兒東西,然後前端在再用相同的socket.io-parser來進行decode,變成原來的 packet 包。

這就是socket.io-parser所做的事情,當然我們也可以自訂一個 parser ,如果你有需要的話,例如你想使用 xml 來當傳輸格式,這時你就要自訂一個 parser 了,雖然覺得應該不會想用xml來傳。

socket.io-adapter

在理解這套件之前,我們先看看adapter這代表什麼意思呢 ? 我們直接用這個單字去 google 圖片一下,然後可以看到下圖的結果 :

嗯哼,就是電源轉接器,在插頭和我們的機器之間所需要的東西,在程式開發中,你有沒有遇過下面這種問題呢 ?

我和 A 要 api 來使用,但發覺它的 api 我需要調整過後才能使用。

所以通常你這時,應該中間會有一個東西,來呼叫這隻 api ,然後再裡面先整理一下,然後才傳出去給主要的方法來使用,對吧 ? 這時那中間的東東就是所謂的adapter它本身就是一種設計模式。

好回來到socket.io-adapter,那我們的插頭和機器各代表什麼呢 ? 先來說說機器,機器就是我們的 socket.io 那插頭呢 ? 嚴格來說是儲存空間,儲啥呢 ? 就是namespace、rooms、sids這些東東。

socket.io-adapter預設是存放在記憶體之內,所以如果你要用 redis 或 mq 之類的來做儲放,你就只要調整你的 adapter 就好,目前官方有提供socket.io-redis可使用,它也是一個 adapter。

基本上你要進行任何 socket.io 提供的 emit、broadcast、join room 這些功能,都一定會到這一層來做處理。

socket.io-client

這個東東就是讓我們在前端使用的東西,假設我們在後端 server 建立好喔,我們就可以如下面這樣,連建立連線 :

<script src="/socket.io/socket.io.js"></script>
<script>
  var socket = io('http://localhost');
  socket.on('connect', function(){});
  socket.on('event', function(data){});
  socket.on('disconnect', function(){});
</script>

然後如果你在後端要寫整合測試時,想要模擬前端,也可以如下使用 :

var socket = require('socket.io-client')('http://localhost');
socket.on('connect', function(){});
socket.on('event', function(data){});
socket.on('disconnect', function(){});

這邊還有一個重點,它與 socket.io 一樣核心都是engine.io-client並且也是由它來決定我們要用什麼傳輸方式(polling、websocket)。

socket.io-protocol

最後是socket.io-protocol,這個事實上上面有說明過了,我們在來複習一次。

它是個協定,它不是程式碼、套件或其它可以執行的東西,它是一個規定,它定議好socket.io 要如何的傳輸資料,它主要定義了以下二個主題 :

Parser API

Parser API上面有提到,它是用來將 packet 包進行 encode 與 decode 的東東,在 protocol 中,它實際定義一個 parser api 需要有那些東西。

下面為它的主要定義

  • Encoder.encode(Object: packet, Function: callback)
  • Decoder.add(Object:encoding)

Encoder就是用來進行 encode 的類別,然後它需要提供encode方法,並且有兩個參數分別為packet 和 callback

Decoder就是要將 packet 進行 decod 會類別,並且需要有個add方法,來進行處理。

像我們上面提到的socket.io-parser就是根據socket.io-protocol所定義的parser api實作出的程式碼。

var parser = require('socket.io-parser');
var encoder = new parser.Encoder();
var packet = {
  type: parser.EVENT,
  data: 'test-packet',
  id: 13
};
encoder.encode(packet, function(encodedPackets) {
  var decoder = new parser.Decoder();
  decoder.on('decoded', function(decodedPacket) {
    // decodedPacket.type == parser.EVENT
    // decodedPacket.data == 'test-packet'
    // decodedPacket.id == 13
  });

  for (var i = 0; i < encodedPackets.length; i++) {
    decoder.add(encodedPackets[i]);
  }
});

Packet

Packetsocket.io世界裡的溝通包包,像你如果要放送訊息或是進行 connect 時,它們都是傳送的都是packet

在 protocol 中,它定義好了,一個 packet 要長什麼樣子 :

{
    type: Number,
    data: [],
    id: Number,
}

type

就是用來決定,這個包是要做什麼事情,protocol 它定義了以下幾種類型 :

  • CONNECT(0)
  • DISCONNECT(1)
  • EVENT(2)
  • ACK(3)
  • ERROR(4)
  • BINARY_EVENT(5)
  • INARRY_ACK(6)

data

就是你這個包傳送的資訊,它常都是我們自已的,不過記好,它是要放成一個陣列。

id

用來識別這個包是誰的,需要時在設定。

總結

最後我們來使用下面這張圖,來總結一下 socket.io 這個 framework 的架構 :

上圖為 socket.io 的整體架構,我們最後來複習一次。

首先最主要的主體為engino.io,所有的連線、傳輸方式的核心都是他,然後當你想要與其它東西進行溝通時,它們統一的溝通元件packet都需要使用socket.io-parser來進行 encode 與 decode ,因為某些傳輸方式 websocket 只能用文字與二進位數據,還有這些定義都會寫在 socket.io-protocol裡面,最後如果想你要與儲存元件溝通,請建立一個 adapter 來處理。

參考資料

comments powered by Disqus