189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
/**
|
||
* 网络带宽监控模块
|
||
* 用于跟踪系统的上传和下载速度
|
||
*
|
||
* 注意:这里监控的是服务器整体的网络流量,不是单个用户的速度
|
||
* - 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)
|
||
}
|