Skip to content

WebSocket客户端应用

认识WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的网络协议,它实现了浏览器与服务器之间的双向实时通信。相比传统的HTTP请求-响应模式,WebSocket具有以下优势:

  • 双向通信:服务器可以主动向客户端推送数据,而不需要客户端发起请求
  • 低延迟:建立连接后,消息传递的开销很小,没有HTTP头部的负担
  • 实时性强:数据可以即时传递,无需轮询
  • 保持连接:长连接不会像HTTP那样每次请求后断开

WebSocket vs HTTP轮询

使用场景

WebSocket技术适用于需要实时交互的应用场景:

  1. 聊天应用:即时消息传递、在线状态更新
  2. 协作工具:文档共同编辑、白板协作
  3. 实时监控:数据可视化、系统监控面板
  4. 在线游戏:多人游戏、实时对战
  5. 金融应用:股票行情、交易通知
  6. IoT设备:传感器数据实时上报、设备控制

前端WebSocket API

基本用法

浏览器原生支持WebSocket API,使用非常简单:

javascript
// 创建WebSocket连接
const socket = new WebSocket('ws://example.com/socket');

// 连接建立时触发
socket.onopen = function(event) {
  console.log('WebSocket连接已建立');
  
  // 发送消息
  socket.send('你好,服务器!');
};

// 接收消息
socket.onmessage = function(event) {
  console.log('收到消息:', event.data);
};

// 连接关闭时触发
socket.onclose = function(event) {
  console.log('WebSocket连接已关闭:', event.code, event.reason);
};

// 发生错误时触发
socket.onerror = function(error) {
  console.error('WebSocket错误:', error);
};

发送不同类型的数据

WebSocket可以发送文本或二进制数据:

javascript
// 发送文本
socket.send('Hello World');

// 发送JSON对象
const message = { type: 'chat', content: 'Hello', timestamp: Date.now() };
socket.send(JSON.stringify(message));

// 发送二进制数据 - ArrayBuffer
const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);
view[0] = 10; view[1] = 20; view[2] = 30; view[3] = 40;
socket.send(buffer);

// 发送二进制数据 - Blob
const blob = new Blob(['Binary data here'], { type: 'application/octet-stream' });
socket.send(blob);

连接状态

WebSocket对象的readyState属性表示当前连接状态:

javascript
const socket = new WebSocket('ws://example.com/socket');

// 检查连接状态
function checkState() {
  switch(socket.readyState) {
    case WebSocket.CONNECTING: // 0
      console.log('连接中...');
      break;
    case WebSocket.OPEN: // 1
      console.log('连接已打开');
      break;
    case WebSocket.CLOSING: // 2
      console.log('连接关闭中...');
      break;
    case WebSocket.CLOSED: // 3
      console.log('连接已关闭');
      break;
  }
}

关闭连接

javascript
// 正常关闭连接
socket.close();

// 带状态码和原因关闭
socket.close(1000, '操作完成');

// 常见关闭状态码
// 1000: 正常关闭
// 1001: 离开页面
// 1002: 协议错误
// 1003: 无法接受的数据
// 1008: 策略违规
// 1011: 服务器内部错误

实用WebSocket客户端实践

1. 健壮的重连机制

javascript
class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.socket = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectInterval = options.reconnectInterval || 1000;
    this.reconnectDecay = options.reconnectDecay || 1.5;
    this.maxReconnectInterval = options.maxReconnectInterval || 30000;
    this.messageQueue = [];
    
    this.onopen = null;
    this.onmessage = null;
    this.onclose = null;
    this.onerror = null;
    
    this.connect();
  }
  
  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = (event) => {
      this.isConnected = true;
      this.reconnectAttempts = 0;
      
      // 发送队列中的消息
      while(this.messageQueue.length > 0) {
        const message = this.messageQueue.shift();
        this.send(message);
      }
      
      if(this.onopen) this.onopen(event);
    };
    
    this.socket.onmessage = (event) => {
      if(this.onmessage) this.onmessage(event);
    };
    
    this.socket.onclose = (event) => {
      this.isConnected = false;
      
      if(!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
        const delay = Math.min(
          this.reconnectInterval * Math.pow(this.reconnectDecay, this.reconnectAttempts),
          this.maxReconnectInterval
        );
        
        this.reconnectAttempts++;
        console.log(`WebSocket连接断开,${delay}ms后尝试重连...`);
        
        setTimeout(() => this.connect(), delay);
      }
      
      if(this.onclose) this.onclose(event);
    };
    
    this.socket.onerror = (error) => {
      if(this.onerror) this.onerror(error);
    };
  }
  
  send(data) {
    if(this.isConnected && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      this.messageQueue.push(data);
    }
  }
  
  close(code, reason) {
    if(this.socket) {
      this.reconnectAttempts = this.maxReconnectAttempts; // 禁止重连
      this.socket.close(code, reason);
    }
  }
}

// 使用方法
const ws = new ReconnectingWebSocket('ws://example.com/socket', {
  maxReconnectAttempts: 10,
  reconnectInterval: 2000
});

