From: http://marklin-blog.logdown.com/posts/2906519
-------------------------------------------------
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-client
是socket.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
Packet
是socket.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
來處理。