在前六篇文章中,我们深入探讨了 WebSocket 的基础原理、服务端开发、客户端实现、安全实践、性能优化和测试调试。今天,让我们通过一个实战案例,看看如何将这些知识应用到实际项目中。我曾在一个大型在线教育平台中,通过 WebSocket 实现了实时互动课堂,支持了数万名师生的同时在线。
项目背景
我们要实现一个实时互动课堂系统,主要功能包括:
- 实时音视频
- 课堂互动
- 共享白板
- 实时聊天
- 课堂管理
让我们从系统设计开始。
系统架构
实现系统架构:
// app.js
const express = require('express')
const https = require('https')
const fs = require('fs')
const path = require('path')
const WebSocket = require('ws')
const Redis = require('ioredis')
const { ClusterManager } = require('./cluster-manager')
const { ConnectionPool } = require('./connection-pool')
const { MessageQueue } = require('./message-queue')
const { RoomManager } = require('./room-manager')
const { UserManager } = require('./user-manager')
const { MediaServer } = require('./media-server')
class ClassroomServer {
constructor(options = {}) {
this.options = {
port: 8080,
sslPort: 8443,
redisUrl: 'redis://localhost:6379',
mediaServer: 'localhost:8000',
...options
}
// 初始化组件
this.cluster = new ClusterManager()
this.pool = new ConnectionPool()
this.queue = new MessageQueue()
this.rooms = new RoomManager()
this.users = new UserManager()
this.media = new MediaServer(this.options.mediaServer)
this.initialize()
}
// 初始化服务器
async initialize() {
// 创建 Express 应用
this.app = express()
this.setupExpress()
// 创建 HTTPS 服务器
this.server = https.createServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert')
}, this.app)
// 创建 WebSocket 服务器
this.wss = new WebSocket.Server({
server: this.server,
path: '/ws'
})
// 连接 Redis
this.redis = new Redis(this.options.redisUrl)
// 设置事件处理器
this.setupEventHandlers()
// 启动服务器
await this.start()
}
// 设置 Express
setupExpress() {
// 静态文件
this.app.use(express.static('public'))
// API 路���
this.app.use('/api', require('./routes/api'))
// 错误处理
this.app.use((err, req, res, next) => {
console.error('Express error:', err)
res.status(500).json({ error: 'Internal server error' })
})
}
// 设置事件处理器
setupEventHandlers() {
// WebSocket 连接
this.wss.on('connection', (ws, req) => {
this.handleConnection(ws, req)
})
// Redis 订阅
this.redis.on('message', (channel, message) => {
this.handleRedisMessage(channel, message)
})
// 进程消息
process.on('message', (message) => {
this.handleProcessMessage(message)
})
}
// 处理 WebSocket 连接
async handleConnection(ws, req) {
try {
// 验证用户
const user = await this.users.authenticate(req)
// 创建连接
const connection = this.pool.createConnection(ws, user)
// 加入房间
const roomId = req.query.roomId
if (roomId) {
await this.rooms.joinRoom(roomId, connection)
}
// 设置消息处理器
ws.on('message', (message) => {
this.handleMessage(connection, message)
})
// 设置关闭处理器
ws.on('close', () => {
this.handleClose(connection)
})
// 发送欢迎消息
connection.send({
type: 'welcome',
data: {
user: user.toJSON(),
room: roomId ? await this.rooms.getRoomInfo(roomId) : null
}
})
} catch (error) {
console.error('Connection error:', error)
ws.close()
}
}
// 处理消息
async handleMessage(connection, message) {
try {
const data = JSON.parse(message)
// 验证消息
if (!this.validateMessage(data)) {
throw new Error('Invalid message format')
}
// 处理不同类型的消息
switch (data.type) {
case 'chat':
await this.handleChatMessage(connection, data)
break
case 'whiteboard':
await this.handleWhiteboardMessage(connection, data)
break
case 'media':
await this.handleMediaMessage(connection, data)
break
case 'control':
await this.handleControlMessage(connection, data)
break
default:
throw new Error('Unknown message type')
}
} catch (error) {
console.error('Message error:', error)
connection.send({
type: 'error',
error: error.message
})
}
}
// 处理聊天消息
async handleChatMessage(connection, message) {
const { roomId, content } = message.data
// 验证权限
if (!await this.rooms.canChat(connection.user, roomId)) {
throw new Error('No permission to chat')
}
// 创建聊天消息
const chat = {
id: generateId(),
roomId,
userId: connection.user.id,
content,
timestamp: Date.now()
}
// 保存到数据库
await this.rooms.saveChatMessage(chat)
// 广播到房间
await this.rooms.broadcast(roomId, {
type: 'chat',
data: chat
})
}
// 处理白板消息
async handleWhiteboardMessage(connection, message) {
const { roomId, action } = message.data
// 验证权限
if (!await this.rooms.canDraw(connection.user, roomId)) {
throw new Error('No permission to draw')
}
// 处理白板动作
const result = await this.rooms.handleWhiteboardAction(roomId, action)
// 广播到房间
await this.rooms.broadcast(roomId, {
type: 'whiteboard',
data: {
action,
result
}
})
}
// 处理媒体消息
async handleMediaMessage(connection, message) {
const { roomId, stream } = message.data
// 验证权限
if (!await this.rooms.canPublish(connection.user, roomId)) {
throw new Error('No permission to publish')
}
// 处理媒体流
const result = await this.media.handleStream(roomId, stream)
// 广播到房间
await this.rooms.broadcast(roomId, {
type: 'media',
data: {
stream,
result
}
})
}
// 处理控制消息
async handleControlMessage(connection, message) {
const { roomId, action } = message.data
// 验证权限
if (!await this.rooms.canControl(connection.user, roomId)) {
throw new Error('No permission to control')
}
// 处理控制命令
const result = await this.rooms.handleControlAction(roomId, action)
// 广播到房间
await this.rooms.broadcast(roomId, {
type: 'control',
data: {
action,
result
}
})
}
// 处理连接关闭
async handleClose(connection) {
try {
// 离开房间
const roomId = connection.roomId
if (roomId) {
await this.rooms.leaveRoom(roomId, connection)
}
// 清理连接
this.pool.removeConnection(connection)
// 广播离开消息
if (roomId) {
await this.rooms.broadcast(roomId, {
type: 'user_left',
data: {
userId: connection.user.id
}
})
}
} catch (error) {
console.error('Close error:', error)
}
}
// 处理 Redis 消息
handleRedisMessage(channel, message) {
try {
const data = JSON.parse(message)
// 处理不同类型的消息
switch (channel) {
case 'room_update':
this.handleRoomUpdate(data)
break
case 'user_update':
this.handleUserUpdate(data)
break
case 'system_update':
this.handleSystemUpdate(data)
break
}
} catch (error) {
console.error('Redis message error:', error)
}
}
// 处理进程消息
handleProcessMessage(message) {
try {
// 处理不同类型的消息
switch (message.type) {
case 'status':
this.handleStatusUpdate(message.data)
break
case 'reload':
this.handleReload(message.data)
break
case 'shutdown':
this.handleShutdown(message.data)
break
}
} catch (error) {
console.error('Process message error:', error)
}
}
// 启动服务器
async start() {
// 启动 HTTP 服务器
this.server.listen(this.options.sslPort, () => {
console.log(`HTTPS server running on port ${this.options.sslPort}`)
})
// 启动 HTTP 重定向
const redirectServer = express()
.use((req, res) => {
res.redirect(`https://${req.headers.host}${req.url}`)
})
.listen(this.options.port, () => {
console.log(`HTTP redirect server running on port ${this.options.port}`)
})
}
// 关闭服务器
async shutdown() {
console.log('Shutting down classroom server...')
// 关闭 WebSocket 服务器
this.wss.close()
// 关闭 HTTP 服务器
this.server.close()
// 关闭 Redis 连接
await this.redis.quit()
// 清理资源
await this.pool.shutdown()
await this.queue.shutdown()
await this.rooms.shutdown()
await this.media.shutdown()
console.log('Classroom server shutdown complete')
}
}
房间管理
实现房间管理:
// room-manager.js
class RoomManager {
constructor(options = {}) {
this.options = {
maxRooms: 1000,
maxUsersPerRoom: 100,
...options
}
this.rooms = new Map()
this.stats = new Stats()
this.initialize()
}
// 初始化房间管理器
initialize() {
// 监控房间数
this.stats.gauge('rooms.total', () => this.rooms.size)
this.stats.gauge('rooms.active', () => this.getActiveRooms().size)
}
// 创建房间
async createRoom(options) {
// 检查房间数限制
if (this.rooms.size >= this.options.maxRooms) {
throw new Error('Room limit reached')
}
// 创建房间
const room = {
id: generateId(),
name: options.name,
type: options.type,
createdAt: Date.now(),
users: new Map(),
state: {
whiteboard: [],
chat: [],
media: []
},
...options
}
this.rooms.set(room.id, room)
this.stats.increment('rooms.created')
return room
}
// 加入房间
async joinRoom(roomId, connection) {
const room = this.rooms.get(roomId)
if (!room) {
throw new Error('Room not found')
}
// 检查人数限制
if (room.users.size >= this.options.maxUsersPerRoom) {
throw new Error('Room is full')
}
// 添加用户
room.users.set(connection.user.id, {
connection,
joinedAt: Date.now(),
state: {}
})
// 更新连接
connection.roomId = roomId
this.stats.increment('room.users.joined')
// 广播加入消息
await this.broadcast(roomId, {
type: 'user_joined',
data: {
user: connection.user.toJSON()
}
})
return room
}
// 离开房间
async leaveRoom(roomId, connection) {
const room = this.rooms.get(roomId)
if (!room) return
// 移除用户
room.users.delete(connection.user.id)
// 更新连接
delete connection.roomId
this.stats.increment('room.users.left')
// 如果房间为空,清理房间
if (room.users.size === 0) {
await this.cleanupRoom(roomId)
}
}
// 广播消息
async broadcast(roomId, message, excludeId = null) {
const room = this.rooms.get(roomId)
if (!room) return 0
let count = 0
room.users.forEach((user, userId) => {
if (userId !== excludeId) {
try {
user.connection.send(message)
count++
} catch (error) {
console.error('Broadcast error:', error)
}
}
})
this.stats.increment('room.messages.broadcast', count)
return count
}
// 获取房间信息
async getRoomInfo(roomId) {
const room = this.rooms.get(roomId)
if (!room) {
throw new Error('Room not found')
}
return {
id: room.id,
name: room.name,
type: room.type,
users: Array.from(room.users.values()).map(user => ({
id: user.connection.user.id,
name: user.connection.user.name,
role: user.connection.user.role,
joinedAt: user.joinedAt
})),
state: room.state
}
}
// 更新房间状态
async updateRoomState(roomId, update) {
const room = this.rooms.get(roomId)
if (!room) {
throw new Error('Room not found')
}
// 更新状态
room.state = {
...room.state,
...update
}
// 广播更新
await this.broadcast(roomId, {
type: 'room_state_updated',
data: {
state: room.state
}
})
return room.state
}
// 清理房间
async cleanupRoom(roomId) {
const room = this.rooms.get(roomId)
if (!room) return
// 保存房间数据
await this.saveRoomData(room)
// 删除房间
this.rooms.delete(roomId)
this.stats.increment('rooms.cleaned')
}
// 保存房间数据
async saveRoomData(room) {
// 实现数据持久化逻辑
}
// 获取活跃房间
getActiveRooms() {
const activeRooms = new Map()
this.rooms.forEach((room, id) => {
if (room.users.size > 0) {
activeRooms.set(id, room)
}
})
return activeRooms
}
// 获取统计信息
getStats() {
return {
rooms: {
total: this.rooms.size,
active: this.getActiveRooms().size
},
...this.stats.getAll()
}
}
// 关闭管理器
async shutdown() {
// 保存所有房间数据
for (const room of this.rooms.values()) {
await this.saveRoomData(room)
}
// 清理资源
this.rooms.clear()
}
}
用户管理
实现用户管理:
// user-manager.js
class UserManager {
constructor(options = {}) {
this.options = {
sessionTimeout: 3600000, // 1 小时
...options
}
this.users = new Map()
this.sessions = new Map()
this.stats = new Stats()
this.initialize()
}
// 初始化用户管理器
initialize() {
// 启动会话清理
setInterval(() => {
this.cleanupSessions()
}, 300000) // 5 分钟
// 监控用户数
this.stats.gauge('users.total', () => this.users.size)
this.stats.gauge('users.online', () => this.getOnlineUsers().size)
}
// 认证用户
async authenticate(req) {
const token = this.extractToken(req)
if (!token) {
throw new Error('No token provided')
}
// 验证会话
const session = this.sessions.get(token)
if (!session) {
throw new Error('Invalid session')
}
// 更新会话
session.lastActivity = Date.now()
return session.user
}
// 创建会话
async createSession(user) {
const token = generateToken()
this.sessions.set(token, {
user,
createdAt: Date.now(),
lastActivity: Date.now()
})
this.stats.increment('sessions.created')
return token
}
// 清理会话
cleanupSessions() {
const now = Date.now()
let cleaned = 0
this.sessions.forEach((session, token) => {
if (now - session.lastActivity > this.options.sessionTimeout) {
this.sessions.delete(token)
cleaned++
}
})
if (cleaned > 0) {
this.stats.increment('sessions.cleaned', cleaned)
}
}
// 获取在线用户
getOnlineUsers() {
const onlineUsers = new Map()
this.sessions.forEach(session => {
onlineUsers.set(session.user.id, session.user)
})
return onlineUsers
}
// 获取用户信息
async getUserInfo(userId) {
const user = this.users.get(userId)
if (!user) {
throw new Error('User not found')
}
return {
id: user.id,
name: user.name,
role: user.role,
online: this.isUserOnline(userId)
}
}
// 检查用户是否在线
isUserOnline(userId) {
return Array.from(this.sessions.values())
.some(session => session.user.id === userId)
}
// 获取统计信息
getStats() {
return {
users: {
total: this.users.size,
online: this.getOnlineUsers().size
},
sessions: {
total: this.sessions.size
},
...this.stats.getAll()
}
}
}
媒体服务器
实现媒体服务器:
// media-server.js
class MediaServer {
constructor(url, options = {}) {
this.url = url
this.options = {
maxStreams: 1000,
...options
}
this.streams = new Map()
this.stats = new Stats()
this.initialize()
}
// 初始化媒体服务器
initialize() {
// 监控���数量
this.stats.gauge('streams.total', () => this.streams.size)
this.stats.gauge('streams.active', () => this.getActiveStreams().size)
}
// 处理媒体流
async handleStream(roomId, stream) {
// 检查流数量限制
if (this.streams.size >= this.options.maxStreams) {
throw new Error('Stream limit reached')
}
// 创建流
const mediaStream = {
id: generateId(),
roomId,
type: stream.type,
createdAt: Date.now(),
state: 'new'
}
// 处理不同类型的流
switch (stream.type) {
case 'video':
await this.handleVideoStream(mediaStream, stream)
break
case 'audio':
await this.handleAudioStream(mediaStream, stream)
break
case 'screen':
await this.handleScreenStream(mediaStream, stream)
break
}
this.streams.set(mediaStream.id, mediaStream)
this.stats.increment('streams.created')
return mediaStream
}
// 处理视频流
async handleVideoStream(mediaStream, stream) {
// 实现视频流处理逻辑
}
// 处理音频流
async handleAudioStream(mediaStream, stream) {
// 实现音频流处理逻辑
}
// 处理屏幕共享流
async handleScreenStream(mediaStream, stream) {
// 实现屏幕共享处理逻辑
}
// 停止流
async stopStream(streamId) {
const stream = this.streams.get(streamId)
if (!stream) return
// 停止流
stream.state = 'stopped'
// 清理资源
this.streams.delete(streamId)
this.stats.increment('streams.stopped')
}
// 获取活跃流
getActiveStreams() {
const activeStreams = new Map()
this.streams.forEach((stream, id) => {
if (stream.state === 'active') {
activeStreams.set(id, stream)
}
})
return activeStreams
}
// 获取统计信息
getStats() {
return {
streams: {
total: this.streams.size,
active: this.getActiveStreams().size
},
...this.stats.getAll()
}
}
// 关闭服务器
async shutdown() {
// 停止所有流
for (const stream of this.streams.values()) {
await this.stopStream(stream.id)
}
}
}
部署配置
实现部署配置:
// config.js
module.exports = {
// 服务器配置
server: {
port: process.env.PORT || 8080,
sslPort: process.env.SSL_PORT || 8443,
host: process.env.HOST || 'localhost'
},
// Redis 配置
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
options: {
retryStrategy: (times) => {
return Math.min(times * 50, 2000)
}
}
},
// 媒体服务器配置
media: {
url: process.env.MEDIA_SERVER || 'localhost:8000',
options: {
maxStreams: 1000
}
},
// 集群配置
cluster: {
workers: process.env.WORKERS || require('os').cpus().length,
restartDelay: 1000
},
// 安全配置
security: {
ssl: {
key: process.env.SSL_KEY || 'server.key',
cert: process.env.SSL_CERT || 'server.cert'
},
cors: {
origin: process.env.CORS_ORIGIN || '*'
}
},
// 房间配置
room: {
maxRooms: 1000,
maxUsersPerRoom: 100
},
// 用户配置
user: {
sessionTimeout: 3600000
},
// 监控配置
monitor: {
enabled: true,
interval: 1000,
historySize: 3600
},
// 日志配置
log: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'classroom.log'
}
}
最佳实践
-
系统设计
- 模块化架构
- 可扩展设计
- 高可用配置
-
功能实现
- 实时通讯
- 媒体处理
- 状态同步
-
性能优化
- 连接池管理
- 消息队列
- 集群部署
-
运维支持
- 监控系统
- 日志记录
- 故障恢复
-
安全保障
- 身份认证
- 数据加密
- 权限控制
写在最后
通过这个实战案例,我们深入探讨了如何构建一个完整的 WebSocket 应用。从系统设计到具体实现,从功能开发到性能优化,我们不仅关注了技术细节,更注重了实际应用中的各种挑战。
记住,一个优秀的实时应用需要在功能、性能、安全等多个方面取得平衡。在实际开发中,我们要根据具体需求选择合适的实现方案,确保应用能够稳定高效地运行。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