返回首页

说说对 WebSocket 的理解?应用场景

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 理解 WebSocket 的基本概念和特点
  • 掌握 WebSocket 握手过程和协议细节
  • 了解 WebSocket 与 HTTP 轮询的区别
  • 理解 WebSocket 的应用场景和最佳实践
  • 了解 WebSocket 连接管理和安全性

核心概念(基础知识点)

什么是 WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间可以建立持久连接,双方都可以随时发送数据,实现了真正的实时双向通信。

WebSocket 的核心特点:

特点 说明
全双工通信 客户端和服务器可以同时发送和接收数据
持久连接 一次握手后保持连接,无需重复建立
低延迟 避免了 HTTP 轮询的开销,实时性高
轻量级头部 数据帧头部很小(2-14 字节),节省带宽
基于 TCP 使用 TCP 传输,可靠有序

WebSocket vs HTTP

HTTP 轮询(Polling):
┌─────────┐     请求      ┌─────────┐
│  客户端  │─────────────►│  服务器  │
│         │◄─────────────│         │
│         │   响应(无新数据)        │
│         │              │         │
│         │     请求     │         │
│         │─────────────►│         │
│         │◄─────────────│         │
│         │   响应(无新数据)        │
│         │              │         │
│         │     请求     │         │
│         │─────────────►│         │
│         │◄─────────────│         │
│         │   响应(有新数据)        │
└─────────┘              └─────────┘
问题:大量无效请求,浪费带宽和服务器资源


WebSocket:
┌─────────┐   握手请求   ┌─────────┐
│  客户端  │────────────►│  服务器  │
│         │◄────────────│         │
│         │   握手响应   │         │
│◄═══════►│◄══════════►│◄═══════►│
│  双向   │   持久连接   │  双向   │
│  通信   │              │  通信   │
│         │   任意时刻   │         │
│         │   互相推送   │         │
└─────────┘              └─────────┘
优势:一次连接,持续通信,实时高效

WebSocket 协议栈

┌─────────────────────────────────────────────────────────────┐
│  应用层:WebSocket 协议(RFC 6455)                          │
│  - 数据帧格式定义                                            │
│  - 握手协议(基于 HTTP Upgrade)                              │
│  - 控制帧(Ping/Pong/Close)                                 │
├─────────────────────────────────────────────────────────────┤
│  传输层:TCP                                                 │
│  - 可靠传输                                                  │
│  - 端口 80/443(与 HTTP 相同,便于穿透防火墙)                 │
├─────────────────────────────────────────────────────────────┤
│  安全层:TLS/SSL(wss://)                                    │
│  - WebSocket over TLS                                        │
│  - 端口 443                                                  │
└─────────────────────────────────────────────────────────────┘

URI 格式:
- ws://example.com/socket    (WebSocket)
- wss://example.com/socket   (WebSocket Secure)

详细解答(代码示例)

WebSocket 握手过程

// 1. 客户端握手请求(HTTP Upgrade)
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate

// 关键头部说明:
// Upgrade: websocket - 表示要升级到 WebSocket 协议
// Connection: Upgrade - 表示连接要升级
// Sec-WebSocket-Key: Base64 编码的 16 字节随机数
// Sec-WebSocket-Version: WebSocket 协议版本(13 是当前标准)


// 2. 服务器握手响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

// 关键头部说明:
// 101 状态码:协议切换成功
// Sec-WebSocket-Accept: 对 Sec-WebSocket-Key 的响应
//   计算方式:Base64(SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
// Sec-WebSocket-Accept 计算验证
const crypto = require('crypto');

function calculateAcceptKey(key) {
  const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  return crypto
    .createHash('sha1')
    .update(key + GUID)
    .digest('base64');
}

// 示例
const clientKey = 'dGhlIHNhbXBsZSBub25jZQ==';
const serverAccept = calculateAcceptKey(clientKey);
console.log(serverAccept); // s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

WebSocket 客户端实现

// 浏览器原生 WebSocket API

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

// 2. 连接建立回调
ws.onopen = function(event) {
  console.log('连接已建立');

  // 发送消息
  ws.send('Hello Server!');

  // 发送 JSON 数据
  ws.send(JSON.stringify({
    type: 'join',
    room: 'room-123',
    user: 'Alice'
  }));

  // 发送二进制数据
  const blob = new Blob(['binary data'], { type: 'text/plain' });
  ws.send(blob);

  // 发送 ArrayBuffer
  const buffer = new ArrayBuffer(8);
  ws.send(buffer);
};

// 3. 接收消息回调
ws.onmessage = function(event) {
  console.log('收到消息:', event.data);

  // 根据数据类型处理
  if (typeof event.data === 'string') {
    // 文本消息
    const message = JSON.parse(event.data);
    handleMessage(message);
  } else if (event.data instanceof Blob) {
    // 二进制消息
    handleBinaryData(event.data);
  }
};

// 4. 错误处理
ws.onerror = function(error) {
  console.error('WebSocket 错误:', error);
};

// 5. 连接关闭回调
ws.onclose = function(event) {
  console.log('连接已关闭');
  console.log('关闭代码:', event.code);
  console.log('关闭原因:', event.reason);
  console.log('是否干净关闭:', event.wasClean);

  // 可选:自动重连
  if (!event.wasClean) {
    setTimeout(reconnect, 3000);
  }
};

// 6. 关闭连接
ws.close(1000, '正常关闭');

// 7. 连接状态
console.log('连接状态:', ws.readyState);
// 0: CONNECTING - 连接正在建立中
// 1: OPEN - 连接已建立,可以通信
// 2: CLOSING - 连接正在关闭
// 3: CLOSED - 连接已关闭或无法建立

// 8. 完整封装类
class WebSocketClient {
  constructor(url, options = {}) {
    this.url = url;
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectAttempts = 0;
    this.listeners = new Map();
    this.connect();
  }

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

    this.ws.onopen = () => {
      console.log('WebSocket 连接成功');
      this.reconnectAttempts = 0;
      this.emit('open');
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.emit('message', data);

        // 按类型分发
        if (data.type) {
          this.emit(data.type, data);
        }
      } catch (e) {
        this.emit('message', event.data);
      }
    };

    this.ws.onclose = (event) => {
      this.emit('close', event);
      this.attemptReconnect();
    };

    this.ws.onerror = (error) => {
      this.emit('error', error);
    };
  }

  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
      setTimeout(() => this.connect(), this.reconnectInterval);
    }
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
    } else {
      console.warn('WebSocket 未连接');
    }
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach(callback => callback(data));
    }
  }

  close() {
    this.ws.close();
  }
}

