translate comment

This commit is contained in:
david_bai
2025-06-21 17:08:11 +08:00
parent 18fad98cbd
commit 4455ba598f
11 changed files with 90 additions and 89 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
module.exports = {
apps: [{
name: "signaling-server",
script: "./dist/server.js", // 指向编译后的文件
script: "./dist/server.js", // Point to the compiled file
watch: false,
env: {
"NODE_ENV": "production",
@@ -13,6 +13,6 @@ module.exports = {
max_memory_restart: "500M",
instances: 1,
exec_mode: "fork",
group: "ssl-cert" // 添加这行,指定进程运行的组
group: "ssl-cert" // Add this line to specify the group the process runs as
}]
}
+13 -13
View File
@@ -3,7 +3,7 @@ const Redis = require('ioredis');
const fs = require('fs/promises');
const path = require('path');
// Redis 客户端配置
// Redis client configuration
const redis = new Redis({
host: 'localhost',
port: 6379,
@@ -15,16 +15,16 @@ async function exportTrackingData() {
const fileName = `tracking-data-${timestamp}.txt`;
const filePath = path.join(__dirname, '../logs', fileName);
// 创建输出内容
// Create output content
let output = '=== Tracking Data Export ===\n';
output += `Generated at: ${new Date().toISOString()}\n\n`;
// 1. 获取所有来源
// 1. Get all sources
const sources = await redis.smembers('referrers:sources');
output += '=== Referral Sources ===\n';
output += sources.join(', ') + '\n\n';
// 2. 获取总计数
// 2. Get total counts
const totalCounts = await redis.hgetall('referrers:count');
output += '=== Total Counts by Source ===\n';
for (const [source, count] of Object.entries(totalCounts)) {
@@ -32,21 +32,21 @@ async function exportTrackingData() {
}
output += '\n';
// 3. 获取每日统计数据并按日期排序
// 3. Get daily statistics and sort by date
output += '=== Daily Statistics ===\n';
const dailyKeys = await redis.keys('referrers:daily:*');
// 将 key 转换为包含日期对象的数组,并按日期排序
// Convert keys to an array of objects with dates and sort by date
const dailyData = await Promise.all(dailyKeys.map(async (key) => {
const date = key.split(':')[2];
const dailyStats = await redis.hgetall(key);
return { date: new Date(date), data: dailyStats };
}));
dailyData.sort((a, b) => b.date.getTime() - a.date.getTime()); // 按日期降序排序(最近到最远)
dailyData.sort((a, b) => b.date.getTime() - a.date.getTime()); // Sort by date in descending order (most recent to oldest)
for (const item of dailyData) {
const dateString = item.date.toISOString().split('T')[0]; //日期格式化字符串
const dateString = item.date.toISOString().split('T')[0]; // Format date string
output += `\nDate: ${dateString}\n`;
for (const [source, count] of Object.entries(item.data)) {
output += ` ${source}: ${count}\n`;
@@ -55,14 +55,14 @@ async function exportTrackingData() {
output += '\n';
// 5. 添加基本统计信息
// 5. Add basic statistics
output += '\n=== Summary ===\n';
output += `Total Sources: ${sources.length}\n`;
// 确保日志目录存在
// Ensure the log directory exists
await fs.mkdir(path.join(__dirname, '../logs'), { recursive: true });
// 写入文件
// Write to file
await fs.writeFile(filePath, output, 'utf8');
console.log(`Data exported successfully to: ${filePath}`);
@@ -71,7 +71,7 @@ async function exportTrackingData() {
console.log(output.slice(0, 500) + '...');
console.log('='.repeat(50));
// 关闭 Redis 连接
// Close Redis connection
await redis.quit();
} catch (error) {
@@ -80,5 +80,5 @@ async function exportTrackingData() {
}
}
// 执行导出
// Execute export
exportTrackingData();
+6 -6
View File
@@ -1,7 +1,7 @@
import dotenv from "dotenv";
import path from "path";
// 定义配置对象的类型
// Define the type for the configuration object
interface AppConfig {
PORT: number;
CORS_ORIGIN: string;
@@ -12,23 +12,23 @@ interface AppConfig {
};
}
// 根据环境加载对应的.env文件
// Load the corresponding .env file based on the environment
dotenv.config({
path:
process.env.NODE_ENV === "production"
? path.resolve(process.cwd(), ".env.production.local")
: path.resolve(process.cwd(), ".env.development.local"),
});
// 检查必要的 Redis 环境变量
// Check for necessary Redis environment variables
if (!process.env.REDIS_HOST) {
console.error("FATAL ERROR: REDIS_HOST environment variable is not set.");
process.exit(1); // 或者抛出错误 new Error("REDIS_HOST environment variable is not set.");
process.exit(1); // Or throw an error: new Error("REDIS_HOST environment variable is not set.");
}
if (!process.env.REDIS_PORT) {
console.error("FATAL ERROR: REDIS_PORT environment variable is not set.");
process.exit(1); // 或者抛出错误 new Error("REDIS_PORT environment variable is not set.");
process.exit(1); // Or throw an error: new Error("REDIS_PORT environment variable is not set.");
}
// 导出类型安全的配置对象
// Export the type-safe configuration object
export const CONFIG: AppConfig = {
PORT: parseInt(process.env.BACKEND_PORT || "3001", 10),
CORS_ORIGIN: process.env.CORS_ORIGIN!,
+6 -6
View File
@@ -1,6 +1,6 @@
import { CorsOptions } from 'cors';
import { CONFIG } from './env';
// 配置 CORS
// Configure CORS
export const corsOptions: CorsOptions = CONFIG.NODE_ENV === 'production'
? {
origin: CONFIG.CORS_ORIGIN,
@@ -9,23 +9,23 @@ export const corsOptions: CorsOptions = CONFIG.NODE_ENV === 'production'
allowedHeaders: ['Content-Type', 'Authorization']
}
: {
origin: true, // 开发环境允许所有源
origin: true, // Allow all origins in development environment
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
// 配置 Socket.IO 的 CORS
// Configure CORS for Socket.IO
export const corsWSOptions = CONFIG.NODE_ENV === 'production'
? {
origin: CONFIG.CORS_ORIGIN,// 允许的源,替换为你的Next.js应用的URL
origin: CONFIG.CORS_ORIGIN, // Allowed origin, replace with your Next.js application's URL
methods: ['GET', 'POST'],
credentials: true
}
: {
// 开发环境下允许多个源
// Allow multiple origins in development environment
origin: [
CONFIG.CORS_ORIGIN,
/^http:\/\/192\.168\.\d+\.\d+:3000$/,// 匹配所有 192.168.x.x:3000 格式的局域网地址
/^http:\/\/192\.168\.\d+\.\d+:3000$/, // Match all LAN addresses in the format 192.168.x.x:3000
],
methods: ['GET', 'POST'],
credentials: true
+10 -10
View File
@@ -20,7 +20,7 @@ import { ReferrerTrack, LogMessage } from "../types/room";
const router = Router();
// 定义接口提高代码可读性和类型安全性
// Define interfaces to improve code readability and type safety
interface CreateRoomRequest {
roomId: string;
}
@@ -29,7 +29,7 @@ interface CheckRoomRequest {
roomId: string;
}
// 创建房间的路由处理函数
// Route handler for creating a room
const createRoomHandler: RequestHandler<{}, any, CreateRoomRequest> = async (
req,
res
@@ -58,7 +58,7 @@ const createRoomHandler: RequestHandler<{}, any, CreateRoomRequest> = async (
}
};
// 获取房间的路由处理函数
// Route handler for getting a room
const getRoomHandler: RequestHandler = async (req, res) => {
try {
const roomId = await roomService.getAvailableRoomId();
@@ -70,7 +70,7 @@ const getRoomHandler: RequestHandler = async (req, res) => {
}
};
// 检查房间的路由处理函数
// Route handler for checking a room
const checkRoomHandler: RequestHandler<{}, any, CheckRoomRequest> = async (
req,
res
@@ -90,7 +90,7 @@ const checkRoomHandler: RequestHandler<{}, any, CheckRoomRequest> = async (
}
};
// 设置跟踪的路由处理函数
// Route handler for setting tracking
const setTrackHandler: RequestHandler<{}, any, ReferrerTrack> = async (
req,
res
@@ -102,16 +102,16 @@ const setTrackHandler: RequestHandler<{}, any, ReferrerTrack> = async (
try {
const { ref, timestamp, path } = req.body;
// 按日期统计
// Statistics by date
const date = new Date(timestamp).toISOString().split("T")[0];
const dailyKey = `referrers:daily:${date}`;
const thirtyDaysInSeconds = 30 * 24 * 60 * 60;
// 使用MULTI确保hincrbyexpire的原子性
// Use MULTI to ensure atomicity of hincrby and expire
await redis
.multi()
.hincrby(dailyKey, ref, 1) // \"referrers:daily:2024-01-20\" : { \"producthunt\": \"5\", \"twitter\": \"3\" }
.expire(dailyKey, thirtyDaysInSeconds) // 设置30天过期
.expire(dailyKey, thirtyDaysInSeconds) // Set a 30-day expiration
.exec();
res.status(200).json({ success: true });
@@ -121,7 +121,7 @@ const setTrackHandler: RequestHandler<{}, any, ReferrerTrack> = async (
}
};
// 日志调试的路由处理函数
// Route handler for log debugging
const logsDebugHandler: RequestHandler<{}, any, LogMessage> = async (
req,
res
@@ -136,7 +136,7 @@ const logsDebugHandler: RequestHandler<{}, any, LogMessage> = async (
}
};
// 注册路由
// Register routes
router.post("/api/create_room", createRoomHandler);
router.get("/api/get_room", getRoomHandler);
router.post("/api/check_room", checkRoomHandler);
+4 -4
View File
@@ -1,14 +1,14 @@
import express from "express"; //express: 用于创建一个简洁且灵活的Node.js web应用框架
import express from "express"; // express: A minimalist and flexible Node.js web application framework
import cors from "cors";
import http from "http";
import { Server } from "socket.io"; //实时通信库,基于WebSocket协议,实现双向通信
import { Server } from "socket.io"; // socket.io: A library for real-time web applications, enables real-time, bi-directional communication between web clients and servers.
import { CONFIG } from "./config/env";
import { corsOptions, corsWSOptions } from "./config/server";
import apiRouter from "./routes/api";
import { setupSocketHandlers } from "./socket/handlers";
const app = express(); //创建一个Express应用
app.use(cors(corsOptions)); // 添加 CORS 中间件
const app = express(); // Create an Express application
app.use(cors(corsOptions)); // Add CORS middleware
app.use(express.json());
app.use(apiRouter);
+3 -3
View File
@@ -20,8 +20,8 @@
import { redis } from './redis';
const RATE_LIMIT_PREFIX = 'ratelimit:join:';
const RATE_WINDOW = 5; // 5秒时间窗口
const RATE_LIMIT = 2; // 允许的最大请求次数
const RATE_WINDOW = 5; // 5-second time window
const RATE_LIMIT = 2; // Maximum number of requests allowed
export async function checkRateLimit(ip: string): Promise<{
allowed: boolean;
@@ -32,7 +32,7 @@ export async function checkRateLimit(ip: string): Promise<{
const now = Date.now();
const windowStart = now - (RATE_WINDOW * 1000);
// 使用 Redis MULTI 命令开启事务
// Use Redis's MULTI command to start a transaction
const pipeline = redis.pipeline();
// 1. Add current request's timestamp
+6 -6
View File
@@ -1,21 +1,21 @@
import { Redis } from 'ioredis';
import { CONFIG } from '../config/env';
// 房间前缀和过期时间(秒)
// Room prefix and expiration time (seconds)
export const ROOM_PREFIX = 'room:';
export const SOCKET_PREFIX = 'socket:';
export const ROOM_EXPIRY = 3600 * 24; // 24 hours
// Redis 配置选项
// Redis configuration options
const redisConfig = {
host: CONFIG.REDIS.HOST,
port: CONFIG.REDIS.PORT,
// Redis 持久化配置需要在 redis.conf 中设置,而不是在客户端
// appendonly: 'yes',// 启用 AOF 持久化
// save: '900 1 300 10',// 启用 RDB 快照
// Redis persistence configuration needs to be set in redis.conf, not in the client
// appendonly: 'yes',// Enable AOF persistence
// save: '900 1 300 10',// Enable RDB snapshot
};
export const redis = new Redis(redisConfig);
// 可以在这里添加连接事件监听
// Connection event listeners can be added here
redis.on('connect', () => {
console.log('Redis connected successfully');
});
+20 -19
View File
@@ -29,7 +29,7 @@ import { redis, ROOM_PREFIX, SOCKET_PREFIX, ROOM_EXPIRY } from './redis';
const MAX_NUMERIC_ID_ATTEMPTS = 10;
const MAX_ALPHANUMERIC_ID_ATTEMPTS = 50;
// 生成随机房间号--4位数字
// Generate a random 4-digit numeric room ID
function generateNumericRoomId(length: number = 4): string {
let id = '';
for (let i = 0; i < length; i++) {
@@ -37,6 +37,7 @@ function generateNumericRoomId(length: number = 4): string {
}
return id;
}
// Generate a random 4-character alphanumeric room ID
function generateAlphanumericRoomId(length: number = 4): string {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
@@ -46,11 +47,11 @@ function generateAlphanumericRoomId(length: number = 4): string {
}
return result;
}
// 检查房间是否存在
// Check if a room exists
export async function isRoomExist(roomId: string): Promise<boolean> {
return await redis.hexists(ROOM_PREFIX + roomId, 'created_at') === 1;//hsethexists方法操作哈希,created_at是字段名
return await redis.hexists(ROOM_PREFIX + roomId, 'created_at') === 1; // hset and hexists operate on hashes, 'created_at' is the field name
}
// 创建新房间
// Create a new room
// (Hash)
// "room:1234" : {
// "created_at": "1705123456789"
@@ -59,16 +60,16 @@ export async function createRoom(roomId: string): Promise<void> {
const roomKey = ROOM_PREFIX + roomId;
const socketsKey = `${roomKey}:sockets`;
await redis.multi()
.hset(roomKey, 'created_at', Date.now())//设置hash,存储房间的创建时间;
.expire(roomKey, ROOM_EXPIRY)//设置过期时间
.hset(roomKey, 'created_at', Date.now()) // Set hash to store the room's creation time
.expire(roomKey, ROOM_EXPIRY) // Set expiration time
.expire(socketsKey, ROOM_EXPIRY)
.exec();
}
// 删除房间
// Delete a room
export async function deleteRoom(roomId: string): Promise<void> {
await redis.del(ROOM_PREFIX + roomId);
}
// 刷新房间过期时间
// Refresh a room's expiration time
export async function refreshRoom(roomId: string, expiry: number = 0): Promise<void> {
const actualExpiry = expiry > 0 ? expiry : ROOM_EXPIRY;
const roomKey = ROOM_PREFIX + roomId;
@@ -79,7 +80,7 @@ export async function refreshRoom(roomId: string, expiry: number = 0): Promise<v
.expire(socketsKey, actualExpiry)
.exec();
}
// 获取可用房间号
// Get an available room ID
export async function getAvailableRoomId(): Promise<string> {
let roomId: string;
let attempts = 0;
@@ -106,32 +107,32 @@ export async function getAvailableRoomId(): Promise<string> {
}
return roomId;
}
// socket.id与房间号绑定
// Bind a socket.id to a room ID
export async function bindSocketToRoom(socketId: string, roomId: string): Promise<void> {
await redis.multi()
//字符串,存储与该socket ID相关联的房间号,"socket:abcd1234" : "1234"
// String, stores the room ID associated with this socket ID, e.g., "socket:abcd1234" : "1234"
.set(SOCKET_PREFIX + socketId, roomId, 'EX', ROOM_EXPIRY+3600) // Set with expiry
//添加集合,房间内的 Socket 列表 (Set),"room:1234:sockets" : ["socket1", "socket2", ...]
// Set, list of sockets in the room, e.g., "room:1234:sockets" : ["socket1", "socket2", ...]
.sadd(ROOM_PREFIX + roomId + ':sockets', socketId)
.exec();
}
// 获取socket.id对应的房间号
// Get the room ID for a given socket.id
export async function getRoomBySocketId(socketId: string): Promise<string | null> {
return await redis.get(SOCKET_PREFIX + socketId);
}
// 解绑socket.id与房间号
// Unbind a socket.id from a room ID
export async function unbindSocketFromRoom(socketId: string, roomId: string): Promise<void> {
await redis.multi()
.del(SOCKET_PREFIX + socketId)//解绑socket ID与房间号
.srem(ROOM_PREFIX + roomId + ':sockets', socketId)//从房间的集合中移除socket ID
.del(SOCKET_PREFIX + socketId) // Unbind socket ID from room ID
.srem(ROOM_PREFIX + roomId + ':sockets', socketId) // Remove socket ID from the room's set
.exec();
}
// 检查房间是否为空
// Check if a room is empty
export async function isRoomEmpty(roomId: string): Promise<boolean> {
const count = await redis.scard(ROOM_PREFIX + roomId + ':sockets');//返回集合中元素的数量
const count = await redis.scard(ROOM_PREFIX + roomId + ':sockets'); // Returns the number of elements in the set
return count === 0;
}
// 检查房间连接数
// Get the number of connections in a room
export async function roomNumOfConnection(roomId: string): Promise<number> {
return await redis.scard(ROOM_PREFIX + roomId + ':sockets');
}
+19 -19
View File
@@ -2,12 +2,12 @@ import { Server, Socket } from 'socket.io';
import * as roomService from '../services/room';
import { JoinData, SignalingData, InitiatorData, RecipientData } from '../types/socket';
import { checkRateLimit } from '../services/rateLimit';
// 房间管理:
// 使用 roomId 进行广播消息(socket.to(roomId).emit()
// 场景:新用户加入通知、房间状态更新等
// WebRTC 信令:
// 使用 peerId 进行点对点通信(socket.to(peerId).emit()
// 场景:offeranswerice-candidate 等 WebRTC 连接建立过程中的所有信令
// Room Management:
// Use roomId to broadcast messages (socket.to(roomId).emit())
// Scenarios: Notifying new user joined, room status updates, etc.
// WebRTC Signaling:
// Use peerId for peer-to-peer communication (socket.to(peerId).emit())
// Scenarios: All signaling during WebRTC connection setup, like offer, answer, ice-candidate.
export function setupSocketHandlers(io: Server): void {
io.on('connection', (socket: Socket) => {
console.log('New client connected:', socket.id);
@@ -15,10 +15,10 @@ export function setupSocketHandlers(io: Server): void {
socket.on('join', async (data: JoinData) => {
const { roomId: targetRoomId } = data; // Renamed for clarity
try {
// 获取客户端IP
// Get client IP
const clientIp = socket.handshake.headers['x-forwarded-for'] ||
socket.handshake.address;
// 检查频率限制
// Check rate limit
const rateLimitCheck = await checkRateLimit(clientIp as string);
if (!rateLimitCheck.allowed) {
socket.emit('joinResponse', {
@@ -35,16 +35,16 @@ export function setupSocketHandlers(io: Server): void {
}
const existingRoomId = await roomService.getRoomBySocketId(socket.id);
if (!existingRoomId) {//socket.id不在房间里面 才允许新连接进入房间
if (!existingRoomId) { // Only allow new connection to join if the socket.id is not already in a room
socket.join(targetRoomId);
console.log(`Client ${socket.id} joined room ${targetRoomId}`);
await roomService.bindSocketToRoom(socket.id, targetRoomId);
}
await roomService.refreshRoom(targetRoomId);
// 通知用户加入成功
// Notify the user that the join was successful
socket.emit('joinResponse', { success: true, message: 'Successfully joined room', roomId: targetRoomId });
// 通知房间内所有其他用户有新成员加入
// Notify all other users in the room that a new member has joined
socket.to(targetRoomId).emit('ready', { peerId: socket.id });
} catch (error) {
console.error('Error joining room:', error);
@@ -55,16 +55,16 @@ export function setupSocketHandlers(io: Server): void {
});
}
});
// 处理WebRTC信令--直接转发
// offer, answer, ice-candidate: 这些事件处理WebRTC连接的信令。它们负责转发客户端之间的连接请求和网络协商消息。
// offer: 当一个客户端发起连接请求时,会发送一个offer给服务器,服务器将其转发给相同房间中的其他客户端。
// answer: 被邀请的客户端接收到offer后,生成一个answer,通过服务器返回给发起连接的客户端。
// ice-candidate: WebRTC需要穿透NAT防火墙时,会生成ICE候选者,客户端通过服务器相互交换这些信息,帮助建立P2P连接。
// Handle WebRTC signaling - direct forwarding
// offer, answer, ice-candidate: These events handle WebRTC connection signaling. They are responsible for forwarding connection requests and network negotiation messages between clients.
// offer: When a client initiates a connection request, it sends an offer to the server, which forwards it to other clients in the same room.
// answer: The invited client receives the offer, generates an answer, and sends it back to the initiating client via the server.
// ice-candidate: When WebRTC needs to traverse NAT firewalls, it generates ICE candidates. Clients exchange this information through the server to help establish a P2P connection.
socket.on('offer', (data: SignalingData) => {
socket.to(data.peerId).emit('offer', {
offer: data.offer,
from: data.from,
peerId: socket.id // 发送方的ID
peerId: socket.id // Sender's ID
});
});
@@ -83,13 +83,13 @@ export function setupSocketHandlers(io: Server): void {
peerId: socket.id
});
});
// 处理发起方重新上线的通知--广播给房间内的其他用户
// Handle notification for initiator coming back online -- broadcast to other users in the room
socket.on('initiator-online', (data: InitiatorData) => {
socket.to(data.roomId).emit('initiator-online', {
roomId: data.roomId
});
});
// 处理接收方的响应
// Handle recipient's response
socket.on('recipient-ready', (data: RecipientData) => {
socket.to(data.roomId).emit('recipient-ready', {
peerId: data.peerId
+1 -1
View File
@@ -2,7 +2,7 @@ export interface JoinData {
roomId: string;
}
// 添加 WebRTC 相关类型定义
// Add WebRTC related type definitions
declare global {
interface RTCSessionDescriptionInit {
type: RTCSdpType;