基於 socket.io 快速實現一個實時通訊應用

薄荷前端發表於2019-04-22

隨著web技術的發展,使用場景和需求也越來越複雜,客戶端不再滿足於簡單的請求得到狀態的需求。實時通訊越來越多應用於各個領域。

HTTP是最常用的客戶端與服務端的通訊技術,但是HTTP通訊只能由客戶端發起,無法及時獲取服務端的資料改變。只能依靠定期輪詢來獲取最新的狀態。時效性無法保證,同時更多的請求也會增加伺服器的負擔。

WebSocket技術應運而生。

WebSocket概念

不同於HTTP半雙工協議,WebSocket是基於TCP 連線的全雙工協議,支援客戶端服務端雙向通訊。

WebSocket使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。

WebSocket API中,瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以資料互相傳送。

HTTP與websocket對比

實現

原生實現

WebSocket物件一共支援四個訊息 onopen, onmessage, onclose和onerror。

建立連線

通過javascript可以快速的建立一個WebSocket連線:

    var Socket = new WebSocket(url, [protocol] );
複製程式碼

以上程式碼中的第一個引數url, 指定連線的URL。第二個引數 protocol是可選的,指定了可接受的子協議。

同http協議使用http://開頭一樣,WebSocket協議的URL使用ws://開頭,另外安全的WebSocket協議使用wss://開頭。

  1. 當Browser和WebSocketServer連線成功後,會觸發onopen訊息。
    Socket.onopen = function(evt) {};
複製程式碼
  1. 如果連線失敗,傳送、接收資料失敗或者處理資料出現錯誤,browser會觸發onerror訊息。
    Socket.onerror = function(evt) { };
複製程式碼
  1. 當Browser接收到WebSocketServer端傳送的關閉連線請求時,就會觸發onclose訊息。
    Socket.onclose = function(evt) { };
複製程式碼

收發訊息

  1. 當Browser接收到WebSocketServer傳送過來的資料時,就會觸發onmessage訊息,引數evt中包含server傳輸過來的資料。
    Socket.onmessage = function(evt) { };
複製程式碼
  1. send用於向服務端傳送訊息。
    Socket.send();
複製程式碼

socket

WebSocket是跟隨HTML5一同提出的,所以在相容性上存在問題,這時一個非常好用的庫就登場了——Socket.io

socket.io封裝了websocket,同時包含了其它的連線方式,你在任何瀏覽器裡都可以使用socket.io來建立非同步的連線。socket.io包含了服務端和客戶端的庫,如果在瀏覽器中使用了socket.io的js,服務端也必須同樣適用。

socket.io是基於 Websocket 的Client-Server 實時通訊庫。

socket.io底層是基於engine.io這個庫。engine.io為 socket.io 提供跨瀏覽器/跨裝置的雙向通訊的底層庫。engine.io使用了 Websocket 和 XHR 方式封裝了一套 socket 協議。在低版本的瀏覽器中,不支援Websocket,為了相容使用長輪詢(polling)替代。

engine.io

API文件

Socket.io允許你觸發或響應自定義的事件,除了connect,message,disconnect這些事件的名字不能使用之外,你可以觸發任何自定義的事件名稱。

建立連線

    const socket = io("ws://0.0.0.0:port"); // port為自己定義的埠號
    let io = require("socket.io")(http);
    io.on("connection", function(socket) {})
複製程式碼

訊息收發

一、傳送資料

    socket.emit(自定義傳送的欄位, data);
複製程式碼

二、接收資料

    socket.on(自定義傳送的欄位, function(data) {
        console.log(data);
    })
複製程式碼

斷開連線

一、全部斷開連線

    let io = require("socket.io")(http);
    io.close();
複製程式碼

二、某個客戶端斷開與服務端的連結

    // 客戶端
    socket.emit("close", {});
複製程式碼
    // 服務端
    socket.on("close", data => {
        socket.disconnect(true);
    });
複製程式碼

room和namespace

有時候websocket有如下的使用場景:1.服務端傳送的訊息有分類,不同的客戶端需要接收的分類不同;2.服務端並不需要對所有的客戶端都傳送訊息,只需要針對某個特定群體傳送訊息;

針對這種使用場景,socket中非常實用的namespace和room就上場了。

先來一張圖看看namespace與room之間的關係:

namespace與room的關係

namespace