// 使用示例
const client = new WebSocketClient('wss://example.com/socket');

client.on('open', () => {
  client.send({ type: 'join', room: 'lobby' });
});

client.on('chat', (data) => {
  console.log('收到聊天消息:', data);
});

client.on('user_joined', (data) => {
  console.log('用户加入:', data.user);
});

WebSocket 服务器实现(Node.js)

// 使用 ws 库
const WebSocket = require('ws');
const http = require('http');

// 1. 创建 HTTP 服务器
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket Server\n');
});

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

// 3. 连接管理
const clients = new Map();

wss.on('connection', (ws, req) => {
  const clientId = generateClientId();
  const clientInfo = {
    id: clientId,
    ws: ws,
    rooms: new Set(),
    userData: {}
  };

  clients.set(clientId, clientInfo);
  console.log(`客户端 ${clientId} 已连接`);

  // 发送欢迎消息
  ws.send(JSON.stringify({
    type: 'connected',
    clientId: clientId,
    message: '欢迎连接到 WebSocket 服务器'
  }));

  // 接收消息
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data);
      handleMessage(clientId, message);
    } catch (e) {
      console.error('消息解析失败:', e);
      ws.send(JSON.stringify({
        type: 'error',
        message: '无效的消息格式'
      }));
    }
  });

  // 连接关闭
  ws.on('close', (code, reason) => {
    console.log(`客户端 ${clientId} 已断开: ${code} ${reason}`);

    // 从所有房间移除
    const client = clients.get(clientId);
    client.rooms.forEach(room => {
      leaveRoom(clientId, room);
    });

    clients.delete(clientId);
  });

  // 错误处理
  ws.on('error', (error) => {
    console.error(`客户端 ${clientId} 错误:`, error);
  });

  // Ping/Pong 保活
  ws.isAlive = true;
  ws.on('pong', () => {
    ws.isAlive = true;
  });
});

