SockJS 中文教程

1. 核心概念

SockJS 是一个浏览器 JavaScript 库,提供了一个类似 WebSocket 的对象,用于在浏览器和服务器之间建立低延迟、全双工的通信通道。它由 Marek Majkowski 开发和维护。

1.1 什么是 SockJS?

SockJS 是一个实时通信库,提供了:

  • 类似 WebSocket 的 API
  • 自动降级到其他传输方式(如 XHR Streaming、XHR Polling 等)
  • 跨域支持
  • 轻量级的服务器实现
  • 与各种前端框架的集成

1.2 为什么选择 SockJS?

  • 兼容性:支持所有现代浏览器,包括那些不支持 WebSocket 的浏览器
  • 简单易用:提供了类似 WebSocket 的 API,易于上手
  • 可靠性:自动处理连接断开和重连
  • 跨域支持:内置了跨域支持
  • 轻量级:客户端库体积小,服务器实现高效

2. 安装与配置

2.1 基本安装

在 Node.js 项目中安装 SockJS 服务器:

npm install sockjs

在前端项目中安装 SockJS 客户端:

npm install sockjs-client

2.2 基本配置

创建一个简单的 SockJS 服务器:

import http from 'http';
import sockjs from 'sockjs';

// 创建 HTTP 服务器
const server = http.createServer();

// 创建 SockJS 服务器
const sockjsServer = sockjs.createServer({
  // 配置选项
  sockjs_url: 'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js',
  prefix: '/echo'
});

// 监听连接事件
sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  
  // 监听消息事件
  conn.on('data', (message) => {
    console.log('收到消息:', message);
    // 发送消息回客户端
    conn.write(message);
  });
  
  // 监听关闭事件
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
  });
});

// 将 SockJS 服务器挂载到 HTTP 服务器
sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

// 启动服务器
server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

2.3 客户端配置

创建一个简单的 SockJS 客户端:

import SockJS from 'sockjs-client';

// 创建 SockJS 连接
const sock = new SockJS('http://localhost:3000/echo');

// 监听连接打开事件
sock.onopen = function() {
  console.log('连接打开');
  // 发送消息
  sock.send('Hello SockJS!');
};

// 监听消息事件
sock.onmessage = function(e) {
  console.log('收到消息:', e.data);
};

// 监听连接关闭事件
sock.onclose = function() {
  console.log('连接关闭');
};

// 监听错误事件
sock.onerror = function(error) {
  console.error('连接错误:', error);
};

3. 基本使用

3.1 事件系统

SockJS 使用事件系统进行通信,服务器和客户端都可以触发和监听事件:

3.1.1 服务器端事件

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer();

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  
  // 监听数据事件
  conn.on('data', (message) => {
    console.log('收到消息:', message);
    
    try {
      // 解析消息
      const data = JSON.parse(message);
      
      // 根据消息类型处理
      switch (data.type) {
        case 'chat':
          // 处理聊天消息
          console.log('聊天消息:', data.content);
          conn.write(JSON.stringify({
            type: 'chat',
            content: `服务器收到: ${data.content}`
          }));
          break;
        case 'userJoin':
          // 处理用户加入
          console.log('用户加入:', data.username);
          conn.write(JSON.stringify({
            type: 'system',
            content: `${data.username} 加入了聊天`
          }));
          break;
        default:
          console.log('未知消息类型:', data.type);
      }
    } catch (error) {
      console.error('解析消息错误:', error);
      conn.write(JSON.stringify({
        type: 'error',
        content: '无效的消息格式'
      }));
    }
  });
  
  // 监听关闭事件
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
  });
});

sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

3.1.2 客户端事件

import SockJS from 'sockjs-client';

const sock = new SockJS('http://localhost:3000/echo');

sock.onopen = function() {
  console.log('连接打开');
  
  // 发送用户加入消息
  sock.send(JSON.stringify({
    type: 'userJoin',
    username: 'John Doe'
  }));
  
  // 发送聊天消息
  sock.send(JSON.stringify({
    type: 'chat',
    content: 'Hello everyone!'
  }));
};

