WebSocket 完全指南

目录


概述

WebSocket 是一种全双工通信协议,允许服务器主动向客户端推送数据。

核心特点

特点说明
全双工客户端和服务器可以同时发送消息
持久连接建立一次连接,可以多次双向通信
实时性服务器可以主动推送,无需轮询
基于 TCP可靠的传输协议

HTTP vs WebSocket

HTTP 轮询(传统方式)

客户端                    服务器
   │                         │
   │──── GET /api/data ─────>│
   │<─── Response ───────────│
   │                         │
   │──── GET /api/data ─────>│  ← 每隔几秒请求一次
   │<─── Response ───────────│
   │                         │
   │──── GET /api/data ─────>│
   │<─── Response ───────────│

问题:浪费资源,延迟高

WebSocket(实时方式)

客户端                    服务器
   │                         │
   │──── 建立连接 ────────────>│
   │<──── 连接成功 ────────────│
   │                         │
   │<──── 服务器推送 ──────────│  ← 服务器随时可以发
   │──── 客户端发送 ──────────>│  ← 客户端随时可以发
   │                         │
   │──── 保持连接 ────────────>│  ← 长连接

WebSocket 原理

建立连接过程(握手)

1. 客户端发送 HTTP 请求(带有 Upgrade 头)
   GET /ws HTTP/1.1
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

2. 服务器响应 101 状态码
   HTTP/1.1 101 Switching Protocols
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYG3hQbDA==

3. 连接升级为 WebSocket 协议

连接状态

状态说明
CONNECTING0连接中
OPEN1已连接
CLOSING2关闭中
CLOSED3已关闭

前端实现

1. 原生 WebSocket API

建立连接

// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:3000/ws');
 
// 或加密版本
const ws = new WebSocket('wss://example.com/ws');

事件处理

// 连接成功
ws.onopen = (event) => {
  console.log('WebSocket 已连接');
  ws.send('Hello Server');  // 发送消息
};
 
// 收到消息
ws.onmessage = (event) => {
  const data = event.data;
  console.log('收到消息:', data);
  // 处理数据
};
 
// 连接错误
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error);
};
 
// 连接关闭
ws.onclose = (event) => {
  console.log('WebSocket 已关闭', event.code, event.reason);
};

发送消息

// 发送字符串
ws.send('Hello');
 
// 发送 JSON
ws.send(JSON.stringify({
  type: 'message',
  content: 'Hello',
  timestamp: Date.now()
}));
 
// 发送二进制(较少用)
ws.send(new ArrayBuffer(data));

关闭连接

// 主动关闭连接
ws.close();                    // 正常关闭
ws.close(1000, '正常关闭');    // 带状态码和原因
 
// 状态码
// 1000: 正常关闭
// 1001: 服务器关闭
// 1002: 协议错误
// 1003: 不支持的数据类型
// 1006: 非正常关闭

2. Vue3 中使用 WebSocket

方式一:组合式函数(推荐)

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
 
const messages = ref([]);
const ws = ref(null);
const connected = ref(false);
 
// 建立连接
function connect() {
  ws.value = new WebSocket('ws://localhost:3000/ws');
 
  ws.value.onopen = () => {
    connected.value = true;
    console.log('已连接');
  };
 
  ws.value.onmessage = (event) => {
    const data = JSON.parse(event.data);
    messages.value.push(data);
  };
 
  ws.value.onclose = () => {
    connected.value = false;
    console.log('连接关闭');
  };
 
  ws.value.onerror = (error) => {
    console.error('WebSocket 错误:', error);
  };
}
 
// 发送消息
function sendMessage(content) {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.send(JSON.stringify({
      type: 'message',
      content: content,
      timestamp: Date.now()
    }));
  }
}
 
// 主动断开
function disconnect() {
  if (ws.value) {
    ws.value.close();
    ws.value = null;
  }
}
 
onMounted(() => {
  connect();
});
 
onUnmounted(() => {
  disconnect();
});
</script>
 
<template>
  <div>
    <div v-if="!connected">连接中...</div>
    <div v-else>
      <div v-for="msg in messages" :key="msg.timestamp">
        {{ msg.content }}
      </div>
      <button @click="sendMessage('Hello')">发送</button>
    </div>
  </div>
</template>

方式二:封装成工具函数

// composables/useWebSocket.js
import { ref, onUnmounted } from 'vue';
 
