客戶端與伺服器之間雙向通訊的5種方式總結(完整程式碼演示)

Echoyya、發表於2021-05-24

首先簡單說一下常用的http協議的特點:http是客戶端/伺服器模式中請求-響應所用的協議,在這種模式中,客戶端(一般是web瀏覽器)向伺服器提交HTTP請求,伺服器響應請求的資源。

HTTP是半雙工協議,也就是說,在同一時刻資料只能是單向流動,客戶端向伺服器傳送請求(單向的),伺服器響應請求(單向的)。

那麼如果想要實時通訊,能使伺服器實時地將更新的資訊傳送到客戶端,而無需客戶端發出請求,目前有以下幾種方式:

1. polling 輪循

輪循:客戶端和伺服器之間會一直進行連線,每隔一段時間就詢問一次(setInterval)

特點:連線數會很多,一個接收,一個傳送,而且每次傳送請求都會消耗流量,也會消耗CPU的利用率 。

(由於http資料包的頭部資料往往很大,通常有400多個位元組,但是真正被伺服器需要的資料卻很少,有時只有10位元組左右,這樣的資料包在網路上週期性的傳輸,難免對網路頻寬是一種浪費)

程式碼實現:

index.html:


  <!--輪循 polling -->
  <div id="clock"></div>
  <script>
    let clock = document.getElementById('clock')
    setInterval(() => {
      let xhr = new XMLHttpRequest;
      xhr.open('get','/clock',true)
      xhr.onreadystatechange = function(){
        if(xhr.readyState === 4 && xhr.status === 200){
          clock.innerText = xhr.responseText  
        }
      }
      xhr.send()
    }, 1000);
  </script>

app.js:node模擬伺服器,執行app.js, http://localhost:3000/訪問index.html

let express = require('express')
let app = express()
app.use(express.static(__dirname))

app.get('/clock',function(req,res){
  res.send(new Date().toLocaleString())
})

app.listen(3000)

可以看到的效果就是,每隔1秒傳送一次請求,伺服器返回更新後的資訊。

2. long-polling 長輪循

長輪循:是對輪循的改良版,客戶端傳送請求給伺服器之後,需要滿足一些條件才返回新的資料,反之若沒有新資料就一直等待。

當有新訊息時才會返回給客戶端,在某種程度上減少了網路頻寬和CPU利用率的問題。

連結會一直保持,直到有資料更新或連結超時,此時伺服器不能在傳送資料。

程式碼實現:

index.html:

  <body>
    <div id="clock"></div>
  </body>
  <script>
    let clock = document.getElementById('clock')
    function send(){
      let xhr = new XMLHttpRequest;
      xhr.open('get','/clock',true)
      xhr.onreadystatechange = function(){
        if(xhr.readyState === 4 && xhr.status === 200){
          clock.innerText = xhr.responseText  
          send()    // 伺服器響應之後,在傳送第二次請求
        }
      }
      xhr.send()
    }
    send()
  </script>

app.js:node模擬伺服器,執行app.js, http://localhost:3000/訪問index.html

let express = require('express')
let app = express()
app.use(express.static(__dirname))

app.get('/clock',function(req,res){
  let $timer = setInterval(() => {
    let date = new Date()
    let seconds = date.getSeconds()
    if(seconds % 5 == 0){     // 需要滿足一些條件下,才會進行資料的返回
      res.send(date.toLocaleString())
      clearInterval($timer)   // 清除定時器
    }
  }, 1000);
})

app.listen(3000)

可以看到的效果就是,伺服器每隔5秒更新資料。然後在返回給客戶端,結束一次請求響應,開始第二次的請求響應,而實際應用場景中,伺服器的響應時候,會更長一些。

3. iframe 流

在html 頁面嵌入一個隱藏的iframe,將這個iframe的src 屬性設為對一個長連結的請求,伺服器端就能源源不斷的向客戶端推送資料

程式碼實現:

index.html:


  <style>
    div{
      height: 100px;
      width:230px; 
      border: 1px solid slateblue;
      line-height: 100px;
      text-align: center;
    }
  </style>
  <body>
    <!-- iframe 流 -->
    <div id="clock"></div>
    <iframe src="/clock" frameborder="0"></iframe>
  </body>
  <script>
    let clock = document.getElementById('clock')
    function setTime(st) { 
      clock.innerText = st
    }
  </script>

