当前位置: 首页 > news >正文

WebSocket 实战案例:从设计到部署

在前六篇文章中,我们深入探讨了 WebSocket 的基础原理、服务端开发、客户端实现、安全实践、性能优化和测试调试。今天,让我们通过一个实战案例,看看如何将这些知识应用到实际项目中。我曾在一个大型在线教育平台中,通过 WebSocket 实现了实时互动课堂,支持了数万名师生的同时在线。

项目背景

我们要实现一个实时互动课堂系统,主要功能包括:

  1. 实时音视频
  2. 课堂互动
  3. 共享白板
  4. 实时聊天
  5. 课堂管理

让我们从系统设计开始。

系统架构

实现系统架构:

// 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'})// 连接 Redisthis.redis = new Redis(this.options.redisUrl)// 设置事件处理器this.setupEventHandlers()// 启动服务器await this.start()}// 设置 ExpresssetupExpress() {// 静态文件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.roomIdif (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)breakcase 'whiteboard':await this.handleWhiteboardMessage(connection, data)breakcase 'media':await this.handleMediaMessage(connection, data)breakcase 'control':await this.handleControlMessage(connection, data)breakdefault: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.roomIdif (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)breakcase 'user_update':this.handleUserUpdate(data)breakcase '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)breakcase 'reload':this.handleReload(message.data)breakcase '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 = roomIdthis.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.roomIdthis.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 0let count = 0room.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 = 0this.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 = urlthis.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)breakcase 'audio':await this.handleAudioStream(mediaStream, stream)breakcase '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'}
}

最佳实践

  1. 系统设计

    • 模块化架构
    • 可扩展设计
    • 高可用配置
  2. 功能实现

    • 实时通讯
    • 媒体处理
    • 状态同步
  3. 性能优化

    • 连接池管理
    • 消息队列
    • 集群部署
  4. 运维支持

    • 监控系统
    • 日志记录
    • 故障恢复
  5. 安全保障

    • 身份认证
    • 数据加密
    • 权限控制

写在最后

通过这个实战案例,我们深入探讨了如何构建一个完整的 WebSocket 应用。从系统设计到具体实现,从功能开发到性能优化,我们不仅关注了技术细节,更注重了实际应用中的各种挑战。

记住,一个优秀的实时应用需要在功能、性能、安全等多个方面取得平衡。在实际开发中,我们要根据具体需求选择合适的实现方案,确保应用能够稳定高效地运行。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍

相关文章:

WebSocket 实战案例:从设计到部署

在前六篇文章中,我们深入探讨了 WebSocket 的基础原理、服务端开发、客户端实现、安全实践、性能优化和测试调试。今天,让我们通过一个实战案例,看看如何将这些知识应用到实际项目中。我曾在一个大型在线教育平台中,通过 WebSocket 实现了实时互动课堂,支持了数万名师生的同时在…...

selenium合集

环境搭建步骤 安装selenium pip install selenium 安装浏览器 安装浏览器驱动 谷歌浏览器:chromdriver.exe ie浏览器:ieserverdriver.exe FireFox浏览器:geckodriver.exe 特别注意⚠️:下载驱动版本必须与浏览器版本一致 下载地址 淘宝镜像&#xff1…...

JVM生产环境常用参数配置及调优建议

一、生产常用参数配置 JAVA_OPTS"-server -Xms3000m -Xmx3000m -Xmn1500m -XX:UseG1GC -XX:ConcGCThreads8 -XX:PrintGCDetails -XX:PrintGCTimeStamps -Xloggc:./g1-gc.log -XX:MaxMetaspaceSize256m -XX:-UseGCOverheadLimit -XX:UseCompressedOops -XX:HeapDumpOnOu…...

Spring Boot 3 实现 MySQL 主从数据库之间的数据同步

