# WebSocket 介绍

4 阅读9分钟

WebSocket 介绍

什么是 WebSocket?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许客户端和服务器之间进行实时、双向的数据传输。

核心特点

  • 持久连接:建立连接后保持打开状态,直到主动关闭
  • 双向通信:客户端和服务器都可以主动发送消息
  • 低延迟:无需每次通信都建立新连接
  • 协议升级:基于 HTTP 协议,通过 Upgrade 头升级为 WebSocket 协议

工作原理

  1. 握手阶段:客户端发送 HTTP 请求,请求升级为 WebSocket 协议

    GET /chat HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Version: 13
    
  2. 服务端响应:服务端确认升级,返回 101 状态码

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    
  3. 数据传输:升级成功后,双方通过 WebSocket 帧进行数据传输


WebSocket 的优势

1. 实时性强

  • 消息可以立即推送,无需客户端轮询
  • 延迟低,适合实时应用场景

2. 性能优势

  • 减少 HTTP 请求:一次握手,多次通信
  • 降低服务器压力:不需要频繁建立和关闭连接
  • 减少带宽消耗:WebSocket 帧头只有 2-14 字节,比 HTTP 请求头小得多

3. 双向通信

  • 客户端和服务器都可以主动发送消息
  • 不需要客户端主动请求就能接收服务器推送

4. 协议开销小

  • WebSocket 帧格式简单,开销远小于 HTTP 请求/响应

5. 支持二进制和文本数据

  • 可以传输文本、JSON、二进制数据等多种格式

WebSocket 的劣势

1. 浏览器兼容性

  • 需要浏览器支持(IE 10+)
  • 移动端浏览器支持良好,但需要考虑降级方案

2. 连接管理复杂

  • 需要处理连接断开、重连、心跳检测等
  • 网络不稳定时连接可能频繁断开

3. 服务器资源占用

  • 每个连接都需要保持,大量并发连接会占用服务器资源
  • 需要合理设计连接池和资源管理

4. 代理和防火墙问题

  • 某些代理服务器可能不支持 WebSocket
  • 需要处理代理穿透问题

5. 调试相对困难

  • 不像 HTTP 请求那样容易在浏览器 Network 面板查看
  • 需要专门的工具或浏览器扩展来调试

6. 不支持 HTTP 缓存

  • 无法利用浏览器和 CDN 的缓存机制

与其他方案的对比

1. HTTP 短轮询(Short Polling)

原理:客户端定时向服务器发送请求

setInterval(() => {
  fetch('/api/data').then(res => res.json());
}, 1000);

对比

  • ❌ 实时性差,有延迟
  • ❌ 服务器压力大,频繁请求
  • ❌ 浪费带宽,很多请求是无用的
  • ✅ 实现简单,兼容性好

2. HTTP 长轮询(Long Polling)

原理:客户端发送请求,服务器保持连接直到有数据或超时

function longPoll() {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => {
      // 处理数据
      longPoll(); // 立即发起下一次请求
    });
}

对比

  • ✅ 实时性较好
  • ⚠️ 服务器需要保持连接,资源占用中等
  • ⚠️ 实现相对复杂
  • ❌ 仍然需要频繁建立连接

3. Server-Sent Events (SSE)

原理:服务器向客户端推送数据,单向通信

const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
  console.log(event.data);
};

对比

  • ✅ 实现简单,基于 HTTP
  • ✅ 自动重连
  • ✅ 浏览器原生支持
  • 只能服务器向客户端推送(单向)
  • ❌ IE 不支持

4. WebSocket

对比

  • ✅ 双向通信
  • ✅ 实时性最好
  • ✅ 性能最优
  • ✅ 支持二进制数据
  • ⚠️ 实现相对复杂
  • ⚠️ 需要处理连接管理

对比总结表

方案实时性双向通信服务器压力实现复杂度适用场景
短轮询❌ 高⭐ 简单更新频率低
长轮询⭐⭐⚠️ 中⭐⭐实时性要求不高
SSE⭐⭐⭐❌ 单向⚠️ 中⭐⭐服务器推送
WebSocket⭐⭐⭐⚠️ 中⭐⭐⭐实时双向通信

什么情况选择使用 WebSocket?