sock.onmessage = function(e) {
  console.log('收到消息:', e.data);
  
  try {
    // 解析消息
    const data = JSON.parse(e.data);
    
    // 根据消息类型处理
    switch (data.type) {
      case 'chat':
        // 处理聊天消息
        console.log('聊天消息:', data.content);
        break;
      case 'system':
        // 处理系统消息
        console.log('系统消息:', data.content);
        break;
      case 'error':
        // 处理错误消息
        console.error('错误消息:', data.content);
        break;
      default:
        console.log('未知消息类型:', data.type);
    }
  } catch (error) {
    console.error('解析消息错误:', error);
  }
};

sock.onclose = function() {
  console.log('连接关闭');
};

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

3.2 连接管理

SockJS 提供了连接管理功能,包括连接状态检查、重连等:

3.2.1 客户端连接管理

import SockJS from 'sockjs-client';

// 创建连接
let sock = new SockJS('http://localhost:3000/echo');

// 连接状态
let isConnected = false;

// 重连尝试次数
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;

// 重连延迟(毫秒)
const reconnectDelay = 1000;

sock.onopen = function() {
  console.log('连接打开');
  isConnected = true;
  reconnectAttempts = 0;
};

sock.onmessage = function(e) {
  console.log('收到消息:', e.data);
};

sock.onclose = function() {
  console.log('连接关闭');
  isConnected = false;
  
  // 尝试重连
  if (reconnectAttempts < maxReconnectAttempts) {
    reconnectAttempts++;
    console.log(`尝试重连 ${reconnectAttempts}/${maxReconnectAttempts}`);
    
    setTimeout(() => {
      sock = new SockJS('http://localhost:3000/echo');
      setupSocketListeners(sock);
    }, reconnectDelay * reconnectAttempts);
  } else {
    console.log('重连失败,已达到最大尝试次数');
  }
};

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

// 设置 Socket 监听器
function setupSocketListeners(socket) {
  socket.onopen = sock.onopen;
  socket.onmessage = sock.onmessage;
  socket.onclose = sock.onclose;
  socket.onerror = sock.onerror;
}

// 发送消息
function sendMessage(message) {
  if (isConnected) {
    sock.send(message);
  } else {
    console.error('连接未打开,无法发送消息');
  }
}

// 关闭连接
function closeConnection() {
  if (sock) {
    sock.close();
  }
}

3.3 跨域支持

SockJS 内置了跨域支持,无需额外配置:

3.3.1 服务器端跨域配置

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer({
  // 允许的来源
  origins: ['*']
});

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  
  conn.on('data', (message) => {
    console.log('收到消息:', message);
    conn.write(message);
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
  });
});

sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

3.3.2 客户端跨域连接

import SockJS from 'sockjs-client';

// 连接到不同域名的服务器
const sock = new SockJS('http://other-domain.com/echo');

sock.onopen = function() {
  console.log('跨域连接打开');
  sock.send('Hello from different domain!');
};

sock.onmessage = function(e) {
  console.log('收到消息:', e.data);
};

sock.onclose = function() {
  console.log('连接关闭');
};

4. 高级特性

4.1 与 Express 集成

SockJS 可以与 Express 框架集成:

import express from 'express';
import http from 'http';
import sockjs from 'sockjs';

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

// 创建 SockJS 服务器
const sockjsServer = sockjs.createServer();

// 监听连接事件
sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  
  conn.on('data', (message) => {
    console.log('收到消息:', message);
    conn.write(message);
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
  });
});

// 挂载 SockJS 服务器
sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

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

// 启动服务器
server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

4.2 认证和授权

在 SockJS 应用中实现认证和授权:

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer();

