WebSocket 入门

384 阅读6分钟

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,广泛应用于实时应用程序,例如在线聊天、实时数据更新、游戏等。 在本文中,我们将介绍如何使用 WebSocket 构建一个前端应用,包括错误处理、心跳保活和跨域处理。

1. WebSocket 基本原理

WebSocket 使得客户端和服务器之间的通信变得更加高效和实时。与传统的 HTTP 请求/响应模型不同,WebSocket 允许服务器和客户端随时发送数据,从而实现低延迟的双向通信。

WebSocket 通信过程:

  1. 握手阶段:客户端向服务器发送 WebSocket 握手请求,服务器返回握手响应,建立连接。
  2. 数据传输阶段:客户端和服务器可以随时发送数据。
  3. 关闭阶段:任一方可以随时关闭连接。

2. 创建 WebSocket 客户端

接下来,我们在浏览器中创建一个简单的 WebSocket 客户端。

<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Client</title>
  </head>
  <body>
    <h1>WebSocket Client</h1>
    <script>
      // 创建 WebSocket 对象
      const socket = new WebSocket('ws://localhost:8080');

      // 连接成功事件
      socket.addEventListener('open', (event) => {
        console.log('Connected to server');
        socket.send('Hello Server');
      });

      // 接收消息事件
      socket.addEventListener('message', (event) => {
        console.log('Message from server:', event.data);
      });

      // 连接关闭事件
      socket.addEventListener('close', (event) => {
        console.log('Disconnected from server');
      });

      // 错误事件
      socket.addEventListener('error', (event) => {
        console.error('WebSocket error:', event);
      });
    </script>
  </body>
</html>

2.1 错误处理与重连机制

在实际应用中,WebSocket 连接可能会因为网络问题或服务器问题中断。因此,客户端需要实现自动重连机制,以确保连接的稳定性。

实现错误处理与重连机制

下面是一个示例,展示如何在客户端实现错误处理与重连机制:

<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Client with Reconnect</title>
  </head>
  <body>
    <h1>WebSocket Client with Reconnect</h1>
    <script>
      let socket;
      let reconnectAttempts = 0;
      const maxReconnectAttempts = 10;

      function createWebSocket() {
        socket = new WebSocket('ws://localhost:8080');

        socket.addEventListener('open', (event) => {
          console.log('Connected to server');
          reconnectAttempts = 0;  // 重置重连尝试计数
          socket.send('Hello Server');
        });

        socket.addEventListener('message', (event) => {
          console.log('Message from server:', event.data);
        });

        socket.addEventListener('close', (event) => {
          console.log('Disconnected from server');
          if (reconnectAttempts < maxReconnectAttempts) {
            const timeout = Math.min(1000 * (2 ** reconnectAttempts), 30000); // 指数退避算法
            setTimeout(() => {
              console.log('Attempting to reconnect...');
              reconnectAttempts++;
              createWebSocket();
            }, timeout);
          } else {
            console.error('Max reconnect attempts reached.');
          }
        });

        socket.addEventListener('error', (event) => {
          console.error('WebSocket error:', event);
        });
      }

      createWebSocket();
    </script>
  </body>
</html>
  1. 初始设置:定义初始延迟时间 baseDelay 为 1000 毫秒(1 秒),最大延迟时间 maxDelay 为 30000 毫秒(30 秒),最大重连尝试次数 maxReconnectAttempts 为 10 次。
  2. 连接 WebSocket:调用 createWebSocket 函数来创建 WebSocket 连接。
  3. 事件监听
    • open 事件:重置重连尝试计数 reconnectAttempts。
    • message 事件:打印从服务器接收到的消息。
    • close 事件:当连接关闭时,计算下一次重连的延迟时间 timeout,并在该时间后重新尝试连接。
    • error 事件:打印错误信息。
  1. 指数退避算法:每次重连失败后,重连的延迟时间按指数级增长,即 baseDelay * (2 ^ reconnectAttempts),同时确保延迟时间不超过 maxDelay。

为什么在 close 事件中处理重连

  1. 确保连接已断开:在 close 事件中处理重连可以确保连接已经完全关闭,避免重连机制在连接仍然存在时被触发,导致不必要的资源浪费和潜在的错误。
  2. 统一处理:close 事件在连接断开时总会触发,无论是由于网络错误、服务器关闭还是其他原因。将重连逻辑放在 close 事件中可以统一处理所有导致连接断开的情况。
  3. 避免重复处理:如果在 error 事件中处理重连,可能会导致在连接还未完全关闭时触发重连逻辑,而 close 事件也会随后触发,导致重连逻辑被重复执行。

指数退避算法详解

在网络通信中,尤其是涉及重连机制时,指数退避算法(Exponential Backoff Algorithm)是一种常用的策略,用来控制重试间隔时间。它通过逐渐增加重试等待时间来减轻网络和服务器的压力,提高系统的鲁棒性和性能。

