websocket 介绍

发布时间:2023年12月28日

1,前端如何实现即时通讯

websocket 协议出现之前,前端想实现即时通讯,只能通过下面2种方式:

  • 短轮询 short polling
  • 长轮询 long polling

短轮询

客户端每隔一小段时间,就向服务器请求一次,询问有没有新消息。

实现起来很简单,只需要开启一个计时器不断发送请求即可。但缺点比较明显:

  • 会产生大量无意义的请求。
  • 会频繁打开关闭 TCP 连接。
  • 实时性并不高。

长轮询

为了解决短轮询的问题,出现了长轮询。原理如下图:

在这里插入图片描述

虽然长轮询让每次请求和响应都变的有意义,但依然存在一些问题:

  • 客户端长时间收不到响应会导致超时,从而主动断开和服务器的连接。

    可以在 ajax 请求因为超时而结束时,立即重新发送请求到服务器。虽然会让之前的请求无意义,但比短轮询好多了。

  • 因为客户端可能【过早的】请求了服务器,所以服务器不得不挂起这个请求,直到新消息出现。
    这会让服务器长时间占用资源却没有做任何事情。

2,websocket

websocket 协议 HTML5 带来的新协议,相对于 http,它是一个持久连接的协议,它利用 http 协议完成握手,然后通过 TCP 连接通道发送消息,使用 websocket 协议可以实现服务器主动推送消息的能力

从上图可以看出:

  • websocket 也是建立在 TCP 协议上的,利用的是 TCP 的全双工通信能力。
  • 使用时会经过2个阶段,握手阶段通信阶段
  • 维持 TCP 连接也是需要耗费资源的,所以看实际需求。

2.1,握手

websocket 协议内容比较复杂,这里只介绍下握手协议。(下面会有例子说明)

当客户端需要和服务器使用 websocket 进行通信时,首先会使用HTTP协议完成一次特殊的请求-响应,这一次的请求-响应就是websocket握手

在握手阶段,首先由客户端向服务器发送一个请求,请求地址格式如下:

# 使用 HTTP
ws://mysite.com/path
# 使用 HTTPS
wss://mysite.com/path

请求头:

Connection: Upgrade /* 协议需要升级,不使用 HTTP了 */
Upgrade: websocket /* 协议升级为 websocket */
Sec-WebSocket-Version: 13 /* websocket协议版本为 13 */
Sec-WebSocket-Key: YWJzZmFkZmFzZmRhYw== /* 连接的 key */

服务器如果同意,响应如下消息:

HTTP/1.1 101 Switching Protocols /* 切换协议,101表示切换协议 */
Connection: Upgrade /* 协议升级 */
Upgrade: websocket /* 升级到 websocket */
Sec-WebSocket-Accept: ZzIzMzQ1Z2V3NDUyMzIzNGVy /* 重新编码后的 key */

Sec-WebSocket-Accept 是将 Sec-WebSocket-Key 使用特殊的算法重新编码生成的。浏览器使用它来确保响应与请求相对应。

握手完成后,后续的消息收发不再使用 HTTP,任何一方都可以主动发消息给对方。

2.2,握手过程举例

客户端

<button>发送数据到服务器</button>
<script>
  // 创建一个websocket,同时,发送连接到服务器
  const ws = new WebSocket("ws://localhost:3002"); 
  ws.onopen = function () {
    // http 握手完成
    console.log("连接已建立");
  };

  ws.onclose = function () {
    console.log("通道关闭");
  };

  document.querySelector("button").onclick = function () {
    ws.send("客户端数据123");
  };
  // ws.close(); //客户端主动断开连接
</script>

服务器

const net = require("net");

const server = net.createServer((socket) => {
  console.log("收到客户端的连接");
  socket.once("data", (chunk) => {
    // 解析请求报文,
    const httpContent = chunk.toString("utf-8");
    let parts = httpContent.split("\r\n");
    parts.shift();
    parts = parts
      .filter((s) => s)
      .map((m) => {
        const i = m.indexOf(":");
        return [m.slice(0, i), m.slice(i + 1).trim()];
      });
    // 变成对象的形式,为了取出请求头 Sec-WebSocket-Key
    const headers = Object.fromEntries(parts);
    const crypto = require("crypto"); // 加密模块
    const hash = crypto.createHash("sha1");
    // 创建 Sec-WebSocket-Accept,后面是一个随机的 guid。
    const key = hash.update(headers["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
    // 响应,注意格式。
    socket.write(`HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ${key}

`);
    // 接收客户端的消息
    socket.on("data", (chunk) => {
      console.log(chunk.toString("utf-8"));
    });
  });
});

server.listen(3002);

注意数据格式为 Buffer 需要转码,因为 websocket 的消息需要特定的格式,数据量较大时会切片传输。但每个切片到达的顺序可能不一样,所以为了保证将接收到的数据,能按照顺序拼接,所以数据格式为 Buffer 二进制的形式。

2.3,socket.io

一般使用 websocket 大多都会使用它 socket.io

测试使用版本 v4.7.2,消息格式都是字符串而不是 Buffer,所以不用转码了。

浏览器:访问地址 http://localhost:5500/index.html

<button>发送数据到服务器</button>
<script src="https://cdn.bootcdn.net/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
<script>
  const socket = io("http://localhost:3002");
  document.querySelector("button").onclick = function () {
    socket.emit("to-server", "来自浏览器的消息");
  };

  // 监听服务器的消息,约定事件名 to-client
  socket.on("to-client", (chunk) => {
    console.log(chunk);
  });

  // 服务器断开连接时触发
  socket.on("disconnect", () => {
    console.log("closed");
  });
</script>

服务器写法参考

启动后的服务器地址:http://localhost:3002,所以会发生跨域。解决

const Koa = require("koa");
const { createServer } = require("http");
const { Server } = require("socket.io");

const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {
  cors: {
    origin: "http://localhost:5500",
  },
});

io.on("connection", (socket) => {
  // 当有一个新的客户端连接到服务器成功之后,触发的事件
  console.log("新的客户端连接进来了");
  // 监听客户端发送的消息,约定事件为 to-server
  socket.on("to-server", (chunk) => {
    // 监听客户端的msg消息
    console.log(chunk);
  });
  let count = 0;
  const timer = setInterval(function () {
    // 每隔两秒钟,发送一个消息给客户端,约定事件为 to-client
    socket.emit("to-client", `来自服务器的第${count++}次消息`);
  }, 2000);

  socket.on("disconnect", () => {
    clearInterval(timer);
    console.log("closed");
  });
});

// 监听端口
httpServer.listen(3002, () => {
  console.log("server listening on 3002");
});

效果展示:

在这里插入图片描述

3,websocket 对比 http 的优势

当页面中需要观察实时数据的变化(比如聊天、k 线图)时,过去我们往往使用两种方式完成(短轮询,长轮询)

无论是哪一种方式,都暴露了 http 协议的弱点,即响应必须在请求之后发生,服务器是被动的,无法主动推送消息。而让客户端不断的发起请求又会占用了资源。

websocket 的出现就是为了解决短轮询,长轮询的缺点,它利用 http 协议完成握手之后,就可以与服务器建立持久的连接,服务器可以在需要的时候主动推送消息给客户端,这样占用的资源最少,同时实时性也最高。


以上。

文章来源:https://blog.csdn.net/qq_40147756/article/details/135217485
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。