为了账号安全,请及时绑定邮箱和手机立即绑定

六大实用模式打造实时Web应用的WebSocket神器

标签:
WebApp

作为一名畅销书作家,我邀请您在我的Amazon上探索我的书。别忘了在Medium上关注我哦,多多支持我。非常感谢您!您的支持对我来说非常重要!

在现代 web 开发的世界中,实时功能已从一种奢侈品变成了不可或缺的必需品。用户期望即时更新、无缝互动体验和响应迅速的用户界面。WebSocket 提供了实现这些功能的技术基础,支持持久连接,使客户端和服务器之间能够进行双向通信。我已经多年在多个项目中实施 WebSocket 解决方案,想分享六个强大的模式,这些模式可以大大提升你的实时应用程序的功能和用户体验。

WebSocket 连接池

连接池对于需要多个WebSocket连接的应用程序来说至关重要。它可以让你共享一组有限的连接,这些连接可以在各个组件或功能之间使用,而不是为每个组件或功能单独创建新的连接。

我记得在一个交易平台上工作时,连接的开销导致了性能问题。通过实现连接池技术,我们将服务器负载减少了40%,同时保留了相同的功能。

    class WebSocketConnectionPool {
      constructor(serverUrl, poolSize = 3) {
        this.serverUrl = serverUrl;
        this.poolSize = poolSize;
        this.connections = [];
        this.connectionIndex = 0;
        this.initialize();
      }

      initialize() {
        for (let i = 0; i < this.poolSize; i++) {
          this.createConnection();
        }
      }

      createConnection() {
        const ws = new WebSocket(this.serverUrl);

        ws.onopen = () => {
          console.log(`连接 ${this.connections.length} 已建立`);
        };

        ws.onerror = (error) => {
          console.error('WebSocket 错误:', error);
          // 处理重连逻辑
          this.handleReconnection(this.connections.indexOf(ws));
        };

        this.connections.push(ws);
        return ws;
      }

      getConnection() {
        // 简单的轮选连接
        const connection = this.connections[this.connectionIndex];
        this.connectionIndex = (this.connectionIndex + 1) % this.poolSize;
        return connection;
      }

      handleReconnection(index) {
        setTimeout(() => {
          if (index >= 0 && index < this.connections.length) {
            this.connections[index] = this.createConnection();
          }
        }, 1000);
      }

      sendMessage(message) {
        const connection = this.getConnection();
        if (connection.readyState === WebSocket.OPEN) { // 如果连接处于打开状态
          connection.send(JSON.stringify(message));
          return true;
        }
        return false;
      }
    }

全屏显示 退出全屏

我发现这个消息池特别有用,可以在多个连接间传递消息,同时控制资源使用量,特别是在消息很多的应用中。

心跳原理

网络连接可能会无声地失败。心跳检测包括定期发送“ping”消息来检查连接是否仍然正常工作。

    class HeartbeatWebSocket {
      constructor(url, heartbeatInterval = 30000) {
        this.url = url;
        this.heartbeatInterval = heartbeatInterval;
        this.connection = null;
        this.heartbeatTimer = null;
        this.connect();
      }

      connect() {
        this.connection = new WebSocket(this.url);

        this.connection.onopen = () => {
          console.log('已建立连接');
          this.startHeartbeat();
        };

        this.connection.onclose = () => {
          console.log('连接已关闭');
          this.stopHeartbeat();
          // 重连逻辑可以放在这里
        };

        this.connection.onmessage = (event) => {
          const message = JSON.parse(event.data);
          if (message.type === 'pong') {
            // 在接收到 pong 时重置心跳
            this.resetHeartbeat();
          } else {
            // 处理常规消息
            this.handleMessage(message);
          }
        };
      }

      startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
          if (this.connection.readyState === WebSocket.OPEN) {
            this.connection.send(JSON.stringify({ type: 'ping' }));

            // 设置超时以检测未收到的 pong
            this.pongTimeoutTimer = setTimeout(() => {
              console.log('未收到 pong,连接可能已断开');
              this.connection.close();
            }, 5000);
          }
        }, this.heartbeatInterval);
      }

      resetHeartbeat() {
        if (this.pongTimeoutTimer) {
          clearTimeout(this.pongTimeoutTimer);
          this.pongTimeoutTimer = null;
        }
      }

      stopHeartbeat() {
        if (this.heartbeatTimer) {
          clearInterval(this.heartbeatTimer);
          this.heartbeatTimer = null;
        }
        this.resetHeartbeat();
      }

      handleMessage(message) {
        // 处理常规消息
        console.log('收到消息:', message);
      }

      send(message) {
        if (this.connection.readyState === WebSocket.OPEN) {
          this.connection.send(JSON.stringify(message));
        }
      }
    }