// 存储已认证的连接
const authenticatedConnections = new Map();

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  
  // 等待认证消息
  conn.on('data', (message) => {
    try {
      const data = JSON.parse(message);
      
      if (data.type === 'authenticate') {
        // 验证 token
        const token = data.token;
        
        // 这里只是示例,实际应用中需要实现真实的验证逻辑
        if (token === 'valid-token') {
          // 认证成功
          const user = {
            id: '1',
            name: 'John Doe'
          };
          
          authenticatedConnections.set(conn.id, user);
          
          // 发送认证成功消息
          conn.write(JSON.stringify({
            type: 'authenticate',
            success: true,
            user
          }));
          
          // 监听后续消息
          conn.on('data', (msg) => {
            handleAuthenticatedMessage(conn, msg);
          });
        } else {
          // 认证失败
          conn.write(JSON.stringify({
            type: 'authenticate',
            success: false,
            error: '无效的 token'
          }));
          conn.close();
        }
      } else {
        // 未认证的连接只能发送认证消息
        conn.write(JSON.stringify({
          type: 'error',
          error: '请先认证'
        }));
      }
    } catch (error) {
      console.error('解析消息错误:', error);
      conn.write(JSON.stringify({
        type: 'error',
        error: '无效的消息格式'
      }));
    }
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
    authenticatedConnections.delete(conn.id);
  });
});

// 处理已认证的消息
function handleAuthenticatedMessage(conn, message) {
  const user = authenticatedConnections.get(conn.id);
  console.log(`收到 ${user.name} 的消息:`, message);
  
  // 处理消息
  conn.write(JSON.stringify({
    type: 'message',
    content: `服务器收到: ${message}`
  }));
}

sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

4.3 消息广播

在 SockJS 应用中实现消息广播:

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer();

// 存储所有连接
const connections = new Set();

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  connections.add(conn);
  
  // 广播新用户加入
  broadcast({
    type: 'system',
    content: `用户 ${conn.id} 加入了聊天`
  }, conn.id);
  
  conn.on('data', (message) => {
    console.log('收到消息:', message);
    
    try {
      const data = JSON.parse(message);
      
      // 广播消息给所有连接
      broadcast({
        type: 'chat',
        user: conn.id,
        content: data.content
      });
    } catch (error) {
      console.error('解析消息错误:', error);
    }
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
    connections.delete(conn);
    
    // 广播用户离开
    broadcast({
      type: 'system',
      content: `用户 ${conn.id} 离开了聊天`
    });
  });
});

// 广播消息给所有连接
function broadcast(message, excludeId = null) {
  const messageStr = JSON.stringify(message);
  
  connections.forEach((conn) => {
    if (conn.id !== excludeId) {
      try {
        conn.write(messageStr);
      } catch (error) {
        console.error('发送消息错误:', error);
      }
    }
  });
}

sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

4.4 性能优化

SockJS 应用的性能优化:

4.4.1 服务器端性能优化

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer({
  // 配置选项
  sockjs_url: 'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js',
  // 禁用不需要的传输方式
  transports: ['websocket', 'xhr-streaming', 'xhr-polling']
});

// 存储连接
const connections = new Map();

// 定期清理无效连接
setInterval(() => {
  connections.forEach((conn) => {
    if (conn.readyState !== 1) {
      connections.delete(conn.id);
    }
  });
  console.log(`当前连接数: ${connections.size}`);
}, 60000); // 每分钟清理一次

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  connections.set(conn.id, conn);
  
  // 限制消息大小
  conn.on('data', (message) => {
    if (message.length > 1024 * 1024) { // 限制为 1MB
      conn.write(JSON.stringify({
        type: 'error',
        error: '消息大小超过限制'
      }));
      return;
    }
    
    console.log('收到消息:', message);
    conn.write(message);
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
    connections.delete(conn.id);
  });
});

sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

4.4.2 客户端性能优化

import SockJS from 'sockjs-client';

// 创建连接
const sock = new SockJS('http://localhost:3000/echo', null, {
  // 配置选项
  transports: ['websocket', 'xhr-streaming', 'xhr-polling'],
  timeout: 5000
});

// 消息队列
const messageQueue = [];
let isProcessingQueue = false;

// 发送消息
function sendMessage(message) {
  if (sock.readyState === SockJS.OPEN) {
    sock.send(message);
  } else {
    // 将消息加入队列
    messageQueue.push(message);
    processMessageQueue();
  }
}

// 处理消息队列
function processMessageQueue() {
  if (isProcessingQueue || messageQueue.length === 0 || sock.readyState !== SockJS.OPEN) {
    return;
  }
  
  isProcessingQueue = true;
  
  const message = messageQueue.shift();
  sock.send(message);
  
  // 延迟处理下一条消息,避免消息积压
  setTimeout(() => {
    isProcessingQueue = false;
    processMessageQueue();
  }, 10);
}

