WebSocket教程

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,非常适合实时应用。本教程将详细介绍如何在 Express 应用中集成 WebSocket 功能。

目录

  1. WebSocket 简介
  2. 选择 WebSocket 库
  3. 基础集成:Express + ws
  4. 高级集成:Express + Socket.io
  5. 实时功能实现
  6. 身份验证与安全
  7. 性能优化与扩展
  8. 部署注意事项
  9. 常见问题解决

WebSocket 简介

为什么需要 WebSocket?

  • HTTP 是无状态的,每次请求都需要建立新连接
  • HTTP 是单向的(客户端请求,服务端响应)
  • WebSocket 提供了:

    • 持久化连接
    • 双向实时通信
    • 低延迟数据传输

适用场景

  • 实时聊天应用
  • 协作编辑工具
  • 实时游戏
  • 股票行情推送
  • 在线拍卖/竞标系统
  • 实时通知系统

选择 WebSocket 库

常用 Node.js WebSocket 库

  1. ws - 轻量级 WebSocket 实现

    • 优点:简单、高效
    • 缺点:功能基础
  2. Socket.io - 功能丰富的实时引擎

    • 优点:自动重连、房间支持、回退机制
    • 缺点:相对复杂、协议不标准
  3. uWebSockets.js - 高性能实现

    • 优点:极高性能
    • 缺点:API 较复杂

本教程将重点介绍 wsSocket.io 两种集成方式。

基础集成:Express + ws

安装 ws

npm install ws

基本集成示例

const express = require('express');
const http = require('http');
const WebSocket = require('ws');

const app = express();
const server = http.createServer(app);

// 初始化 WebSocket 服务器
const wss = new WebSocket.Server({ server });

// Express 路由
app.get('/', (req, res) => {
  res.send('Hello World!');
});

// WebSocket 连接处理
wss.on('connection', (ws) => {
  console.log('New client connected');

  // 接收消息
  ws.on('message', (message) => {
    console.log(`Received: ${message}`);

    // 广播给所有客户端
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(`Server: ${message}`);
      }
    });
  });

  // 连接关闭
  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

server.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});

客户端代码

<!DOCTYPE html>
<html>
<body>
  <script>
    const ws = new WebSocket('ws://localhost:3000');

    ws.onopen = () => {
      console.log('Connected to server');
      ws.send('Hello Server!');
    };

    ws.onmessage = (event) => {
      console.log('Message from server:', event.data);
    };

    ws.onclose = () => {
      console.log('Disconnected from server');
    };
  </script>
</body>
</html>

运行 HTML

共享 Express 中间件

// 创建中间件验证函数
const verifyClient = (info, callback) => {
  // 可以在这里访问 HTTP 请求信息
  const token = info.req.headers['sec-websocket-protocol'];

  if (isValidToken(token)) {
    callback(true);
  } else {
    callback(false, 401, 'Unauthorized');
  }
};

const wss = new WebSocket.Server({ 
  server,
  verifyClient
});

高级集成:Express + Socket.io

安装 Socket.io

npm install socket.io

基本集成示例

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server);

// Express 路由
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

// Socket.io 连接处理
io.on('connection', (socket) => {
  console.log('New client connected');

  // 加入房间
  socket.join('some room');

  // 接收自定义事件
  socket.on('chat message', (msg) => {
    console.log('message: ' + msg);

    // 广播给所有客户端
    io.emit('chat message', msg);

    // 或者广播给特定房间
    io.to('some room').emit('room message', msg);
  });

  // 断开连接
  socket.on('disconnect', () => {
    console.log('Client disconnected');
  });
});

server.listen(3000, () => {
  console.log('Listening on *:3000');
});

客户端代码 (HTML)

<!DOCTYPE html>
<html>
<head>
  <title>Socket.IO Chat</title>
  <script src="/socket.io/socket.io.js"></script>
</head>
<body>
  <ul id="messages"></ul>
  <form id="form" action="">
    <input id="input" autocomplete="off" />
    <button>Send</button>
  </form>

  <script>
    const socket = io();

    // 接收消息
    socket.on('chat message', (msg) => {
      const li = document.createElement('li');
      li.textContent = msg;
      document.getElementById('messages').appendChild(li);
    });

    // 发送消息
    document.getElementById('form').addEventListener('submit', (e) => {
      e.preventDefault();
      const input = document.getElementById('input');
      socket.emit('chat message', input.value);
      input.value = '';
    });
  </script>
</body>
</html>

运行 HTML

共享 Express 会话

const session = require('express-session');
const sharedSession = require('express-socket.io-session');

// 配置 Express 会话
const expressSession = session({
  secret: 'my secret',
  resave: true,
  saveUninitialized: true
});

app.use(expressSession);

// 共享会话给 Socket.io
io.use(sharedSession(expressSession, {
  autoSave: true
}));

// 在 Socket.io 中使用会话
io.on('connection', (socket) => {
  const userId = socket.handshake.session.userId;
  // ...
});

实时功能实现

1. 实时聊天应用

服务端:

io.on('connection', (socket) => {
  // 用户加入聊天室
  socket.on('join', (username) => {
    socket.username = username;
    socket.broadcast.emit('user joined', username);
  });

  // 处理聊天消息
  socket.on('chat message', (msg) => {
    io.emit('chat message', {
      user: socket.username,
      message: msg,
      timestamp: new Date()
    });
  });

  // 用户正在输入
  socket.on('typing', () => {
    socket.broadcast.emit('typing', socket.username);
  });
});