全屏模式 退出全屏

在我开发的一个聊天应用程序中发现,移动网络往往会维持“僵尸”连接,这些连接看似活跃但实际上无效。添加心跳包使消息传递失败减少了95%。

重连方法:

网络中断是不可避免的,但一套稳健的重新连接策略可以确保你的应用程序在连接断开时能优雅地恢复。

    class ReconnectingWebSocket {
      constructor(url, options = {}) {
        this.url = url;
        this.options = {
          maxReconnectAttempts: 10,
          reconnectInterval: 1000,
          maxReconnectInterval: 30000,
          reconnectDecay: 1.5,
          ...options
        };

        this.reconnectAttempts = 0;
        this.socket = null;
        this.isConnecting = false;
        this.messageQueue = [];

        this.connect();
      }

      connect() {
        if (this.isConnecting) return;

        this.isConnecting = true;
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
          console.log('连接成功建立');
          this.isConnecting = false;
          this.reconnectAttempts = 0;

          while (this.messageQueue.length > 0) {
            const message = this.messageQueue.shift();
            this.send(message);
          }

          if (this.onopen) this.onopen();
        };

        this.socket.onclose = (event) => {
          if (!event.wasClean) {
            this.attemptReconnect();
          }

          if (this.onclose) this.onclose(event);
        };

        this.socket.onerror = (error) => {
          console.error('WebSocket 出错:', error);
          this.socket.close();

          if (this.onerror) this.onerror(error);
        };

        this.socket.onmessage = (event) => {
          if (this.onmessage) this.onmessage(event);
        };
      }

      attemptReconnect() {
        if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
          console.log('已达最大重连次数');
          return;
        }

        const delay = Math.min(
          this.options.reconnectInterval * Math.pow(this.options.reconnectDecay, this.reconnectAttempts),
          this.options.maxReconnectInterval
        );

        this.reconnectAttempts++;
        console.log(`正在尝试重连,间隔 ${delay}ms... (尝试次数 ${this.reconnectAttempts})`);

        setTimeout(() => {
          this.isConnecting = false;
          this.connect();
        }, delay);
      }

      send(message) {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
          this.socket.send(typeof message === 'string' ? message : JSON.stringify(message));
          return true;
        } else {
          this.messageQueue.push(message);
          return false;
        }
      }

      close() {
        if (this.socket) {
          this.socket.close();
        }
      }
    }

进入全屏,退出全屏

此实现使用指数回退以避免在服务器故障期间给服务器带来过大压力。我发现正确处理重连能显著改善用户体验,特别是在网络状况不稳定的地区。

消息队列

当连接断开时,你需要一个策略来处理这些消息。消息队列会在断开期间存储这些消息,并在连接恢复后发送这些消息。

注: "发送的消息" 作为技术术语保留英文原样。

    class QueuedWebSocket {
      constructor(url) {
        this.url = url;
        this.socket = null;
        this.queue = [];
        this.connected = false;
        this.maxQueueSize = 100;
        this.connect();
      }

      connect() {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
          console.log('连接已建立');
          this.connected = true;
          this.flushQueue();
        };

        this.socket.onclose = () => {
          console.log('连接已关闭');
          this.connected = false;
          // 重连逻辑可以在这里实现
        };

        this.socket.onerror = (error) => {
          console.error('WebSocket 错误:', error);
        };

        this.socket.onmessage = (event) => {
          // 处理传入的消息内容
          this.handleMessage(JSON.parse(event.data));
        };
      }

      send(message) {
        const messageObject = {
          id: this.generateId(),
          timestamp: Date.now(),
          content: message,
          attempts: 0
        };

        if (this.connected && this.socket.readyState === WebSocket.OPEN) {
          this.sendMessage(messageObject);
        } else {
          this.enqueueMessage(messageObject);
        }
      }

      sendMessage(messageObject) {
        messageObject.attempts++;
        try {
          this.socket.send(JSON.stringify(messageObject.content));
          return true;
        } catch (error) {
          console.error('发送消息失败:', error);
          this.enqueueMessage(messageObject);
          return false;
        }
      }

      enqueueMessage(messageObject) {
        // 控制队列大小
        if (this.queue.length >= this.maxQueueSize) {
          this.queue.shift(); // 移除最老的消息
        }
        this.queue.push(messageObject);
      }

      flushQueue() {
        if (!this.connected) return;

        const queueCopy = [...this.queue];
        this.queue = [];

        queueCopy.forEach(messageObject => {
          if (!this.sendMessage(messageObject)) {
            // 如果发送失败,则重新加入队列
          }
        });
      }

      handleMessage(message) {
        console.log('收到消息', message);
        // 处理传入的消息内容
      }

      generateId() {
        return Math.random().toString(36).substring(2, 15);
      }
    }