// 4. 消息处理
function handleMessage(clientId, message) {
  const client = clients.get(clientId);

  switch (message.type) {
    case 'join':
      joinRoom(clientId, message.room);
      break;

    case 'leave':
      leaveRoom(clientId, message.room);
      break;

    case 'chat':
      broadcastToRoom(message.room, {
        type: 'chat',
        from: clientId,
        content: message.content,
        timestamp: Date.now()
      }, clientId);
      break;

    case 'private':
      sendToClient(message.to, {
        type: 'private',
        from: clientId,
        content: message.content,
        timestamp: Date.now()
      });
      break;

    default:
      client.ws.send(JSON.stringify({
        type: 'error',
        message: '未知的消息类型'
      }));
  }
}

// 5. 房间管理
const rooms = new Map();

function joinRoom(clientId, roomName) {
  const client = clients.get(clientId);

  if (!rooms.has(roomName)) {
    rooms.set(roomName, new Set());
  }

  rooms.get(roomName).add(clientId);
  client.rooms.add(roomName);

  // 通知房间内其他用户
  broadcastToRoom(roomName, {
    type: 'user_joined',
    room: roomName,
    user: clientId
  }, clientId);

  console.log(`客户端 ${clientId} 加入房间 ${roomName}`);
}

function leaveRoom(clientId, roomName) {
  const client = clients.get(clientId);

  if (rooms.has(roomName)) {
    rooms.get(roomName).delete(clientId);

    // 清理空房间
    if (rooms.get(roomName).size === 0) {
      rooms.delete(roomName);
    }
  }

  client.rooms.delete(roomName);

  broadcastToRoom(roomName, {
    type: 'user_left',
    room: roomName,
    user: clientId
  });

  console.log(`客户端 ${clientId} 离开房间 ${roomName}`);
}

// 6. 广播功能
function broadcastToRoom(roomName, message, excludeClientId = null) {
  if (!rooms.has(roomName)) return;

  const messageStr = JSON.stringify(message);

  rooms.get(roomName).forEach(clientId => {
    if (clientId !== excludeClientId) {
      const client = clients.get(clientId);
      if (client && client.ws.readyState === WebSocket.OPEN) {
        client.ws.send(messageStr);
      }
    }
  });
}

function sendToClient(clientId, message) {
  const client = clients.get(clientId);
  if (client && client.ws.readyState === WebSocket.OPEN) {
    client.ws.send(JSON.stringify(message));
  }
}

function broadcastAll(message, excludeClientId = null) {
  const messageStr = JSON.stringify(message);

  clients.forEach((client, clientId) => {
    if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(messageStr);
    }
  });
}

// 7. 心跳检测
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      return ws.terminate();
    }

    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => {
  clearInterval(interval);
});

// 8. 启动服务器
server.listen(8080, () => {
  console.log('WebSocket 服务器启动在端口 8080');
});

// 辅助函数
function generateClientId() {
  return Math.random().toString(36).substring(2, 15);
}

深入理解(原理剖析)

WebSocket 数据帧格式

WebSocket 数据帧结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - -+
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

字段说明:
- FIN (1 bit): 是否为最后一个分片
- RSV1-3 (3 bits): 保留位,用于扩展
- Opcode (4 bits): 帧类型
  - 0x0: 连续帧
  - 0x1: 文本帧
  - 0x2: 二进制帧
  - 0x8: 关闭连接
  - 0x9: Ping
  - 0xA: Pong
- MASK (1 bit): 是否使用掩码(客户端必须置 1)
- Payload length (7 bits): 负载长度
  - 0-125: 实际长度
  - 126: 后续 2 字节表示长度
  - 127: 后续 8 字节表示长度
- Masking-key (32 bits): 掩码密钥(仅客户端发送时存在)
- Payload data: 实际数据(客户端发送时会被掩码)

WebSocket 与 HTTP 长轮询对比

