目录


1. 什么是 WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它于 2011 年被 IETF 定为标准 RFC 6455,并被 W3C 标准化为浏览器 API。

核心特征

  • 全双工通信:客户端和服务器可以同时发送消息
  • 持久连接:一旦建立,连接保持打开状态
  • 低延迟:无需像 HTTP 那样每次请求都建立新连接
  • 轻量级:数据帧头部开销小(2-14 字节)

与 HTTP 的关系

WebSocket 使用 HTTP/1.1 的 101 Switching Protocols 状态码完成协议升级,之后完全独立于 HTTP 协议运行。


2. 为什么需要 WebSocket

传统 HTTP 的局限性

特性 HTTP WebSocket
通信方向 单向(客户端发起) 双向
连接方式 短连接 长连接
实时性 差(需轮询) 好(推送)
头部开销 大(~800 字节) 小(2-14 字节)
服务端推送 不支持 原生支持

适用场景

  • 💬 即时通讯:微信、Slack、Discord
  • 🎮 在线游戏:实时对战、多人互动
  • 📊 实时数据:股票行情、体育比分、仪表盘
  • 🤝 协同编辑:Google Docs、Figma
  • 📡 位置服务:外卖配送、共享单车

3. WebSocket 核心概念

3.1 URL 格式

ws://example.com/socket           // 非加密
wss://example.com/socket          // 加密(TLS)

3.2 协议帧结构

 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 ...                |
+---------------------------------------------------------------+

3.3 重要字段说明

字段 大小 说明
FIN 1 bit 表示是否为消息的最后一帧
opcode 4 bits 操作码:0x1=文本,0x2=二进制,0x8=关闭,0x9=Ping,0xA=Pong
MASK 1 bit 是否使用掩码(客户端→服务端必须为 1)
Payload len 7 bits 载荷长度:0-125 直接表示,126 时后续 2 字节,127 时后续 8 字节

3.4 操作码(Opcode)

含义
0x0 延续帧
0x1 文本帧
0x2 二进制帧
0x8 连接关闭
0x9 Ping
0xA Pong

4. 工作原理详解

4.1 握手过程

Websocket握手其实就是借HTTP的壳来完成协议切换,整个过程就是一个来回。
客户端先发一个HTTP Get 请求,路径就是WebSocket的地址,关键是要带上请求头。

在这里插入图片描述

4.2 握手关键 Headers

客户端请求头

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket                    // 必须
Connection: Upgrade                   // 必须
Sec-WebSocket-Key: dGhlIHNhbXBsZQ==   // Base64 编码的 16 字节随机值
Sec-WebSocket-Version: 13             // 必须为 13
Sec-WebSocket-Protocol: chat, super   // 可选,子协议
Sec-WebSocket-Extensions: permessage-deflate // 可选,扩展
Origin: http://example.com            // 浏览器自动添加

服务端响应头

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat         // 从客户端列表中选择

4.3 Sec-WebSocket-Accept 计算

const crypto = require('crypto');

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

4.4 数据帧格式详解

客户端发送数据(必须掩码)

// 发送 "Hello"
// 帧头:0x81(FIN=1, opcode=0x1)
// 长度:0x85(MASK=1, payload=5)
// Masking-key: 4 字节随机值
// 载荷:Hello 异或 Masking-key

服务端发送数据(无需掩码)

// 帧头:0x81(FIN=1, opcode=0x1)
// 长度:0x05(MASK=0, payload=5)
// 载荷:Hello

5. API 详解与使用

5.1 浏览器端 API

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

// 连接事件
ws.onopen = function(event) {
  console.log('连接已建立');
  ws.send('Hello Server!');
};

// 接收消息
ws.onmessage = function(event) {
  console.log('收到消息:', event.data);
  // event.data 类型可能是: string, Blob, ArrayBuffer
};

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

// 连接关闭
ws.onclose = function(event) {
  console.log('连接关闭:', event.code, event.reason);
  // event.code: 1000(正常), 1001(离开), 1006(异常)
};

// 发送数据
ws.send('Hello World');              // 文本
ws.send(JSON.stringify({type: 'ping'})); // JSON
ws.send(new Blob(['binary data']));   // 二进制
ws.send(new ArrayBuffer(16));         // ArrayBuffer

// 关闭连接
ws.close(1000, 'Goodbye');

5.2 服务端(Node.js + ws 库)

const WebSocket = require('ws');

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