点击进入全屏,点击退出全屏

在我参与的一个协作文档编辑器项目中,我们实现了消息队列功能,以确保用户所做的编辑在短暂的网络连接中断期间也不会丢失。这显著提高了应用程序的实际可靠性。

协议规范

一个清晰的消息协议确保客户端和服务器能够完美地理解彼此。设定一个结构化的协议可以让您的 WebSocket 交互更加易于维护且减少错误。

    // 协议定义
    const MessageTypes = {
      AUTHENTICATION: 'auth',
      EVENT: 'event',
      COMMAND: 'command',
      QUERY: 'query',
      RESPONSE: 'response',
      ERROR: 'error'
    };

    class WebSocketProtocol {
      constructor(url) {
        this.url = url;
        this.socket = null;
        this.messageHandlers = {};
        this.pendingRequests = new Map();
        this.requestTimeout = 10000; // 10秒
        this.connect();
      }

      connect() {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
          console.log('尝试建立连接');
        };

        this.socket.onmessage = (event) => {
          try {
            const message = JSON.parse(event.data);
            this.handleMessage(message);
          } catch (error) {
            console.error('解析消息失败:', error);
          }
        };

        this.socket.onclose = () => {
          console.log('连接已关闭');
        };

        this.socket.onerror = (error) => {
          console.error('WebSocket 错误信息:', error);
        };
      }

      handleMessage(message) {
        // 验证消息的格式
        if (!message.type || !message.id) {
          console.error('无效的消息格式:', message);
          return;
        }

        // 处理请求的响应
        if (message.type === MessageTypes.RESPONSE && this.pendingRequests.has(message.requestId)) {
          const { resolve } = this.pendingRequests.get(message.requestId);
          resolve(message.payload);
          this.pendingRequests.delete(message.requestId);
          return;
        }

        // 处理错误
        if (message.type === MessageTypes.ERROR && this.pendingRequests.has(message.requestId)) {
          const { reject } = this.pendingRequests.get(message.requestId);
          reject(new Error(message.error));
          this.pendingRequests.delete(message.requestId);
          return;
        }

        // 处理相应消息类型
        if (this.messageHandlers[message.type]) {
          this.messageHandlers[message.type](message.payload, message);
        } else {
          console.warn('没有处理该消息的类型:', message.type);
        }
      }

      sendMessage(type, payload, requestId = null) {
        if (this.socket.readyState !== WebSocket.OPEN) {
          return Promise.reject(new Error('WebSocket 未连接'));
        }

        const id = this.generateId();
        const message = {
          id,
          type,
          timestamp: Date.now(),
          payload
        };

        if (requestId) {
          message.requestId = requestId;
        }

        // 发送消息到服务器
        this.socket.send(JSON.stringify(message));

        // 如果这是需要响应的查询,则返回一个 Promise
        if (type === MessageTypes.QUERY) {
          return new Promise((resolve, reject) => {
            this.pendingRequests.set(id, { resolve, reject });

            // 设置请求超时
            setTimeout(() => {
              if (this.pendingRequests.has(id)) {
                this.pendingRequests.delete(id);
                reject(new Error('请求已超时'));
              }
            }, this.requestTimeout);
          });
        }

        return Promise.resolve();
      }

      on(messageType, handler) {
        this.messageHandlers[messageType] = handler;
      }

      authenticate(credentials) {
        return this.sendMessage(MessageTypes.AUTHENTICATION, credentials);
      }

      query(resource, parameters = {}) {
        return this.sendMessage(MessageTypes.QUERY, { resource, parameters });
      }

      command(action, parameters = {}) {
        return this.sendMessage(MessageTypes.COMMAND, { action, parameters });
      }

      publishEvent(event, data = {}) {
        return this.sendMessage(MessageTypes.EVENT, { event, data });
      }

      generateId() {
        return Math.random().toString(36).substring(2, 15);
      }
    }

全屏模式/退出全屏

这里展示的协议为消息增添了结构,包括类型、ID、时间戳和负载内容。它还支持请求响应模式,并且能够处理错误。在我开发的一款金融应用中,由于有一个明确的协议,集成期间的错误减少了超过70%。