┌─────────────────────────────────────────────────────────────┐
│                    实时通信方案对比                          │
├─────────────────────────────────────────────────────────────┤
│  1. 短轮询(Polling)                                        │
│  - 客户端定时发送请求                                        │
│  - 服务器立即响应(有数据或无数据)                           │
│  - 延迟:取决于轮询间隔                                       │
│  - 开销:大量 HTTP 请求,浪费带宽                             │
├─────────────────────────────────────────────────────────────┤
│  2. 长轮询(Long Polling)                                   │
│  - 客户端发送请求,服务器保持连接直到有数据                   │
│  - 有数据时立即响应,客户端立即发起新请求                     │
│  - 延迟:较低                                                 │
│  - 开销:连接保持时间长,但仍需频繁重建连接                    │
├─────────────────────────────────────────────────────────────┤
│  3. SSE(Server-Sent Events)                                │
│  - 服务器向客户端单向推送                                     │
│  - 基于 HTTP,自动重连                                        │
│  - 适用:股票行情、新闻推送等单向场景                          │
│  - 限制:只能服务器向客户端推送                                │
├─────────────────────────────────────────────────────────────┤
│  4. WebSocket                                                │
│  - 全双工双向通信                                             │
│  - 持久连接,低延迟                                           │
│  - 支持二进制                                                 │
│  - 适用:聊天、游戏、协同编辑等双向场景                        │
└─────────────────────────────────────────────────────────────┘

性能对比:
┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│     方案      │   延迟   │  实时性  │  开销   │  复杂度  │
├──────────────┼──────────┼──────────┼──────────┼──────────┤
│  短轮询       │   高     │   差     │   高    │   低     │
│  长轮询       │   中     │   中     │   中    │   中     │
│  SSE          │   低     │   好     │   低    │   低     │
│  WebSocket    │   极低   │   极好   │   极低  │   高     │
└──────────────┴──────────┴──────────┴──────────┴──────────┘

WebSocket 扩展

WebSocket 扩展(Extensions):

1. permessage-deflate
   - 消息压缩扩展
   - 减少传输数据量
   - 握手时协商:Sec-WebSocket-Extensions: permessage-deflate

2. 多路复用扩展(未广泛支持)
   - 在单个 WebSocket 连接上复用多个逻辑通道

3. 自定义扩展
   - 可以定义应用层协议扩展
// 启用压缩的 WebSocket 服务器
const WebSocket = require('ws');
const wss = new WebSocket.Server({
  port: 8080,
  perMessageDeflate: {
    zlibDeflateOptions: {
      chunkSize: 1024,
      memLevel: 7,
      level: 3
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024
    },
    clientNoContextTakeover: true,
    serverNoContextTakeover: true,
    serverMaxWindowBits: 10,
    concurrencyLimit: 10
  }
});

最佳实践

1. 连接管理

// 连接池管理
class WebSocketPool {
  constructor(maxConnections = 100) {
    this.pools = new Map(); // room -> Set<ws>
    this.maxConnections = maxConnections;
  }

  addToRoom(room, ws) {
    if (!this.pools.has(room)) {
      this.pools.set(room, new Set());
    }

    const roomConnections = this.pools.get(room);

    // 限制房间人数
    if (roomConnections.size >= this.maxConnections) {
      ws.close(1008, 'Room is full');
      return false;
    }

    roomConnections.add(ws);
    ws.room = room;
    return true;
  }

  removeFromRoom(ws) {
    if (ws.room && this.pools.has(ws.room)) {
      this.pools.get(ws.room).delete(ws);
    }
  }

  broadcastToRoom(room, message, excludeWs = null) {
    if (!this.pools.has(room)) return;

    const messageStr = JSON.stringify(message);
    this.pools.get(room).forEach(ws => {
      if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
        ws.send(messageStr);
      }
    });
  }
}

2. 消息协议设计

// 结构化消息协议
const MessageTypes = {
  // 系统消息
  SYSTEM: 'system',
  ERROR: 'error',
  PING: 'ping',
  PONG: 'pong',

  // 用户消息
  AUTH: 'auth',
  JOIN: 'join',
  LEAVE: 'leave',

  // 业务消息
  CHAT: 'chat',
  TYPING: 'typing',
  READ: 'read',

  // 信令消息(WebRTC)
  OFFER: 'offer',
  ANSWER: 'answer',
  ICE_CANDIDATE: 'ice_candidate'
};

// 消息格式
class Message {
  constructor(type, payload, options = {}) {
    this.id = generateMessageId();
    this.type = type;
    this.payload = payload;
    this.timestamp = Date.now();
    this.from = options.from;
    this.to = options.to; // 私聊目标
    this.room = options.room;
  }

  toJSON() {
    return {
      id: this.id,
      type: this.type,
      payload: this.payload,
      timestamp: this.timestamp,
      from: this.from,
      to: this.to,
      room: this.room
    };
  }
}

