关于 WebSocket 和 HTTP 区别的思考以及一个最简单的 WebSocket 的客户端和服务器实现

JerryWang_汪子熙 -
关于 WebSocket 和 HTTP 区别的思考以及一个最简单的 WebSocket 的客户端和服务器实现

笔者之前与一位同事研究了 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 监听函数触发,监听的事件名称为 connection,然后在监听函数的实现体里,打印出客户端连接的 id 值。服务器端接收了客户端的链接后,向客户端通过第 15 行的 emit 方法,发送一个 messages_jerry 的事件,以及一个 JSON 对象作为事件负载。第 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 监听函数触发,该函数监听在 connect 事件上,会在 Web Socket 连接成功建立后自动触发。客户端在第 12 行调用 emit 向服务器发送一个 messages 事件。客户端监听在 messages_jerry 的监听函数触发,说明服务器端有数据到达。使用第 6 行的 console.log 语句打印出这个数据。在第 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 需要客户端和服务器端同时支持才行。

那么会不会 cy.visit 和 visit 参数里指定的 webSite 之间,还存在着一个中间层?

在这里插入图片描述

问题3

WebSocket Connection,HTTP Connection,TCP connection,这三者的区别和联系是什么?

参考资料Difference between HTTP and WebSocket (HTTP 2.0 )
特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

前端htmlhtml5javascriptwebsocket

扩展阅读

加个好友,技术交流

1628738909466805.jpg