筆者之前與一位同僚研究了 Cypress 的 visit 方法,其源碼實作最終是調用了 WebSocket 向 visit 參數裡指定的 website 通行并擷取資料,見下圖變量
ev.data
的值。
我這位同僚的研究成果,通過 Joplin 筆記記錄如下如下。
于是筆者心裡有一個疑問,為什麼 Cypress 的 visit 方法選擇了 WebSocket 作為與目标網站的通信技術呢?為什麼不直接走 HTTP 協定,比如用 ES6 原生支援的 fetch 去通路目标網站呢?
要回答這個問題,我們先要了解到底什麼是 WebSocket,以及它與 HTTP 相比較的優缺點。
誠然,WebSocket 可以在使用者的浏覽器和伺服器之間打開互動式通信會話,浏覽器可以向伺服器發送消息并接收事件驅動的響應,而無需通過輪詢伺服器的方式以獲得響應。
WebSocket 基于 TCP 連接配接,在伺服器和浏覽器間提供了全雙工通信功能,即伺服器可以主動推送資料到浏覽器端,而這在 HTTP 協定中是不可能實作的,HTTP 協定隻支援浏覽器到伺服器端的 Request - Response 方式,即浏覽器用戶端如果想查詢伺服器端是否有最新的事件發生,則隻能采取低效的輪詢方式進行。
舉個例子,當使用者向伺服器發送請求時,該請求以 HTTP 或 HTTPS 的形式發送,伺服器收到請求後向用戶端發送響應,每個請求都與相應的響應相關聯,發送響應後連接配接關閉,每個 HTTP 或 HTTPS 請求每次都會建立與伺服器的新連接配接,并且在獲得響應後,連接配接會自行終止。
筆者注:HTTP 請求頭部的
Connection: keep-alive
字段,
可以實作連接配接重用的需求嗎?
當啟用 Keep-Alive 時,用戶端和伺服器同意為後續請求或響應保持連接配接打開。
預設情況下,HTTP 連接配接在資料事務結束時關閉。 這意味着用戶端建立一個新連接配接來請求頁面的每個檔案,伺服器在發送資料後關閉這些 TCP 連接配接。
但是,如果伺服器需要同時響應多個 HTTP 請求并為每個新的 TCP 連接配接提供一個檔案,則站點頁面的加載時間将會增加。 這可能會導緻糟糕的使用者體驗。
為了克服這個問題,網站所有者需要啟用 Keep-Alive 标頭來限制新連接配接的數量。
通過打開 Keep-Alive 連接配接标頭,用戶端可以通過單個 TCP 連接配接下載下傳所有内容,例如 JavaScript、CSS、圖像和視訊,而不是為每個檔案發送不同的請求。
這是一張示範 Keep-Alive 工作原理的圖檔:
問題:啟用 Keep-Alive 頭部字段後,重用的是 HTTP 連接配接,還是 TCP 連接配接?
WebSocket 并不是将 HTTP 的設計完全推翻重建,而是在 HTTP 的基礎上增添了一些邏輯來,管理用戶端和伺服器端的流。這些流的内容也是 HTTP 請求和響應,保留了舊語義,隻是編碼和打包方式不同。
了解了理論知識後,我們動手開發一套最簡單的 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個關鍵點:
- 伺服器監聽在預設的 3001 端口上。
- 一旦 WebSocket 用戶端有發送到 3001 端口上的連接配接請求時,代碼第 12 行的 on 監聽函數觸發,監聽的事件名稱為
,然後在監聽函數的實作體裡,列印出用戶端連接配接的 id 值。connection
- 伺服器端接收了用戶端的連結後,向用戶端通過第 15 行的 emit 方法,發送一個
的事件,以及一個 JSON 對象作為事件負載。messages_jerry
- 第 17 行伺服器端監聽在
事件上的監聽函數觸發時,說明接收到了從用戶端發送過來的事件,在監聽函數裡列印出用戶端傳遞過來的資料。messages
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');
});
代碼的關鍵點:
- 用戶端通過 connect 方法向 WebSocket 伺服器發起連接配接請求
- 連接配接成功建立後,用戶端第 10 行的 on 監聽函數觸發,該函數監聽在
事件上,會在 Web Socket 連接配接成功建立後自動觸發。connect
- 用戶端在第 12 行調用 emit 向伺服器發送一個 messages 事件。
- 用戶端監聽在
的監聽函數觸發,說明伺服器端有資料到達。使用第 6 行的 console.log 語句列印出這個資料。messages_jerry
- 在第 7 行代碼,用戶端調用 emit,向伺服器端發送一個請求,通知伺服器自己已經收到了伺服器發送過來的資料。
使用指令行
node wsServer.js
啟動伺服器端,看到如下輸出:
新開一個指令行視窗,使用
node wsClient.js
啟動用戶端,能看到用戶端列印出的成功建立連接配接,以及從伺服器端發送過來的資料:
切換回伺服器端,紅色高亮的内容,就是用戶端與伺服器端建立連接配接之後,伺服器端新列印出的資料:
回到本文開頭抛出的問題:
問題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 )