# WebSocket快速入门

# 1. 介绍

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,使得服务器和客户端之间的数据交换变得更加简单。允许服务端主动向客户端推送数据。

# 2. 浏览器客户端

# 2.1 基本使用

// 创建 WebSocket 连接
let socket = new WebSocket("ws://localhost:8888")

// 连接打开时触发
socket.onopen = function(){
    console.log('连接已建立')
    // 向服务器发送消息
    socket.send('hello server')
}

// 接收到服务器消息时触发
socket.onmessage = function(event){
    console.log('收到消息:', event.data)
}

// 连接关闭时触发
socket.onclose = function(){
    console.log('连接已关闭')
}

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

# 2.2 常用方法

// 发送消息
socket.send(data)

// 关闭连接
socket.close()

// 添加事件监听器
socket.addEventListener('message', (event) => {
    console.log(event.data)
})

// 移除事件监听器
socket.removeEventListener('message', handler)

# 2.3 常用属性

// 连接状态
socket.readyState
// 0: CONNECTING - 正在连接
// 1: OPEN - 已连接
// 2: CLOSING - 正在关闭
// 3: CLOSED - 已关闭

// 缓冲区中的消息数量
socket.bufferedAmount

// 子协议
socket.protocol

# 3. Node.js 服务端

# 3.1 安装 ws

npm i ws

# "ws": "^8.16.0"

# 3.2 基本使用

const { Server } = require('ws');

// 创建 WebSocket 服务器
const wsServer = new Server({ port: 8888 })

// 监听客户端连接
wsServer.on('connection', (socket) => {
    console.log('客户端已连接')
    
    // 监听客户端消息
    socket.on('message', (message) => {
        console.log('收到客户端消息:', message.toString())
        // 向客户端发送消息
        socket.send(message)
    })
    
    // 监听连接关闭
    socket.on('close', () => {
        console.log('客户端断开连接')
    })
    
    // 监听错误
    socket.on('error', (error) => {
        console.error('发生错误:', error)
    })
})

// 监听服务器错误
wsServer.on('error', (error) => {
    console.error('服务器错误:', error)
})

console.log('WebSocket 服务器运行在 ws://localhost:8888')

# 3.3 高级用法

const WebSocket = require('ws');

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

wss.on('connection', function connection(ws) {
  // 向所有连接的客户端广播
  wss.clients.forEach(function each(client) {
    if (client !== ws && client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({ msg: '欢迎新成员' }));
    }
  });

  ws.on('message', function incoming(data) {
    // 将消息广播给所有客户端
    wss.clients.forEach(function each(client) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  });
});

# 4. 完整示例

# 4.1 服务端代码

const WebSocket = require('ws');

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

wss.on('connection', (ws) => {
  console.log('客户端连接成功');

  ws.on('message', (message) => {
    console.log(`收到消息:${message}`);
    // 回复消息
    ws.send(`服务器收到:${message}`);
  });

  ws.on('close', () => {
    console.log('客户端断开连接');
  });

  ws.on('error', (error) => {
    console.error('连接错误:', error);
  });
});

console.log('WebSocket 服务器已启动,监听端口 8888');

# 4.2 客户端代码

// 浏览器端
const socket = new WebSocket('ws://localhost:8888');

socket.onopen = () => {
  console.log('连接到服务器');
  socket.send('你好,服务器!');
};

socket.onmessage = (event) => {
  console.log('收到服务器消息:', event.data);
};

socket.onclose = () => {
  console.log('与服务器断开连接');
};

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

# 5. 底层原理

# 5.1 协议握手过程

WebSocket 连接建立需要通过 HTTP 协议进行握手:

客户端请求:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

服务端响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手说明:

  • Sec-WebSocket-Key: 客户端生成的随机 base64 字符串
  • Sec-WebSocket-Accept: 服务端将 Key + GUID 后进行 SHA-1 哈希并 base64 编码
  • GUID: 258EAFA5-E914-47DA-95CA-C5AB0DC85B11

# 5.2 数据帧结构

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): 是否为最后一帧
  • RSV (3 bits): 保留位,通常为 0
  • Opcode (4 bits): 操作码
    • 0x0: 继续帧
    • 0x1: 文本帧
    • 0x2: 二进制帧
    • 0x8: 关闭连接
    • 0x9: Ping
    • 0xA: Pong
  • MASK (1 bit): 是否掩码(客户端发送必须为 1)
  • Payload length: 负载长度

# 5.3 心跳机制(Ping/Pong)

保持连接活跃的心跳实现:

服务端实现(Node.js):

const WebSocket = require('ws');

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

// 定时发送 ping
setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      return ws.terminate();
    }
    ws.isAlive = false;
    ws.ping();
  });
}, 30000); // 每 30 秒发送一次 ping

wss.on('connection', (ws) => {
  ws.isAlive = true;
  
  // 收到 pong 响应
  ws.on('pong', () => {
    ws.isAlive = true;
  });
});

客户端实现:

const socket = new WebSocket('ws://localhost:8888');

// 应用层心跳
let heartbeatTimer = null;
const HEARTBEAT_INTERVAL = 30000;

function startHeartbeat() {
  heartbeatTimer = setInterval(() => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'ping' }));
    }
  }, HEARTBEAT_INTERVAL);
}

