gongxiangp2/lib/network-bandwidth-monitor.ts
2026-07-04 14:31:24 +08:00

189 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 网络带宽监控模块
* 用于跟踪系统的上传和下载速度
*
* 注意:这里监控的是服务器整体的网络流量,不是单个用户的速度
* - rxBytes(服务器接收)= 用户上传到服务器
* - txBytes(服务器发送)= 用户从服务器下载
*/
import { formatHumanSpeed } from './human-readable-transfer'
interface NetworkStats {
rxBytes: number // 接收字节数(用户上传)
txBytes: number // 发送字节数(用户下载)
timestamp: number
interface?: string // 使用的网卡名称
}
// 存储历史采样数据(用于计算平滑的速度)
const SAMPLE_HISTORY_SIZE = 5 // 保留最近5次采样
const sampleHistory: NetworkStats[] = []
// 缓存的网卡名称(避免每次都grep)
let cachedInterface: string | null = null
/**
* 获取主网卡名称
*/
async function getMainInterface(): Promise<string | null> {
if (cachedInterface) return cachedInterface
try {
const { exec } = await import('child_process')
const { promisify } = await import('util')
const execAsync = promisify(exec)
// 优先级:br0 > eth0 > ens* > enp* > 第一个非lo网卡
// br0 通常是桥接网卡,在Docker/虚拟化环境中代表宿主机网络
const priorities = ['br0', 'br-', 'eth0', 'ens', 'enp']
const { stdout } = await execAsync('cat /proc/net/dev')
const lines = stdout.split('\n').filter(l => l.includes(':') && !l.includes('lo:'))
for (const prefix of priorities) {
const found = lines.find(l => l.trim().startsWith(prefix))
if (found) {
cachedInterface = found.split(':')[0].trim()
console.log(`[NetworkBandwidth] 使用网卡: ${cachedInterface}`)
return cachedInterface
}
}
// 如果都没找到,用第一个非lo网卡
if (lines.length > 0) {
cachedInterface = lines[0].split(':')[0].trim()
console.log(`[NetworkBandwidth] 使用网卡(fallback): ${cachedInterface}`)
return cachedInterface
}
return null
} catch (error) {
console.warn('[NetworkBandwidth] 获取网卡名称失败:', error)
return null
}
}
/**
* 获取网络接口统计信息
*/
async function getNetworkStats(): Promise<NetworkStats | null> {
try {
const { exec } = await import('child_process')
const { promisify } = await import('util')
const execAsync = promisify(exec)
const interfaceName = await getMainInterface()
if (!interfaceName) return null
// 读取指定网卡的数据
const { stdout } = await execAsync(`cat /proc/net/dev | grep '${interfaceName}:'`)
// 格式: eth0: 123456 0 0 0 0 0 0 0 987654 0 0 0 0 0 0
// 接收字节数在第二个字段,发送字节数在第十个字段
const parts = stdout.trim().split(/\s+/)
if (parts.length >= 10) {
// 处理 "eth0:123456" 这种冒号后面没有空格的情况
let rxBytes: number
if (parts[0].includes(':')) {
const afterColon = parts[0].split(':')[1]
rxBytes = afterColon ? parseInt(afterColon) : parseInt(parts[1]) || 0
} else {
rxBytes = parseInt(parts[1]) || 0
}
const txBytes = parseInt(parts[9]) || 0
return {
rxBytes,
txBytes,
timestamp: Date.now(),
interface: interfaceName,
}
}
return null
} catch (error) {
console.warn('[NetworkBandwidth] 获取网络统计失败:', error)
return null
}
}
/**
* 计算网络速度(字节/秒)
* 使用滑动窗口平均,避免瞬时波动
*/
export async function getNetworkSpeed(): Promise<{
uploadSpeed: number // 上传速度(字节/秒)- 用户上传到服务器
downloadSpeed: number // 下载速度(字节/秒)- 用户从服务器下载
interface?: string // 使用的网卡
} | null> {
try {
const currentStats = await getNetworkStats()
if (!currentStats) return null
// 添加到历史记录
sampleHistory.push(currentStats)
// 保持历史记录大小
while (sampleHistory.length > SAMPLE_HISTORY_SIZE) {
sampleHistory.shift()
}
// 至少需要2个采样点才能计算速度
if (sampleHistory.length < 2) {
return {
uploadSpeed: 0,
downloadSpeed: 0,
interface: currentStats.interface,
}
}
// 使用最早和最新的采样计算平均速度
const oldest = sampleHistory[0]
const newest = sampleHistory[sampleHistory.length - 1]
const timeDiff = (newest.timestamp - oldest.timestamp) / 1000 // 秒
if (timeDiff <= 0) {
return {
uploadSpeed: 0,
downloadSpeed: 0,
interface: currentStats.interface,
}
}
const rxDiff = newest.rxBytes - oldest.rxBytes
const txDiff = newest.txBytes - oldest.txBytes
// 处理计数器溢出(虽然64位系统很少发生)
const uploadSpeed = rxDiff >= 0 ? rxDiff / timeDiff : 0
const downloadSpeed = txDiff >= 0 ? txDiff / timeDiff : 0
return {
uploadSpeed: Math.max(0, uploadSpeed),
downloadSpeed: Math.max(0, downloadSpeed),
interface: currentStats.interface,
}
} catch (error) {
console.warn('[NetworkBandwidth] 计算网速失败:', error)
return null
}
}
/**
* 从上传会话估算上传速度
* 通过计算最近上传的字节数来估算速度
*/
export function calculateUploadSpeedFromSessions(uploadSessions: Map<string, any>): number {
// 由于上传速度是实时变化的,且前端计算,这里暂时返回0
// 如果需要,可以通过跟踪每个会话的上传字节数变化来计算
// 但这样需要存储历史数据,比较复杂
return 0
}
/**
* 格式化速度(字节/秒 -> 用户友好单位)
* 使用字节单位(B/s、KB/s、MB/s),而非比特单位
*/
export function formatSpeed(bytesPerSecond: number): string {
return formatHumanSpeed(bytesPerSecond)
}