频道订阅

对于需要不同类型实时信息更新的应用,频道订阅功能允许客户端仅接收所需的数据,从而减少带宽使用和处理负担。

    class ChannelWebSocket {
      constructor(url) {
        this.url = url;
        this.socket = null;
        this.subscriptions = new Map();
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 10;
        this.connect();
      }

      connect() {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
          console.log('已建立连接');
          this.reconnectAttempts = 0;

          // 重新订阅所有通道
          for (const [channel, callback] of this.subscriptions.entries()) {
            this.sendSubscription(channel);
          }
        };

        this.socket.onmessage = (event) => {
          try {
            const message = JSON.parse(event.data);
            this.handleMessage(message);
          } catch (error) {
            console.error('解析消息时出错:', error);
          }
        };

        this.socket.onclose = () => {
          console.log('连接断开');
          if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), 30000);
            setTimeout(() => this.connect(), delay);
          }
        };

        this.socket.onerror = (error) => {
          console.error('WebSocket 错误信息:', error);
        };
      }

      handleMessage(message) {
        if (!message.channel || !message.data) {
          console.warn('收到格式错误的消息:', message);
          return;
        }

        // 将消息转发给订阅者
        if (this.subscriptions.has(message.channel)) {
          const callback = this.subscriptions.get(message.channel);
          callback(message.data);
        }

        // 处理系统消息
        if (message.channel === 'system') {
          this.handleSystemMessage(message.data);
        }
      }

      handleSystemMessage(data) {
        if (data.type === 'subscription_confirm') {
          console.log(`订阅 ${data.channel} 成功`);
        } else if (data.type === 'error') {
          console.error('系统发生错误:', data.message);
        }
      }

      subscribe(channel, callback) {
        if (typeof callback !== 'function') {
          throw new Error('回调必须是函数');
        }

        this.subscriptions.set(channel, callback);

        if (this.socket.readyState === WebSocket.OPEN) {
          this.sendSubscription(channel);
        }

        return {
          unsubscribe: () => this.unsubscribe(channel)
        };
      }

      unsubscribe(channel) {
        if (!this.subscriptions.has(channel)) {
          return false;
        }

        this.subscriptions.delete(channel);

        if (this.socket.readyState === WebSocket.OPEN) {
          this.socket.send(JSON.stringify({
            action: 'unsubscribe',
            channel
          }));
        }

        return true;
      }

      sendSubscription(channel) {
        this.socket.send(JSON.stringify({
          action: 'subscribe',
          channel
        }));
      }

      publish(channel, data) {
        if (this.socket.readyState !== WebSocket.OPEN) {
          return false;
        }

        this.socket.send(JSON.stringify({
          action: 'publish',
          channel,
          data
        }));

        return true;
      }
    }

全屏模式(按ESC退出)

这种模式特别适合具有多个数据源的仪表板应用程序。在我构建的一个监控系统中,通过实现通道订阅,用户只会收到他们正在查看的组件的更新,从而将 WebSocket 通信量减少了 80% 之多。

如何扩展WebSocket技术及性能考量

随着您的应用规模的扩大,扩展 WebSockets 变得非常重要。这里有一些我成功的策略:

使用支持WebSocket的负载均衡器(如NGINX或HAProxy)进行水平扩展。

    上游 websocket_servers {
        哈希 $remote_addr 一致性;
        服务器 backend1.example.com:8080;
        服务器 backend2.example.com:8080;
        服务器 backend3.example.com:8080;
    }

    服务器 {
        监听 80;
        服务器名 ws.example.com;

        位置 /ws/ {
            代理传递 http://websocket_servers;
            代理 HTTP 版本1.1;
            设置代理头 Upgrade $http_upgrade;
            设置代理头 Connection "upgrade";
            设置代理头 Host $host;
            设置代理头 X-Real-IP $remote_addr;
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
        }
    }

全屏显示 退出全屏