app.js:node模擬伺服器,執行app.js, http://localhost:3000/訪問index.html

let express = require('express')
let app = express()
app.use(express.static(__dirname))

app.get('/clock',function(req,res){
 setInterval(() => {
   res.write(`     // 此處 注意不能用上述案例中的send方法,send方法會預設執行end結束,
    <script>
      parent.setTime('${new Date().toLocaleString()}')
    </script>
   `)
 }, 1000);
})

app.listen(3000)

可以看到的效果就是,伺服器每隔1秒更新資料。並主動推送給客戶端,但是同樣存在問題,就是頁簽上的icon一直處於loading狀態,表示響應一直未結束。使用者體驗並不是很好。

4. EventSource 流

嚴格地說,HTTP 協議無法做到伺服器主動推送資訊。但是,有一種變通方法,就是伺服器向客戶端宣告,接下來要傳送的是流資訊(streaming)。也就是說,傳送的不是一次性的資料包,而是一個資料流,會連續不斷地傳送過來。此時客戶端不會關閉連線,會一直等著伺服器發過來的新的資料流,視訊播放就是這樣的例子。本質上,這種通訊就是以流資訊的方式,完成一次用時很長的下載

H5規範中提供了服務端事件EventSource,瀏覽器建立一個EventSource連線後,便可收到服務端傳送的資料,這些資料需要遵守一定的格式,直到服務端或者客戶端關閉該流,所以eventSource也叫做SSE(server-send-event)。

SSE 就是利用這種機制,使用流資訊向瀏覽器推送資訊。目前除了 IE/Edge,其他瀏覽器都支援,實現方式對客戶端開發人員而言非常簡單,只需在瀏覽器中監聽對應的事件即可。

另外對於伺服器,SSE使用的也是HTTP傳輸協議,這意味著我們不需要一個特殊的協議或者額外的實現就可以使用。

其實說白了SSE 是單向通道,只能伺服器向瀏覽器傳送,因為流資訊本質上就是下載。如果瀏覽器向伺服器傳送資訊,就變成了另一次 HTTP 請求。應用場景:在股票行情、新聞推送的這種只需要伺服器傳送訊息給客戶端場景中,顯然使用SSE更加合適。

EventSource的實現同樣分為兩部分:

瀏覽器端:

  1. 在瀏覽器端建立一個EventSource例項,向伺服器發起連線

  2. open:連線一旦建立,就會觸發open事件,可以在onopen屬性定義回撥函式

  3. message:客戶端收到伺服器發來的資料,就會觸發message事件,可以在onmessage屬性定義回撥函式。

  4. error:如果發生通訊錯誤(如連線中斷,伺服器返回資料失敗),就會觸發error事件,可以在onerror屬性定義回撥函式。

  5. close:用於關閉 SSE 連線。source.close();

  6. 自定義事件:EventSource規範允許伺服器端執行自定義事件,客戶端監聽該事件即可,需要使用addEventListener

index.html:


  <style>
    div{
      border: 1px solid #ce4;
      width: 300px;
      height: 40px;
      padding: 20px;
      margin-bottom: 20px;
    }
    p{
      color: #888;
      font-size: 14px;
    }
  </style>
  <body>
    <p>預設事件message:</p>
    <div id="clock"></div>
    <p>自定義事件 yya:</p>
    <div id="yya"></div>
  </body>
  <script>
    let sse = new EventSource('/clock')
    let clock = document.getElementById('clock')
    let yya = document.getElementById('yya')
 
    // 監聽連線剛開啟時被呼叫
    sse.onopen = function () {  
      console.log('open');
    }
     // 監聽伺服器發過來的資訊
     sse.onmessage = function (event) { 
      clock.innerText = event.data
    }
    // 監聽連結請求失敗 關閉流
    sse.onerror = function (event) {  
      console.log('error');
      sse.close();
    }
    // 監聽自定義事件, 不能通過on的方式的去繫結
    sse.addEventListener('yya', function (event) {
      yya.innerText = event.data
    }, false);
  </script>