ws.onopen = () => console.log('连接已建立');
ws.onmessage = (event) => console.log('收到消息:', event.data);

2. 心跳检测机制

javascript
class HeartbeatWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.socket = null;
    this.heartbeatInterval = options.heartbeatInterval || 30000;
    this.heartbeatTimer = null;
    this.missedHeartbeats = 0;
    this.maxMissedHeartbeats = options.maxMissedHeartbeats || 3;
    
    this.connect();
  }
  
  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log('WebSocket连接已建立,开始心跳检测');
      this.startHeartbeat();
    };
    
    this.socket.onmessage = (event) => {
      // 如果收到服务器的pong消息,重置计数器
      if(event.data === 'pong') {
        this.missedHeartbeats = 0;
        return;
      }
      
      // 处理其他消息...
      console.log('收到消息:', event.data);
    };
    
    this.socket.onclose = () => {
      console.log('WebSocket连接已关闭');
      this.stopHeartbeat();
    };
  }
  
  startHeartbeat() {
    this.missedHeartbeats = 0;
    this.heartbeatTimer = setInterval(() => {
      // 如果连接断开,停止心跳
      if(this.socket.readyState !== WebSocket.OPEN) {
        this.stopHeartbeat();
        return;
      }
      
      try {
        this.missedHeartbeats++;
        
        // 如果超过最大允许的未响应心跳次数,认为连接已断开
        if(this.missedHeartbeats > this.maxMissedHeartbeats) {
          console.log('服务器未响应心跳,关闭连接并重新连接');
          this.socket.close();
          clearInterval(this.heartbeatTimer);
          this.connect();
          return;
        }
        
        // 发送心跳
        this.socket.send('ping');
        console.log('发送心跳ping...');
      } catch(error) {
        console.error('心跳发送失败:', error);
        this.stopHeartbeat();
      }
    }, this.heartbeatInterval);
  }
  
  stopHeartbeat() {
    clearInterval(this.heartbeatTimer);
  }
  
  send(data) {
    if(this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      console.warn('WebSocket未连接,无法发送消息');
    }
  }
  
  close() {
    this.stopHeartbeat();
    this.socket.close();
  }
}

// 使用方法
const ws = new HeartbeatWebSocket('ws://example.com/socket', {
  heartbeatInterval: 15000,
  maxMissedHeartbeats: 2
});

3. 消息类型处理

javascript
class TypedWebSocket {
  constructor(url) {
    this.socket = new WebSocket(url);
    this.messageHandlers = {};
    
    this.socket.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data);
        
        // 根据消息类型调用对应的处理器
        if(message.type && this.messageHandlers[message.type]) {
          this.messageHandlers[message.type](message);
        } else {
          console.warn('未知消息类型:', message);
        }
      } catch(error) {
        console.error('消息解析失败:', error);
      }
    };
  }
  
  // 注册消息类型处理器
  on(type, handler) {
    this.messageHandlers[type] = handler;
    return this;
  }
  
  // 发送带类型的消息
  send(type, data) {
    const message = {
      type,
      data,
      timestamp: Date.now()
    };
    
    this.socket.send(JSON.stringify(message));
  }
}

// 使用方法
const ws = new TypedWebSocket('ws://example.com/socket');

// 注册消息处理器
ws.on('chat', (message) => {
  console.log(`收到聊天消息: ${message.data.content} 来自: ${message.data.sender}`);
})
.on('notification', (message) => {
  console.log(`收到通知: ${message.data.text}`);
})
.on('error', (message) => {
  console.error(`错误: ${message.data.message}`);
});

// 发送消息
ws.send('chat', {
  content: '你好,大家!',
  sender: 'Alice'
});

前端框架集成

React中使用WebSocket

在React中使用WebSocket通常通过自定义Hook实现:

jsx
// useWebSocket.js
import { useState, useEffect, useCallback, useRef } from 'react';

export function useWebSocket(url) {
  const [isConnected, setIsConnected] = useState(false);
  const [messages, setMessages] = useState([]);
  const socketRef = useRef(null);
  
  // 连接WebSocket
  useEffect(() => {
    const socket = new WebSocket(url);
    
    socket.onopen = () => {
      setIsConnected(true);
    };
    
    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages((prevMessages) => [...prevMessages, message]);
    };
    
    socket.onclose = () => {
      setIsConnected(false);
    };
    
    socketRef.current = socket;
    
    // 清理函数
    return () => {
      socket.close();
    };
  }, [url]);
  
  // 发送消息
  const sendMessage = useCallback((data) => {
    if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
      socketRef.current.send(JSON.stringify(data));
    }
  }, []);
  
  return { isConnected, messages, sendMessage };
}

