Socket.IO打造基礎聊天室

weixin_33860722發表於2017-12-18
1632709-5179c504964a37a6.png
Socket.IO

01 Socket.io 簡介

  • 一個100%由 JavaScript 實現、基於Node.js的用於實時通訊、跨平臺的開源框架,它包括了客戶端的 JavaScript 庫和 伺服器端的 Node.js 服務。
  • 實現了對於其他語言的支援,如 Java、C++、Swift。
  • 提供了一個與 WebSocket 類似的通用 API:
1632709-4e0b26377ba8a22d.png
Socket.io方法與事件

主要特點】:

(1) 可靠性(Reliability):依賴 Engine.IO, 首先建立長輪詢,然後試著升級到更好的傳輸方式,如 WebSocket。
(2)自動重連(Auto-reconnection support):除非手動設定,否則當客戶端斷開連線時會一直嘗試重連。
(3)心跳檢測(Disconnection detection):在 Engine.IO 層面實現的心跳檢測機制,允許伺服器和客戶端知道哪一方不再響應。

Engine.IO:為 Socket.IO 實現的基於傳輸、跨瀏覽器/跨裝置的雙向通訊層。

(4)其它特點(如下圖):

1632709-c14b2ac9cf75bce3.png
socket.io其它特點

02 工作流程

Socket.IO 底層是 Engine.IO,這個庫實現了跨平臺的雙向通訊,使用下面的傳輸方式封裝了一套自己的 Socket 協議(EIO Socket)。

  • polling: XHR / JSONP polling transport
  • websocket: WebSocket transport

預設情況下,一個完整的 EIO Socket 包括多個 XHR 和 WebSocket 連線:

1632709-6d70c914b54cfddf.png
一個完整的EIO Socket連線(預設情況)
1632709-648f88757479781d.png
請求流程

EIO Socket 會首先發起XHR長輪詢,然後服務端會返回以下欄位:

  • 0:open標誌
  • sid:當前連線的socket id
  • upgrade:表示可以把連線方式從長輪詢升級到 websocket
  • pingInterval:心跳間隔
  • pingTimeout:心跳超時時間

前端收到握手的 upgrades 後,EIO 會檢測瀏覽器是否支援 WebSocket,如果支援,就會啟動一個 WebSocket 連線,然後通過這個 WebSocket 往伺服器發一條內容為 probe, 型別為 ping 的資料。如果這時伺服器返回了內容為 probe, 型別為 pong 的資料,前端就會把前面建立的 HTTP 長輪詢停掉,後面只使用 WebSocket 通道進行收發資料。(socket.io 的詳細工作流程是怎樣的?

心跳檢測:
EIO Socket生命週期內,會間隔一段時間 ping - pong 一次,用來測試網路是否正常

1632709-ad539266458e25ef.png
WebSocket訊息幀

綠色:傳送;白色:接收。
型別—> 2:ping,3:pong,4:message。

Socket.IO 在 Engine.IO 的基礎上做了一些封裝,比如 Socket.IO 裡面這樣的程式碼:

io.emit('add user', 'm') ;

在 Engine.io 裡面是這樣:

eio.send('message', '2["add user","m"]') ; // 2 是 socket.io 定義的包型別

因此,message的型別4後面有個2。

傳輸機制設定】:

Socket.io 為我們提供了選項,它的預設情況是以長輪詢開始,我們也可以手動設定成只使用 websocket 方式來進行通訊。

// 設定成只使用 websocket
const socket = io({ 
       transports: ['websocket'] 
}); 

// 重連時,重設選項
// 防止 websocket 可能因為代理、防火牆、瀏覽器等原因連線失敗socket.on('reconnect_attempt', () => { 
       socket.io.opts.transports = ['polling', 'websocket']; 
}); 
1632709-6609cb1bb10298c7.png
只使用websocket

03 核心方法

1632709-191c736e154d262a.png
Socket.io提供的方法

Socket.io 的核心函式:emiton

socket.emit(eventName[, ...args][, ack]):用來發射(觸發)一個事件

  • eventName(string):事件名
  • args:要傳送的資料
  • ack(Function):回撥函式,一般省略,如需對方接受到資訊後立即得到確認時需要用到
  • Returns Socket
socket.emit('ferret', 'tobi', (data) => {
  console.log(data); // data will be 'woot'
});

// server:
//  io.on('connection', (socket) => {
//    socket.on('ferret', (name, fn) => {
//      fn('woot');
//    });
//  });

socket.on(eventName, callback):用來監聽一個 emit 發射的事件

  • eventName(string):監聽的事件名
  • callback(Function):匿名函式,接收對方發來的資料,該匿名函式的第一個引數為接收的資料,若有第二個引數,則為要返回的函式
  • Returns Socket
socket.on('news', (data) => {
  console.log(data);
});

// with multiple arguments
socket.on('news', (arg1, arg2, arg3, arg4) => {
  // ...
});
// with callback
socket.on('news', (cb) => {
  cb(0);
});