// 连接管理
wss.on('connection', function connection(ws, req) {
  console.log('客户端连接:', req.socket.remoteAddress);

  // 发送欢迎消息
  ws.send('Welcome to WebSocket server!');

  // 接收消息
  ws.on('message', function incoming(data) {
    console.log('收到:', data.toString());
    
    // 广播给所有客户端
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  });

  // 错误处理
  ws.on('error', function(error) {
    console.error('连接错误:', error);
  });

  // 关闭处理
  ws.on('close', function(code, reason) {
    console.log('连接关闭:', code, reason.toString());
  });

  // 发送 Ping 保持连接
  const pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 30000);

  ws.on('close', () => clearInterval(pingInterval));
});

console.log('WebSocket 服务器启动在 ws://localhost:8080');

5.3 ReadyState 状态码

常量 描述
CONNECTING 0 正在连接
OPEN 1 已连接,可通信
CLOSING 2 正在关闭
CLOSED 3 已关闭

6. 心跳机制与重连策略

6.1 为什么需要心跳

  • 检测连接存活:网络中断时无法立即发现
  • 防止超时断开:NAT/防火墙会清理空闲连接
  • 保持连接活跃:避免被中间设备断开

6.2 心跳实现方案

方案一:Ping/Pong 帧(推荐)

// 客户端
let isAlive = true;

ws.on('pong', () => {
  isAlive = true;
});

// 服务端定时检测
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      return ws.terminate();
    }
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

方案二:应用层心跳

// 客户端发送心跳
class HeartbeatClient {
  constructor(ws, interval = 30000) {
    this.ws = ws;
    this.interval = interval;
    this.pingTimer = null;
    this.pongTimer = null;
    this.start();
  }

  start() {
    this.pingTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
        // 设置超时检测
        this.pongTimer = setTimeout(() => {
          this.ws.close();
        }, 5000);
      }
    }, this.interval);
  }

  handlePong() {
    clearTimeout(this.pongTimer);
  }

  stop() {
    clearInterval(this.pingTimer);
    clearTimeout(this.pongTimer);
  }
}

6.3 重连策略

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.options = {
      maxRetries: 5,
      retryDelay: 1000,
      maxRetryDelay: 30000,
      backoffMultiplier: 1.5,
      ...options
    };
    
    this.retryCount = 0;
    this.ws = null;
    this.isManualClose = false;
    this.reconnectTimer = null;
    
    this.connect();
  }

  connect() {
    try {
      this.ws = new WebSocket(this.url);
      this.setupEventListeners();
    } catch (error) {
      this.scheduleReconnect();
    }
  }

  setupEventListeners() {
    this.ws.onopen = () => {
      console.log('WebSocket 连接成功');
      this.retryCount = 0;
      this.isManualClose = false;
    };

    this.ws.onclose = (event) => {
      console.log('WebSocket 关闭:', event.code);
      
      // 1000: 正常关闭,不重连
      // 1001: 主动离开,不重连
      if (this.isManualClose || event.code === 1000 || event.code === 1001) {
        return;
      }
      
      this.scheduleReconnect();
    };

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

    this.ws.onmessage = (event) => {
      // 透传消息
      this.onmessage && this.onmessage(event);
    };
  }

  scheduleReconnect() {
    if (this.retryCount >= this.options.maxRetries) {
      console.log('达到最大重连次数,停止重连');
      return;
    }

    const delay = Math.min(
      this.options.retryDelay * Math.pow(this.options.backoffMultiplier, this.retryCount),
      this.options.maxRetryDelay
    );

    this.retryCount++;
    console.log(`${delay}ms 后进行第 ${this.retryCount} 次重连`);

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

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      console.warn('WebSocket 未连接,数据丢弃');
    }
  }

  close(code = 1000, reason = '') {
    this.isManualClose = true;
    clearTimeout(this.reconnectTimer);
    if (this.ws) {
      this.ws.close(code, reason);
    }
  }
}

7. 面试高频题

Q1: WebSocket 和 HTTP 的区别是什么?

维度 HTTP WebSocket
协议 无状态应用层协议 有状态应用层协议
连接 短连接(每次请求独立) 长连接(持久)
方向 单向(客户端→服务端) 双向
实时性 需要轮询 服务端可主动推送
头部大小 ~800 字节 2-14 字节
二进制支持 需编码(Base64) 原生支持
适用范围 请求-响应模型 实时通信场景