sock.onopen = function() {
  console.log('连接打开');
  // 处理队列中的消息
  processMessageQueue();
};

sock.onmessage = function(e) {
  console.log('收到消息:', e.data);
};

sock.onclose = function() {
  console.log('连接关闭');
};

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

// 批量发送消息
function sendBatchMessages(messages) {
  messages.forEach((message) => {
    sendMessage(JSON.stringify(message));
  });
}

5. 最佳实践

5.1 项目结构

推荐的 SockJS 项目结构:

├── src/
│   ├── server/             # 服务器端代码
│   │   ├── index.js        # 服务器入口
│   │   ├── sockjs.js       # SockJS 配置
│   │   ├── handlers/       # 消息处理器
│   │   ├── auth/           # 认证和授权
│   │   ├── broadcast/      # 广播功能
│   │   └── utils/          # 工具函数
│   ├── client/             # 客户端代码
│   │   ├── index.js        # 客户端入口
│   │   ├── sockjs.js       # SockJS 客户端配置
│   │   ├── components/     # 组件
│   │   └── utils/          # 工具函数
│   └── shared/             # 共享代码
│       └── events.js       # 事件名称
├── package.json
└── .env

5.2 安全性

  • 认证和授权:实现用户认证和权限控制
  • 输入验证:验证所有客户端输入
  • 速率限制:防止消息轰炸
  • 加密通信:在生产环境中使用 HTTPS
  • CORS 配置:正确配置 CORS 选项

5.3 错误处理

  • 客户端错误处理:处理连接错误、消息解析错误等
  • 服务器端错误处理:处理消息处理错误、连接错误等
  • 优雅降级:当 WebSocket 不可用时,自动降级到其他传输方式
  • 重连机制:实现自动重连功能

6. 实用案例

6.1 构建实时聊天应用

6.1.1 服务器端实现

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer();

// 存储在线用户
const onlineUsers = new Map();

// 存储连接
const connections = new Map();

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  connections.set(conn.id, conn);
  
  // 监听消息
  conn.on('data', (message) => {
    try {
      const data = JSON.parse(message);
      
      switch (data.type) {
        case 'join':
          // 用户加入
          handleUserJoin(conn, data.username);
          break;
        case 'chat':
          // 聊天消息
          handleChatMessage(conn, data.content);
          break;
        case 'leave':
          // 用户离开
          handleUserLeave(conn);
          break;
        default:
          console.log('未知消息类型:', data.type);
      }
    } catch (error) {
      console.error('解析消息错误:', error);
      conn.write(JSON.stringify({
        type: 'error',
        error: '无效的消息格式'
      }));
    }
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
    handleUserLeave(conn);
    connections.delete(conn.id);
  });
});

// 处理用户加入
function handleUserJoin(conn, username) {
  const userId = conn.id;
  onlineUsers.set(userId, {
    id: userId,
    name: username
  });
  
  console.log(`${username} 加入了聊天`);
  
  // 发送用户加入消息
  conn.write(JSON.stringify({
    type: 'join',
    success: true,
    user: onlineUsers.get(userId)
  }));
  
  // 广播用户加入
  broadcast({
    type: 'userJoined',
    user: onlineUsers.get(userId)
  }, userId);
  
  // 广播在线用户列表
  broadcast({
    type: 'onlineUsers',
    users: Array.from(onlineUsers.values())
  });
}

// 处理聊天消息
function handleChatMessage(conn, content) {
  const userId = conn.id;
  const user = onlineUsers.get(userId);
  
  if (!user) {
    conn.write(JSON.stringify({
      type: 'error',
      error: '请先加入聊天'
    }));
    return;
  }
  
  console.log(`${user.name}: ${content}`);
  
  // 广播聊天消息
  broadcast({
    type: 'chat',
    user: user,
    content: content,
    timestamp: new Date().toISOString()
  });
}