✅ 适合使用的场景

  1. 实时聊天应用

    • 即时通讯、在线客服
    • 需要双向实时通信
  2. 实时数据展示

    • 股票行情、实时监控
    • 数据大屏、实时统计
  3. 在线游戏

    • 多人游戏、实时对战
    • 需要低延迟的双向通信
  4. 协作工具

    • 在线文档编辑、代码协作
    • 多人实时编辑
  5. 实时通知

    • 系统通知、消息推送
    • 订单状态更新
  6. IoT 设备控制

    • 智能家居控制
    • 设备状态监控

❌ 不适合使用的场景

  1. 简单的数据获取

    • 只需要获取一次数据,使用 HTTP 即可
  2. 单向推送

    • 只需要服务器推送,考虑使用 SSE
  3. 更新频率很低

    • 几分钟或更长时间更新一次,使用轮询即可
  4. 需要利用 HTTP 缓存

    • 需要 CDN 缓存、浏览器缓存,使用 HTTP
  5. 对兼容性要求极高

    • 需要支持老旧浏览器,考虑降级方案

WebSocket 基本使用

客户端 API

// 1. 创建连接
const ws = new WebSocket('ws://localhost:8080');

// 2. 连接成功
ws.onopen = function(event) {
    console.log('连接成功');
    ws.send('Hello Server');
};

// 3. 接收消息
ws.onmessage = function(event) {
    console.log('收到消息:', event.data);
    // 可以接收文本或二进制数据
    if (event.data instanceof ArrayBuffer) {
        // 处理二进制数据
    }
};

// 4. 连接关闭
ws.onclose = function(event) {
    console.log('连接关闭', event.code, event.reason);
};

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

// 6. 发送消息
ws.send('文本消息');
ws.send(JSON.stringify({ type: 'message', content: 'Hello' }));
ws.send(new ArrayBuffer(8)); // 二进制数据

// 7. 关闭连接
ws.close();

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

服务端实现(Node.js)

import http from 'http';
import { WebSocketServer } from 'ws';

const server = http.createServer();
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
    // 新连接
    ws.on('message', (data) => {
        // 接收消息
        ws.send(`Echo: ${data}`);
    });
    
    ws.on('close', () => {
        // 连接关闭
    });
});

server.listen(8080);

实际开发中的注意事项

1. 心跳检测(Heartbeat)

防止连接被代理服务器或防火墙关闭:

// 客户端
const heartbeat = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'ping' }));
    }
}, 30000);

ws.onclose = () => {
    clearInterval(heartbeat);
};

2. 自动重连

let ws;
let reconnectTimer;

function connect() {
    ws = new WebSocket('ws://localhost:8080');
    
    ws.onclose = () => {
        reconnectTimer = setTimeout(connect, 3000);
    };
    
    ws.onerror = () => {
        ws.close();
    };
}

connect();

3. 消息队列

连接断开时缓存消息,连接恢复后发送:

const messageQueue = [];

function sendMessage(msg) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(msg);
    } else {
        messageQueue.push(msg);
    }
}

ws.onopen = () => {
    while (messageQueue.length > 0) {
        ws.send(messageQueue.shift());
    }
};

4. 错误处理

ws.onerror = (error) => {
    console.error('WebSocket error:', error);
    // 记录错误日志
    // 通知用户
    // 尝试重连
};

5. 安全性

  • 使用 wss://(WebSocket Secure)加密传输
  • 验证来源(Origin)
  • 使用 Token 进行身份验证
  • 限制连接数和消息频率

相关技术介绍

Socket.io

Socket.io 是一个基于 WebSocket 的实时通信库,提供了更高级的抽象和额外的功能。

核心特点
  1. 自动降级

    • 优先使用 WebSocket
    • 如果不支持,自动降级到长轮询、短轮询等
    • 保证在各种环境下都能工作
  2. 自动重连

    • 内置自动重连机制
    • 连接断开后自动尝试重连
  3. 房间(Rooms)和命名空间(Namespaces)

    • 可以将客户端分组到不同的房间
    • 支持命名空间,实现多应用隔离

    房间(Rooms):将客户端分组,可以向特定房间广播消息

    命名空间(Namespaces):类似于 URL 路径,用于隔离不同的应用或功能模块

  4. 事件系统

    • 基于事件的 API,类似 Node.js 的 EventEmitter
    • 可以自定义事件名称
  5. 广播功能

    • 支持向所有客户端或特定房间广播消息