// 使用示例
function ChatComponent() {
  const { isConnected, messages, sendMessage } = useWebSocket('ws://example.com/chat');
  const [inputValue, setInputValue] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      sendMessage({
        type: 'chat',
        content: inputValue
      });
      setInputValue('');
    }
  };
  
  return (
    <div>
      <div className="status">
        {isConnected ? '已连接' : '连接中...'}
      </div>
      
      <div className="messages">
        {messages.map((msg, index) => (
          <div key={index} className="message">
            {msg.content}
          </div>
        ))}
      </div>
      
      <form onSubmit={handleSubmit}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="输入消息..."
          disabled={!isConnected}
        />
        <button type="submit" disabled={!isConnected}>发送</button>
      </form>
    </div>
  );
}

Vue中使用WebSocket

Vue组合式API(Composition API)中使用WebSocket:

javascript
// useWebSocket.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useWebSocket(url) {
  const socket = ref(null);
  const isConnected = ref(false);
  const messages = ref([]);
  
  const connect = () => {
    socket.value = new WebSocket(url);
    
    socket.value.onopen = () => {
      isConnected.value = true;
    };
    
    socket.value.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data);
        messages.value.push(message);
      } catch (error) {
        console.error('消息解析失败:', error);
      }
    };
    
    socket.value.onclose = () => {
      isConnected.value = false;
    };
  };
  
  const disconnect = () => {
    if (socket.value) {
      socket.value.close();
    }
  };
  
  const sendMessage = (data) => {
    if (socket.value && socket.value.readyState === WebSocket.OPEN) {
      socket.value.send(JSON.stringify(data));
    }
  };
  
  onMounted(() => {
    connect();
  });
  
  onUnmounted(() => {
    disconnect();
  });
  
  return {
    isConnected,
    messages,
    sendMessage
  };
}

// 使用示例
<template>
  <div class="chat">
    <div class="status">
      {{ isConnected ? '已连接' : '连接中...' }}
    </div>
    
    <div class="messages">
      <div v-for="(msg, index) in messages" :key="index" class="message">
        {{ msg.content }}
      </div>
    </div>
    
    <div class="input-area">
      <input v-model="inputMessage" placeholder="输入消息..." :disabled="!isConnected">
      <button @click="sendChatMessage" :disabled="!isConnected">发送</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useWebSocket } from './useWebSocket';

const { isConnected, messages, sendMessage } = useWebSocket('ws://example.com/chat');
const inputMessage = ref('');

function sendChatMessage() {
  if (inputMessage.value.trim()) {
    sendMessage({
      type: 'chat',
      content: inputMessage.value
    });
    inputMessage.value = '';
  }
}
</script>

安全最佳实践

1. 使用安全连接(WSS)

始终使用wss://而不是ws://协议,以确保数据通过TLS/SSL加密传输:

javascript
// 不安全
const insecureSocket = new WebSocket('ws://example.com/socket');

// 安全
const secureSocket = new WebSocket('wss://example.com/socket');

2. 身份验证

使用令牌或会话ID进行连接验证:

javascript
// 使用JWT令牌连接
const token = localStorage.getItem('auth_token');
const socket = new WebSocket(`wss://example.com/socket?token=${token}`);

// 或在连接后立即发送验证消息
socket.onopen = () => {
  socket.send(JSON.stringify({
    type: 'auth',
    token: localStorage.getItem('auth_token')
  }));
};

3. 输入验证

始终验证从服务器接收的数据:

javascript
socket.onmessage = (event) => {
  try {
    const data = JSON.parse(event.data);
    
    // 验证数据结构
    if (!data.type || typeof data.type !== 'string') {
      console.error('无效的消息格式');
      return;
    }
    
    // 根据类型处理
    switch (data.type) {
      case 'chat':
        // 验证聊天消息字段
        if (!data.content || typeof data.content !== 'string') {
          console.error('无效的聊天消息');
          return;
        }
        displayChatMessage(data);
        break;
      // 其他类型...
    }
  } catch (error) {
    console.error('消息解析失败:', error);
  }
};

调试与性能

调试WebSocket

使用Chrome DevTools调试WebSocket连接:

  1. 打开Chrome DevTools
  2. 切换到Network标签页
  3. 筛选WS连接类型
  4. 点击WebSocket连接查看详情
  5. 在Messages标签页中查看所有收发的消息

性能优化

  1. 减少消息大小:只发送必要的数据,避免冗余字段
  2. 批量处理:将多个小消息合并为一个更大的消息
  3. 消息压缩:使用JSON压缩或二进制格式减少数据量
  4. 限制重连:使用指数退避算法,避免频繁重连
  5. 清理资源:在组件卸载时正确关闭WebSocket连接

总结

WebSocket为Web应用提供了强大的实时通信能力,使前端开发者能够构建交互性更强的应用。通过本文介绍的技术和最佳实践,你可以:

  1. 使用原生WebSocket API建立双向通信
  2. 实现健壮的重连和心跳机制
  3. 设计类型化的消息处理系统
  4. 在React或Vue等现代框架中集成WebSocket
  5. 遵循安全最佳实践保护通信安全

随着Web应用对实时性要求的提高,掌握WebSocket技术已成为前端开发者的必备技能。

用知识点燃技术的火山