目录


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 的局限性

特性HTTPWebSocket
通信方向单向(客户端发起)双向
连接方式短连接长连接
实时性差(需轮询)好(推送)
头部开销大(~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 重要字段说明

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

3.4 操作码(Opcode)

值含义
0x0延续帧
0x1文本帧
0x2二进制帧
0x8连接关闭
0x9Ping
0xAPong

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 状态码

常量值描述
CONNECTING0正在连接
OPEN1已连接,可通信
CLOSING2正在关闭
CLOSED3已关闭

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 的区别是什么?

维度HTTPWebSocket
协议无状态应用层协议有状态应用层协议
连接短连接(每次请求独立)长连接(持久)
方向单向(客户端→服务端)双向
实时性需要轮询服务端可主动推送
头部大小~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兼容性好延迟高、开销大旧浏览器兼容
WebRTCP2P、低延迟复杂音视频通话、P2P
MQTT轻量、QoS需 brokerIoT、移动设备

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
  • 设置合理的 超时时间
  • 监控连接数和内存使用
  • 使用 子协议 进行版本管理
  • 生产环境使用成熟的库(如 ws、Socket.IO)

❌

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

10. 参考资源


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

Logo

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

更多推荐