对于 Node.js 服务器端的实现,我常常使用 ws 库结合 Redis 在多个实例之间广播消息:

    const WebSocket = require('ws');
    const Redis = require('ioredis');
    const http = require('http');

    // 创建 Redis 客户端
    const subscriber = new Redis();
    const publisher = new Redis();

    // 创建 HTTP 服务器
    const server = http.createServer();

    // 创建 WebSocket 服务器
    const wss = new WebSocket.Server({ server });

    // 存储连接的客户端及其订阅
    const clients = new Map();

    wss.on('connection', (ws) => {
      const clientId = generateId();
      const clientData = {
        id: clientId,
        subscriptions: new Set()
      };

      clients.set(ws, clientData);

      console.log(`客户端 ${clientId} 已连接上`);

      ws.on('message', (message) => {
        try {
          const data = JSON.parse(message);

          switch (data.action) {
            case 'subscribe':
              handleSubscribe(ws, clientData, data.channel);
              break;
            case 'unsubscribe':
              handleUnsubscribe(ws, clientData, data.channel);
              break;
            case 'publish':
              handlePublish(data.channel, data.data);
              break;
            default:
              console.warn(`未知的操作 ${data.action}:`);
          }
        } catch (error) {
          console.error('处理消息时出现错误:', error);
        }
      });

      ws.on('close', () => {
        // 清理订阅
        clientData.subscriptions.forEach(channel => {
          subscriber.unsubscribe(channel);
        });

        clients.delete(ws);
        console.log(`客户端 ${clientId} 已断开连接`);
      });
    });

    // 处理订阅操作
    function handleSubscribe(ws, clientData, channel) {
      // 订阅 Redis 通道
      subscriber.subscribe(channel);

      // 添加到客户端订阅列表
      clientData.subscriptions.add(channel);

      // 确认订阅
      ws.send(JSON.stringify({
        channel: 'system',
        data: {
          type: 'subscription_confirm',
          channel
        }
      }));
    }

    // 处理取消订阅操作
    function handleUnsubscribe(ws, clientData, channel) {
      // 从客户端订阅列表移除
      clientData.subscriptions.delete(channel);

      // 检查是否有其他订阅者
      let hasOtherSubscribers = false;
      clients.forEach(client => {
        if (client.subscriptions.has(channel)) {
          hasOtherSubscribers = true;
        }
      });

      if (!hasOtherSubscribers) {
        subscriber.unsubscribe(channel);
      }
    }

    // 处理发布操作
    function handlePublish(channel, data) {
      // 发布到 Redis
      publisher.publish(channel, JSON.stringify(data));
    }

    // 处理来自 Redis 的消息
    subscriber.on('message', (channel, message) => {
      // 向订阅该通道的所有客户端广播
      clients.forEach((clientData, ws) => {
        if (clientData.subscriptions.has(channel) && ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({
            channel,
            data: JSON.parse(message)
          }));
        }
      });
    });

    function generateId() {
      return Math.random().toString(36).substring(2, 15);
    }

    // 启动服务器
    const PORT = process.env.PORT || 8080;
    server.listen(PORT, () => {
      console.log(`WebSocket 服务器正在端口 ${PORT} 上监听`);
    });

全屏 退出全屏

这种实现方式允许多个服务器实例通过 Redis 共享 WebSocket 消息的机制,从而实现水平扩展。当我为一个大型电子商务网站实现此模式时,我们可以在六个实例上支持超过 50,000 个同时的 WebSocket 连接。

结论

这六种WebSocket模式在我开发经历中非常有用。连接池有效地管理资源,心跳机制用来确保连接的健康,重连策略用来处理网络中断,消息队列用来防止数据丢失,协议定义使通信更加标准化,通道订阅用来优化网络使用。

提供的代码示例已经在生产环境中经过实际考验,可以根据您的具体情况进行调整。请记住,最佳实现方式取决于您的实际情况——需要考虑的因素包括预期用户数量、消息频率以及实时更新的重要性。

通过应用这些模式,您可以创建不仅功能丰富,而且健壮、易于扩展和维护的 WebSocket 实现。结果将是即使在复杂的网络环境中,也能给用户提供出色体验的实时 web 应用程序。

……

101本书

101 Books 是由作者 Aarav Joshi 共同创立的一家由人工智能驱动的出版公司。通过利用先进的AI技术,我们将出版成本降至极低——有些书售价低至 $4 —让高质量的知识变得触手可及。

来看看我们的书《Golang Clean Code》,现在可以在亚马逊上买到,快来查看吧!

请继续关注更新和激动人心的消息。买书时,搜索 Aarav Joshi 找到我们更多的书。使用提供的链接享受专属优惠!

我们的杰作

一定要看看我们的创作。

投资者中心 | 投资者中心西班牙 | 投资者中心德 | 智慧生活 | 时代的回响 | 谜团 | 印度教民族主义 | 精英开发 | JS学校

……

我们也在 Medium 上

Tech Koala Insights | Epochs& Echoes 世界版 | Medium 投资者中心 | Medium 谜团与谜题 | Medium 科学与纪元 | 现代印度教思潮

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消