✅ Spring Boot 3 实现 MySQL 主从数据库之间的数据同步 在实际项目中,为了提高 系统的读性能 和 数据的可用性,通常会使用 主从数据库架构。Spring Boot 提供了对 多数据源 的良好支持,可以轻松配置 主从数据库 的数据同步,实现…...

【小程序开发】- 小程序版本迭代指南(版本发布教程)

一,版本号 版本号是小程序版本的标识,通常由一系列数字组成,如 1.0.0、1.1.0 等。版本号的格式通常是 主版本号.次版本号.修订号 主版本号:当小程序有重大更新或不兼容的更改时,主版本号会增加。 次版本号&#xff1a…...

MySQL 间隙锁避免“可重复读”出现“幻读”

在数据库事务处理中,可重复读(Repeatable Read)是一个常用的隔离级别,但其默认行为可能导致幻读现象。然而,在 MySQL 的实现中,通过 **间隙锁(Gap Lock)**机制,能够避免幻…...

揭秘区块链隐私黑科技:零知识证明如何改变未来

文章目录 1. 引言:什么是零知识证明?2. 零知识证明的核心概念与三大属性2.1 完备性(Completeness)2.2 可靠性(Soundness)2.3 零知识性(Zero-Knowledge) 3. 零知识证明的工作原理4. 零…...

JavaWeb开发:从入门到精通

近年来,JavaWeb开发已经成为了互联网开发领域的重要技术之一。无论是大型企业还是个人项目,都离不开JavaWeb开发。本文将为您介绍JavaWeb开发的基本概念、常用技术和开发流程,帮助您快速入门并掌握这一技术。 一、JavaWeb开发的基本概念 Jav…...

2025年01月07日Github流行趋势

项目名称:khoj 项目地址url:https://github.com/khoj-ai/khoj项目语言:Python历史star数:20105今日star数:363项目维护者:debanjum, sabaimran, MythicalCow, aam-at, shantanuSakpal项目简介:你…...

c#集成npoi根据excel模板导出excel

NuGet中安装npoi 创建excel模板,替换其中的内容生成新的excel文件。 例子中主要写了这四种情况: 1、替换单个单元格内容; 2、替换横向多个单元格; 3、替换表格; 4、单元格中插入图片; using System.IO; …...

Vue2移动端(H5项目)项目封装switch组件支持动态设置开启关闭背景色、值及组件内显示文字描述、禁用、switch 的宽度

前言 近期产品需求&#xff1a;Vue2移动端项目需要在switch开关内显示文字&#xff0c;看Vantui没有对应功能&#xff0c;因此自己手撸写了这个组件。 一、最终效果 二、参数配置 1、代码示例&#xff1a; <t-switch v-model"check"/>2、配置参数&#xff08;…...

MATLAB语言的语法糖

MATLAB语言的语法糖 引言 在编程语言的发展历程中&#xff0c;语法糖&#xff08;Syntactic Sugar&#xff09;被广泛提及。它指的是一种编程语言的语法特性&#xff0c;旨在使代码更易读、更易写&#xff0c;虽然这些特性并不增加语言的表达能力&#xff0c;但能使程序员的生…...

数字IC设计高频面试题

在数字IC设计领域&#xff0c;面试是评估候选人技术能力和问题解决能力的重要环节。数字IC设计的复杂性和要求在不断提高。面试官通常会提出一系列面试题&#xff0c;以考察应聘者在数字设计、验证、时钟管理、功耗优化等方面的专业知识和实践经验。 这些题目不仅涉及理论知识…...

OpenCV 4.5至4.10版本更新概述

OpenCV 4.5至4.10版本更新概述 OpenCV 从 4.5 到 4.10 版本的更迭中&#xff0c;每个版本都引入了新功能、优化和修复。以下是主要版本的更新内容概述&#xff1a; OpenCV 4.5.x 系列 4.5.0 (2020年10月) 新增对 YOLOv4 的支持。引入 DNN 模块的改进&#xff0c;包括对 ONNX …...

OSPF - LSA对照表