基本使用
// 服务端
const io = require('socket.io')(server);

io.on('connection', (socket) => {
    // 加入房间
    socket.join('room1');
    
    // 监听自定义事件
    socket.on('chat message', (msg) => {
        // 广播到所有客户端
        io.emit('chat message', msg);
        
        // 广播到特定房间
        io.to('room1').emit('chat message', msg);
    });
    
    socket.on('disconnect', () => {
        console.log('用户断开连接');
    });
});

// 客户端
const socket = io('http://localhost:3000');

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

socket.emit('chat message', 'Hello');

socket.on('chat message', (msg) => {
    console.log('收到消息:', msg);
});
房间(Rooms)详解

什么是房间?

房间是 Socket.io 中的一个核心概念,用于将客户端分组。客户端可以加入(join)或离开(leave)房间,服务器可以向特定房间的所有客户端广播消息。

使用场景:

  • 聊天室:不同聊天室的用户互不干扰
  • 在线游戏:不同游戏房间的玩家
  • 协作编辑:不同文档的协作者
  • 实时通知:向特定用户组推送通知

示例代码:

// 服务端
io.on('connection', (socket) => {
    // 用户加入房间
    socket.on('join room', (roomId) => {
        socket.join(roomId);
        // 通知房间内其他用户
        socket.to(roomId).emit('user joined', {
            userId: socket.id,
            message: '新用户加入房间'
        });
    });
    
    // 向房间发送消息
    socket.on('room message', (data) => {
        // 只向指定房间广播,不包括发送者
        socket.to(data.roomId).emit('room message', {
            userId: socket.id,
            message: data.message
        });
        
        // 或者包括发送者
        io.to(data.roomId).emit('room message', {
            userId: socket.id,
            message: data.message
        });
    });
    
    // 离开房间
    socket.on('leave room', (roomId) => {
        socket.leave(roomId);
    });
    
    // 断开连接时自动离开所有房间
    socket.on('disconnect', () => {
        // Socket.io 会自动处理
    });
});

// 客户端
socket.emit('join room', 'room123');

socket.on('room message', (data) => {
    console.log(`${data.userId}: ${data.message}`);
});

房间操作:

  • socket.join(room) - 加入房间
  • socket.leave(room) - 离开房间
  • io.to(room).emit() - 向房间广播(不包括发送者)
  • io.in(room).emit() - 向房间广播(包括发送者)
  • socket.to(room).emit() - 向房间广播(不包括发送者)

实际应用场景:

// 场景1:聊天室应用
socket.on('join chatroom', (chatroomId) => {
    socket.join(`chatroom:${chatroomId}`);
    // 只向这个聊天室的用户发送消息
});

// 场景2:在线游戏
socket.on('join game', (gameId) => {
    socket.join(`game:${gameId}`);
    // 游戏状态只推送给这个游戏的玩家
});

// 场景3:用户通知
socket.on('user online', (userId) => {
    socket.join(`user:${userId}`);
    // 可以向特定用户推送通知
    io.to(`user:${userId}`).emit('notification', { ... });
});

命名空间(Namespaces)详解

什么是命名空间?

命名空间类似于 URL 路径,用于将 Socket.io 应用划分为不同的逻辑单元。每个命名空间都有自己的连接、事件和房间。

默认命名空间: /(根命名空间)

使用场景:

  • 多应用隔离:同一个服务器运行多个应用
  • 功能模块分离:不同功能使用不同的命名空间
  • 权限控制:不同命名空间可以有不同的认证机制

示例代码:

// 创建不同的命名空间
const adminNamespace = io.of('/admin');
const userNamespace = io.of('/user');
const chatNamespace = io.of('/chat');

// 管理后台命名空间
adminNamespace.on('connection', (socket) => {
    // 需要管理员权限
    socket.on('admin action', (data) => {
        // 处理管理员操作
        adminNamespace.emit('admin update', data);
    });
});

// 用户命名空间
userNamespace.on('connection', (socket) => {
    socket.on('user message', (data) => {
        // 处理用户消息
        userNamespace.emit('user update', data);
    });
});

// 聊天命名空间
chatNamespace.on('connection', (socket) => {
    socket.on('chat message', (data) => {
        chatNamespace.emit('chat message', data);
    });
});

客户端连接:

