Socket.IO

這篇文章會介紹有關於 Socket.IO 在 server 端以及 iOS 端的一些實作分享。

Server

首先我們可以先從 Socket.IO 的 Get started 開始,它是以 Node.JS 所編寫的,所以先在資料夾位置執行

npm init

邊可以取得基本的一些檔案和 package.json。

接著安裝 Socket.IO 所需要的 express

npm install --save express@4.15.2

然後新建一個 index.js 的檔案,貼上

var app = require('express')();
var http = require('http').Server(app);

app.get('/', function(req, res){
    res.send('<h1>Hello world</h1>');
});

http.listen(3000, function(){
    console.log('listening on *:3000');
});

這樣待會執行 node index.js 的時候便可以從 http://localhost:3000 來連上這個 server。

確認 server 目前是可以連上後,便開始安裝 Socket.IO 到其中

npm install --save socket.io

並在 index.js 裏頭加上 Socket.IO 的 code:

var io = require('socket.io')(http);
io.on('connection', function(socket){
    console.log('a user connected');
});

這樣便完成監聽 client 連接上 server 的事件了。

‘connection’ 是 socket.io 所定義的 event,而這個 event 會取得 socket 回來,所以以 function(socket) 的方式去接收並處理後續。

不過這裡的 io 可以想成是一個 server,所以每收到一個 client 的 connection 事件,都會執行 function(socket);而我們若要監聽個別連線的事件,則是使用 socket 來處理:

socket.on('disconnect', function(){
    console.log('user disconnected');
});

也就是說,像是登入、傳送訊息等,和個別使用者相關的動作,我們都是以 socket 來處理。

而除了監聽(on)外,發送(emit)的動作也是如此,像是我想推一段文字給特定的使用者:

socket.emit('new message', '嗨!');

在 client 以及 server 端先彼此定義好 event 名稱,這樣就可以知道要監聽的事件為哪些。

什麼時候該用 io 發送,什麼時候該用 socket 呢?

舉個例子,我們建立一個 Socket.IO 的大聊天室,任何人只要連上這個網址,便等同於加入這個聊天室。

  • 使用者上、下線會顯示提醒
  • 說髒話的人會個別收到「此訊息無法傳送」的訊息
io.on('connection', function(socket) {
    io.emit('new user', '有人加入了這個聊天室!');
    socket.on('add message', function(from, message) {
        if (message.indexOf('馬的') > -1) {
            socket.emit('new message', '此訊息無法傳送');
        } else {
            io.emit('new message', from + ' 說:' + message);
        }
    })
    socket.on('disconnect', function() {
        io.emit('user leave', '有人離開了這個聊天室!');
    });
});

上面便可以得知哪種時候應該要大家都可以接收到、而哪種是只會有個別的使用者接收到。

再來介紹一下 ack,在發送一個 event 時,可以在後面補上一個 ack,而當對方收到的時候,可以透過 ack 來傳遞 data,而非再發送一個新的 event。

有點像是 HTTP request 的概念,發送一個 request(emit),接著對方會回傳 response(ack),不過這並不一定會有,也就是說你發送(emit)了一個 event 過去,雖然有夾帶著 ack,但是對方若沒接收那個 ack 的話也是沒用。

這邊是一個例子,我們從 server 給沒有收過這則廣告的使用者傳送一則過去,並希望使用者真的有收到,若沒收到下次就再傳一次:

// 判斷使用者是否有收到過廣告,若沒有的話執行
socket.emit('new advertise', '恭喜您獲得 $1,000 元折扣!', function(userID) {
    console.log('使用者(' + userID + ')收到廣告了!');
    //去資料庫更新,下次不用再推給這個 userID
)};

其中,function(userID) 便是一個 ack 或者可以說是 callback。

相對的,收到訊息的時候,server 也可以回傳 ack 回去給 client 告知:

socket.on('add message', function(text, ack) {
    console.log('收到: ' + text);
    ack('伺服器收到你的訊息了!請放心!);
});

如此一來,便會在收到訊息之後,以 ack 的方式回傳訊息回去。

而 ack 和 emit 所發送出去有什麼不同?

在 Socket.IO 的 protocol 裏頭有定義:

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

所以其實雖然動作類似,但 Socket.IO 可以辨別其中的差異,進而可以在 Socket 之中達到 Request、Response 的概念。

Client

Client 的部分,可以使用 Socket.IO 所提供的 Swift framework,它是基於 Starscream 所開發出來的,如同上述說的有使用到 WebSocket 來連接。

以 Swift 來說:

let manager = SocketManager(socketURL: URL(string: "http://localhost:3000")!, config: [configs])
let socket = manager.defaultSocket

這邊介紹幾個我所使用到的 configs

  • .log(true):開啟 LOG 的功能。
  • .forceWebsockets(true):若沒有使用這個的話,會以 HTTP polling 的方式連接,從 header 來看的話,就會顯示 connection: keep-alive;而使用了 .forceWebsockets(true) 的話,則會使用 WebSocket 來連線,則會顯示 connection: upgrade。
  • .reconnectAttempts(int):重新嘗試連線 n 次,超過就放棄。
  • .connectParams([String: Any]):這邊可以放 token 來做 Authentication。

為什麼不在 header 裏頭加上 Authentication 的欄位?

這邊 Socket.IO 有做解釋,為什麼不建議在 extraHeaders 加東西。

而 on / emit / emitWithAck 這幾個的用法就和 server 的概念一樣,這邊就不再多做解釋,

之後實作有遇到什麼事情再來補充(或是新文章)!