// 处理用户离开
function handleUserLeave(conn) {
  const userId = conn.id;
  const user = onlineUsers.get(userId);
  
  if (user) {
    console.log(`${user.name} 离开了聊天`);
    onlineUsers.delete(userId);
    
    // 广播用户离开
    broadcast({
      type: 'userLeft',
      user: user
    });
    
    // 广播在线用户列表
    broadcast({
      type: 'onlineUsers',
      users: Array.from(onlineUsers.values())
    });
  }
}

// 广播消息
function broadcast(message, excludeId = null) {
  const messageStr = JSON.stringify(message);
  
  connections.forEach((conn) => {
    if (conn.id !== excludeId) {
      try {
        conn.write(messageStr);
      } catch (error) {
        console.error('发送消息错误:', error);
      }
    }
  });
}

sockjsServer.installHandlers(server, {
  prefix: '/chat'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

6.1.2 客户端实现

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>实时聊天应用</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: Arial, sans-serif;
      line-height: 1.6;
      background-color: #f4f4f4;
    }
    
    .container {
      max-width: 800px;
      margin: 20px auto;
      background-color: #fff;
      border-radius: 8px;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }
    
    .header {
      background-color: #333;
      color: #fff;
      padding: 15px;
      text-align: center;
    }
    
    .chat-container {
      display: flex;
      height: 500px;
    }
    
    .users-list {
      width: 200px;
      background-color: #f9f9f9;
      border-right: 1px solid #ddd;
      padding: 15px;
      overflow-y: auto;
    }
    
    .users-list h3 {
      margin-bottom: 10px;
      color: #333;
    }
    
    .users-list ul {
      list-style: none;
    }
    
    .users-list li {
      padding: 8px;
      margin-bottom: 5px;
      background-color: #e8f4f8;
      border-radius: 4px;
    }
    
    .messages {
      flex: 1;
      padding: 15px;
      overflow-y: auto;
    }
    
    .message {
      margin-bottom: 15px;
      padding: 10px;
      border-radius: 8px;
      max-width: 70%;
    }
    
    .message.own {
      background-color: #e3f2fd;
      align-self: flex-end;
      margin-left: auto;
    }
    
    .message.other {
      background-color: #f1f1f1;
      align-self: flex-start;
    }
    
    .message.system {
      background-color: #e8f5e8;
      align-self: center;
      margin-left: auto;
      margin-right: auto;
      max-width: 90%;
      text-align: center;
    }
    
    .message-info {
      font-size: 12px;
      color: #666;
      margin-bottom: 5px;
    }
    
    .message-text {
      font-size: 14px;
    }
    
    .input-area {
      display: flex;
      padding: 15px;
      border-top: 1px solid #ddd;
    }
    
    .input-area input {
      flex: 1;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      margin-right: 10px;
    }
    
    .input-area button {
      padding: 10px 20px;
      background-color: #333;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    
    .input-area button:hover {
      background-color: #555;
    }
    
    .join-form {
      padding: 20px;
      text-align: center;
    }
    
    .join-form input {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      margin-right: 10px;
    }
    
    .join-form button {
      padding: 10px 20px;
      background-color: #333;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>实时聊天应用</h1>
    </div>
    
    <!-- 加入表单 -->
    <div class="join-form" id="join-form">
      <input type="text" id="username" placeholder="请输入用户名">
      <button id="join-button">加入聊天</button>
    </div>
    
    <!-- 聊天区域 -->
    <div class="chat-container" id="chat-container" style="display: none;">
      <div class="users-list">
        <h3>在线用户</h3>
        <ul id="users-list"></ul>
      </div>
      <div class="messages" id="messages"></div>
    </div>
    
    <!-- 输入区域 -->
    <div class="input-area" id="input-area" style="display: none;">
      <input type="text" id="message-input" placeholder="输入消息...">
      <button id="send-button">发送</button>
    </div>
  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
  <script>
    // 全局变量
    let sock;
    let username;
    let userId;
    
    // DOM 元素
    const joinForm = document.getElementById('join-form');
    const usernameInput = document.getElementById('username');
    const joinButton = document.getElementById('join-button');
    const chatContainer = document.getElementById('chat-container');
    const usersList = document.getElementById('users-list');
    const messagesContainer = document.getElementById('messages');
    const inputArea = document.getElementById('input-area');
    const messageInput = document.getElementById('message-input');
    const sendButton = document.getElementById('send-button');
    
    // 加入聊天
    function joinChat() {
      username = usernameInput.value.trim();
      
      if (!username) {
        alert('请输入用户名');
        return;
      }
      
      // 创建 SockJS 连接
      sock = new SockJS('http://localhost:3000/chat');
      
      // 监听连接打开
      sock.onopen = function() {
        console.log('连接打开');
        // 发送加入消息
        sock.send(JSON.stringify({
          type: 'join',
          username: username
        }));
      };
      
      // 监听消息
      sock.onmessage = function(e) {
        console.log('收到消息:', e.data);
        handleMessage(JSON.parse(e.data));
      };
      
      // 监听连接关闭
      sock.onclose = function() {
        console.log('连接关闭');
        showSystemMessage('连接已关闭');
      };
      
      // 监听错误
      sock.onerror = function(error) {
        console.error('连接错误:', error);
        showSystemMessage('连接错误,请刷新页面重试');
      };
    }
    
    // 处理消息
    function handleMessage(message) {
      switch (message.type) {
        case 'join':
          if (message.success) {
            userId = message.user.id;
            // 显示聊天界面
            joinForm.style.display = 'none';
            chatContainer.style.display = 'flex';
            inputArea.style.display = 'flex';
            showSystemMessage('加入聊天成功');
          } else {
            showSystemMessage('加入聊天失败: ' + message.error);
          }
          break;
        case 'chat':
          showChatMessage(message);
          break;
        case 'userJoined':
          showSystemMessage(`${message.user.name} 加入了聊天`);
          break;
        case 'userLeft':
          showSystemMessage(`${message.user.name} 离开了聊天`);
          break;
        case 'onlineUsers':
          updateUsersList(message.users);
          break;
        case 'error':
          showSystemMessage('错误: ' + message.error);
          break;
        default:
          console.log('未知消息类型:', message.type);
      }
    }
    
    // 显示聊天消息
    function showChatMessage(message) {
      const messageElement = document.createElement('div');
      messageElement.className = `message ${message.user.id === userId ? 'own' : 'other'}`;
      
      const messageInfo = document.createElement('div');
      messageInfo.className = 'message-info';
      messageInfo.textContent = `${message.user.name} · ${new Date(message.timestamp).toLocaleTimeString()}`;
      
      const messageText = document.createElement('div');
      messageText.className = 'message-text';
      messageText.textContent = message.content;
      
      messageElement.appendChild(messageInfo);
      messageElement.appendChild(messageText);
      messagesContainer.appendChild(messageElement);
      
      // 滚动到底部
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }
    
    // 显示系统消息
    function showSystemMessage(content) {
      const messageElement = document.createElement('div');
      messageElement.className = 'message system';
      messageElement.textContent = content;
      messagesContainer.appendChild(messageElement);
      
      // 滚动到底部
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }
    
    // 更新用户列表
    function updateUsersList(users) {
      usersList.innerHTML = '';
      users.forEach(user => {
        const userElement = document.createElement('li');
        userElement.textContent = user.name;
        if (user.id === userId) {
          userElement.style.fontWeight = 'bold';
          userElement.style.backgroundColor = '#c3e6cb';
        }
        usersList.appendChild(userElement);
      });
    }
    
    // 发送消息
    function sendMessage() {
      const content = messageInput.value.trim();
      
      if (!content) {
        return;
      }
      
      // 发送消息
      sock.send(JSON.stringify({
        type: 'chat',
        content: content
      }));
      
      // 清空输入框
      messageInput.value = '';
    }
    
    // 事件监听器
    joinButton.addEventListener('click', joinChat);
    
    usernameInput.addEventListener('keypress', function(e) {
      if (e.key === 'Enter') {
        joinChat();
      }
    });
    
    sendButton.addEventListener('click', sendMessage);
    
    messageInput.addEventListener('keypress', function(e) {
      if (e.key === 'Enter') {
        sendMessage();
      }
    });
  </script>
</body>
</html>

6.2 与 React 集成

SockJS 可以与 React 框架集成:

6.2.1 服务器端实现

import http from 'http';
import sockjs from 'sockjs';

const server = http.createServer();
const sockjsServer = sockjs.createServer();

// 存储连接
const connections = new Map();

sockjsServer.on('connection', (conn) => {
  console.log('新连接:', conn.id);
  connections.set(conn.id, conn);
  
  conn.on('data', (message) => {
    console.log('收到消息:', message);
    
    try {
      const data = JSON.parse(message);
      
      // 广播消息
      broadcast({
        type: 'message',
        id: conn.id,
        content: data.content,
        timestamp: new Date().toISOString()
      });
    } catch (error) {
      console.error('解析消息错误:', error);
    }
  });
  
  conn.on('close', () => {
    console.log('连接关闭:', conn.id);
    connections.delete(conn.id);
  });
});

// 广播消息
function broadcast(message) {
  const messageStr = JSON.stringify(message);
  
  connections.forEach((conn) => {
    try {
      conn.write(messageStr);
    } catch (error) {
      console.error('发送消息错误:', error);
    }
  });
}

sockjsServer.installHandlers(server, {
  prefix: '/echo'
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

6.2.2 React 客户端实现

import React, { useState, useEffect, useRef } from 'react';
import SockJS from 'sockjs-client';

function App() {
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const sockRef = useRef(null);
  const messagesEndRef = useRef(null);
  
  // 初始化 SockJS 连接
  useEffect(() => {
    sockRef.current = new SockJS('http://localhost:3000/echo');
    
    sockRef.current.onopen = function() {
      console.log('连接打开');
    };
    
    sockRef.current.onmessage = function(e) {
      console.log('收到消息:', e.data);
      const message = JSON.parse(e.data);
      setMessages(prevMessages => [...prevMessages, message]);
    };
    
    sockRef.current.onclose = function() {
      console.log('连接关闭');
    };
    
    // 清理函数
    return () => {
      if (sockRef.current) {
        sockRef.current.close();
      }
    };
  }, []);
  
  // 滚动到底部
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  // 发送消息
  const sendMessage = () => {
    if (inputValue.trim() && sockRef.current) {
      sockRef.current.send(JSON.stringify({
        content: inputValue
      }));
      setInputValue('');
    }
  };
  
  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <h1>React SockJS 聊天</h1>
      
      <div 
        style={{
          height: '400px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          padding: '10px',
          overflowY: 'auto',
          marginBottom: '20px'
        }}
      >
        {messages.map((message, index) => (
          <div 
            key={index} 
            style={{
              padding: '10px',
              marginBottom: '10px',
              backgroundColor: '#f1f1f1',
              borderRadius: '8px'
            }}
          >
            <div style={{ fontSize: '12px', color: '#666' }}>
              {new Date(message.timestamp).toLocaleTimeString()}
            </div>
            <div>{message.content}</div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      
      <div style={{ display: 'flex' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="输入消息..."
          style={{
            flex: 1,
            padding: '10px',
            border: '1px solid #ddd',
            borderRadius: '4px',
            marginRight: '10px'
          }}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
        />
        <button
          onClick={sendMessage}
          style={{
            padding: '10px 20px',
            backgroundColor: '#333',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          发送
        </button>
      </div>
    </div>
  );
}

export default App;

6. 总结

SockJS 是一个功能强大、兼容性好的实时通信库,为开发者提供了构建实时应用的完整解决方案。通过本教程的学习,你应该已经掌握了:

  • SockJS 的核心概念和基本原理
  • 如何安装和配置 SockJS
  • 如何使用 SockJS 的基本功能,如连接管理、消息发送和接收
  • 如何使用 SockJS 的高级特性,如与 Express 集成、认证和授权、广播功能
  • 最佳实践和实用案例

SockJS 适用于各种实时应用场景,如聊天应用、实时游戏、实时协作工具等。它提供了类似 WebSocket 的 API,同时支持那些不支持 WebSocket 的浏览器,是构建实时应用的理想选择。

无论是构建小型应用还是大型企业系统,SockJS 都能满足你的实时通信需求。

7. 参考资料

« 上一篇 Socket.io 中文教程 下一篇 » Primus 中文教程