socket.onopen = () => {
  startHeartbeat();
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === 'pong') {
    console.log('收到 pong 响应');
  }
};

socket.onclose = () => {
  clearInterval(heartbeatTimer);
};

# 5.4 断线重连机制

完整的重连实现:

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.reconnectDelay = 1000;
    this.shouldReconnect = true;
  }

  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log('连接成功');
      this.reconnectAttempts = 0;
      this.onConnected();
    };

    this.socket.onmessage = (event) => {
      this.onMessage(event.data);
    };

    this.socket.onclose = (event) => {
      console.log(`连接关闭:code=${event.code}, reason=${event.reason}`);
      this.attemptReconnect();
    };

    this.socket.onerror = (error) => {
      console.error('连接错误:', error);
    };
  }

  attemptReconnect() {
    if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.log('放弃重连');
      return;
    }

    this.reconnectAttempts++;
    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
    
    console.log(`准备重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts}), 延迟 ${delay}ms`);
    
    setTimeout(() => {
      console.log('开始重连...');
      this.connect();
    }, delay);
  }

  send(data) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      console.warn('连接未打开,消息发送失败');
    }
  }

  disconnect() {
    this.shouldReconnect = false;
    if (this.socket) {
      this.socket.close();
    }
  }

  // 回调方法,可被子类重写
  onConnected() {}
  onMessage(data) {}
}

// 使用示例
const client = new WebSocketClient('ws://localhost:8888');
client.onConnected = () => {
  console.log('已连接,可以开始通信');
};
client.onMessage = (data) => {
  console.log('收到消息:', data);
};
client.connect();

# 5.5 二进制数据传输

处理 ArrayBuffer 和 Blob:

// 发送二进制数据
const socket = new WebSocket('ws://localhost:8888');
socket.binaryType = 'arraybuffer'; // 或 'blob'

// 发送图片
const canvas = document.querySelector('canvas');
canvas.toBlob((blob) => {
  socket.send(blob);
});

// 接收二进制数据
socket.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    console.log('收到 ArrayBuffer:', event.data);
    // 转换为 Uint8Array
    const uint8Array = new Uint8Array(event.data);
  } else if (event.data instanceof Blob) {
    console.log('收到 Blob:', event.data);
    // 读取为文本
    const reader = new FileReader();
    reader.onload = () => console.log(reader.result);
    reader.readAsText(event.data);
  }
};

// 服务端接收二进制数据
wsServer.on('connection', (socket) => {
  socket.on('message', (data) => {
    if (data instanceof Buffer) {
      console.log('收到二进制数据:', data);
      // 处理图片、文件等
    }
  });
});

# 5.6 子协议协商

使用子协议进行更严格的接口定义:

// 客户端指定子协议
const socket = new WebSocket('ws://localhost:8888', ['protocol1', 'protocol2']);

socket.onopen = () => {
  console.log('使用的协议:', socket.protocol);
};

// 服务端选择协议
const WebSocket = require('ws');
const wss = new WebSocket.Server({ 
  port: 8888,
  handleProtocols: (protocols) => {
    console.log('客户端支持的协议:', protocols);
    // 选择一个协议
    if (protocols.has('protocol1')) {
      return 'protocol1';
    }
    return false; // 拒绝连接
  }
});

# 5.7 性能优化

消息合并(批量发送):

class BatchedWebSocket {
  constructor(ws) {
    this.ws = ws;
    this.messageQueue = [];
    this.flushTimer = null;
    this.BATCH_DELAY = 10; // 10ms
  }

  send(message) {
    this.messageQueue.push(message);
    
    if (!this.flushTimer) {
      this.flushTimer = setTimeout(() => {
        this.flush();
      }, this.BATCH_DELAY);
    }
  }

  flush() {
    if (this.messageQueue.length > 0) {
      const batchData = JSON.stringify({
        batch: true,
        messages: this.messageQueue
      });
      this.ws.send(batchData);
      this.messageQueue = [];
    }
    this.flushTimer = null;
  }
}

消息压缩(permessage-deflate):

const WebSocket = require('ws');

const wss = new WebSocket.Server({
  port: 8888,
  perMessageDeflate: {
    zlibDeflateOptions: {
      chunkSize: 1024,
      memLevel: 7,
      level: 3
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024
    },
    clientNoContextTakeover: true,
    serverNoContextTakeover: true,
    serverMaxWindowBits: 10,
    threshold: 1024 // 小于 1KB 的消息不压缩
  }
});

# 6. 注意事项

  1. 协议

    • ws:// - 非加密的 WebSocket 连接
    • wss:// - 加密的 WebSocket 连接(推荐在生产环境使用)
  2. 跨域问题:WebSocket 不受同源策略限制,但需要注意 CORS 配置

  3. 心跳检测:建议实现心跳机制以保持连接活跃

  4. 重连机制:网络不稳定时需要实现自动重连

  5. 消息格式:建议使用 JSON 格式进行数据传输

  6. 连接数限制:浏览器对同一域名有最大连接数限制(通常 6-8 个)

  7. 内存泄漏:及时清理事件监听器和定时器

  8. 安全性:验证 Origin 头,防止 CSRF 攻击

# 7. 参考

本章目录