Skip to content

WebSocket

WebSocket简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议,提供了实时通信能力。

WebSocket特点

  • 全双工通信:客户端和服务器可以同时发送数据
  • 实时性高:无需轮询,减少延迟
  • 连接保持:一次连接,长期有效
  • 低开销:头部开销小,节省带宽
  • 跨域支持:支持跨域通信
  • 基于TCP:使用TCP协议,可靠传输

WebSocket与HTTP的区别

特性HTTPWebSocket
连接方式短连接,请求-响应长连接,全双工
通信方向单向(客户端→服务器)双向(客户端↔服务器)
头部开销
实时性低(需要轮询)高(实时通信)
服务器推送
协议标识http://, https://ws://, wss://

WebSocket基本用法

连接建立

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

// 连接打开
socket.onopen = function() {
  console.log('WebSocket连接已打开');
  socket.send('Hello Server');
};

// 接收消息
socket.onmessage = function(event) {
  console.log('收到消息:', event.data);
};

// 连接关闭
socket.onclose = function() {
  console.log('WebSocket连接已关闭');
};

// 连接错误
socket.onerror = function(error) {
  console.error('WebSocket错误:', error);
};

// 关闭连接
socket.close();

发送消息

javascript
// 发送文本消息
socket.send('Hello Server');

// 发送JSON数据
socket.send(JSON.stringify({ type: 'message', content: 'Hello' }));

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

// 发送ArrayBuffer
const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);
view[0] = 0x48; // 'H'
view[1] = 0x65; // 'e'
view[2] = 0x6C; // 'l'
view[3] = 0x6C; // 'l'
socket.send(buffer);

接收消息

javascript
socket.onmessage = function(event) {
  // 文本消息
  if (typeof event.data === 'string') {
    console.log('文本消息:', event.data);
    try {
      const data = JSON.parse(event.data);
      console.log('JSON消息:', data);
    } catch (e) {
      console.log('普通文本:', event.data);
    }
  }
  
  // 二进制消息
  else if (event.data instanceof Blob) {
    const reader = new FileReader();
    reader.onload = function() {
      console.log('Blob消息:', reader.result);
    };
    reader.readAsText(event.data);
  }
  
  // ArrayBuffer消息
  else if (event.data instanceof ArrayBuffer) {
    const view = new Uint8Array(event.data);
    console.log('ArrayBuffer消息:', view);
  }
};

WebSocket心跳检测

心跳检测原理

心跳检测是通过定期发送心跳包来检测连接是否正常的机制,防止连接被网络设备(如防火墙)断开。

实现心跳检测

javascript
class WebSocketWithHeartbeat {
  constructor(url, heartbeatInterval = 30000) {
    this.url = url;
    this.heartbeatInterval = heartbeatInterval;
    this.heartbeatTimer = null;
    this.reconnectTimer = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.socket = null;
    this.connect();
  }
  
  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log('WebSocket连接已打开');
      this.reconnectAttempts = 0;
      this.startHeartbeat();
    };
    
    this.socket.onmessage = (event) => {
      if (event.data === 'pong') {
        console.log('收到心跳响应');
      } else {
        console.log('收到消息:', event.data);
      }
    };
    
    this.socket.onclose = () => {
      console.log('WebSocket连接已关闭');
      this.stopHeartbeat();
      this.reconnect();
    };
    
    this.socket.onerror = (error) => {
      console.error('WebSocket错误:', error);
    };
  }
  
  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.socket && this.socket.readyState === WebSocket.OPEN) {
        this.socket.send('ping');
        console.log('发送心跳');
      }
    }, this.heartbeatInterval);
  }
  
  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }
  
  reconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
      this.reconnectTimer = setTimeout(() => {
        this.connect();
      }, 3000);
    } else {
      console.error('重连失败');
    }
  }
  
  send(data) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      console.error('WebSocket未连接');
    }
  }
  
  close() {
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
    if (this.socket) {
      this.socket.close();
    }
  }
}

const socket = new WebSocketWithHeartbeat('ws://localhost:8080');
socket.send('Hello Server');

WebSocket断线重连

断线重连策略

  1. 指数退避:重连间隔逐渐增加
  2. 最大重连次数:防止无限重连
  3. 网络状态检测:在网络恢复时重连

实现断线重连

javascript
class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.options = {
      maxReconnectAttempts: 5,
      reconnectInterval: 3000,
      maxReconnectInterval: 30000,
      ...options
    };
    this.socket = null;
    this.reconnectAttempts = 0;
    this.reconnectTimer = null;
    this.isClosing = false;
    this.eventListeners = {
      open: [],
      message: [],
      close: [],
      error: []
    };
    this.connect();
  }
  
  connect() {
    try {
      this.socket = new WebSocket(this.url);
      
      this.socket.onopen = (event) => {
        console.log('WebSocket连接已打开');
        this.reconnectAttempts = 0;
        this.eventListeners.open.forEach(listener => listener(event));
      };
      
      this.socket.onmessage = (event) => {
        this.eventListeners.message.forEach(listener => listener(event));
      };
      
      this.socket.onclose = (event) => {
        console.log('WebSocket连接已关闭');
        this.eventListeners.close.forEach(listener => listener(event));
        
        if (!this.isClosing) {
          this.scheduleReconnect();
        }
      };
      
      this.socket.onerror = (error) => {
        console.error('WebSocket错误:', error);
        this.eventListeners.error.forEach(listener => listener(error));
      };
    } catch (error) {
      console.error('WebSocket连接失败:', error);
      this.scheduleReconnect();
    }
  }
  
  scheduleReconnect() {
    if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = Math.min(
        this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
        this.options.maxReconnectInterval
      );
      
      console.log(`尝试重连 (${this.reconnectAttempts}/${this.options.maxReconnectAttempts}),延迟 ${delay}ms`);
      
      this.reconnectTimer = setTimeout(() => {
        this.connect();
      }, delay);
    } else {
      console.error('重连失败,已达到最大重连次数');
    }
  }
  
  send(data) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
      return true;
    } else {
      console.error('WebSocket未连接,无法发送消息');
      return false;
    }
  }
  
  close() {
    this.isClosing = true;
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
    if (this.socket) {
      this.socket.close();
    }
  }
  
  on(event, listener) {
    if (this.eventListeners[event]) {
      this.eventListeners[event].push(listener);
    }
  }
  
  off(event, listener) {
    if (this.eventListeners[event]) {
      this.eventListeners[event] = this.eventListeners[event].filter(l => l !== listener);
    }
  }
  
  get readyState() {
    return this.socket ? this.socket.readyState : WebSocket.CLOSED;
  }
}

