7.3 KiB
7.3 KiB
PrivyDrop - 后端架构详解
一、概述
1.1 核心职责
PrivyDrop 的后端是一个基于 Node.js 和 Express.js 的轻量级服务器。它的核心职责并非直接传输文件,而是作为 WebRTC 连接建立过程中的“信令服务器”和“房间协调员”。
主要功能包括:
- HTTP API 服务: 提供 RESTful 接口用于房间的创建、查询和状态检查。
- WebRTC 信令: 通过 Socket.IO 实时转发客户端之间的信令消息(SDP Offers/Answers, ICE Candidates),以促成 P2P 连接。
- 房间生命周期管理: 使用 Redis 高效地管理房间和参与者的状态,并利用其 TTL 机制实现自动清理。
- 基础的安全性: 实现了基于 IP 的速率限制以防止服务被滥用。
1.2 设计原则
- 无状态 (Stateless): 后端服务本身不持有任何与房间或用户相关的状态。所有状态都被委托给外部的 Redis 服务进行管理,这使得后端应用可以轻松地进行水平扩展。
- 轻量级信令: 服务器仅作为信令消息的中转站,不解析也不存储信令内容,确保了端到端通信的隐私性。
- 高效率与低延迟: 采用 Redis 作为内存数据库来管理房间状态,并通过 Socket.IO 进行实时通信,最大限度地降低了信令交换的延迟。
- 职责单一: 每个模块(API、Socket 处理、Redis 服务)都有明确且单一的职责,易于理解、维护和测试。
二、项目结构
后端源代码遵循功能模块化的组织方式,主要位于 src/ 目录中:
backend/
├── src/
│ ├── config/ # 环境变量和服务器配置 (CORS)
│ │ ├── env.ts
│ │ └── server.ts
│ ├── routes/ # API 路由定义 (Express 路由)
│ │ └── api.ts
│ ├── services/ # 核心业务逻辑 (房间, Redis, 速率限制)
│ │ ├── rateLimit.ts
│ │ ├── redis.ts
│ │ └── room.ts
│ ├── socket/ # Socket.IO 事件处理程序和信令逻辑
│ │ └── handlers.ts
│ ├── types/ # TypeScript 类型定义和接口
│ │ ├── room.ts
│ │ └── socket.ts
│ └── server.ts # 主应用程序入口点: Express 和 Socket.IO 设置
├── ecosystem.config.js # PM2 配置文件
├── package.json
└── tsconfig.json
三、核心模块详解
3.1 应用入口 (src/server.ts)
这是应用的启动文件。它负责:
- 加载环境变量。
- 初始化 Express 应用实例。
- 配置 CORS、JSON 解析等中间件。
- 挂载
/api路由。 - 创建 HTTP 服务器并附加 Socket.IO 服务。
- 调用
initializeSocketHandlers设置所有 Socket.IO 事件监听器。 - 启动服务器并监听指定端口。
3.2 API 路由 (src/routes/api.ts)
定义了所有供前端调用的 HTTP RESTful API。
POST /api/create_room: 接收前端指定的roomId,检查是否可用,如果可用则创建新房间。GET /api/get_room: 生成一个唯一的、随机的房间 ID,创建房间后返回给前端。POST /api/check_room: 检查给定的roomId是否已存在。POST /api/set_track: 用于追踪流量来源。POST /api/logs_debug: 一个简单的调试端点,用于接收前端日志并打印在后端控制台。
3.3 Socket.IO 事件处理 (src/socket/handlers.ts)
这是信令交换的核心。initializeSocketHandlers 函数为传入的 socket 连接绑定了一系列事件处理器。
- 连接与断开:
connection: 当一个新客户端连接时,记录其socket.id。disconnect: 当客户端断开时,从其所在的房间中移除,并通知房间内其他对等方 (peer-disconnected)。
- 房间逻辑:
join: 处理客户端加入房间的请求。它会验证房间是否存在,并将该客户端的socket.id添加到房间的成员集合中,最后向请求方发送joinResponse。initiator-online: 由房间创建者(发起者)发出(当 web 被切到后台掉线时),用于通知接收者“我已经上线了,准备重新建立连接”。recipient-ready: 由接收者发出,通知房间内的发起者“准备就绪,可以开始重连”,这通常是触发 WebRTCoffer流程的信号。
- WebRTC 信令转发:
offer,answer,ice-candidate: 这三个事件是纯粹的信使,负责将一个对等方的 WebRTC 信令消息准确无误地转发给房间内的另一个对等方。
3.4 服务层 (src/services/)
封装了与外部依赖(如 Redis)和核心业务逻辑的交互。
redis.ts: 提供了 Redis 客户端的单例实例。所有与 Redis 的交互都应通过此模块。room.ts: 封装了所有与房间相关的 Redis 操作。例如createRoom,isRoomExist,bindSocketToRoom等。它将业务逻辑(如“将用户添加到房间”)与底层 Redis 命令(如SADD,HSET)解耦。rateLimit.ts: 实现了一个基于 IP 和 Redis Sorted Set 的速率限制器,用于限制用户在短时间内频繁创建或加入房间。
四、Redis 数据结构详解
Redis 是后端的关键组件,用于存储所有临时状态。我们巧妙地利用了不同的数据结构来满足业务需求,并为所有键设置了 TTL,以确保数据能自动清理。
-
1. 房间信息 (
Hash):- 键模式:
room:<roomId>(例如:room:ABCD12) - 用途: 存储房间的元数据。
- 字段:
created_at: 房间创建时的时间戳。
- 示例:
HSET room:ABCD12 created_at 1705123456789
- 键模式:
-
2. 房间内的套接字 (
Set):- 键模式:
room:<roomId>:sockets(例如:room:ABCD12:sockets) - 用途: 存储一个房间内所有客户端的
socketId。使用 Set 可以保证成员的唯一性,并方便地进行添加和删除。 - 成员: 客户端的
socketId。 - 示例:
SADD room:ABCD12:sockets "socketId_A" "socketId_B"
- 键模式:
-
3. 套接字到房间的映射 (
String):- 键模式:
socket:<socketId>(例如:socket:xgACY6QcQCojsOQaAAAB) - 用途: 将一个
socketId反向映射到它所属的roomId。这在处理客户端断开连接时非常有用,我们仅需socketId即可快速找到其房间并执行清理。 - 值:
roomId。 - 示例:
SET socket:xgACY6QcQCojsOQaAAAB ABCD12
- 键模式:
-
4. 速率限制 (
Sorted Set):- 键模式:
ratelimit:join:<ipAddress>(例如:ratelimit:join:192.168.1.100) - 用途: 记录特定 IP 地址在指定时间窗口内的所有请求时间戳。
- 成员:
timestamp-randomNumber(例如:1678886400000-0.12345)。使用随机数后缀确保同一毫秒内多个请求的唯一性。 - 分数: 请求的 Unix 时间戳(毫秒)。
- 逻辑: 通过
ZREMRANGEBYSCORE移除时间窗口外的旧记录,再用ZCARD统计窗口内的请求数,从而判断是否超出限制。
- 键模式:
-
5. 来源跟踪 (
Hash):- 键模式:
referrers:daily:<YYYY-MM-DD>(例如:referrers:daily:2023-03-15) - 用途: 按天统计不同来源(Referrer)的访问次数。
- 字段: 来源域名 (例如:
google.com,github.com)。 - 值: 当天的累计访问次数。
- 逻辑: 使用
HINCRBY命令原子性地增加指定来源的计数值。
- 键模式: