Socket.io 简介

Socket.io 是一个实时应用框架,基于 WebSocket 的实时双向通信库,它允许服务器和客户端之间建立持久的连接,实现实时数据传输。Socket.io 不仅支持 WebSocket,还提供了自动降级机制,当 WebSocket 不可用时,会自动切换到其他传输方式,如 HTTP 长轮询,确保在各种环境下都能正常工作。

核心特点

  • 实时双向通信:服务器和客户端之间可以实时发送和接收数据。
  • 事件系统:基于事件的通信模型,使用 emiton 方法发送和接收事件。
  • 房间机制:支持将客户端分组到不同的房间,实现定向消息发送。
  • 命名空间:支持创建多个命名空间,实现逻辑上的隔离。
  • 自动降级:当 WebSocket 不可用时,自动切换到 HTTP 长轮询等其他传输方式。
  • 跨浏览器兼容:支持所有现代浏览器,包括移动设备。
  • 可扩展:提供了丰富的 API 和中间件机制,便于扩展功能。
  • 可靠性:自动重连机制,确保连接的稳定性。

安装与配置

安装 Socket.io

使用 npm 或 yarn 安装 Socket.io:

# 使用 npm 安装
npm install socket.io

# 使用 yarn 安装
yarn add socket.io

创建第一个 Socket.io 应用

创建一个简单的 Socket.io 应用,实现服务器和客户端之间的实时通信:

服务器端代码

// server.js
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);

// 提供静态文件
app.use(express.static('public'));

// 监听连接事件
io.on('connection', (socket) => {
  console.log('A user connected');

  // 监听客户端发送的消息
  socket.on('chat message', (msg) => {
    console.log('Message:', msg);
    // 广播消息给所有客户端
    io.emit('chat message', msg);
  });

  // 监听断开连接事件
  socket.on('disconnect', () => {
    console.log('User disconnected');
  });
});

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

客户端代码

创建 public/index.html 文件:

<!DOCTYPE html>
<html>
<head>
  <title>Socket.io Chat</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: Arial, sans-serif; }
    .container { max-width: 600px; margin: 20px auto; }
    form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; max-width: 600px; }
    form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
    form button { width: 9%; background: #828282; border: none; padding: 10px; }
    #messages { list-style-type: none; margin: 0; padding: 0; }
    #messages li { padding: 5px 10px; }
    #messages li:nth-child(odd) { background: #eee; }
  </style>
</head>
<body>
  <div class="container">
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>
  </div>
  <script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
  <script>
    // 连接到服务器
    const socket = io();

    // 监听服务器发送的消息
    socket.on('chat message', (msg) => {
      const item = document.createElement('li');
      item.textContent = msg;
      document.getElementById('messages').appendChild(item);
      window.scrollTo(0, document.body.scrollHeight);
    });

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

运行应用:

node server.js

然后在浏览器中访问 http://localhost:3000,打开多个标签页,就可以看到实时聊天效果。

核心概念

连接

当客户端连接到服务器时,会创建一个 Socket 实例,代表服务器和客户端之间的连接。

// 服务器端
io.on('connection', (socket) => {
  console.log('A user connected');
  
  // 断开连接时触发
  socket.on('disconnect', () => {
    console.log('User disconnected');
  });
});

事件

Socket.io 使用事件系统进行通信,客户端和服务器都可以发送和接收事件。

发送事件

// 服务器端发送事件
socket.emit('event name', data);

// 客户端发送事件
socket.emit('event name', data);

接收事件

// 服务器端接收事件
socket.on('event name', (data) => {
  console.log(data);
});

// 客户端接收事件
socket.on('event name', (data) => {
  console.log(data);
});

房间

房间是 Socket.io 中的一个重要概念,它允许将客户端分组,实现定向消息发送。

加入房间

// 服务器端
io.on('connection', (socket) => {
  // 加入房间
  socket.join('room1');
  
  // 向房间内所有客户端发送消息
  io.to('room1').emit('message', 'Welcome to room1!');
  
  // 向房间内除了当前客户端以外的所有客户端发送消息
  socket.to('room1').emit('message', 'A new user joined room1!');
});

离开房间

// 服务器端
io.on('connection', (socket) => {
  // 加入房间
  socket.join('room1');
  
  // 离开房间
  socket.leave('room1');
});

命名空间

命名空间允许创建多个独立的通信通道,实现逻辑上的隔离。

创建命名空间

// 服务器端
const io = new Server(server);

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

// 监听命名空间的连接事件
adminNamespace.on('connection', (socket) => {
  console.log('Admin connected');
});

userNamespace.on('connection', (socket) => {
  console.log('User connected');
});

客户端连接命名空间

// 客户端连接默认命名空间
const socket = io();

// 客户端连接指定命名空间
const adminSocket = io('/admin');
const userSocket = io('/user');

实用案例分析

构建实时聊天应用

下面是一个使用 Socket.io 构建的实时聊天应用,实现了基本的聊天功能,包括消息发送、房间管理等。

项目结构

├── server.js
├── public/
│   ├── index.html
│   └── style.css
└── package.json

代码实现

  1. 服务器端代码 (server.js):
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);