优点
  • 降低网络负载:指数退避算法有效地减少了频繁重试带来的网络负载。
  • 避免资源浪费:逐渐增加的重试间隔时间可以防止客户端在短时间内频繁发起连接请求,从而避免资源浪费。
  • 提高系统稳定性:通过控制重连频率,可以提高系统的稳定性和可靠性
指数退避算法原理

指数退避算法的核心思想是,每次重试失败后,等待时间按指数级增长。例如,第 n 次重试的等待时间为 baseDelay * (2 ^ n),其中 baseDelay 是基本延迟时间。这样可以避免频繁的重试操作占用大量资源,并且在一定程度上防止了网络拥塞。

公式

给定一个基础延迟时间 baseDelay 和最大重试次数 maxRetries,每次重试的等待时间可以表示为:

其中,retries 是当前重试次数,maxDelay 是设置的最大延迟时间。

2.2 心跳保活机制

心跳保活机制用于检测客户端和服务器之间的连接状态,防止连接因长时间闲置而被中断。心跳消息通常是简单 的 Ping/Pong 消息。

2.2.1 服务器端实现心跳保活机制

在服务器端实现心跳保活机制,定期向客户端发送 Ping 消息,客户端回复 Pong 消息,以确保连接有效。

const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (ws) => {
  console.log('Client connected');

  const interval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
    }
  }, 30000); // 每30秒发送一次心跳

  ws.on('message', (message) => {
    const data = JSON.parse(message);
    if (data.type === 'pong') {
      console.log('Received pong');
    } else {
      console.log(`Received: ${message}`);
      ws.send(`Server received: ${message}`);
    }
  });

  ws.on('close', () => {
    console.log('Client disconnected');
    clearInterval(interval);
  });

  ws.send('Welcome to WebSocket server');
});

console.log('WebSocket server is listening on ws://localhost:8080');

2.2.2 客户端实现心跳保活机制

在客户端实现心跳保活机制,定期向服务器发送 Pong 消息以响应 Ping 消息。

<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Client with Heartbeat</title>
  </head>
  <body>
    <h1>WebSocket Client with Heartbeat</h1>
    <script>
      let socket;
      let reconnectAttempts = 0;
      const maxReconnectAttempts = 10;
      let heartbeatInterval;

      function createWebSocket() {
        socket = new WebSocket('ws://localhost:8080');

        socket.addEventListener('open', (event) => {
          console.log('Connected to server');
          reconnectAttempts = 0;
          clearInterval(heartbeatInterval);
          heartbeatInterval = setInterval(() => {
            if (socket.readyState === WebSocket.OPEN) {
              socket.send(JSON.stringify({ type: 'pong' }));
            }
          }, 30000); // 每30秒发送一次心跳响应
        });

        socket.addEventListener('message', (event) => {
          const data = JSON.parse(event.data);
          if (data.type === 'ping') {
            console.log('Received ping, sending pong');
            socket.send(JSON.stringify({ type: 'pong' }));
          } else {
            console.log('Message from server:', event.data);
          }
        });

        socket.addEventListener('close', (event) => {
          console.log('Disconnected from server');
          clearInterval(heartbeatInterval);
          if (reconnectAttempts < maxReconnectAttempts) {
            const timeout = Math.min(1000 * (2 ** reconnectAttempts), 30000);
            setTimeout(() => {
              console.log('Attempting to reconnect...');
              reconnectAttempts++;
              createWebSocket();
            }, timeout);
          } else {
            console.error('Max reconnect attempts reached.');
          }
        });

        socket.addEventListener('error', (event) => {
          console.error('WebSocket error:', event);
        });
      }

      createWebSocket();
    </script>
  </body>
</html>

3. 服务端实现

使用 Node.js 和 Koa.js 创建 WebSocket 服务器,并处理 WebSocket 连接、消息和跨域问题。

3.1 安装依赖

首先,安装必要的依赖:

npm install koa koa-router ws

3.2服务器代码

const Koa = require('koa');
const Router = require('koa-router');
const WebSocket = require('ws');
const http = require('http');
const app = new Koa();
const router = new Router();
const wss = new WebSocket.Server({ server });

router.get('/', async (ctx) => {
  ctx.body = 'WebSocket Server is running';
});

app.use(router.routes()).use(router.allowedMethods());

wss.on('connection', (ws) => {
  console.log('Client connected');

  ws.on('message', (message) => {
    console.log('Received:', message);
    if (data.type === 'pong') {
      console.log('Received pong');
    } else {
      ws.send(`Server received: ${message}`);
    }
  });

  ws.on('close', () => {
    console.log('Client disconnected');
  });

  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
});

// 跨域处理
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*');
  ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
  if (ctx.method === 'OPTIONS') {
    ctx.status = 204;
  } else {
    await next();
  }
});

const port = 8080;
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});