export function useWebSocket(url) {
  const messages = ref([]);
  const connected = ref(false);
  let ws = null;
 
  function connect() {
    ws = new WebSocket(url);
 
    ws.onopen = () => {
      connected.value = true;
    };
 
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      messages.value.push(data);
    };
 
    ws.onclose = () => {
      connected.value = false;
    };
  }
 
  function send(data) {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(typeof data === 'string' ? data : JSON.stringify(data));
    }
  }
 
  function close() {
    if (ws) {
      ws.close();
      ws = null;
    }
  }
 
  onUnmounted(() => {
    close();
  });
 
  return {
    messages,
    connected,
    connect,
    send,
    close
  };
}

3. React 中使用 WebSocket

方式一:Class 组件

class Chat extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      messages: [],
      connected: false
    };
    this.ws = null;
  }
 
  componentDidMount() {
    this.ws = new WebSocket('ws://localhost:3000/ws');
 
    this.ws.onopen = () => {
      this.setState({ connected: true });
    };
 
    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.setState(state => ({
        messages: [...state.messages, data]
      }));
    };
 
    this.ws.onclose = () => {
      this.setState({ connected: false });
    };
  }
 
  componentWillUnmount() {
    if (this.ws) {
      this.ws.close();
    }
  }
 
  sendMessage = (content) => {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ content }));
    }
  };
 
  render() {
    return (
      <div>
        {!this.state.connected && <div>连接中...</div>}
        {this.state.messages.map((msg, i) => (
          <div key={i}>{msg.content}</div>
        ))}
        <button onClick={() => this.sendMessage('Hello')}>发送</button>
      </div>
    );
  }
}

方式二:Hook(推荐)

import { useState, useEffect, useRef, useCallback } from 'react';
 
function useWebSocket(url) {
  const [messages, setMessages] = useState([]);
  const [connected, setConnected] = useState(false);
  const wsRef = useRef(null);
 
  useEffect(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;
 
    ws.onopen = () => setConnected(true);
    ws.onclose = () => setConnected(false);
 
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setMessages(prev => [...prev, data]);
    };
 
    return () => {
      ws.close();
    };
  }, [url]);
 
  const send = useCallback((data) => {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(typeof data === 'string' ? data : JSON.stringify(data));
    }
  }, []);
 
  return { messages, connected, send };
}
 
// 使用
function Chat() {
  const { messages, connected, send } = useWebSocket('ws://localhost:3000/ws');
 
  return (
    <div>
      <div>{connected ? '已连接' : '连接中...'}</div>
      {messages.map((msg, i) => (
        <div key={i}>{msg.content}</div>
      ))}
      <button onClick={() => send('Hello')}>发送</button>
    </div>
  );
}

4. 常用 WebSocket 库

说明
socket.io最流行,自动重连,降级支持
sockjs提供跨浏览器兼容
wsNode.js 轻量级 WebSocket 库

Socket.IO 客户端示例

// 客户端
import { io } from 'socket.io-client';
 
const socket = io('http://localhost:3000', {
  transports: ['websocket'],  // 强制使用 WebSocket
  reconnection: true,         // 自动重连
  reconnectionDelay: 1000,    // 重连间隔
});
 
// 事件
socket.on('connect', () => {
  console.log('已连接:', socket.id);
});
 
socket.on('message', (data) => {
  console.log('收到消息:', data);
});
 
// 发送
socket.emit('message', 'Hello');
 
// 断开
socket.disconnect();

后端实现

1. Node.js 原生实现

const http = require('http');
const crypto = require('crypto');
 
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end('HTTP Server');
});
 