// 提供静态文件
app.use(express.static('public'));

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

// 监听连接事件
io.on('connection', (socket) => {
  console.log('A user connected:', socket.id);

  // 监听用户登录
  socket.on('login', (username) => {
    // 存储用户信息
    onlineUsers.set(socket.id, username);
    // 加入默认房间
    socket.join('general');
    // 广播用户加入消息
    io.emit('user joined', {
      username,
      message: `${username} joined the chat`
    });
    // 发送在线用户列表
    io.emit('online users', Array.from(onlineUsers.values()));
  });

  // 监听发送消息
  socket.on('chat message', (data) => {
    const username = onlineUsers.get(socket.id);
    // 发送消息到指定房间
    io.to(data.room).emit('chat message', {
      username,
      message: data.message,
      room: data.room
    });
  });

  // 监听加入房间
  socket.on('join room', (room) => {
    const username = onlineUsers.get(socket.id);
    socket.join(room);
    io.to(room).emit('user joined room', {
      username,
      room,
      message: `${username} joined ${room} room`
    });
  });

  // 监听离开房间
  socket.on('leave room', (room) => {
    const username = onlineUsers.get(socket.id);
    socket.leave(room);
    io.to(room).emit('user left room', {
      username,
      room,
      message: `${username} left ${room} room`
    });
  });

  // 监听断开连接
  socket.on('disconnect', () => {
    const username = onlineUsers.get(socket.id);
    if (username) {
      onlineUsers.delete(socket.id);
      // 广播用户离开消息
      io.emit('user left', {
        username,
        message: `${username} left the chat`
      });
      // 发送在线用户列表
      io.emit('online users', Array.from(onlineUsers.values()));
    }
    console.log('User disconnected:', socket.id);
  });
});

// 启动服务器
server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});
  1. 客户端代码 (public/index.html):