客户端:

// 加入聊天
socket.emit('join', prompt('Enter your username'));

// 发送消息
input.addEventListener('keypress', () => {
  socket.emit('typing');
});

form.addEventListener('submit', () => {
  socket.emit('chat message', input.value);
});

2. 实时数据更新

服务端:

// 模拟实时数据更新
setInterval(() => {
  const data = {
    timestamp: new Date(),
    value: Math.random() * 100
  };
  io.emit('data update', data);
}, 1000);

客户端:

socket.on('data update', (data) => {
  updateChart(data); // 更新图表
});

3. 协作编辑

服务端:

let documentState = '';

io.on('connection', (socket) => {
  // 发送当前文档状态给新用户
  socket.emit('document update', documentState);

  // 处理编辑操作
  socket.on('edit', (operation) => {
    // 应用操作到文档状态
    documentState = applyOperation(documentState, operation);

    // 广播给其他用户
    socket.broadcast.emit('remote edit', operation);
  });
});

身份验证与安全

1. JWT 认证

服务端:

const jwt = require('jsonwebtoken');

// Socket.io 认证中间件
io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const decoded = jwt.verify(token, 'your-secret-key');
    socket.user = decoded;
    next();
  } catch (err) {
    next(new Error('Authentication error'));
  }
});

io.on('connection', (socket) => {
  console.log('Authenticated user:', socket.user.username);
});

客户端:

const token = localStorage.getItem('token');
const socket = io({
  auth: {
    token: token
  }
});

2. 防止跨站 WebSocket 劫持 (CSWSH)

const wss = new WebSocket.Server({
  verifyClient: (info, callback) => {
    const origin = info.origin || info.req.headers.origin;
    if (allowedOrigins.includes(origin)) {
      callback(true);
    } else {
      callback(false, 403, 'Forbidden');
    }
  }
});

3. 消息验证

socket.on('message', (data) => {
  if (typeof data !== 'string' || data.length > 1000) {
    return socket.disconnect(true);
  }

  // 处理有效消息
});

性能优化与扩展

1. 多进程扩展

使用 cluster 模块和 Redis 适配器:

npm install @socket.io/redis-adapter redis

服务端:

const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
} else {
  const app = express();
  const server = http.createServer(app);
  const io = socketIo(server);

  const pubClient = createClient({ host: 'localhost', port: 6379 });
  const subClient = pubClient.duplicate();

  io.adapter(createAdapter(pubClient, subClient));

  server.listen(3000);
}

2. 消息压缩

Socket.io 默认启用消息压缩:

const io = socketIo(server, {
  perMessageDeflate: {
    threshold: 1024 // 大于 1KB 的消息才压缩
  }
});

3. 连接限制

const io = socketIo(server, {
  maxHttpBufferSize: 1e8, // 最大消息大小 (100MB)
  pingTimeout: 60000,     // 60秒无响应断开
  pingInterval: 25000     // 每25秒发送ping
});

部署注意事项

1. 反向代理配置 (Nginx)

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

2. SSL/TLS 配置

const fs = require('fs');
const https = require('https');
const server = https.createServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert')
}, app);

const io = socketIo(server);

3. 负载均衡

  • 使用 Redis 适配器跨多服务器同步状态
  • 确保粘性会话 (sticky sessions) 配置正确

常见问题解决

1. 连接失败

  • 检查协议 (ws:// 或 wss://)
  • 验证端口和路径是否正确
  • 检查防火墙设置

2. 消息丢失

  • 实现确认机制:
  // 服务端
  socket.emit('message', data, (ack) => {
    console.log('Client acknowledged:', ack);
  });

  // 客户端
  socket.on('message', (data, callback) => {
    console.log(data);
    callback('Received');
  });

3. 内存泄漏

  • 定期检查连接数
  • 确保正确清理事件监听器
  • 使用 socket.disconnect() 而不是 socket.close()

4. 跨域问题

const io = socketIo(server, {
  cors: {
    origin: ["https://yourdomain.com", "https://anotherdomain.com"],
    methods: ["GET", "POST"],
    allowedHeaders: ["my-custom-header"],
    credentials: true
  }
});

总结

本教程详细介绍了 Express 与 WebSocket 的集成方法,包括:

  1. 基础集成:使用 ws 库实现简单 WebSocket 功能
  2. 高级集成:使用 Socket.io 实现功能丰富的实时应用
  3. 实时功能:聊天、数据更新、协作编辑等实现
  4. 安全措施:认证、授权和消息验证
  5. 性能优化:多进程扩展、消息压缩和连接限制
  6. 部署实践:反向代理、SSL 和负载均衡配置

WebSocket 为 Express 应用带来了实时通信能力,使你可以构建更加交互式和响应迅速的应用。根据你的项目需求选择合适的库:

  • 需要简单轻量级方案 → 选择 ws
  • 需要丰富功能和自动回退 → 选择 Socket.io
  • 需要极致性能 → 考虑 uWebSockets.js

通过本教程的学习,你应该能够将 WebSocket 功能无缝集成到你的 Express 应用中,并构建出高效的实时应用程序。









results matching ""

    No results matching ""