服務端

    io.of("/post").on("connection", function(socket) {
        socket.emit("new message", { mess: `這是post的名稱空間` });
    });
    
    io.of("/get").on("connection", function(socket) {
        socket.emit("new message", { mess: `這是get的名稱空間` });
    });
複製程式碼

客戶端

    // index.js
    const socket = io("ws://0.0.0.0:****/post");
    socket.on("new message", function(data) {
        console.log('index',data);
    }
    
    //message.js
    const socket = io("ws://0.0.0.0:****/get");
    socket.on("new message", function(data) {
        console.log('message',data);
    }
複製程式碼

room

客戶端

    //可用於客戶端進入房間;
    socket.join('room one');
    //用於離開房間;
    socket.leave('room one');
複製程式碼

服務端

    io.sockets.on('connection',function(socket){
        //提交者會被排除在外(即不會收到訊息)
        socket.broadcast.to('room one').emit('new messages', data);
        // 向所有使用者傳送訊息
        io.sockets.to(data).emit("recive message", "hello,房間中的使用者");      
    }
複製程式碼

用socket.io實現一個實時接收資訊的例子

終於來到應用的階段啦,服務端用node.js模擬了服務端介面。以下的例子都在本地伺服器中實現。

服務端

先來看看服務端,先來開啟一個服務,安裝expresssocket.io

安裝依賴

    npm install --Dev express
    npm install --Dev socket.io
複製程式碼

構建node伺服器

    let app = require("express")();
    let http = require("http").createServer(handler);
    let io = require("socket.io")(http);
    let fs = require("fs");
    
    http.listen(port); //port:輸入需要的埠號
    
    function handler(req, res) {
      fs.readFile(__dirname + "/index.html", function(err, data) {
        if (err) {
          res.writeHead(500);
          return res.end("Error loading index.html");
        }
    
        res.writeHead(200);
        res.end(data);
      });
    }
    
    io.on("connection", function(socket) {
        console.log('連線成功');
        //連線成功之後傳送訊息
        socket.emit("new message", { mess: `初始訊息` });
        
    });
複製程式碼

客戶端

核心程式碼——index.html(向服務端傳送資料)

    <div>傳送資訊</div>
    <input placeholder="請輸入要傳送的資訊" />
    <button onclick="postMessage()">傳送</button>
複製程式碼
    // 接收到服務端傳來的name匹配的訊息
    socket.on("new message", function(data) {
      console.log(data);
    });
    
    function postMessage() {
      socket.emit("recive message", {
        message: content,
        time: new Date()
      });
      messList.push({
        message: content,
        time: new Date()
      });
    }
複製程式碼

核心程式碼——message.html(從服務端接收資料)

    socket.on("new message", function(data) {
      console.log(data);
    });
複製程式碼

效果

實時通訊效果

實時通訊效果

客戶端全部斷開連線

全部斷開

某客戶端斷開連線

某客戶端斷開連線

namespace應用

namespace

加入房間

加入房間

離開房間

離開房間

框架中的應用

npm install socket.io-client

    const socket = require('socket.io-client')('http://localhost:port');

    componentDidMount() {
        socket.on('login', (data) => {
            console.log(data)
        });
        socket.on('add user', (data) => {
            console.log(data)
        });
        socket.on('new message', (data) => {
            console.log(data)
        });
    }
複製程式碼

分析webSocket協議

Headers

Headers

請求包

    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
    Cache-Control: no-cache
    Connection: Upgrade
    Cookie: MEIQIA_VISIT_ID=1IcBRlE1mZhdVi1dEFNtGNAfjyG; token=0b81ffd758ea4a33e7724d9c67efbb26; io=ouI5Vqe7_WnIHlKnAAAG
    Host: 0.0.0.0:2699
    Origin: http://127.0.0.1:5500
    Pragma: no-cache
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Sec-WebSocket-Key: PJS0iPLxrL0ueNPoAFUSiA==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
複製程式碼

請求包說明:

  • 必須是有效的http request 格式;
  • HTTP request method 必須是GET,協議應不小於1.1 如: Get / HTTP/1.1;
  • 必須包括Upgrade頭域,並且其值為“websocket”,用於告訴伺服器此連線需要升級到websocket;
  • 必須包括”Connection” 頭域,並且其值為“Upgrade”;
  • 必須包括”Sec-WebSocket-Key”頭域,其值採用base64編碼的隨機16位元組長的字元序列;
  • 如果請求來自瀏覽器客戶端,還必須包括Origin頭域 。 該頭域用於防止未授權的跨域指令碼攻擊,伺服器可以從Origin決定是否接受該WebSocket連線;
  • 必須包括“Sec-webSocket-Version”頭域,是當前使用協議的版本號,當前值必須是13;
  • 可能包括“Sec-WebSocket-Protocol”,表示client(應用程式)支援的協議列表,server選擇一個或者沒有可接受的協議響應之;
  • 可能包括“Sec-WebSocket-Extensions”, 協議擴充套件, 某類協議可能支援多個擴充套件,通過它可以實現協議增強;
  • 可能包括任意其他域,如cookie.

應答包

應答包說明:

    Connection: Upgrade
    Sec-WebSocket-Accept: I4jyFwm0r1J8lrnD3yN+EvxTABQ=
    Sec-WebSocket-Extensions: permessage-deflate
    Upgrade: websocket
複製程式碼
  • 必須包括Upgrade頭域,並且其值為“websocket”;
  • 必須包括Connection頭域,並且其值為“Upgrade”;
  • 必須包括Sec-WebSocket-Accept頭域,其值是將請求包“Sec-WebSocket-Key”的值,與”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″這個字串進行拼接,然後對拼接後的字串進行sha-1運算,再進行base64編碼,就是“Sec-WebSocket-Accept”的值;
  • 應答包中冒號後面有一個空格;
  • 最後需要兩個空行作為應答包結束。

請求資料

    EIO: 3
    transport: websocket
    sid: 8Uehk2UumXoHVJRzAAAA
複製程式碼
  • EIO:3 表示使用的是engine.io協議版本3
  • transport 表示傳輸採用的型別
  • sid: session id (String)

Frames

WebSocket協議使用幀(Frame)收發資料,在控制檯->Frames中可以檢視傳送的幀資料。

其中幀資料前的數字代表什麼意思呢?

這是 Engine.io協議,其中的數字是資料包編碼:

[]

  • 0 open——在開啟新傳輸時從伺服器傳送(重新檢查)

  • 1 close——請求關閉此傳輸,但不關閉連線本身。

  • 2 ping——由客戶端傳送。伺服器應該用包含相同資料的乓包應答

    客戶端傳送:2probe探測幀

  • 3 pong——由伺服器傳送以響應ping資料包。

    伺服器傳送:3probe,響應客戶端

  • 4 message——實際訊息,客戶端和伺服器應該使用資料呼叫它們的回撥。

  • 5 upgrade——在engine.io切換傳輸之前,它測試,如果伺服器和客戶端可以通過這個傳輸進行通訊。如果此測試成功,客戶端傳送升級資料包,請求伺服器重新整理其在舊傳輸上的快取並切換到新傳輸。

  • 6 noop——noop資料包。主要用於在接收到傳入WebSocket連線時強制輪詢週期。

例項

傳送資料

接收資料

以上的截圖是上述例子中資料傳輸的例項,分析一下大概過程就是:

  1. connect握手成功
  2. 客戶端會傳送2 probe探測幀
  3. 服務端傳送響應幀3probe
  4. 客戶端會傳送內容為5的Upgrade幀
  5. 服務端回應內容為6的noop幀
  6. 探測幀檢查通過後,客戶端停止輪詢請求,將傳輸通道轉到websocket連線,轉到websocket後,接下來就開始定期(預設是25秒)的 ping/pong
  7. 客戶端、服務端收發資料,4表示的是engine.io的message訊息,後面跟隨收發的訊息內容

為了知道Client和Server連結是否正常,專案中使用的ClientSocket和ServerSocket都有一個心跳的執行緒,這個執行緒主要是為了檢測Client和Server是否正常連結,Client和Server是否正常連結主要是用ping pong流程來保證的。

該心跳定期傳送的間隔是socket.io預設設定的25m,在上圖中也可觀察發現。該間隔可通過配置修改。

socket通訊流程

參考engine.io-protocol

參考文章

Web 實時推送技術的總結 engine.io 原理詳解

另外,我們最近開放了招聘職位,歡迎來撩!> 薄荷前端招聘

廣而告之

本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。

歡迎討論,點個贊再走吧 。◕‿◕。 ~

相關文章