const socket = new ReconnectingWebSocket('ws://localhost:8080');

socket.on('open', () => {
  console.log('连接已打开,发送消息');
  socket.send('Hello Server');
});

socket.on('message', (event) => {
  console.log('收到消息:', event.data);
});

socket.on('close', () => {
  console.log('连接已关闭');
});

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

// 关闭连接
// socket.close();

WebSocket服务器实现

Node.js实现

javascript
const WebSocket = require('ws');

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

wss.on('connection', (ws) => {
  console.log('客户端已连接');
  
  // 发送欢迎消息
  ws.send('Welcome to WebSocket Server');
  
  // 接收消息
  ws.on('message', (message) => {
    console.log('收到消息:', message);
    
    // 处理心跳
    if (message === 'ping') {
      ws.send('pong');
      return;
    }
    
    // 广播消息
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(`Server: ${message}`);
      }
    });
  });
  
  // 连接关闭
  ws.on('close', () => {
    console.log('客户端已断开连接');
  });
  
  // 连接错误
  ws.on('error', (error) => {
    console.error('连接错误:', error);
  });
});

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

使用Socket.io

javascript
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);

io.on('connection', (socket) => {
  console.log('客户端已连接');
  
  // 发送欢迎消息
  socket.emit('message', 'Welcome to Socket.io Server');
  
  // 接收消息
  socket.on('message', (message) => {
    console.log('收到消息:', message);
    
    // 广播消息
    io.emit('message', `Server: ${message}`);
  });
  
  // 房间操作
  socket.on('joinRoom', (room) => {
    socket.join(room);
    console.log(`客户端加入房间: ${room}`);
    io.to(room).emit('message', `有人加入了房间 ${room}`);
  });
  
  // 连接关闭
  socket.on('disconnect', () => {
    console.log('客户端已断开连接');
  });
});

server.listen(8080, () => {
  console.log('Socket.io服务器已启动,监听端口8080');
});

WebSocket安全

使用WSS

javascript
// 使用wss://协议
const socket = new WebSocket('wss://example.com');

认证

javascript
// 使用token认证
const token = 'your-auth-token';
const socket = new WebSocket(`ws://localhost:8080?token=${token}`);

消息验证

javascript
// 消息格式验证
function validateMessage(message) {
  try {
    const data = JSON.parse(message);
    return data.type && typeof data.type === 'string';
  } catch (e) {
    return false;
  }
}

socket.onmessage = function(event) {
  if (validateMessage(event.data)) {
    const data = JSON.parse(event.data);
    // 处理消息
  } else {
    console.error('无效的消息格式');
  }
};

WebSocket应用场景

  • 实时聊天:即时通讯应用
  • 在线游戏:实时多人游戏
  • 实时协作:在线文档编辑
  • 实时数据:股票行情、体育赛事
  • 推送通知:系统通知、消息提醒
  • 视频通话:WebRTC信令

面试常见问题

1. WebSocket的特点?

  • 全双工通信
  • 实时性高
  • 连接保持
  • 低开销
  • 跨域支持

2. WebSocket与HTTP的区别?

  • HTTP:短连接,请求-响应,单向通信,头部开销大
  • WebSocket:长连接,全双工,双向通信,头部开销小

3. WebSocket的协议标识?

  • ws://:未加密
  • wss://:加密(基于TLS)

4. 如何实现WebSocket的心跳检测?

通过定期发送心跳包(如ping),服务器响应(如pong)来检测连接是否正常。

5. 如何实现WebSocket的断线重连?

在onclose事件中触发重连逻辑,使用指数退避策略,设置最大重连次数。

6. WebSocket支持哪些数据类型?

  • 字符串
  • Blob
  • ArrayBuffer

7. WebSocket的readyState有哪些状态?

  • 0: CONNECTING - 连接中
  • 1: OPEN - 连接已打开
  • 2: CLOSING - 关闭中
  • 3: CLOSED - 连接已关闭

8. 如何处理WebSocket的错误?

通过onerror事件监听器捕获错误,实现错误处理逻辑。

9. WebSocket的优势是什么?

  • 实时性高
  • 减少网络开销
  • 简化代码逻辑
  • 支持服务器推送

10. WebSocket的局限性是什么?

  • 服务器资源消耗较大
  • 可能被防火墙阻止
  • 不支持所有浏览器(旧版本)

通过理解WebSocket的各种特性和应用场景,可以更好地构建实时通信应用。

好好学习,天天向上