// 连接到默认命名空间
const socket = io('http://localhost:3000');

// 连接到指定命名空间
const adminSocket = io('http://localhost:3000/admin');
const userSocket = io('http://localhost:3000/user');
const chatSocket = io('http://localhost:3000/chat');

命名空间的特点:

  • 每个命名空间是独立的,互不干扰
  • 每个命名空间有自己的房间系统
  • 可以有不同的中间件和认证机制
  • 可以有不同的连接处理逻辑

实际应用场景:

// 场景1:多租户应用
const tenant1 = io.of('/tenant1');
const tenant2 = io.of('/tenant2');
// 不同租户的数据完全隔离

// 场景2:功能模块分离
const notificationNs = io.of('/notifications');
const chatNs = io.of('/chat');
const gameNs = io.of('/game');
// 不同功能模块使用不同命名空间

// 场景3:权限控制
io.of('/admin').use((socket, next) => {
    // 中间件:验证管理员权限
    if (isAdmin(socket.handshake.auth.token)) {
        next();
    } else {
        next(new Error('Unauthorized'));
    }
});

房间 vs 命名空间:

特性房间(Rooms)命名空间(Namespaces)
作用范围单个命名空间内整个应用级别
隔离程度消息隔离完全隔离(连接、事件、房间)
使用场景分组客户端分离应用/功能模块
客户端连接自动加入需要指定命名空间 URL
性能影响中等(每个命名空间独立管理)

组合使用示例:

// 在命名空间中使用房间
const chatNs = io.of('/chat');

chatNs.on('connection', (socket) => {
    // 用户加入聊天室(房间)
    socket.on('join chatroom', (chatroomId) => {
        socket.join(`chatroom:${chatroomId}`);
    });
    
    // 向特定聊天室发送消息
    socket.on('send message', (data) => {
        // 在命名空间内的特定房间广播
        chatNs.to(`chatroom:${data.chatroomId}`).emit('new message', {
            userId: socket.id,
            message: data.message
        });
    });
});
Socket.io vs 原生 WebSocket
特性Socket.io原生 WebSocket
兼容性✅ 自动降级,兼容性好⚠️ 需要浏览器支持
自动重连✅ 内置❌ 需要自己实现
房间/命名空间✅ 支持❌ 不支持
事件系统✅ 基于事件⚠️ 需要自己封装
二进制数据✅ 支持✅ 支持
性能⚠️ 有额外开销✅ 性能更好
包大小⚠️ 较大(~100KB)✅ 原生 API
适用场景
  • ✅ 需要兼容老旧浏览器
  • ✅ 需要房间、命名空间等高级功能
  • ✅ 需要快速开发,不想处理底层细节
  • ✅ 需要自动重连、心跳等开箱即用的功能

RabbitMQ

RabbitMQ 是一个开源的消息队列中间件,实现了 AMQP(Advanced Message Queuing Protocol)协议。

核心概念
  1. 消息队列(Message Queue)

    • 存储消息的缓冲区
    • 生产者发送消息到队列,消费者从队列接收消息
  2. 生产者(Producer)

    • 发送消息的应用
  3. 消费者(Consumer)

    • 接收和处理消息的应用
  4. 交换机(Exchange)

    • 接收生产者发送的消息,根据路由规则将消息路由到队列
  5. 路由键(Routing Key)

    • 用于决定消息如何路由到队列
核心特点
  1. 可靠性

    • 消息持久化
    • 支持消息确认机制
    • 支持事务
  2. 灵活的路由

    • Direct(直接路由)
    • Topic(主题路由)
    • Fanout(广播)
    • Headers(头部路由)
  3. 高可用性

    • 支持集群
    • 支持镜像队列
  4. 多协议支持

    • AMQP、MQTT、STOMP 等
在 WebSocket 中的应用

场景:多服务器 WebSocket 负载均衡

客户端1 ──┐
          ├──> 服务器1 (WebSocket)
客户端2 ──┘        │
                   ├──> RabbitMQ ──> 服务器2 (WebSocket) ──> 客户端3
客户端4 ──┐        │
          ├──> 服务器3 (WebSocket)
客户端5 ──┘

实现原理:

  1. 多个 WebSocket 服务器实例
  2. 客户端连接到不同的服务器
  3. 服务器1收到消息后,通过 RabbitMQ 发布消息
  4. 其他服务器订阅 RabbitMQ,收到消息后推送给各自的客户端
  5. 实现跨服务器的消息广播