<!DOCTYPE html>
<html>
<head>
  <title>Socket.io Chat App</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="chat-container">
    <!-- 登录界面 -->
    <div id="login-container">
      <h2>Chat App</h2>
      <input type="text" id="username" placeholder="Enter your username" autocomplete="off">
      <button id="login-btn">Login</button>
    </div>

    <!-- 聊天界面 -->
    <div id="chat-container" style="display: none;">
      <!-- 侧边栏 -->
      <div class="sidebar">
        <h3>Rooms</h3>
        <ul id="rooms">
          <li class="room-item active" data-room="general">General</li>
          <li class="room-item" data-room="tech">Tech</li>
          <li class="room-item" data-room="gaming">Gaming</li>
          <li class="room-item" data-room="music">Music</li>
        </ul>
        <h3>Online Users</h3>
        <ul id="online-users"></ul>
      </div>

      <!-- 聊天区域 -->
      <div class="chat-area">
        <div class="chat-header">
          <h3 id="current-room">General</h3>
        </div>
        <div class="messages" id="messages"></div>
        <div class="input-area">
          <input type="text" id="message-input" placeholder="Type your message..." autocomplete="off">
          <button id="send-btn">Send</button>
        </div>
      </div>
    </div>
  </div>

  <script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
  <script>
    const socket = io();
    let currentRoom = 'general';
    let username = '';

    // 登录
    document.getElementById('login-btn').addEventListener('click', () => {
      username = document.getElementById('username').value.trim();
      if (username) {
        socket.emit('login', username);
        document.getElementById('login-container').style.display = 'none';
        document.getElementById('chat-container').style.display = 'flex';
      }
    });

    // 监听用户加入消息
    socket.on('user joined', (data) => {
      addMessage(data.username, data.message, 'system');
    });

    // 监听用户离开消息
    socket.on('user left', (data) => {
      addMessage(data.username, data.message, 'system');
    });

    // 监听用户加入房间消息
    socket.on('user joined room', (data) => {
      addMessage(data.username, data.message, 'system');
    });

    // 监听用户离开房间消息
    socket.on('user left room', (data) => {
      addMessage(data.username, data.message, 'system');
    });

    // 监听聊天消息
    socket.on('chat message', (data) => {
      if (data.room === currentRoom) {
        addMessage(data.username, data.message, 'chat');
      }
    });

    // 监听在线用户列表
    socket.on('online users', (users) => {
      const usersList = document.getElementById('online-users');
      usersList.innerHTML = '';
      users.forEach(user => {
        const li = document.createElement('li');
        li.textContent = user;
        usersList.appendChild(li);
      });
    });

    // 发送消息
    document.getElementById('send-btn').addEventListener('click', sendMessage);
    document.getElementById('message-input').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        sendMessage();
      }
    });

    function sendMessage() {
      const message = document.getElementById('message-input').value.trim();
      if (message) {
        socket.emit('chat message', {
          message,
          room: currentRoom
        });
        document.getElementById('message-input').value = '';
      }
    }

    // 切换房间
    document.querySelectorAll('.room-item').forEach(item => {
      item.addEventListener('click', () => {
        const room = item.dataset.room;
        // 移除所有房间的活跃状态
        document.querySelectorAll('.room-item').forEach(i => i.classList.remove('active'));
        // 添加当前房间的活跃状态
        item.classList.add('active');
        // 更新当前房间
        currentRoom = room;
        document.getElementById('current-room').textContent = room;
        // 清空消息列表
        document.getElementById('messages').innerHTML = '';
        // 加入房间
        socket.emit('join room', room);
      });
    });

    // 添加消息到界面
    function addMessage(username, message, type) {
      const messagesContainer = document.getElementById('messages');
      const messageDiv = document.createElement('div');
      messageDiv.className = `message ${type}`;
      if (type === 'chat') {
        messageDiv.innerHTML = `<strong>${username}:</strong> ${message}`;
      } else {
        messageDiv.innerHTML = `<em>${message}</em>`;
      }
      messagesContainer.appendChild(messageDiv);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }
  </script>
</body>
</html>
  1. 样式文件 (public/style.css):
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: Arial, sans-serif;
  background-color: #f0f0f0;
}

.chat-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}

#login-container {
  background-color: white;
  padding: 40px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  text-align: center;
}

#login-container h2 {
  margin-bottom: 20px;
  color: #333;
}

#login-container input {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

#login-container button {
  width: 100%;
  padding: 10px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

#login-container button:hover {
  background-color: #2980b9;
}

#chat-container {
  display: flex;
  width: 800px;
  height: 600px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.sidebar {
  width: 200px;
  background-color: #f5f5f5;
  padding: 20px;
  border-right: 1px solid #ddd;
}

.sidebar h3 {
  margin-bottom: 10px;
  color: #333;
  font-size: 14px;
}

.sidebar ul {
  list-style: none;
  margin-bottom: 20px;
}

.sidebar li {
  padding: 8px;
  margin-bottom: 5px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.sidebar li:hover {
  background-color: #e0e0e0;
}

.sidebar li.active {
  background-color: #3498db;
  color: white;
}

.chat-area {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.chat-header {
  padding: 20px;
  background-color: #f5f5f5;
  border-bottom: 1px solid #ddd;
}

.chat-header h3 {
  color: #333;
}

.messages {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}

.message {
  margin-bottom: 10px;
  padding: 10px;
  border-radius: 4px;
}

.message.chat {
  background-color: #f0f8ff;
}

.message.system {
  background-color: #f8f8f8;
  font-style: italic;
  color: #666;
}

.input-area {
  padding: 20px;
  border-top: 1px solid #ddd;
  display: flex;
}

.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: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.input-area button:hover {
  background-color: #2980b9;
}

测试应用

  1. 运行服务器:
node server.js
  1. 在浏览器中访问 http://localhost:3000,输入用户名登录。

  2. 打开多个浏览器标签页,使用不同的用户名登录,测试实时聊天功能。

  3. 尝试切换不同的房间,发送和接收消息,查看在线用户列表。

实时数据可视化

Socket.io 也可以用于实时数据可视化,如实时监控、实时仪表盘等。下面是一个简单的实时数据可视化示例:

项目结构

├── server.js
├── public/
│   ├── index.html
│   └── script.js
└── package.json

代码实现

  1. 服务器端代码 (server.js):
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);

// 提供静态文件
app.use(express.static('public'));

// 模拟实时数据
setInterval(() => {
  const data = {
    timestamp: new Date().toISOString(),
    value: Math.random() * 100
  };
  // 广播数据给所有客户端
  io.emit('data', data);
}, 1000);

// 启动服务器
server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});
  1. 客户端代码 (public/index.html):
