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

隨着web技術的發展,使用場景和需求也愈來愈複雜,客戶端再也不知足於簡單的請求獲得狀態的需求。實時通信愈來愈多應用於各個領域。javascript

HTTP是最經常使用的客戶端與服務端的通訊技術,可是HTTP通訊只能由客戶端發起,沒法及時獲取服務端的數據改變。只能依靠按期輪詢來獲取最新的狀態。時效性沒法保證,同時更多的請求也會增長服務器的負擔。html

WebSocket技術應運而生。前端

WebSocket概念

不一樣於HTTP半雙工協議,WebSocket是基於TCP 鏈接的全雙工協議,支持客戶端服務端雙向通訊。java

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。node

WebSocket API中,瀏覽器和服務器只須要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送。git

HTTP與websocket對比

實現

原生實現

WebSocket對象一共支持四個消息 onopen, onmessage, onclose和onerror。github

創建鏈接

經過javascript能夠快速的創建一個WebSocket鏈接:web

var Socket = new WebSocket(url, [protocol] );

以上代碼中的第一個參數url, 指定鏈接的URL。第二個參數 protocol是可選的,指定了可接受的子協議。express

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

  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) {})

消息收發

1、發送數據

socket.emit(自定義發送的字段, data);

2、接收數據

socket.on(自定義發送的字段, function(data) {
        console.log(data);
    })

斷開鏈接

1、所有斷開鏈接

let io = require("socket.io")(http);
    io.close();

2、某個客戶端斷開與服務端的連接

// 客戶端
    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協議,其中的數字是數據包編碼:

<Packet type id> [<data>]

  • 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 ★,轉載請註明出處。

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

相關文章
相關標籤/搜索