Q2: WebSocket 握手过程是怎样的?

  1. 客户端发送 HTTP GET 请求,包含 Upgrade: websocket
  2. 服务端验证请求,返回 101 状态码
  3. 连接升级为 WebSocket 协议
  4. 后续通信使用 WebSocket 帧格式

关键验证:服务端根据 Sec-WebSocket-Key 计算 Sec-WebSocket-Accept

Q3: WebSocket 如何保证安全性?

  • WSS(WebSocket Secure):使用 TLS/SSL 加密
  • Origin 验证:服务端验证 Origin 头防止 CSRF
  • 子协议协商Sec-WebSocket-Protocol 限制协议
  • 关闭码验证:检查 close 帧的合法状态码

Q4: WebSocket 的二进制数据传输方式?

// 方式1:ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 12345);
ws.send(buffer);

// 方式2:Blob
const blob = new Blob(['binary data']);
ws.send(blob);

// 方式3:TypedArray
const uint8 = new Uint8Array([1, 2, 3, 4]);
ws.send(uint8);

Q5: WebSocket 如何处理粘包问题?

WebSocket 协议本身基于帧(Frame),不存在传统 TCP 的粘包问题。但应用层消息可能分片:

let buffer = '';

ws.onmessage = (event) => {
  buffer += event.data;
  
  try {
    const message = JSON.parse(buffer);
    buffer = ''; // 清空缓冲
    handleMessage(message);
  } catch (e) {
    // 消息不完整,继续等待
  }
};

Q6: 如何实现 WebSocket 的鉴权?

// 方案1:URL 参数
const ws = new WebSocket(`wss://example.com?token=${token}`);

// 方案2:Cookie(浏览器自动携带)
// 需要同域,且 Cookie 设置了 SameSite

// 方案3:协议头(需自定义实现)
// 握手时传递自定义头(部分浏览器支持有限)

// 方案4:握手后鉴权
ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'auth', token: 'xxx' }));
};

Q7: WebSocket 的注意事项和限制?

  • 代理支持:某些 HTTP 代理不支持 WebSocket
  • 负载均衡:需要 sticky session 或独立服务
  • 连接数限制:浏览器限制(通常 256 个)
  • 内存管理:长连接需注意内存泄漏
  • 自动重连:网络变化时需手动实现

Q8: WebSocket 的替代方案有哪些?

方案 优点 缺点 适用场景
SSE 简单、自动重连 单向 服务器推送(通知、股票)
Long Polling 兼容性好 延迟高、开销大 旧浏览器兼容
WebRTC P2P、低延迟 复杂 音视频通话、P2P
MQTT 轻量、QoS 需 broker IoT、移动设备

Q9: WebSocket 的关闭码含义?

含义 描述
1000 正常关闭 正常结束会话
1001 离开 端点离开(如页面跳转)
1002 协议错误 协议违规
1003 数据类型错误 收到不支持的数据
1005 无状态码 预期接收但未收到
1006 异常关闭 非正常断开(无 close 帧)
1007 数据不一致 帧数据不符合类型
1008 策略违规 违反服务器策略
1009 消息太大 无法处理
1011 服务端错误 内部错误

8. 源码实现示例

8.1 简易 WebSocket 客户端

class SimpleWebSocket {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.callbacks = {
      open: [],
      message: [],
      close: [],
      error: []
    };
    this.connect();
  }

  connect() {
    if (this.socket) return;
    
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = (event) => {
      this.emit('open', event);
    };
    
    this.socket.onmessage = (event) => {
      this.emit('message', event);
    };
    
    this.socket.onclose = (event) => {
      this.emit('close', event);
      this.socket = null;
    };
    
    this.socket.onerror = (error) => {
      this.emit('error', error);
    };
  }

  on(event, callback) {
    if (this.callbacks[event]) {
      this.callbacks[event].push(callback);
    }
    return this;
  }

  off(event, callback) {
    if (this.callbacks[event]) {
      this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
    }
    return this;
  }

  emit(event, data) {
    if (this.callbacks[event]) {
      this.callbacks[event].forEach(callback => callback(data));
    }
  }

  send(data) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      throw new Error('WebSocket is not connected');
    }
  }

  close(code = 1000, reason = '') {
    if (this.socket) {
      this.socket.close(code, reason);
    }
  }

  get readyState() {
    return this.socket ? this.socket.readyState : WebSocket.CLOSED;
  }
}

8.2 简易 WebSocket 服务端(原生实现)

const http = require('http');
const crypto = require('crypto');

class WebSocketServer {
  constructor(options) {
    this.server = http.createServer();
    this.clients = new Set();
    this.handleUpgrade();
    this.server.listen(options.port || 8080);
  }