<!DOCTYPE html>
<html>
<head>
  <title>Real-time Data Visualization</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
  <div style="width: 800px; margin: 20px auto;">
    <h2>Real-time Data Visualization</h2>
    <canvas id="chart"></canvas>
  </div>
  <script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
  <script src="script.js"></script>
</body>
</html>
  1. 客户端脚本 (public/script.js):
const socket = io();

// 初始化图表
const ctx = document.getElementById('chart').getContext('2d');
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: [],
    datasets: [{
      label: 'Real-time Data',
      data: [],
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.1
    }]
  },
  options: {
    responsive: true,
    animation: {
      duration: 0
    },
    scales: {
      y: {
        beginAtZero: true,
        max: 100
      }
    }
  }
});

// 存储数据点
const dataPoints = 20;
const labels = [];
const data = [];

// 监听数据
socket.on('data', (newData) => {
  // 添加新数据点
  const time = new Date(newData.timestamp).toLocaleTimeString();
  labels.push(time);
  data.push(newData.value);
  
  // 保持数据点数量
  if (labels.length > dataPoints) {
    labels.shift();
    data.shift();
  }
  
  // 更新图表
  chart.data.labels = labels;
  chart.data.datasets[0].data = data;
  chart.update();
});

测试应用

  1. 运行服务器:
node server.js
  1. 在浏览器中访问 http://localhost:3000,可以看到实时更新的图表。

性能优化

1. 使用命名空间和房间

合理使用命名空间和房间可以减少不必要的消息传递,提高性能:

// 使用命名空间隔离不同类型的通信
const adminNamespace = io.of('/admin');
const userNamespace = io.of('/user');

// 使用房间定向发送消息
io.to('room1').emit('message', 'Hello room1!');

2. 减少消息大小

尽量减少消息的大小,避免发送不必要的数据:

// 不推荐:发送完整对象
io.emit('user update', {
  id: user.id,
  name: user.name,
  email: user.email,
  age: user.age,
  // 其他大量字段
});

// 推荐:只发送必要的字段
io.emit('user update', {
  id: user.id,
  name: user.name
});

3. 使用压缩

Socket.io 支持消息压缩,可以减少网络传输量:

const io = new Server(server, {
  perMessageDeflate: true // 启用消息压缩
});

4. 限制连接数

根据服务器的资源情况,合理限制同时连接的客户端数量:

const io = new Server(server, {
  maxHttpBufferSize: 1e6, // 限制消息大小
  pingTimeout: 60000, // 心跳超时
  pingInterval: 25000 // 心跳间隔
});

// 监控连接数
let connectionCount = 0;
io.on('connection', (socket) => {
  connectionCount++;
  console.log(`Connection count: ${connectionCount}`);
  
  socket.on('disconnect', () => {
    connectionCount--;
    console.log(`Connection count: ${connectionCount}`);
  });
});

5. 使用 Redis 适配器

对于多个 Socket.io 服务器实例的场景,可以使用 Redis 适配器实现消息共享:

# 安装 Redis 适配器
npm install socket.io-redis
const { Server } = require('socket.io');
const { createAdapter } = require('socket.io-redis');
const io = new Server(server);

// 使用 Redis 适配器
io.adapter(createAdapter({
  host: 'localhost',
  port: 6379
}));

总结

Socket.io 是一个强大的实时通信库,它基于 WebSocket 提供了实时双向通信能力,同时支持自动降级机制,确保在各种环境下都能正常工作。Socket.io 的核心特性包括事件系统、房间机制、命名空间等,这些特性使得它非常适合构建实时应用,如聊天应用、实时游戏、实时监控等。

通过本教程,你应该已经了解了 Socket.io 的核心概念和基本用法,包括连接管理、事件处理、房间操作、命名空间使用等,以及如何使用 Socket.io 构建实时聊天应用和实时数据可视化应用。你还学习了一些性能优化的方法,如使用命名空间和房间、减少消息大小、启用压缩等。

Socket.io 的易用性和强大功能使其成为构建实时应用的理想选择,它的生态系统也非常丰富,有大量的插件和扩展可供使用。要深入学习 Socket.io,建议查阅 官方文档 和实践更多的项目案例,以掌握其高级特性和最佳实践。

« 上一篇 Koa 教程 下一篇 » Prisma 教程