基本使用示例
// 发布消息(服务器1)
const amqp = require('amqplib');

async function publishMessage() {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();
    
    const queue = 'websocket_messages';
    await channel.assertQueue(queue, { durable: true });
    
    // 发布消息
    channel.sendToQueue(queue, Buffer.from(JSON.stringify({
        type: 'chat',
        message: 'Hello',
        userId: '123'
    })));
    
    console.log('消息已发送');
}

// 消费消息(服务器2)
async function consumeMessages() {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();
    
    const queue = 'websocket_messages';
    await channel.assertQueue(queue, { durable: true });
    
    channel.consume(queue, (msg) => {
        if (msg) {
            const content = JSON.parse(msg.content.toString());
            // 推送给 WebSocket 客户端
            broadcastToClients(content);
            channel.ack(msg); // 确认消息已处理
        }
    });
}
RabbitMQ vs 其他消息队列
特性RabbitMQRedis Pub/SubKafka
协议AMQPRedis 协议Kafka 协议
消息持久化✅ 支持⚠️ 不支持✅ 支持
消息确认✅ 支持❌ 不支持✅ 支持
性能⚠️ 中等✅ 高✅ 非常高
适用场景通用消息队列简单发布订阅大数据流处理
学习曲线⭐⭐⭐⭐⭐
Socket.io + RabbitMQ 组合使用

Socket.io 提供了 Redis adapter,但也可以使用 RabbitMQ:

// 使用 Socket.io Redis Adapter
const io = require('socket.io')(server);
const redisAdapter = require('socket.io-redis');
io.adapter(redisAdapter({ host: 'localhost', port: 6379 }));

// 或者使用自定义的 RabbitMQ adapter
// 实现跨服务器的消息同步

优势:

  • 多服务器实例之间可以共享消息
  • 客户端连接到任意服务器都能收到消息
  • 实现水平扩展

常见面试问题

Q1: WebSocket 和 HTTP 的区别?

A:

  • HTTP 是无状态的请求-响应协议,每次请求都需要建立连接
  • WebSocket 是持久连接,建立后可以双向通信
  • HTTP 是单向的(客户端请求,服务器响应)
  • WebSocket 支持双向通信

Q2: WebSocket 如何实现心跳检测?

A: 客户端定时发送 ping 消息,服务器回复 pong。如果超时未收到回复,认为连接断开,进行重连。

Q3: WebSocket 连接断开如何处理?

A:

  1. 监听 onclose 事件
  2. 实现指数退避重连机制
  3. 使用消息队列缓存未发送的消息
  4. 通知用户连接状态

Q4: 如何保证 WebSocket 消息的顺序性?

A:

  1. 在消息中添加序列号
  2. 客户端维护接收缓冲区
  3. 按序列号顺序处理消息
  4. 处理乱序和丢包情况

Q5: WebSocket 支持哪些数据格式?

A:

  • 文本字符串
  • JSON 字符串
  • ArrayBuffer(二进制)
  • Blob(二进制)

Q6: 如何实现 WebSocket 的负载均衡?

A:

  1. Sticky Session(会话粘性)

    • 通过负载均衡器(如 Nginx)将同一客户端的请求路由到同一服务器
    • 优点:实现简单
    • 缺点:服务器宕机会导致连接丢失,无法真正实现水平扩展
  2. 使用 Redis 共享连接状态

    • 将连接信息存储在 Redis 中
    • 服务器之间通过 Redis 发布订阅机制同步消息
    • Socket.io 的 Redis adapter 就是这种实现
  3. 使用消息队列(RabbitMQ/Kafka)

    • 服务器收到消息后发布到消息队列
    • 其他服务器订阅消息队列,收到消息后推送给各自的客户端
    • 优点:解耦、可靠、支持复杂路由
  4. 专门的 WebSocket 网关

    • 使用 API Gateway(如 Kong、Zuul)的 WebSocket 支持
    • 或使用专门的 WebSocket 代理(如 Socket.io 的 Redis adapter)

推荐方案: 对于生产环境,推荐使用 Socket.io + Redis adapter 或自定义的 RabbitMQ 方案,既能实现负载均衡,又能保证消息的可靠传递。