  handleUpgrade() {
    this.server.on('upgrade', (req, socket, head) => {
      // 验证 WebSocket 握手
      if (!this.isValidWebSocketRequest(req)) {
        socket.destroy();
        return;
      }

      // 计算 Accept
      const acceptKey = this.computeAccept(req.headers['sec-websocket-key']);
      
      // 发送握手响应
      const response = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        `Sec-WebSocket-Accept: ${acceptKey}`,
        '\r\n'
      ].join('\r\n');
      
      socket.write(response);
      
      // 保存客户端
      this.clients.add(socket);
      
      // 设置事件
      socket.on('data', (data) => {
        this.handleData(socket, data);
      });
      
      socket.on('close', () => {
        this.clients.delete(socket);
        this.emit('close', socket);
      });
      
      socket.on('error', (error) => {
        console.error('Socket error:', error);
      });
      
      this.emit('connection', socket);
    });
  }

  isValidWebSocketRequest(req) {
    return req.headers.upgrade === 'websocket' &&
           req.headers.connection === 'Upgrade' &&
           req.headers['sec-websocket-version'] === '13';
  }

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

  handleData(socket, data) {
    // 简化解析,仅处理文本帧
    if (data.length < 2) return;
    
    const opcode = data[0] & 0x0F;
    const isMasked = (data[1] & 0x80) !== 0;
    let payloadLen = data[1] & 0x7F;
    
    let offset = 2;
    
    // 扩展长度
    if (payloadLen === 126) {
      payloadLen = data.readUInt16BE(offset);
      offset += 2;
    } else if (payloadLen === 127) {
      payloadLen = Number(data.readBigUInt64BE(offset));
      offset += 8;
    }
    
    let payload = data.slice(offset + (isMasked ? 4 : 0));
    
    // 解码掩码
    if (isMasked) {
      const mask = data.slice(offset, offset + 4);
      for (let i = 0; i < payload.length; i++) {
        payload[i] = payload[i] ^ mask[i % 4];
      }
    }
    
    // 处理不同帧类型
    if (opcode === 0x1) { // 文本帧
      const message = payload.toString('utf8');
      this.emit('message', socket, message);
    } else if (opcode === 0x8) { // 关闭帧
      socket.end();
    } else if (opcode === 0x9) { // Ping
      this.sendFrame(socket, 0xA, payload); // Pong
    }
  }

  sendFrame(socket, opcode, payload) {
    let header = [0x80 | opcode];
    const len = payload.length;
    
    if (len < 126) {
      header.push(len);
    } else if (len < 65536) {
      header.push(126);
      header = header.concat([(len >> 8) & 0xFF, len & 0xFF]);
    } else {
      header.push(127);
      const buf = Buffer.alloc(8);
      buf.writeBigUInt64BE(BigInt(len));
      header = header.concat([...buf]);
    }
    
    socket.write(Buffer.from([...header, ...payload]));
  }

  broadcast(message) {
    const data = Buffer.from(message);
    this.clients.forEach(client => {
      if (client.writable) {
        this.sendFrame(client, 0x1, data);
      }
    });
  }

  on(event, callback) {
    this[`_${event}Listeners`] = this[`_${event}Listeners`] || [];
    this[`_${event}Listeners`].push(callback);
    return this;
  }

  emit(event, ...args) {
    const listeners = this[`_${event}Listeners`] || [];
    listeners.forEach(cb => cb(...args));
  }
}

module.exports = WebSocketServer;

9. 实践总结

  • 使用 WSS 加密连接
  • 实现心跳机制检测连接状态
  • 实现自动重连逻辑(指数退避)
  • 处理二进制数据时使用 ArrayBuffer
  • 设置合理的 超时时间
  • 监控连接数和内存使用
  • 使用 子协议 进行版本管理
  • 生产环境使用成熟的库(如 wsSocket.IO

  • 不要在连接建立前发送数据
  • 不要依赖 readyState 做状态同步
  • 不要在 onmessage 中执行阻塞操作
  • 不要忽略 onerror 事件
  • 不要在生产环境使用未经测试的简易实现
  • 不要忘记处理连接关闭清理资源

10. 参考资源


写在最后:WebSocket 作为实时通信的基石,理解其原理和最佳实践对现代 Web 开发至关重要。希望这篇文章能帮助你构建高质量的应用,并在面试中游刃有余。如有疑问或补充,欢迎交流讨论!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