天天看點

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

筆者之前與一位同僚研究了 Cypress 的 visit 方法,其源碼實作最終是調用了 WebSocket 向 visit 參數裡指定的 website 通行并擷取資料,見下圖變量 ​

​ev.data​

​ 的值。

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

我這位同僚的研究成果,通過 Joplin 筆記記錄如下​​如下​​。

于是筆者心裡有一個疑問,為什麼 Cypress 的 visit 方法選擇了 WebSocket 作為與目标網站的通信技術呢?為什麼不直接走 HTTP 協定,比如用 ES6 原生支援的 fetch 去通路目标網站呢?

要回答這個問題,我們先要了解到底什麼是 WebSocket,以及它與 HTTP 相比較的優缺點。

誠然,WebSocket 可以在使用者的浏覽器和伺服器之間打開互動式通信會話,浏覽器可以向伺服器發送消息并接收事件驅動的響應,而無需通過輪詢伺服器的方式以獲得響應。

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

WebSocket 基于 TCP 連接配接,在伺服器和浏覽器間提供了全雙工通信功能,即伺服器可以主動推送資料到浏覽器端,而這在 HTTP 協定中是不可能實作的,HTTP 協定隻支援浏覽器到伺服器端的 Request - Response 方式,即浏覽器用戶端如果想查詢伺服器端是否有最新的事件發生,則隻能采取低效的輪詢方式進行。

舉個例子,當使用者向伺服器發送請求時,該請求以 HTTP 或 HTTPS 的形式發送,伺服器收到請求後向用戶端發送響應,每個請求都與相應的響應相關聯,發送響應後連接配接關閉,每個 HTTP 或 HTTPS 請求每次都會建立與伺服器的新連接配接,并且在獲得響應後,連接配接會自行終止。

筆者注:HTTP 請求頭部的 ​

​Connection: keep-alive​

​​ 字段,​

​可以實作連接配接重用的需求嗎?​

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

當啟用 Keep-Alive 時,用戶端和伺服器同意為後續請求或響應保持連接配接打開。

預設情況下,HTTP 連接配接在資料事務結束時關閉。 這意味着用戶端建立一個新連接配接來請求頁面的每個檔案,伺服器在發送資料後關閉這些 TCP 連接配接。

但是,如果伺服器需要同時響應多個 HTTP 請求并為每個新的 TCP 連接配接提供一個檔案,則站點頁面的加載時間将會增加。 這可能會導緻糟糕的使用者體驗。

為了克服這個問題,網站所有者需要啟用 Keep-Alive 标頭來限制新連接配接的數量。

通過打開 Keep-Alive 連接配接标頭,用戶端可以通過單個 TCP 連接配接下載下傳所有内容,例如 JavaScript、CSS、圖像和視訊,而不是為每個檔案發送不同的請求。

這是一張示範 Keep-Alive 工作原理的圖檔:

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

問題:啟用 Keep-Alive 頭部字段後,重用的是 HTTP 連接配接,還是 TCP 連接配接?

WebSocket 并不是将 HTTP 的設計完全推翻重建,而是在 HTTP 的基礎上增添了一些邏輯來,管理用戶端和伺服器端的流。這些流的内容也是 HTTP 請求和響應,保留了舊語義,隻是編碼和打包方式不同。

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

了解了理論知識後,我們動手開發一套最簡單的 WebSocket 伺服器端和用戶端實作。

WebSocket 伺服器端實作

var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
var defaultPort = 3001;

var port = process.env.PORT || defaultPort;
var i = 0;

console.log("Server is listening on port: " + defaultPort);
server.listen(port);

io.on('connection', function (socket) {
  console.log("connect comming from client: " + socket.id);
  
  socket.emit('messages_jerry', { hello: 'world greeting from Server!' });
  
  socket.on('messages', function (data) {
    console.log("data received from Client:" + JSON.stringify(data,2,2));
  });
});      

代碼實作包含了4個關鍵點:

  1. 伺服器監聽在預設的 3001 端口上。
  2. 一旦 WebSocket 用戶端有發送到 3001 端口上的連接配接請求時,代碼第 12 行的 on 監聽函數觸發,監聽的事件名稱為 ​

    ​connection​

    ​,然後在監聽函數的實作體裡,列印出用戶端連接配接的 id 值。
  3. 伺服器端接收了用戶端的連結後,向用戶端通過第 15 行的 emit 方法,發送一個 ​

    ​messages_jerry​

    ​ 的事件,以及一個 JSON 對象作為事件負載。
  4. 第 17 行伺服器端監聽在 ​

    ​messages​

    ​ 事件上的監聽函數觸發時,說明接收到了從用戶端發送過來的事件,在監聽函數裡列印出用戶端傳遞過來的資料。
關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

WebSocket 用戶端實作

// #!/usr/bin/env node
const io = require('socket.io-client');
var socket = io.connect('http://localhost:3001');

socket.on('messages_jerry', function (data) {
    console.log("data sent from Server:" + JSON.stringify(data,2,2));
    socket.emit('messages', { my: 'data sent from Client' });
  });

socket.on('connect', function (socket2) {
    console.log('Connection with Server established!');
        socket.emit('messages', 'Client has established connection with Server');
});      

代碼的關鍵點:

  1. 用戶端通過 connect 方法向 WebSocket 伺服器發起連接配接請求
  2. 連接配接成功建立後,用戶端第 10 行的 on 監聽函數觸發,該函數監聽在 ​

    ​connect​

    ​ 事件上,會在 Web Socket 連接配接成功建立後自動觸發。
  3. 用戶端在第 12 行調用 emit 向伺服器發送一個 messages 事件。
  4. 用戶端監聽在 ​

    ​messages_jerry​

    ​ 的監聽函數觸發,說明伺服器端有資料到達。使用第 6 行的 console.log 語句列印出這個資料。
  5. 在第 7 行代碼,用戶端調用 emit,向伺服器端發送一個請求,通知伺服器自己已經收到了伺服器發送過來的資料。
關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

使用指令行 ​

​node wsServer.js​

​ 啟動伺服器端,看到如下輸出:

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

新開一個指令行視窗,使用 ​

​node wsClient.js​

​ 啟動用戶端,能看到用戶端列印出的成功建立連接配接,以及從伺服器端發送過來的資料:

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

切換回伺服器端,紅色高亮的内容,就是用戶端與伺服器端建立連接配接之後,伺服器端新列印出的資料:

關于 WebSocket 和 HTTP 差別的思考以及一個最簡單的 WebSocket 的用戶端和伺服器實作

回到本文開頭抛出的問題:

問題1

為什麼 Cypress 的 visit 方法選擇了 WebSocket 作為與目标網站的通信技術呢?為什麼不直接走 HTTP 協定,比如用 ES6 原生支援的 fetch 去通路目标網站呢?

筆者猜測,是不是因為 Cypress 裡某些 API,比如 ​

​cy.XXX​

​ 需要利用到 WebSocket 這種全雙工通信的特性才能夠充分發揮作用?

問題2

那麼問題又來了,在我們 ​

​cy.visit('http://xxx.com')​

​​ 的代碼裡,如果說最終 Cypress 通過 WebSocket 協定向 ​

​http://xxx.com​

​​ 發送資料報,但是 ​

​http://xxx.com​

​ 不支援 WebSocket 怎麼辦?就像本文前一部分介紹的例子一樣,WebSocket 需要用戶端和伺服器端同時支援才行。

問題3

參考資料

  • ​​Difference between HTTP and WebSocket (HTTP 2.0 )​​

繼續閱讀