Socket.io 提供了三種預設的事件(客戶端和伺服器都有):

  • connect:當與對方建立連線後自動觸發;
  • message:當收到對方發來的資料後觸發;
  • disconnect:當對方關閉連結後觸發。

除了 socket.io 自身提供的事件之外,還支援自定義事件,豐富了通訊:

// 如:
socket.on(‘new message’, function(data) {});
socket.emit(‘new message’, { message: message });

服務端廣播的三種情況】:

1632709-a57c9983231965c3.png
伺服器廣播

客戶端:

const socket = io();
// 監聽事件
socket.on(‘message’, (data) => {});
// 觸發事件
socket.emit(‘message’, { message }); 

服務端:

io.on(‘connection’, function(socket) {
        socket.on(‘message’, function(data) {
              // 1.廣播給自己
              socket.emit(‘message’, data);
              // 2. 廣播給除了自己的其它客戶端
              socket.broadcast.emit(‘message’, data);
              // 3. 廣播給所有客戶端
              io.emit(‘message’, data);  // 等同於 io.sockets.emit()
        });
});

04 Rooms 和名稱空間

【作用】:減少TCP連線數的同時區分不同的通訊頻道(在不同的路由層面能體現該作用,具體請參考 socket.io 中namespace 和 room的概念)、實現私聊

預設的名稱空間:io.sockets、io

io.on('connection', function(socket){    
      socket.on('disconnect', function(){ });
}); 

自定義名稱空間:

// 伺服器端
var nsp = io.of('/my-namespace'); 
nsp.on('connection', function(socket){    
      socket.on('disconnect', function(){ });
}); 

// 客戶端
var socket = io('/my-namespace'); 

Rooms:

// 自定義room
io.on('connection', function(socket){    
      socket.join('some room')); // 加入房間
      socket.leave('some room'); // 離開房間
}); 

// 向房間裡的所有客戶端傳送訊息
io.to('some room').emit('some event'); 

// 預設房間(每一個id一個room)
socket.on('say to someone', function(id, msg){    
       socket.broadcast.to(id).emit('my message', msg); 
}); 

獲取房間資訊:socket.adapter.rooms

1632709-58d5770a1e4f43d2.png
rooms物件

預設情況下,每一個 id 便自成一個房間,房間名為 socket.id(指定名稱空間之後,前面會帶上名稱空間);
自定義房間之後,原先的預設房間仍然存在;
房間為一個物件,包含當前進入房間的 sockets 以及長度。

05 打造基礎聊天室

從官網最基礎的聊天小例子入門,又分析了一下 Demos 中的 Chat demo 原始碼之後,自己試著用 react 實現了一遍,具體的功能及原理如下:

  • 最基礎聊天功能

    【實現原理】:服務端運用 io.emit 進行廣播給每個建立連線的客戶端。

  • 登入(Chat Demo)

    【核心操作】:使用者登入時(login 事件),服務端為當前的客戶端儲存 username。

    socket.username = username
    
  • 顯示使用者進入/離開

    【實現原理】:使用者登入(login)時,觸發 user joind 事件;使用者斷開連線(disconnection)時,觸發 user left 事件—> 均通過 socket.broadcase.emit 廣播給其它客戶端。

  • 顯示當前聊天室人數

    【實現原理】:操作 numUsers 變數—> 監聽 connection 事件:++numUsers;監聽 disconnection 事件:--numUsers 。

  • 顯示各自暱稱

    【實現原理】:觸發 chat 事件的時候將當前連線的 username 通過 io.emit 廣播給每個給客戶端。

  • 提示對方正在輸入

    【實現原理】:監聽 typingstop typing 事件 —> 均通過 socket.broadcast.emit 廣播給其它客戶端。(typing 通過監聽輸入框的 onKeyPress 事件進行觸發)

    注意:當傳送完資訊之後,需要清空“***正在輸入”,客戶端在監聽到 chat 時可將提示置空。

  • 實現私人聊天

1632709-d45c7c39ae3db043.png
私人聊天

【實現原理】:運用 Room,通過 socket.adapter.rooms 獲取當前 room 的資訊,包括每個 room 中的 id。

// 加入房間
socket.join('some room'); 
// 離開房間
socket.leave('some room'); 

// 向房間裡的所有客戶端傳送訊息
io.to('some room').emit('some event'); 

// 向房間中的除了自己的客戶端傳送訊息
socket.broadcast.to ('some room')
    .emit('my message', msg);
自娛自樂的實現版本:

github地址:ioChat

1632709-7b22fc89edecb9c0.png
新增暱稱+xxx正在輸入
1632709-820f5ac78dae01e9.png
監聽使用者進入或離開/顯示線上人數
1632709-8c924288c50b7f68.png
Rooms 實現私人聊天

ps:下一次我會出一篇聊天室實現 emoji 表情傳送的文章,敬請期待喲~~٩(๑>◡<๑)۶ ~~

§ 參考資料

相關文章