LSA的三要素&#xff0c;如何唯一表示一条LSA  Type&#xff1a;表示是几类的LSA  Link-id&#xff1a;这个比较特殊&#xff0c;不同的LSA的Link-ID不同  Advertising router&#xff1a;谁产生的LSA 常用的就是1、2、3、4、5、7型LSA 点击蓝字跳转LSA详细介绍(持续更新中…...

游戏引擎学习第77天

仓库: https://gitee.com/mrxiao_com/2d_game 回顾昨天的 bug 今天我们继续开发进度&#xff0c;进行调试昨天代码的问题&#xff0c;主要是关于如何跟踪玩家和敌人在世界中的高度位置。虽然我们做的是一款 2D 游戏&#xff0c;但我们希望能够处理多层的房间&#xff0c;玩家…...

【项目实战1】五子棋游戏

目录 C语言编程实现五子棋&#xff1a;&#xff1a; game.h game.c 1.打印菜单 2.打印棋盘 3.玩家下棋 4.判断五子连珠 5.判断输赢 6.游戏运行 game.c完整源代码展示 test.c C语言编程实现五子棋&#xff1a;&#xff1a; game.h #pragma once #include<stdio.h> …...

HTML5 动画效果:淡入淡出(Fade In/Out)详解

HTML5 动画效果&#xff1a;淡入淡出&#xff08;Fade In/Out&#xff09;详解 淡入淡出&#xff08;Fade In/Out&#xff09;是一种常见的动画效果&#xff0c;使元素逐渐显现或消失&#xff0c;增强用户体验。以下是淡入淡出的详细介绍及实现示例。 1. 淡入淡出的特点 平滑…...

Conmi的正确答案——Cordova使用“src-cordova/config.xml”编辑“Android平台”的“uses-permission”

Cordova版本&#xff1a;12.0.0 (cordova-lib12.0.1) 1、配置例程&#xff1a; <platform name"android"><config-file target"AndroidManifest.xml" parent"/manifest"><uses-permission android:name"android.permission…...

在Mysql环境下对数据进行增删改查

一、插入数据&#xff1a; insert into 表名 [(字段名)] values (字段对应的值1,字段对应的值2,…)[,(字段对应的值1,字段对应的值2,…)]; insert into students (id,name,age,height,gender,cls_id,is_delete) values (0,小明,18,180.00,2,1,0)在学生表中插入“小明”数据的…...

stm32G473的flash模式是单bank还是双bank?

今天突然有人stm32G473的flash模式是单bank还是双bank&#xff1f;由于时间太久&#xff0c;我真忘记了。搜搜发现&#xff0c;还真有人和我一样。见下面的链接&#xff1a;https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

React第五十七节 Router中RouterProvider使用详解及注意事项

前言 在 React Router v6.4 中&#xff0c;RouterProvider 是一个核心组件&#xff0c;用于提供基于数据路由&#xff08;data routers&#xff09;的新型路由方案。 它替代了传统的 <BrowserRouter>&#xff0c;支持更强大的数据加载和操作功能&#xff08;如 loader 和…...

深入理解JavaScript设计模式之单例模式

目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式&#xff08;Singleton Pattern&#…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容

目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法&#xff0c;当前调用一个医疗行业的AI识别算法后返回…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

企业如何增强终端安全?

在数字化转型加速的今天&#xff0c;企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机&#xff0c;到工厂里的物联网设备、智能传感器&#xff0c;这些终端构成了企业与外部世界连接的 “神经末梢”。然而&#xff0c;随着远程办公的常态化和设备接入的爆炸式…...

人机融合智能 | “人智交互”跨学科新领域

本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...

C++.OpenGL (20/64)混合(Blending)

混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...

GO协程(Goroutine)问题总结

在使用Go语言来编写代码时&#xff0c;遇到的一些问题总结一下 [参考文档]&#xff1a;https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现&#xff1a; 今天在看到这个教程的时候&#xff0c;在自己的电…...