伺服器端:

  1. 事件流的對應MIME格式為text/event-stream

  2. 伺服器向瀏覽器傳送的 SSE 資料,必須是 UTF-8 編碼的文字

  3. 服務端返回資料需要特殊的格式,分為四種訊息型別,且訊息的每個欄位使用"\n"來做分割,

    • Event: 事件型別,支援自定義事件

    • Data: 傳送的資料內容,如果資料很長,可以分成多行,用\n結尾,最後一行用\n\n結尾。

    • ID: 每一條事件流的ID,相當於每一條資料的編號,

    • Retry:指定瀏覽器重新發起連線的時間間隔。在自動重連過程中,之前收到的最後一個ID會被髮送到服務端。

app.js:node模擬伺服器,執行app.js, http://localhost:3000/訪問index.html

let express = require('express')
let app = express()
app.use(express.static(__dirname))

let counter = 0
app.get('/clock',function(req,res){
  res.header('Content-Type','text/event-stream')
  let $timer = setInterval(() => {
    // 第一種寫法
    res.write(`id:${counter++}\nevent:message\ndata:${new Date().toLocaleString()}\n\n`)

    // 另一種寫法
    res.write(`event:yya\n`)     // 觸發 自定義事件
    res.write(`data:${counter}\n\n`)
  }, 1000 );
  
  res.on('close',function(){
    counter = 0
    clearInterval($timer)
  })
})

app.listen(3000)

實現效果:可以看到已經很好的解決了在iframe流中遺留的問題,也就是頁籤一直loading的現象。

5. websocket

WebSocket 是H5下一種新的協議,它誕生於2008年,2011年成為國際標準,現在所有瀏覽器都已支援。它實現了瀏覽器與伺服器全雙工通訊,能更好的節省伺服器資源和頻寬,並達到實時通訊的目的,最大特點就是:伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊,是真正的雙向平等對話

優勢及特點:

  1. 在客戶端和伺服器之間保有一個持有的連線,兩邊可以隨時給對方傳送資料,有很強的實時性;

  2. 屬於應用層協議,基於TCP傳輸協議,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器;

  3. 可以傳送文字,也可以支援二進位制資料的傳輸;

  4. 資料格式比較輕量,效能開銷小,通訊高效;

  5. 沒有同源限制,客戶端可以與任意伺服器通訊;

  6. 協議識別符號是ws(如果加密,則為wss),伺服器網址就是 URL;

WebSocket所涉及的內容遠不止於此,這裡只是拋磚引玉,帶大家入門,知道有這麼個東西,可以幹一件什麼樣的事兒,要想完全掌握還需要下一番功夫嘞,那麼就簡單的用程式碼實現一下雙工通訊,並使用序號標識出執行順序:

index.html:


  <script>
    let socket = new WebSocket('ws://localhost:8888')
    socket.onopen = function () {
      console.log('1. 客戶端連線上了伺服器',new Date().getTime());
      socket.send('3. 你好')
    }
    socket.onmessage = function (e) {
      console.log('6',e.data);
    }
  </script>

app.js:node模擬伺服器,執行app.js, http://localhost:3000/訪問index.html

let express = require('express')
let app = express()
app.use(express.static(__dirname))

app.listen(3000)

let WebSocket = require('ws')
let wss = new WebSocket.Server({port:8888})
wss.on('connection',function(ws){
  console.log('2.伺服器監聽到了客戶端的連線',new Date().getTime());
  ws.on('message',function(data){
    console.log('4.客戶端發來的訊息',data);
    ws.send('5.服務端說:你也好')
  })
})

因為我在執行的時候,突然有一個疑問,就是客戶端連線伺服器 和 伺服器監聽客戶端的連線,那個方法先執行,於是做了一個實驗,在列印的時候,分別列印出了對應的時間戳,列印結果顯示大部分情況是2先執行,個別時候是同時執行,我試了很多次,沒有出現12先的情況。期待有懂的小夥伴評論區指點一二~~~

以上就是我今天要分享的全部內容!

相關文章