// WebSocket 升级处理
server.on('upgrade', (request, socket, head) => {
  // 验证 WebSocket 握手
  const key = request.headers['sec-websocket-key'];
  const acceptKey = generateAcceptKey(key);
 
  const responseHeaders = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${acceptKey}`,
    '',
    ''
  ].join('\r\n');
 
  socket.write(responseHeaders);
 
  // 连接升级完成,可以收发消息
  socket.on('data', (buffer) => {
    // 解码消息
    const message = decodeMessage(buffer);
    console.log('收到:', message);
 
    // 发送消息给客户端
    const response = encodeMessage('Hello from server');
    socket.write(response);
  });
 
  socket.on('close', () => {
    console.log('连接关闭');
  });
});
 
// 生成握手 key
function generateAcceptKey(key) {
  const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  const acceptKey = crypto
    .createHash('sha1')
    .update(key + GUID)
    .digest('base64');
  return acceptKey;
}
 
// 解码 WebSocket 帧
function decodeMessage(buffer) {
  const firstByte = buffer[0];
  const opcode = firstByte & 0x0f;
  const secondByte = buffer[1];
  const isMasked = (secondByte & 0x80) !== 0;
  let offset = 2;
 
  if (isMasked) {
    offset += 4;
  }
 
  let payloadLength = secondByte & 0x7f;
  if (payloadLength === 126) {
    payloadLength = buffer.readUInt16BE(offset);
    offset += 2;
  } else if (payloadLength === 127) {
    offset += 8;
  }
 
  const payload = buffer.slice(offset, offset + payloadLength);
  return payload.toString();
}
 
// 编码消息为 WebSocket 帧
function encodeMessage(message) {
  const msgBuffer = Buffer.from(message);
  const length = msgBuffer.length;
 
  let offset = 2;
  if (length > 65535) {
    offset += 8;
  } else if (length > 125) {
    offset += 2;
  }
 
  const buffer = Buffer.alloc(offset + length);
  buffer[0] = 0x81;  // FIN + text frame
  buffer[1] = length > 65535 ? 127 : (length > 125 ? 126 : length);
 
  if (length > 65535) {
    buffer.writeUInt32BE(0, 2);
    buffer.writeUInt32BE(length, 6);
  } else if (length > 125) {
    buffer.writeUInt16BE(length, 2);
  }
 
  msgBuffer.copy(buffer, offset);
  return buffer;
}
 
server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

2. 使用 ws 库(推荐)

npm install ws
const { WebSocketServer } = require('ws');
 
const wss = new WebSocketServer({ port: 3000, path: '/ws' });
 
// 广播消息给所有客户端
function broadcast(message) {
  wss.clients.forEach(client => {
    if (client.readyState === 1) {  // OPEN
      client.send(JSON.stringify(message));
    }
  });
}
 
wss.on('connection', (ws, req) => {
  console.log('新客户端连接');
 
  // 发送欢迎消息
  ws.send(JSON.stringify({
    type: 'system',
    content: '欢迎连接'
  }));
 
  // 收到消息
  ws.on('message', (data) => {
    const message = data.toString();
    console.log('收到:', message);
 
    // 回复客户端
    ws.send(JSON.stringify({
      type: 'response',
      content: `服务器收到: ${message}`
    }));
 
    // 广播给所有客户端
    broadcast({
      type: 'broadcast',
      content: message
    });
  });
 
  // 连接关闭
  ws.on('close', () => {
    console.log('客户端断开');
  });
 
  // 错误处理
  ws.on('error', (error) => {
    console.error('WebSocket 错误:', error);
  });
});
 
console.log('WebSocket 服务器运行在 ws://localhost:3000');

3. Socket.IO 服务端

npm install socket.io
const { Server } = require('socket.io');
 
const io = new Server(3000, {
  cors: {
    origin: '*'  // 允许跨域
  }
});
 
io.on('connection', (socket) => {
  console.log('客户端连接:', socket.id);
 
  // 加入房间
  socket.on('join-room', (room) => {
    socket.join(room);
    console.log(`${socket.id} 加入房间 ${room}`);
  });
 
  // 发送给特定用户
  socket.on('private-message', ({ to, message }) => {
    io.to(to).emit('message', {
      from: socket.id,
      content: message
    });
  });
 
  // 广播给房间
  socket.on('room-message', ({ room, message }) => {
    io.to(room).emit('message', {
      from: socket.id,
      content: message
    });
  });
 
  // 发送给所有人
  socket.on('broadcast', (message) => {
    io.emit('message', {
      from: socket.id,
      content: message
    });
  });
 
  // 断开连接
  socket.on('disconnect', () => {
    console.log('客户端断开:', socket.id);
  });
});
 
console.log('Socket.IO 服务器运行在 http://localhost:3000');

4. Express + Socket.IO 组合

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
 
const app = express();
const server = http.createServer(app);
const io = new Server(server);
 
// HTTP 路由
app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello' });
});
 
// Socket.IO
io.on('connection', (socket) => {
  console.log('WebSocket 连接:', socket.id);
 
  socket.on('message', (data) => {
    console.log('收到:', data);
    socket.emit('response', '服务器收到');
  });
 
  socket.on('disconnect', () => {
    console.log('断开连接');
  });
});
 
server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

安全注意事项

1. 使用 WSS(加密)

// ❌ 不安全(明文)
const ws = new WebSocket('ws://example.com/ws');
 
// ✅ 安全(加密)
const ws = new WebSocket('wss://example.com/ws');

2. 验证来源(Origin)

// 服务端验证 Origin
wss.on('connection', (ws, req) => {
  const origin = req.headers.origin;
  if (origin !== 'https://your-domain.com') {
    ws.close(1003, '不允许的来源');
    return;
  }
  // 处理连接
});

3. 身份验证

// 方式1:URL 参数(不安全)
const ws = new WebSocket('wss://example.com/ws?token=xxx');
 
// 方式2:建立连接后立即发送认证消息
ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'auth', token: 'xxx' }));
};
 
// 方式3:握手时验证(最佳)
// 在 upgrade 事件中验证 session/token

4. 输入验证

// 服务端必须验证客户端发来的数据
ws.on('message', (data) => {
  try {
    const message = JSON.parse(data);
 
    // 验证数据格式
    if (typeof message.content !== 'string') {
      return;
    }
 
    // 限制长度
    if (message.content.length > 1000) {
      return;
    }
 
    // 处理消息
    handleMessage(message);
  } catch (e) {
    // 忽略无效数据
  }
});

5. 防止 DoS 攻击

// 限制连接数
const MAX_CONNECTIONS = 1000;
let connectionCount = 0;
 
wss.on('connection', (ws, req) => {
  if (connectionCount >= MAX_CONNECTIONS) {
    ws.close(1001, '服务器繁忙');
    return;
  }
  connectionCount++;
 
  ws.on('close', () => {
    connectionCount--;
  });
});
 
// 限制消息大小
ws.on('message', (data) => {
  if (data.length > 1024 * 1024) {  // 1MB
    ws.close(1009, '消息太大');
  }
});

实战场景

1. 即时聊天

// 服务端:聊天室
io.on('connection', (socket) => {
  socket.on('join', ({ username, room }) => {
    socket.join(room);
    io.to(room).emit('system', `${username} 加入了聊天室`);
  });
 
  socket.on('message', ({ room, content }) => {
    io.to(room).emit('message', {
      username: socket.username,
      content,
      time: Date.now()
    });
  });
 
  socket.on('leave', ({ username, room }) => {
    socket.leave(room);
    io.to(room).emit('system', `${username} 离开了聊天室`);
  });
});

2. 实时通知

// 服务端
io.on('connection', (socket) => {
  // 用户登录后加入自己的房间
  socket.on('login', (userId) => {
    socket.join(`user:${userId}`);
  });
});
 
// 发送通知给特定用户
function sendNotification(userId, notification) {
  io.to(`user:${userId}`).emit('notification', notification);
}
 
// 触发通知
sendNotification(123, {
  title: '新消息',
  content: '您有新消息'
});

3. 在线状态

// 服务端
const onlineUsers = new Set();
 
io.on('connection', (socket) => {
  socket.on('login', (userId) => {
    onlineUsers.add(userId);
    io.emit('online-users', Array.from(onlineUsers));
  });
 
  socket.on('disconnect', () => {
    onlineUsers.delete(socket.userId);
    io.emit('online-users', Array.from(onlineUsers));
  });
});

4. 游戏实时同步

// 服务端
io.on('connection', (socket) => {
  socket.on('player-move', ({ x, y }) => {
    // 更新玩家位置
    gameState.players[socket.id] = { x, y };
    // 广播给房间内其他玩家
    socket.to(gameState.room).emit('player-moved', {
      id: socket.id,
      x, y
    });
  });
});

常见问题

1. 心跳/保活

WebSocket 连接可能因为网络问题断开,需要心跳机制:

// 客户端:每 30 秒发送心跳
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000);
 
// 服务端:回复心跳
ws.on('message', (data) => {
  const msg = JSON.parse(data);
  if (msg.type === 'ping') {
    ws.send(JSON.stringify({ type: 'pong' }));
  }
});

2. 重连机制

// 客户端自动重连
function connect() {
  const ws = new WebSocket('ws://localhost:3000');
 
  ws.onclose = () => {
    console.log('连接关闭,5秒后重连...');
    setTimeout(connect, 5000);
  };
}

3. 跨域处理

// 服务端(CORS)
const io = new Server(server, {
  cors: {
    origin: ['https://your-domain.com'],
    methods: ['GET', 'POST']
  }
});

4. 负载均衡

WebSocket 长时间连接不适合轮询负载均衡,需要:

方案说明
Sticky Session同一连接路由到同一服务器
Redis AdapterSocket.IO 用 Redis 同步消息
MQTT适合大规模消息推送

5. 监控与调试

// 服务端日志
io.on('connection', (socket) => {
  console.log(`连接: ${socket.id} - ${new Date().toISOString()}`);
});
 
// 客户端调试
const ws = new WebSocket('ws://localhost:3000');
ws.onopen = () => console.log('连接成功');
ws.onclose = (e) => console.log('断开:', e.code, e.reason);
ws.onerror = (e) => console.error('错误:', e);

总结

项目说明
协议ws(明文)/ wss(加密)
特点全双工、持久连接、实时推送
适用场景聊天、实时协作、游戏、通知
安全性使用 WSS、验证 Origin、身份认证
注意心跳保活、自动重连、输入验证