// 使用示例
const message = new Message(MessageTypes.CHAT, {
  text: 'Hello!',
  attachments: []
}, { from: 'user-123', room: 'room-456' });

ws.send(JSON.stringify(message));

3. 安全性

// WebSocket 安全最佳实践

// 1. 使用 WSS(WebSocket Secure)
// ws:// → wss://

// 2. 身份验证
wss.on('connection', async (ws, req) => {
  // 从 URL 参数或 Cookie 获取 Token
  const url = new URL(req.url, 'http://localhost');
  const token = url.searchParams.get('token');

  try {
    const user = await verifyToken(token);
    ws.userId = user.id;
    ws.authorized = true;
  } catch (e) {
    ws.close(1008, 'Authentication failed');
    return;
  }
});

// 3. 速率限制
const rateLimiter = new Map();

function checkRateLimit(clientId, limit = 100, windowMs = 60000) {
  const now = Date.now();
  const clientData = rateLimiter.get(clientId) || { count: 0, resetTime: now + windowMs };

  if (now > clientData.resetTime) {
    clientData.count = 0;
    clientData.resetTime = now + windowMs;
  }

  clientData.count++;
  rateLimiter.set(clientId, clientData);

  return clientData.count <= limit;
}

// 4. 消息大小限制
wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    if (data.length > 1024 * 1024) { // 1MB 限制
      ws.close(1009, 'Message too large');
      return;
    }
  });
});

// 5. 输入验证
const Joi = require('joi');

const messageSchema = Joi.object({
  type: Joi.string().valid(...Object.values(MessageTypes)).required(),
  payload: Joi.object().required(),
  room: Joi.string().alphanum().max(50)
});

function validateMessage(data) {
  return messageSchema.validate(data);
}

4. 水平扩展

单服务器架构:
┌─────────┐         ┌─────────┐
│  客户端  │◄───────►│  WS 服务器│
└─────────┘         └─────────┘

多服务器架构(使用 Redis 发布订阅):
┌─────────┐         ┌─────────┐         ┌─────────┐
│  客户端 A │◄───────►│  WS 服务器 1 │◄───────►│         │
└─────────┘         └─────────┘         │         │
                                        │  Redis  │
┌─────────┐         ┌─────────┐         │  Pub/Sub│
│  客户端 B │◄───────►│  WS 服务器 2 │◄───────►│         │
└─────────┘         └─────────┘         └─────────┘

实现代码:
// 使用 Redis 实现多服务器消息广播
const Redis = require('ioredis');
const redisPub = new Redis();
const redisSub = new Redis();

// 订阅频道
redisSub.subscribe('websocket:broadcast');

redisSub.on('message', (channel, message) => {
  const data = JSON.parse(message);

  // 广播给本服务器上的所有客户端
  wss.clients.forEach((ws) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(data));
    }
  });
});

// 发送消息时同时发布到 Redis
function broadcastAll(message) {
  // 本地广播
  wss.clients.forEach((ws) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(message));
    }
  });

  // 发布到其他服务器
  redisPub.publish('websocket:broadcast', JSON.stringify(message));
}

面试要点

  1. WebSocket 握手

    • 基于 HTTP Upgrade 机制
    • 关键头部:Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Accept
    • 101 状态码表示协议切换成功
  2. WebSocket 与 HTTP 区别

    • HTTP 是半双工,WebSocket 是全双工
    • HTTP 是无状态、短连接,WebSocket 是有状态、长连接
    • WebSocket 头部更小,实时性更好
  3. 心跳机制

    • 原因:检测连接是否存活,防止 NAT 超时
    • 实现:Ping/Pong 帧或应用层心跳
    • 频率:通常 30-60 秒
  4. 应用场景

    • 实时聊天
    • 在线游戏
    • 股票行情
    • 协同编辑
    • 实时通知
  5. 面试高频问题

    • WebSocket 如何兼容 HTTP 代理和防火墙?(使用 80/443 端口,Upgrade 头部)
    • 如何处理 WebSocket 断线重连?
    • WebSocket 如何进行身份认证?(URL Token、Cookie、握手后发送认证消息)
    • 如何实现 WebSocket 集群?(Redis Pub/Sub、消息队列)
    • WebSocket 与 SSE 的区别?(双向 vs 单向)