├── .env.example ├── assets ├── dsm.jpg ├── FlowMaster.jpg ├── FlowMaster1.1.2.jpg └── FlowMaster1.1.3.jpg ├── public ├── favicon.png ├── css │ ├── dark-theme.css │ └── main.css └── index.html ├── ecosystem.config.js ├── .gitignore ├── package.json ├── LICENSE ├── backup_vnstat.sh ├── install.sh ├── README.md └── server.js /.env.example: -------------------------------------------------------------------------------- 1 | # 服务器端口配置 2 | PORT=10089 3 | 4 | # 其他配置项 5 | NODE_ENV=production -------------------------------------------------------------------------------- /assets/dsm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbskycn/FlowMaster/HEAD/assets/dsm.jpg -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbskycn/FlowMaster/HEAD/public/favicon.png -------------------------------------------------------------------------------- /assets/FlowMaster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbskycn/FlowMaster/HEAD/assets/FlowMaster.jpg -------------------------------------------------------------------------------- /assets/FlowMaster1.1.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbskycn/FlowMaster/HEAD/assets/FlowMaster1.1.2.jpg -------------------------------------------------------------------------------- /assets/FlowMaster1.1.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbskycn/FlowMaster/HEAD/assets/FlowMaster1.1.3.jpg -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: "flowmaster", 4 | script: "server.js", 5 | env: { 6 | PORT: 10089 7 | } 8 | }] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # IDEs and editors 29 | .idea/ 30 | .vscode/ 31 | *.swp 32 | *.swo 33 | .DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flowmaster", 3 | "version": "1.1.16", 4 | "description": "专业的网络流量实时监控系统", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js" 9 | }, 10 | "keywords": [ 11 | "vnstat", 12 | "network", 13 | "monitor", 14 | "traffic", 15 | "flow" 16 | ], 17 | "author": "vbskycn", 18 | "license": "MIT", 19 | "dependencies": { 20 | "cors": "^2.8.5", 21 | "express": "^4.18.2" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^3.0.3" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/vbskycn/FlowMaster.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/vbskycn/FlowMaster/issues" 32 | }, 33 | "homepage": "https://github.com/vbskycn/FlowMaster#readme" 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 vbskycn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /public/css/dark-theme.css: -------------------------------------------------------------------------------- 1 | /* FlowMaster 暗色主题样式 */ 2 | 3 | body.dark .table { 4 | background-color: transparent; 5 | color: #f1f5fa; 6 | border-color: rgba(255,255,255,0.08); 7 | } 8 | body.dark .table-bordered > :not(caption) > * > * { 9 | border-color: rgba(255,255,255,0.08); 10 | } 11 | body.dark .table thead th, body.dark .table thead td { 12 | background-color: #23272e; 13 | color: #f1f5fa; 14 | border-bottom: 2px solid rgba(255,255,255,0.12); 15 | } 16 | body.dark .table tbody tr { 17 | background-color: #181b20 !important; 18 | } 19 | body.dark .table-striped > tbody > tr:nth-of-type(odd) { 20 | background-color: #23272e !important; 21 | } 22 | body.dark .table-striped > tbody > tr:nth-of-type(even) { 23 | background-color: #181b20 !important; 24 | } 25 | body.dark .table tbody td, body.dark .table tbody th { 26 | color: #fff; 27 | } 28 | body.dark .table th, body.dark .table td { 29 | border-color: rgba(255,255,255,0.08); 30 | } 31 | body.dark .table-hover > tbody > tr:hover { 32 | background-color: #23272e !important; 33 | } 34 | body.dark .table thead { 35 | border-bottom: 2px solid rgba(255,255,255,0.12); 36 | } 37 | body.dark .btn-dark, body.dark .btn-light { 38 | background-color: #23272e; 39 | color: #fff; 40 | border: 1px solid #3d3d3d; 41 | } 42 | body.dark .btn-dark:hover, body.dark .btn-light:hover { 43 | background-color: #31363f; 44 | color: #fff; 45 | } 46 | body.dark .form-select { 47 | background-color: #23272e; 48 | color: #fff; 49 | border-color: #3d3d3d; 50 | } 51 | body.dark .form-select:focus { 52 | background-color: #23272e; 53 | color: #fff; 54 | border-color: #0d6efd; 55 | } 56 | body.dark .form-check-input { 57 | background-color: #23272e; 58 | border-color: #3d3d3d; 59 | } 60 | body.dark .form-check-input:checked { 61 | background-color: #0d6efd; 62 | border-color: #0d6efd; 63 | } 64 | body.dark .version-badge { 65 | background-color: #23272e; 66 | border-color: #3d3d3d; 67 | color: #b0b8c1; 68 | } 69 | body.dark .footer { 70 | background-color: #1a1a1a; 71 | border-top-color: #2d2d2d; 72 | } 73 | body.dark .footer-text, 74 | body.dark .copyright-text, 75 | body.dark .author-link { 76 | color: #b0b8c1; 77 | } 78 | body.dark .author-link:hover, 79 | body.dark .github-link:hover { 80 | color: #fff; 81 | } 82 | body.dark ::-webkit-scrollbar { 83 | background: #23272e; 84 | } 85 | body.dark ::-webkit-scrollbar-thumb { 86 | background: #444; 87 | } 88 | body.dark .table td, body.dark .table th { 89 | background-color: inherit !important; 90 | color: #f1f5fa; 91 | } 92 | body.dark { 93 | background-color: #1a1a1a; 94 | color: #fff; 95 | } 96 | body.dark .card { 97 | background-color: #2d2d2d; 98 | border-color: #3d3d3d; 99 | } 100 | body.dark .card-header { 101 | background-color: #252525; 102 | border-bottom-color: #3d3d3d; 103 | } 104 | body.dark pre { 105 | background: #252525; 106 | color: #fff; 107 | } 108 | body.dark .text-muted { 109 | color: #a0a0a0 !important; 110 | } 111 | body.dark .version-badge { 112 | background-color: #2d2d2d; 113 | border-color: #3d3d3d; 114 | color: #a0a0a0; 115 | } 116 | body.dark .form-select { 117 | background-color: #2d2d2d; 118 | color: #fff; 119 | border-color: #3d3d3d; 120 | } 121 | body.dark .form-select:focus { 122 | background-color: #2d2d2d; 123 | color: #fff; 124 | border-color: #0d6efd; 125 | } 126 | body.dark .form-check-input { 127 | background-color: #2d2d2d; 128 | border-color: #3d3d3d; 129 | } 130 | body.dark .form-check-input:checked { 131 | background-color: #0d6efd; 132 | border-color: #0d6efd; 133 | } 134 | body.dark .footer { 135 | background-color: #1a1a1a; 136 | border-top-color: #2d2d2d; 137 | } 138 | body.dark .footer-text, 139 | body.dark .copyright-text, 140 | body.dark .author-link { 141 | color: #a0a0a0; 142 | } 143 | body.dark .author-link:hover, 144 | body.dark .github-link:hover { 145 | color: #fff; 146 | } -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | /* FlowMaster 主要样式文件 */ 2 | 3 | pre { 4 | background: #f8f9fa; 5 | padding: 15px; 6 | border-radius: 5px; 7 | white-space: pre-wrap; 8 | font-size: 14px; 9 | overflow-x: auto; 10 | transition: all 0.3s ease; 11 | } 12 | canvas { 13 | min-width: 200px; 14 | min-height: 100px; 15 | } 16 | .loading { 17 | position: relative; 18 | opacity: 0.7; 19 | } 20 | [v-cloak] { 21 | display: none; 22 | } 23 | .update-time { 24 | font-size: 12px; 25 | color: #6c757d; 26 | margin-top: 5px; 27 | } 28 | .refresh-control { 29 | margin-bottom: 15px; 30 | min-width: 150px; 31 | } 32 | .version-badge { 33 | background-color: #f8f9fa; 34 | color: #6c757d; 35 | border: 1px solid #dee2e6; 36 | padding: 0.35em 0.65em; 37 | font-size: 0.75em; 38 | font-weight: 500; 39 | border-radius: 0.25rem; 40 | } 41 | .loading::after { 42 | content: ""; 43 | position: absolute; 44 | top: 50%; 45 | left: 50%; 46 | width: 30px; 47 | height: 30px; 48 | margin: -15px 0 0 -15px; 49 | border: 3px solid #f3f3f3; 50 | border-top: 3px solid #3498db; 51 | border-radius: 50%; 52 | animation: spin 1s linear infinite; 53 | } 54 | @keyframes spin { 55 | 0% { transform: rotate(0deg); } 56 | 100% { transform: rotate(360deg); } 57 | } 58 | .card { 59 | transition: all 0.3s ease; 60 | } 61 | .card:hover { 62 | transform: translateY(-2px); 63 | box-shadow: 0 4px 8px rgba(0,0,0,0.1); 64 | } 65 | @keyframes update-flash { 66 | 0% { background-color: rgba(13, 110, 253, 0.1); } 67 | 100% { background-color: transparent; } 68 | } 69 | .data-updated { 70 | animation: update-flash 1s ease; 71 | } 72 | .form-select:focus, 73 | .form-check-input:focus, 74 | .github-link:focus { 75 | outline: 2px solid #0d6efd; 76 | outline-offset: 2px; 77 | } 78 | .text-muted { 79 | color: #666 !important; 80 | } 81 | .date-query .form-control { 82 | background-color: var(--input-bg, #fff); 83 | color: var(--input-color, #212529); 84 | border-color: var(--input-border, #dee2e6); 85 | } 86 | .date-query .btn-primary { 87 | width: 100%; 88 | } 89 | .card-body.py-2 { 90 | padding-top: 0.75rem !important; 91 | padding-bottom: 0.75rem !important; 92 | } 93 | .form-label.small { 94 | font-size: 0.875rem; 95 | margin-bottom: 0.25rem; 96 | } 97 | .form-control-sm { 98 | height: calc(1.5em + 0.5rem + 2px); 99 | padding: 0.25rem 0.5rem; 100 | font-size: 0.875rem; 101 | } 102 | .mb-3 { 103 | margin-bottom: 1rem !important; 104 | } 105 | .py-4 { 106 | padding-top: 1.5rem !important; 107 | padding-bottom: 1.5rem !important; 108 | } 109 | .footer { 110 | border-top: 1px solid #eee; 111 | padding: 15px 0; 112 | margin-top: auto; 113 | background-color: #fff; 114 | } 115 | .footer-content { 116 | display: flex; 117 | justify-content: space-between; 118 | align-items: center; 119 | } 120 | .footer-brand { 121 | display: flex; 122 | align-items: center; 123 | } 124 | .footer-text { 125 | color: #666; 126 | font-size: 14px; 127 | font-weight: 500; 128 | } 129 | .copyright-text { 130 | color: #666; 131 | font-size: 14px; 132 | } 133 | .author-link { 134 | color: #666; 135 | text-decoration: none; 136 | transition: color 0.2s ease; 137 | } 138 | .author-link:hover { 139 | color: #333; 140 | text-decoration: underline; 141 | } 142 | .github-link { 143 | color: #666; 144 | font-size: 14px; 145 | text-decoration: none; 146 | transition: color 0.2s ease; 147 | display: flex; 148 | align-items: center; 149 | gap: 5px; 150 | } 151 | .github-link i { 152 | font-size: 18px; 153 | } 154 | .github-link:hover { 155 | color: #333; 156 | } 157 | /* 响应式设计调整 */ 158 | @media (max-width: 768px) { 159 | .container { 160 | padding-left: 10px; 161 | padding-right: 10px; 162 | } 163 | h1 { 164 | font-size: 1.5rem; 165 | } 166 | .card-header { 167 | padding: 0.75rem; 168 | } 169 | .card-body { 170 | padding: 0.75rem; 171 | } 172 | pre { 173 | font-size: 11px; 174 | padding: 8px; 175 | } 176 | .version-badge { 177 | font-size: 0.7em; 178 | } 179 | .card { 180 | margin-bottom: 15px; 181 | } 182 | .footer { 183 | padding: 1rem 0; 184 | } 185 | .footer-content { 186 | flex-direction: column; 187 | align-items: center; 188 | gap: 10px; 189 | text-align: center; 190 | } 191 | .footer-brand { 192 | flex-direction: column; 193 | gap: 5px; 194 | } 195 | .copyright-text { 196 | margin-left: 0 !important; 197 | } 198 | .refresh-control { 199 | margin-top: 1rem; 200 | text-align: left !important; 201 | } 202 | .d-flex.justify-content-between { 203 | flex-direction: column; 204 | align-items: flex-start; 205 | } 206 | .date-query .col-md-4 { 207 | margin-bottom: 1rem; 208 | } 209 | } 210 | @media (min-width: 769px) and (max-width: 1024px) { 211 | pre { 212 | font-size: 13px; 213 | } 214 | .card { 215 | margin-bottom: 20px; 216 | } 217 | } 218 | @media (min-width: 769px) and (max-width: 1200px) { 219 | .container { 220 | max-width: 95%; 221 | } 222 | } 223 | @media (max-width: 576px) { 224 | .col-md-6 { 225 | width: 100%; 226 | } 227 | } 228 | @media (prefers-reduced-motion: reduce) { 229 | * { 230 | animation-duration: 0.01ms !important; 231 | animation-iteration-count: 1 !important; 232 | transition-duration: 0.01ms !important; 233 | scroll-behavior: auto !important; 234 | } 235 | } 236 | 237 | -------------------------------------------------------------------------------- /backup_vnstat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # vnstat数据备份和还原脚本 - 简化版 4 | # 使用方法: sudo ./backup_vnstat_simple.sh 5 | 6 | set -e 7 | 8 | # 定义颜色 9 | RED='\033[0;31m' 10 | GREEN='\033[0;32m' 11 | YELLOW='\033[1;33m' 12 | BLUE='\033[0;34m' 13 | NC='\033[0m' 14 | 15 | # 配置 16 | BACKUP_BASE_DIR="/backup/vnstat" 17 | VNSTAT_DATA_DIR="/var/lib/vnstat" 18 | LOG_FILE="/var/log/vnstat-backup.log" 19 | 20 | # 检查是否为root用户 21 | if [ "$EUID" -ne 0 ]; then 22 | echo -e "${RED}请使用 root 权限运行此脚本${NC}" 23 | exit 1 24 | fi 25 | 26 | # 显示菜单 27 | show_menu() { 28 | clear 29 | echo -e "${GREEN}================================${NC}" 30 | echo -e "${GREEN} vnstat 数据管理工具${NC}" 31 | echo -e "${GREEN}================================${NC}" 32 | echo -e "1) 备份 vnstat 数据" 33 | echo -e "2) 还原 vnstat 数据" 34 | echo -e "3) 列出所有备份" 35 | echo -e "4) 退出" 36 | echo 37 | echo -e "请选择操作 [1-4]: \c" 38 | } 39 | 40 | # 停止相关服务 41 | stop_services() { 42 | echo -e "${YELLOW}正在停止相关服务...${NC}" 43 | 44 | # 停止 FlowMaster 45 | if command -v pm2 &> /dev/null; then 46 | pm2 stop flowmaster 2>/dev/null || true 47 | echo "已停止 FlowMaster 服务" 48 | fi 49 | 50 | # 停止 vnstat 服务 51 | systemctl stop vnstat 2>/dev/null || service vnstat stop 2>/dev/null || true 52 | echo "已停止 vnstat 服务" 53 | } 54 | 55 | # 启动相关服务 56 | start_services() { 57 | echo -e "${YELLOW}正在启动相关服务...${NC}" 58 | 59 | # 启动 vnstat 服务 60 | systemctl start vnstat 2>/dev/null || service vnstat start 2>/dev/null || true 61 | echo "已启动 vnstat 服务" 62 | 63 | # 启动 FlowMaster 64 | if command -v pm2 &> /dev/null; then 65 | pm2 start flowmaster 2>/dev/null || true 66 | pm2 save 2>/dev/null || true 67 | echo "已启动 FlowMaster 服务" 68 | fi 69 | } 70 | 71 | # 备份函数 72 | backup_data() { 73 | local backup_dir="$BACKUP_BASE_DIR-$(date +%Y%m%d-%H%M%S)" 74 | 75 | echo -e "${GREEN}开始备份vnstat数据...${NC}" 76 | echo "$(date): 开始备份vnstat数据..." | tee -a $LOG_FILE 77 | mkdir -p $backup_dir 78 | 79 | # 检查vnstat数据目录是否存在 80 | if [ ! -d "$VNSTAT_DATA_DIR" ]; then 81 | echo -e "${RED}错误: vnstat数据目录不存在: $VNSTAT_DATA_DIR${NC}" 82 | echo "$(date): 错误: vnstat数据目录不存在: $VNSTAT_DATA_DIR" | tee -a $LOG_FILE 83 | return 1 84 | fi 85 | 86 | # 备份数据库文件 87 | echo -e "${YELLOW}备份数据库文件...${NC}" 88 | echo "$(date): 备份数据库文件..." | tee -a $LOG_FILE 89 | cp -r $VNSTAT_DATA_DIR/* $backup_dir/ 90 | 91 | # 导出文本格式数据 92 | echo -e "${YELLOW}导出文本格式数据...${NC}" 93 | echo "$(date): 导出文本格式数据..." | tee -a $LOG_FILE 94 | vnstat --dumpdb > $backup_dir/vnstat-dump.txt 2>/dev/null || echo "无法导出文本格式数据" 95 | 96 | # 显示备份信息 97 | echo -e "${GREEN}备份完成!${NC}" 98 | echo "$(date): 备份完成!" | tee -a $LOG_FILE 99 | echo -e "${BLUE}备份目录: $backup_dir${NC}" 100 | echo "备份文件:" 101 | ls -la $backup_dir | tee -a $LOG_FILE 102 | 103 | # 显示备份大小 104 | BACKUP_SIZE=$(du -sh $backup_dir | cut -f1) 105 | echo -e "${BLUE}备份大小: $BACKUP_SIZE${NC}" 106 | 107 | echo "$(date): 备份完成,备份位置: $backup_dir" | tee -a $LOG_FILE 108 | } 109 | 110 | # 还原函数 111 | restore_data() { 112 | echo -e "${GREEN}可用的备份列表:${NC}" 113 | 114 | # 列出所有备份目录 115 | local backup_parent_dir="/backup" 116 | if [ ! -d "$backup_parent_dir" ]; then 117 | echo -e "${RED}没有找到备份目录: $backup_parent_dir${NC}" 118 | return 1 119 | fi 120 | 121 | local backups=($(ls -1td $backup_parent_dir/vnstat-* 2>/dev/null | head -10)) 122 | 123 | if [ ${#backups[@]} -eq 0 ]; then 124 | echo -e "${RED}没有找到任何备份${NC}" 125 | return 1 126 | fi 127 | 128 | echo -e "${BLUE}最近的备份:${NC}" 129 | for i in "${!backups[@]}"; do 130 | local backup_name=$(basename ${backups[$i]}) 131 | local backup_time=$(echo $backup_name | sed 's/.*-\([0-9]\{8\}-[0-9]\{6\}\)/\1/') 132 | local backup_size=$(du -sh ${backups[$i]} | cut -f1) 133 | echo -e "$((i+1))) $backup_name (大小: $backup_size, 时间: $backup_time)" 134 | done 135 | 136 | echo 137 | echo -e "请选择要还原的备份 [1-${#backups[@]}]: \c" 138 | read choice 139 | 140 | if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then 141 | echo -e "${RED}无效的选择${NC}" 142 | return 1 143 | fi 144 | 145 | local selected_backup=${backups[$((choice-1))]} 146 | 147 | echo -e "${YELLOW}确认要还原备份: $(basename $selected_backup) ? [y/N]: \c" 148 | read confirm 149 | 150 | if [[ ! "$confirm" =~ ^[Yy]$ ]]; then 151 | echo -e "${YELLOW}取消还原操作${NC}" 152 | return 0 153 | fi 154 | 155 | # 停止服务 156 | stop_services 157 | 158 | # 备份当前数据 159 | if [ -d "$VNSTAT_DATA_DIR" ]; then 160 | local current_backup="/var/lib/vnstat.backup.$(date +%Y%m%d-%H%M%S)" 161 | echo -e "${YELLOW}备份当前数据到: $current_backup${NC}" 162 | mv $VNSTAT_DATA_DIR $current_backup 163 | fi 164 | 165 | # 恢复数据 166 | echo -e "${YELLOW}正在恢复数据...${NC}" 167 | mkdir -p $VNSTAT_DATA_DIR 168 | cp -r $selected_backup/* $VNSTAT_DATA_DIR/ 169 | 170 | # 设置正确的权限 171 | chown -R vnstat:vnstat $VNSTAT_DATA_DIR 2>/dev/null || chown -R root:root $VNSTAT_DATA_DIR 172 | chmod -R 755 $VNSTAT_DATA_DIR 173 | 174 | # 启动服务 175 | start_services 176 | 177 | echo -e "${GREEN}数据恢复完成!${NC}" 178 | echo "$(date): 数据恢复完成,使用备份: $selected_backup" | tee -a $LOG_FILE 179 | 180 | # 验证数据 181 | echo -e "${BLUE}验证数据:${NC}" 182 | echo "检查vnstat数据库文件:" 183 | ls -la $VNSTAT_DATA_DIR/ 184 | echo 185 | echo "检查vnstat服务状态:" 186 | systemctl status vnstat --no-pager -l 187 | echo 188 | echo "检查可用的网络接口:" 189 | vnstat --iflist 2>/dev/null || echo "无法获取网络接口列表" 190 | } 191 | 192 | # 列出所有备份 193 | list_backups() { 194 | echo -e "${GREEN}所有备份列表:${NC}" 195 | 196 | local backup_parent_dir="/backup" 197 | if [ ! -d "$backup_parent_dir" ]; then 198 | echo -e "${RED}没有找到备份目录: $backup_parent_dir${NC}" 199 | return 1 200 | fi 201 | 202 | local backups=($(ls -1td $backup_parent_dir/vnstat-* 2>/dev/null)) 203 | 204 | if [ ${#backups[@]} -eq 0 ]; then 205 | echo -e "${RED}没有找到任何备份${NC}" 206 | return 1 207 | fi 208 | 209 | echo -e "${BLUE}备份列表 (按时间倒序):${NC}" 210 | printf "%-4s %-30s %-10s %-20s\n" "序号" "备份名称" "大小" "修改时间" 211 | echo "----------------------------------------------------------------" 212 | 213 | for i in "${!backups[@]}"; do 214 | local backup_name=$(basename ${backups[$i]}) 215 | local backup_size=$(du -sh ${backups[$i]} | cut -f1) 216 | local backup_time=$(stat -c %y ${backups[$i]} | cut -d' ' -f1,2 | cut -d'.' -f1) 217 | printf "%-4s %-30s %-10s %-20s\n" "$((i+1))" "$backup_name" "$backup_size" "$backup_time" 218 | done 219 | } 220 | 221 | # 主程序 222 | main() { 223 | while true; do 224 | show_menu 225 | read choice 226 | 227 | case $choice in 228 | 1) 229 | backup_data 230 | echo 231 | echo -e "${GREEN}按任意键继续...${NC}" 232 | read -n 1 -r 233 | ;; 234 | 2) 235 | restore_data 236 | echo 237 | echo -e "${GREEN}按任意键继续...${NC}" 238 | read -n 1 -r 239 | ;; 240 | 3) 241 | list_backups 242 | echo 243 | echo -e "${GREEN}按任意键继续...${NC}" 244 | read -n 1 -r 245 | ;; 246 | 4) 247 | echo -e "${GREEN}退出程序${NC}" 248 | exit 0 249 | ;; 250 | *) 251 | echo -e "${RED}无效的选择,请重新选择${NC}" 252 | sleep 2 253 | ;; 254 | esac 255 | done 256 | } 257 | 258 | # 执行主程序 259 | main 260 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 定义颜色 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[1;33m' 7 | NC='\033[0m' 8 | 9 | # 检查是否为root用户 10 | if [ "$EUID" -ne 0 ]; then 11 | echo -e "${RED}请使用 root 权限运行此脚本${NC}" 12 | exit 1 13 | fi 14 | 15 | # 检查是否已安装 16 | check_installation() { 17 | if [ -d "/opt/flowmaster" ] || command -v flowmaster &> /dev/null; then 18 | return 0 # 已安装 19 | else 20 | return 1 # 未安装 21 | fi 22 | } 23 | 24 | # 显示菜单 25 | show_menu() { 26 | local is_installed=$1 27 | 28 | echo -e "${GREEN}================================${NC}" 29 | echo -e "${GREEN} FlowMaster 管理菜单v1.1.16${NC}" 30 | echo -e "${GREEN}================================${NC}" 31 | 32 | if [ "$is_installed" = "true" ]; then 33 | echo -e "1) 重新安装 FlowMaster" 34 | echo -e "2) 卸载 FlowMaster" 35 | echo -e "3) 更新 FlowMaster" 36 | echo -e "4) 退出脚本" 37 | echo 38 | echo -e "检测到系统已安装 FlowMaster" 39 | else 40 | echo -e "1) 安装 FlowMaster" 41 | echo -e "2) 卸载 FlowMaster" 42 | echo -e "3) 退出脚本" 43 | echo 44 | echo -e "系统未安装 FlowMaster" 45 | fi 46 | 47 | echo -e "请选择操作: " 48 | read choice 49 | echo "$choice" 50 | } 51 | 52 | # 卸载函数 53 | uninstall() { 54 | echo -e "\n${YELLOW}正在卸载 FlowMaster...${NC}" 55 | 56 | # 停止和删除 PM2 实例 57 | if command -v pm2 &> /dev/null; then 58 | pm2 stop flowmaster 2>/dev/null || true 59 | pm2 delete flowmaster 2>/dev/null || true 60 | pm2 save 61 | fi 62 | 63 | # 删除安装目录 64 | rm -rf /opt/flowmaster 65 | 66 | # 删除控制脚本 67 | rm -f /usr/local/bin/flowmaster 68 | 69 | # 清理 vnstat 数据库 70 | systemctl stop vnstat 71 | rm -f /var/lib/vnstat/* 72 | 73 | echo -e "${GREEN}FlowMaster 已成功卸载!${NC}" 74 | } 75 | 76 | # 函数:检查并安装依赖 77 | check_and_install() { 78 | if ! command -v $1 &> /dev/null; then 79 | echo -e "${YELLOW}正在安装 $1...${NC}" 80 | if [ -x "$(command -v apt-get)" ]; then 81 | # 首先尝试修复可能的 dpkg 中断问题 82 | dpkg --configure -a || true 83 | 84 | # 更新包列表 85 | apt-get update 86 | 87 | # 尝试安装 88 | if ! apt-get install -y $1; then 89 | echo -e "${RED}安装 $1 失败,尝试修复依赖关系...${NC}" 90 | # 尝试修复依赖关系 91 | apt-get -f install -y 92 | # 重新尝试安装 93 | apt-get install -y $1 94 | fi 95 | elif [ -x "$(command -v yum)" ]; then 96 | yum install -y $1 97 | else 98 | echo -e "${RED}无法确定包管理器,请手动安装 $1${NC}" 99 | exit 1 100 | fi 101 | else 102 | echo -e "${GREEN}$1 已安装${NC}" 103 | fi 104 | } 105 | 106 | # 安装基本依赖 107 | install_dependencies() { 108 | echo -e "\n${GREEN}[1/6] 检查并安装系统依赖...${NC}" 109 | 110 | # 修复可能的包管理器问题 111 | if [ -x "$(command -v apt-get)" ]; then 112 | echo -e "${YELLOW}检查并修复包管理器状态...${NC}" 113 | dpkg --configure -a || true 114 | apt-get update 115 | apt-get -f install -y 116 | fi 117 | 118 | # 检查并安装必要的包 119 | check_and_install "vnstat" 120 | check_and_install "curl" 121 | check_and_install "nodejs" 122 | check_and_install "npm" 123 | check_and_install "bc" 124 | check_and_install "expect" 125 | 126 | # 确保 vnstat 服务正常运行 127 | echo -e "${YELLOW}正在启动 vnstat 服务...${NC}" 128 | systemctl start vnstat || true 129 | systemctl enable vnstat || true 130 | 131 | # 等待服务启动 132 | sleep 2 133 | 134 | detect_network_interface 135 | } 136 | 137 | # 在 install_dependencies 函数中添加以下内容 138 | detect_network_interface() { 139 | echo -e "\n${GREEN}检测网络接口...${NC}" 140 | 141 | # 获取所有活动接口,排除 lo 和 docker/veth 接口 142 | PHYSICAL_INTERFACES=$(ip -o link show up | grep -v -E "lo:|veth|docker|br-|cni" | awk -F': ' '{print $2}') 143 | 144 | # 如果没有找到物理接口,再获取所有接口(包括虚拟接口) 145 | if [ -z "$PHYSICAL_INTERFACES" ]; then 146 | PHYSICAL_INTERFACES=$(ip -o link show up | grep -v "lo:" | awk -F': ' '{print $2}') 147 | fi 148 | 149 | # 创建优先级数组 150 | declare -A INTERFACE_PRIORITY 151 | 152 | # 遍历所有接口并设置优先级 153 | for interface in $PHYSICAL_INTERFACES; do 154 | # 初始化优先级为0 155 | priority=0 156 | 157 | # eth* 接口优先级最高 158 | if [[ $interface =~ ^eth[0-9]+ ]]; then 159 | priority=1000 160 | # ens* 接口次之 161 | elif [[ $interface =~ ^ens[0-9]+ ]]; then 162 | priority=900 163 | # en* 接口再次之 164 | elif [[ $interface =~ ^en[0-9]+ ]]; then 165 | priority=800 166 | # bond* 接口 167 | elif [[ $interface =~ ^bond[0-9]+ ]]; then 168 | priority=700 169 | # 其他物理接口 170 | else 171 | priority=100 172 | fi 173 | 174 | # 检查接口是否有流量数据并且是否能ping通外网 175 | if ping -c 1 -W 1 -I "$interface" 8.8.8.8 >/dev/null 2>&1; then 176 | priority=$((priority + 500)) 177 | fi 178 | 179 | # 检查接口速率 180 | SPEED=$(cat /sys/class/net/$interface/speed 2>/dev/null) 181 | if [ ! -z "$SPEED" ] && [ "$SPEED" -gt 0 ]; then 182 | priority=$((priority + SPEED)) 183 | fi 184 | 185 | INTERFACE_PRIORITY[$interface]=$priority 186 | echo -e "${YELLOW}接口 $interface 优先级: $priority${NC}" 187 | done 188 | 189 | # 根据优先级选择接口 190 | SELECTED_INTERFACE="" 191 | HIGHEST_PRIORITY=0 192 | 193 | for interface in "${!INTERFACE_PRIORITY[@]}"; do 194 | priority=${INTERFACE_PRIORITY[$interface]} 195 | if [ $priority -gt $HIGHEST_PRIORITY ]; then 196 | HIGHEST_PRIORITY=$priority 197 | SELECTED_INTERFACE=$interface 198 | fi 199 | done 200 | 201 | # 如果还是没有找到合适的接口,使用默认接口 202 | if [ -z "$SELECTED_INTERFACE" ]; then 203 | # 尝试找到默认路由接口 204 | DEFAULT_ROUTE_INTERFACE=$(ip route | grep default | awk '{print $5}' | head -n 1) 205 | if [ ! -z "$DEFAULT_ROUTE_INTERFACE" ]; then 206 | SELECTED_INTERFACE=$DEFAULT_ROUTE_INTERFACE 207 | else 208 | # 如果还是找不到,使用第一个非lo接口 209 | SELECTED_INTERFACE=$(ip -o link show up | grep -v "lo:" | awk -F': ' '{print $2}' | head -n 1) 210 | fi 211 | fi 212 | 213 | if [ -n "$SELECTED_INTERFACE" ]; then 214 | echo -e "${GREEN}检测到网络接口: ${SELECTED_INTERFACE}${NC}" 215 | 216 | # 停止 vnstat 服务 217 | systemctl stop vnstat 218 | 219 | # 删除旧数据库 220 | rm -f /var/lib/vnstat/* 221 | 222 | # 获取 vnstat 版本 223 | VNSTAT_VERSION=$(vnstat --version | head -n1 | awk '{print $2}') 224 | echo -e "${GREEN}检测到 vnstat 版本: ${VNSTAT_VERSION}${NC}" 225 | 226 | # 初始化数据库 227 | if vnstat --add -i "$SELECTED_INTERFACE" &>/dev/null; then 228 | echo -e "${GREEN}使用新版本命令初始化接口${NC}" 229 | elif vnstat -u -i "$SELECTED_INTERFACE" &>/dev/null; then 230 | echo -e "${GREEN}使用旧版本命令初始化接口${NC}" 231 | else 232 | echo -e "${GREEN}尝试直接创建接口${NC}" 233 | systemctl restart vnstat 234 | fi 235 | 236 | # 修改配置文件以加快数据收集 237 | if [ -f "/etc/vnstat.conf" ]; then 238 | cp /etc/vnstat.conf /etc/vnstat.conf.bak 239 | echo -e "${GREEN}备份原配置文件到 /etc/vnstat.conf.bak${NC}" 240 | 241 | # 更新配置 242 | sed -i 's/^UpdateInterval.*/UpdateInterval 30/' /etc/vnstat.conf 243 | sed -i 's/^SaveInterval.*/SaveInterval 60/' /etc/vnstat.conf 244 | 245 | # 确保接口在配置文件中 246 | if ! grep -q "^Interface \"$SELECTED_INTERFACE\"" /etc/vnstat.conf; then 247 | echo "Interface \"$SELECTED_INTERFACE\"" >> /etc/vnstat.conf 248 | fi 249 | 250 | echo -e "${GREEN}已更新配置文件${NC}" 251 | fi 252 | 253 | # 重启服务 254 | systemctl restart vnstat 255 | 256 | # 等待初始数据收集 257 | echo -e "${YELLOW}等待初始数据收集(约1分钟)...${NC}" 258 | sleep 60 259 | 260 | # 验证接口是否正常工作 261 | if vnstat -i "$SELECTED_INTERFACE" &>/dev/null; then 262 | echo -e "${GREEN}接口 ${SELECTED_INTERFACE} 已成功初始化${NC}" 263 | else 264 | echo -e "${RED}警告:接口初始化可能不完整,但这不影响继续安装${NC}" 265 | fi 266 | 267 | else 268 | echo -e "${RED}未检测到活动的网络接口${NC}" 269 | exit 1 270 | fi 271 | } 272 | 273 | # 安装 PM2 274 | install_pm2() { 275 | echo -e "\n${GREEN}[2/6] 安装 PM2...${NC}" 276 | if ! command -v pm2 &> /dev/null; then 277 | npm install -g pm2 278 | else 279 | echo -e "${GREEN}PM2 已安装${NC}" 280 | fi 281 | } 282 | 283 | # 安装 FlowMaster 284 | install_flowmaster() { 285 | echo -e "\n${GREEN}[3/6] 安装 FlowMaster...${NC}" 286 | 287 | # 创建安装目录 288 | mkdir -p /opt/flowmaster 289 | cd /opt/flowmaster 290 | 291 | # 下载项目文件 292 | echo -e "${YELLOW}下载项目文件...${NC}" 293 | curl -L https://github.zhoujie218.top/https://github.com/vbskycn/FlowMaster/archive/main.tar.gz | tar xz --strip-components=1 294 | 295 | # 安装依赖 296 | echo -e "${YELLOW}安装项目依赖...${NC}" 297 | npm install 298 | } 299 | 300 | # 配置 PM2 301 | setup_pm2() { 302 | echo -e "\n${GREEN}[4/6] 配置 PM2...${NC}" 303 | 304 | # 停止已存在的实例 305 | pm2 stop flowmaster 2>/dev/null || true 306 | pm2 delete flowmaster 2>/dev/null || true 307 | 308 | # 启动新实例 309 | cd /opt/flowmaster 310 | pm2 start server.js --name flowmaster 311 | 312 | # 保存 PM2 配置 313 | pm2 save 314 | 315 | # 设置开机自启 316 | pm2 startup 317 | } 318 | 319 | # 创建服务控制脚本 320 | create_control_script() { 321 | echo -e "\n${GREEN}[5/6] 创建控制脚本...${NC}" 322 | 323 | cat > /usr/local/bin/flowmaster << 'EOF' 324 | #!/bin/bash 325 | case "$1" in 326 | start) 327 | pm2 start flowmaster 328 | ;; 329 | stop) 330 | pm2 stop flowmaster 331 | ;; 332 | restart) 333 | pm2 restart flowmaster 334 | ;; 335 | status) 336 | pm2 show flowmaster 337 | ;; 338 | uninstall) 339 | pm2 stop flowmaster 340 | pm2 delete flowmaster 341 | rm -rf /opt/flowmaster 342 | rm -f /usr/local/bin/flowmaster 343 | echo "FlowMaster 已卸载" 344 | ;; 345 | *) 346 | echo "用法: flowmaster {start|stop|restart|status|uninstall}" 347 | exit 1 348 | ;; 349 | esac 350 | EOF 351 | 352 | chmod +x /usr/local/bin/flowmaster 353 | } 354 | 355 | # 完成安装 356 | finish_installation() { 357 | echo -e "\n${GREEN}[6/6] 完成安装...${NC}" 358 | echo -e "\n${GREEN}FlowMaster 安装完成!${NC}" 359 | echo -e "\n使用方法:" 360 | echo -e "${YELLOW}启动: ${NC}flowmaster start" 361 | echo -e "${YELLOW}停止: ${NC}flowmaster stop" 362 | echo -e "${YELLOW}重启: ${NC}flowmaster restart" 363 | echo -e "${YELLOW}状态: ${NC}flowmaster status" 364 | echo -e "${YELLOW}卸载: ${NC}flowmaster uninstall" 365 | 366 | # 获取服务器IP地址 367 | # 首先尝试获取外网IP 368 | PUBLIC_IP=$(curl -s -4 ip.sb || curl -s -4 ifconfig.me || curl -s -4 api.ipify.org) 369 | 370 | if [ -n "$PUBLIC_IP" ]; then 371 | echo -e "\n${GREEN}访问地址: http://${PUBLIC_IP}:10089${NC}" 372 | else 373 | # 如果无法获取外网IP,则尝试获取内网IP 374 | INTERNAL_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -n 1) 375 | if [ -n "$INTERNAL_IP" ]; then 376 | echo -e "\n${GREEN}访问地址: http://${INTERNAL_IP}:10089${NC}" 377 | echo -e "${YELLOW}注意:这是内网地址,如需外网访问请使用服务器公网IP${NC}" 378 | else 379 | echo -e "\n${RED}无法获取服务器IP地址,请手动使用服务器IP访问端口10089${NC}" 380 | fi 381 | fi 382 | } 383 | 384 | # 更新函数的具体实现 385 | update_flowmaster() { 386 | echo -e "\n${YELLOW}正在更新 FlowMaster...${NC}" 387 | 388 | # 1. 停止当前运行的服务 389 | pm2 stop flowmaster 2>/dev/null || true 390 | 391 | # 2. 创建临时目录用于更新 392 | TMP_DIR=$(mktemp -d) 393 | cd "$TMP_DIR" 394 | 395 | # 3. 下载最新代码到临时目录 396 | echo -e "${YELLOW}下载最新代码...${NC}" 397 | curl -L https://github.zhoujie218.top/https://github.com/vbskycn/FlowMaster/archive/main.tar.gz | tar xz --strip-components=1 398 | 399 | # 4. 备份重要文件 400 | echo -e "${YELLOW}备份配置文件...${NC}" 401 | if [ -f "/opt/flowmaster/config.js" ]; then 402 | cp /opt/flowmaster/config.js "$TMP_DIR/config.js.bak" 403 | fi 404 | 405 | # 5. 保留vnstat数据库 406 | echo -e "${YELLOW}保留vnstat数据...${NC}" 407 | if [ -d "/opt/flowmaster/vnstat" ]; then 408 | cp -r /opt/flowmaster/vnstat "$TMP_DIR/vnstat.bak" 409 | fi 410 | 411 | # 6. 更新文件 412 | echo -e "${YELLOW}更新文件...${NC}" 413 | # 删除旧文件,但保留vnstat目录 414 | find /opt/flowmaster -mindepth 1 ! -name 'vnstat' ! -path '/opt/flowmaster/vnstat/*' -delete 415 | 416 | # 复制新文件,排除vnstat目录 417 | cp -r "$TMP_DIR"/* /opt/flowmaster/ 2>/dev/null || true 418 | 419 | # 7. 恢复备份的文件 420 | echo -e "${YELLOW}恢复配置文件...${NC}" 421 | if [ -f "$TMP_DIR/config.js.bak" ]; then 422 | mv "$TMP_DIR/config.js.bak" /opt/flowmaster/config.js 423 | fi 424 | if [ -d "$TMP_DIR/vnstat.bak" ]; then 425 | rm -rf /opt/flowmaster/vnstat 426 | mv "$TMP_DIR/vnstat.bak" /opt/flowmaster/vnstat 427 | fi 428 | 429 | # 8. 清理临时目录 430 | rm -rf "$TMP_DIR" 431 | 432 | # 9. 更新依赖 433 | cd /opt/flowmaster 434 | echo -e "${YELLOW}更新项目依赖...${NC}" 435 | npm install 436 | 437 | # 10. 重启服务 438 | echo -e "${YELLOW}重启服务...${NC}" 439 | pm2 restart flowmaster 440 | pm2 save 441 | 442 | echo -e "${GREEN}FlowMaster 更新完成!${NC}" 443 | } 444 | 445 | # 修改主程序入口 446 | main() { 447 | local is_installed=false 448 | if check_installation; then 449 | is_installed=true 450 | fi 451 | 452 | # 显示菜单 453 | echo -e "${GREEN}================================${NC}" 454 | echo -e "${GREEN} FlowMaster 管理菜单v1.1.16${NC}" 455 | echo -e "${GREEN}================================${NC}" 456 | 457 | if [ "$is_installed" = "true" ]; then 458 | echo -e "1) 重新安装 FlowMaster" 459 | echo -e "2) 卸载 FlowMaster" 460 | echo -e "3) 更新 FlowMaster" 461 | echo -e "4) 退出脚本" 462 | echo 463 | echo -e "检测到系统已安装 FlowMaster" 464 | else 465 | echo -e "1) 安装 FlowMaster" 466 | echo -e "2) 卸载 FlowMaster" 467 | echo -e "3) 退出脚本" 468 | echo 469 | echo -e "系统未安装 FlowMaster" 470 | fi 471 | 472 | echo -e "请选择操作 [1-4]: " 473 | read choice 474 | 475 | # 处理选择 476 | if [ "$is_installed" = "true" ]; then 477 | case $choice in 478 | 1) 479 | echo -e "\n${YELLOW}准备重新安装 FlowMaster...${NC}" 480 | uninstall 481 | echo -e "\n${GREEN}开始新安装...${NC}" 482 | sleep 2 483 | install_dependencies 484 | install_pm2 485 | install_flowmaster 486 | setup_pm2 487 | create_control_script 488 | finish_installation 489 | ;; 490 | 2) 491 | uninstall 492 | ;; 493 | 3) 494 | update_flowmaster 495 | ;; 496 | 4) 497 | echo -e "\n${GREEN}退出程序${NC}" 498 | exit 0 499 | ;; 500 | *) 501 | echo -e "\n${YELLOW}无效的选择,请重新运行脚本${NC}" 502 | exit 1 503 | ;; 504 | esac 505 | else 506 | case $choice in 507 | 1) 508 | echo -e "\n${GREEN}开始安装 FlowMaster...${NC}" 509 | install_dependencies 510 | install_pm2 511 | install_flowmaster 512 | setup_pm2 513 | create_control_script 514 | finish_installation 515 | ;; 516 | 2) 517 | echo -e "\n${GREEN}系统未安装,无需卸载${NC}" 518 | ;; 519 | 3) 520 | echo -e "\n${GREEN}退出程序${NC}" 521 | exit 0 522 | ;; 523 | *) 524 | echo -e "\n${YELLOW}无效的选择,请重新运行脚本${NC}" 525 | exit 1 526 | ;; 527 | esac 528 | fi 529 | } 530 | 531 | # 执行主程序 532 | main -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlowMaster - 专业的网络流量实时监控系统 2 | 3 | [](https://github.com/vbskycn/FlowMaster) 4 | [](LICENSE) 5 | [](https://nodejs.org/) 6 | [](https://vuejs.org/) 7 | 8 |  9 | 10 | ## 📋 目录 11 | 12 | - [项目介绍](#项目介绍) 13 | - [✨ 核心特性](#-核心特性) 14 | - [🛠️ 技术架构](#️-技术架构) 15 | - [📦 快速开始](#-快速开始) 16 | - [🔧 配置说明](#-配置说明) 17 | - [📚 API 文档](#-api-文档) 18 | - [🚀 高级功能](#-高级功能) 19 | - [🔍 故障排除](#-故障排除) 20 | - [⚡ 性能优化](#-性能优化) 21 | - [👨💻 开发指南](#-开发指南) 22 | - [🤝 贡献指南](#-贡献指南) 23 | - [📄 开源协议](#-开源协议) 24 | 25 | ## 📝 项目介绍 26 | 27 | FlowMaster 是一个基于 **vnstat** 的专业网络流量监控系统,采用现代化的 Web 技术栈构建。系统提供实时流量监控、多维度数据分析、智能缓存管理、性能监控等高级功能,让网络流量监控变得简单而强大。 28 | 29 | ### 🎯 设计理念 30 | 31 | - **高性能**: 智能缓存系统,LRU算法优化,内存使用监控 32 | - **高可用**: 完善的错误处理,自动诊断,降级机制 33 | - **易扩展**: 模块化设计,RESTful API,支持多网卡 34 | - **用户友好**: 响应式界面,深色主题,实时更新 35 | 36 | ## ✨ 核心特性 37 | 38 | ### 🚀 实时监控 39 | - **实时流量图表**: 基于Chart.js的动态图表,支持实时数据更新 40 | - **速度监控**: 接收/发送速度实时显示(kb/秒) 41 | - **数据包统计**: 实时数据包传输统计 42 | - **自动刷新**: 可配置的自动刷新间隔(1秒-60秒) 43 | 44 | ### 📊 多维度统计 45 | - **分钟统计**: 最近5分钟流量数据,30秒缓存 46 | - **小时统计**: 最近12小时数据,1分钟缓存 47 | - **日统计**: 最近12天数据,2分钟缓存 48 | - **月统计**: 最近30天数据,5分钟缓存 49 | - **年统计**: 年度数据,10分钟缓存 50 | 51 | ### 🌐 智能网卡管理 52 | - **自动检测**: 自动发现和验证可用网络接口 53 | - **优先级排序**: 物理网卡优先,虚拟接口靠后 54 | - **多接口支持**: 支持eth、ens、enp、wlan、bond、docker等接口 55 | - **接口验证**: 自动验证接口有效性 56 | 57 | ### 💾 智能缓存系统 58 | - **LRU缓存**: 最近最少使用算法 59 | - **内存监控**: 实时监控缓存内存使用 60 | - **缓存统计**: 命中率、缓存条目、内存使用统计 61 | - **自动清理**: 定期清理过期缓存 62 | - **可配置**: 支持自定义缓存大小和内存限制 63 | 64 | ### 🔍 系统监控 65 | - **性能监控**: 响应时间、请求计数、平均响应时间 66 | - **内存监控**: RSS、堆内存、外部内存使用 67 | - **服务器状态**: 运行时间、平台信息、vnstat状态 68 | - **诊断功能**: 自动诊断连接问题和vnstat配置 69 | 70 | ### 🎨 用户界面 71 | - **响应式设计**: 完美适配桌面和移动设备 72 | - **深色主题**: 支持深色/浅色主题切换 73 | - **实时图表**: 基于Chart.js的交互式图表 74 | - **数据表格**: 格式化的数据展示 75 | - **加载状态**: 优雅的加载动画 76 | 77 | ### 📅 高级查询 78 | - **日期范围查询**: 自定义时间段数据查询 79 | - **单位统一**: 自动单位转换和归一化 80 | - **数据过滤**: 智能过滤无效数据 81 | - **图表渲染**: 查询结果图表展示 82 | 83 | ## 🛠️ 技术架构 84 | 85 | ### 后端技术栈 86 | | 组件 | 技术 | 版本 | 说明 | 87 | |------|------|------|------| 88 | | **运行时** | Node.js | 14.0.0+ | JavaScript运行时环境 | 89 | | **Web框架** | Express.js | 4.18.2+ | 轻量级Web应用框架 | 90 | | **跨域处理** | CORS | 2.8.5+ | 跨域资源共享 | 91 | | **进程管理** | PM2 | 最新 | 生产环境进程管理器 | 92 | | **监控工具** | vnstat | 2.0.0+ | 网络流量监控工具 | 93 | 94 | ### 前端技术栈 95 | | 组件 | 技术 | 版本 | 说明 | 96 | |------|------|------|------| 97 | | **框架** | Vue.js | 3.2.31+ | 渐进式JavaScript框架 | 98 | | **UI框架** | Bootstrap | 5.1.3+ | 响应式CSS框架 | 99 | | **图表库** | Chart.js | 4.4.1+ | 交互式图表库 | 100 | | **HTTP客户端** | Axios | 0.26.0+ | Promise-based HTTP客户端 | 101 | | **图标库** | Bootstrap Icons | 1.8.0+ | 图标字体库 | 102 | 103 | ### 系统架构 104 | ``` 105 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 106 | │ 前端界面 │ │ API网关 │ │ vnstat工具 │ 107 | │ (Vue.js) │◄──►│ (Express) │◄──►│ (系统命令) │ 108 | └─────────────────┘ └─────────────────┘ └─────────────────┘ 109 | │ 110 | ▼ 111 | ┌─────────────────┐ 112 | │ 缓存系统 │ 113 | │ (LRU Cache) │ 114 | └─────────────────┘ 115 | ``` 116 | 117 | ## 📦 快速开始 118 | 119 | ### 环境要求 120 | 121 | | 组件 | 最低版本 | 推荐版本 | 说明 | 122 | |------|----------|----------|------| 123 | | **Node.js** | 14.0.0 | 18.0.0+ | JavaScript运行时 | 124 | | **vnstat** | 2.0.0 | 2.10+ | 网络监控工具 | 125 | | **npm** | 6.0.0 | 8.0.0+ | 包管理器 | 126 | | **操作系统** | Linux | Ubuntu 20.04+ | 支持vnstat的系统 | 127 | 128 | ### 🚀 一键部署 129 | 130 | **国际网络:** 131 | ```bash 132 | curl -o install.sh https://raw.githubusercontent.com/vbskycn/FlowMaster/main/install.sh && chmod +x install.sh && sudo ./install.sh 133 | ``` 134 | 135 | **国内网络:** 136 | ```bash 137 | curl -o install.sh https://gh-proxy.com/https://raw.githubusercontent.com/vbskycn/FlowMaster/main/install.sh && chmod +x install.sh && sudo ./install.sh 138 | ``` 139 | 140 | ### 📋 服务管理命令 141 | 142 | ```bash 143 | # 启动服务 144 | flowmaster start 145 | 146 | # 停止服务 147 | flowmaster stop 148 | 149 | # 重启服务 150 | flowmaster restart 151 | 152 | # 查看状态 153 | flowmaster status 154 | 155 | # 查看日志 156 | flowmaster logs 157 | 158 | # 卸载服务 159 | flowmaster uninstall 160 | ``` 161 | 162 | ### 🌐 访问系统 163 | 164 | 安装完成后,通过浏览器访问:`http://服务器IP:10089` 165 | 166 | > ⚠️ **注意**: 请确保防火墙已放行 10089 端口 167 | 168 | ## 🔧 配置说明 169 | 170 | ### 环境变量配置 171 | 172 | 创建 `.env` 文件或设置环境变量: 173 | 174 | ```bash 175 | # 服务器配置 176 | PORT=10089 # 服务端口 177 | NODE_ENV=production # 运行环境 178 | 179 | # 缓存配置 180 | CACHE_MAX_SIZE=100 # 最大缓存条目数 181 | CACHE_MAX_MEMORY_MB=50 # 最大缓存内存(MB) 182 | CACHE_CLEANUP_INTERVAL=60000 # 缓存清理间隔(ms) 183 | MEMORY_MONITOR_INTERVAL=300000 # 内存监控间隔(ms) 184 | ``` 185 | 186 | ### PM2 配置 187 | 188 | 创建 `ecosystem.config.js` 文件: 189 | 190 | ```javascript 191 | module.exports = { 192 | apps: [{ 193 | name: "flowmaster", 194 | script: "server.js", 195 | instances: 1, 196 | autorestart: true, 197 | watch: false, 198 | max_memory_restart: "1G", 199 | env: { 200 | NODE_ENV: "production", 201 | PORT: 10089, 202 | CACHE_MAX_SIZE: 100, 203 | CACHE_MAX_MEMORY_MB: 50 204 | }, 205 | env_production: { 206 | NODE_ENV: "production" 207 | } 208 | }] 209 | }; 210 | ``` 211 | 212 | ### 手动安装步骤 213 | 214 | #### 1. 安装依赖 215 | 216 | ```bash 217 | # Ubuntu/Debian 218 | sudo apt-get update 219 | sudo apt-get install vnstat 220 | 221 | # CentOS/RHEL 222 | sudo yum install vnstat 223 | 224 | # 启动vnstat服务 225 | sudo systemctl enable vnstat 226 | sudo systemctl start vnstat 227 | ``` 228 | 229 | #### 2. 安装Node.js和PM2 230 | 231 | ```bash 232 | # 安装Node.js (推荐使用nvm) 233 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 234 | source ~/.bashrc 235 | nvm install 18 236 | nvm use 18 237 | 238 | # 安装PM2 239 | npm install -g pm2 240 | pm2 startup 241 | ``` 242 | 243 | #### 3. 部署应用 244 | 245 | ```bash 246 | # 克隆项目 247 | git clone https://github.com/vbskycn/FlowMaster.git 248 | cd FlowMaster 249 | 250 | # 安装依赖 251 | npm install 252 | 253 | # 启动服务 254 | pm2 start ecosystem.config.js 255 | pm2 save 256 | 257 | # 调试启动 258 | npm install 259 | node server.js 260 | ``` 261 | 262 | #### 6. PM2 管理命令 263 | 264 | ```bash 265 | # 查看服务状态 266 | pm2 status flowmaster 267 | 268 | # 查看服务日志 269 | pm2 logs flowmaster 270 | 271 | # 重启服务 272 | pm2 restart flowmaster 273 | 274 | # 停止服务 275 | pm2 stop flowmaster 276 | 277 | # 删除服务 278 | pm2 delete flowmaster 279 | 280 | # 查看详细信息 281 | pm2 show flowmaster 282 | 283 | # 监控服务 284 | pm2 monit 285 | ``` 286 | 287 | 默认访问地址:`http://localhost:10089` 288 | 289 | #### 7. 更新脚本 290 | 291 | ##### 一键自动更新 292 | 293 | - **国际网络用户**(可直接访问 GitHub): 294 | 295 | ```bash 296 | curl -o install.sh https://raw.githubusercontent.com/vbskycn/FlowMaster/main/install.sh && chmod +x install.sh && sudo ./install.sh 297 | ``` 298 | 299 | - **国内网络用户**(推荐使用加速代理): 300 | 301 | ```bash 302 | curl -o install.sh https://gh-proxy.com/https://raw.githubusercontent.com/vbskycn/FlowMaster/main/install.sh && chmod +x install.sh && sudo ./install.sh 303 | ``` 304 | 305 | - 运行后,脚本会弹出菜单,**请选择数字 3(更新 FlowMaster)**,即可自动拉取最新代码并重启服务。 306 | - 如果你是在/root 目录安装的,也可以直接 `sudo ./install.sh`,然后选择 3更新。 307 | 308 | ##### 手动更新 309 | 310 | 如果你是手动用源码部署的: 311 | 312 | ```bash 313 | cd FlowMaster #进入脚本目录 314 | git pull #更新仓库 315 | pm2 restart flowmaster进程 #重启flowmaster进程 316 | ``` 317 | 318 | ### 🔧 配置说明 319 | 320 | 使用 PM2 设置环境变量: 321 | 322 | ```bash 323 | # 设置端口 324 | pm2 start server.js --name flowmaster --env PORT=10089 325 | 326 | # 或在 ecosystem.config.js 中配置 327 | echo 'module.exports = { 328 | apps: [{ 329 | name: "flowmaster", 330 | script: "server.js", 331 | env: { 332 | PORT: 10089 333 | } 334 | }] 335 | }' > ecosystem.config.js 336 | 337 | # 使用配置文件启动 338 | pm2 start ecosystem.config.js 339 | ``` 340 | 341 | ## 📖 使用说明 342 | 343 | 1. 系统启动后,自动检测可用网卡 344 | 2. 在界面上选择要监控的网卡 345 | 3. 查看实时流量和历史统计数据 346 | 4. 可开启自动刷新功能,实时更新数据 347 | 348 | ## 📚 API 文档 349 | 350 | ### 基础信息 351 | 352 | - **基础URL**: `http://localhost:10089` 353 | - **内容类型**: `application/json` 354 | - **字符编码**: `UTF-8` 355 | 356 | ### 接口列表 357 | 358 | #### 1. 获取网络接口列表 359 | 360 | ```http 361 | GET /api/interfaces 362 | ``` 363 | 364 | **响应示例:** 365 | ```json 366 | { 367 | "interfaces": ["eth0", "wlan0", "docker0"] 368 | } 369 | ``` 370 | 371 | #### 2. 获取统计数据 372 | 373 | ```http 374 | GET /api/stats/{interface}/{period} 375 | ``` 376 | 377 | **参数说明:** 378 | - `interface`: 网络接口名称 (如: eth0) 379 | - `period`: 统计周期 380 | - `l`: 实时数据 381 | - `5`: 5分钟统计 382 | - `h`: 小时统计 383 | - `d`: 日统计 384 | - `m`: 月统计 385 | - `y`: 年统计 386 | 387 | **响应示例:** 388 | ```json 389 | { 390 | "data": [ 391 | "eth0 / 5 minute", 392 | "时间 | 接收(MiB) | 发送(MiB) | 总计(MiB) | 平均速率", 393 | "---", 394 | "14:55 | 12.34 | 5.67 | 18.01 | 2.40 kb/s" 395 | ] 396 | } 397 | ``` 398 | 399 | #### 3. 日期范围查询 400 | 401 | ```http 402 | GET /api/stats/{interface}/range/{startDate}/{endDate} 403 | ``` 404 | 405 | **参数说明:** 406 | - `interface`: 网络接口名称 407 | - `startDate`: 开始日期 (YYYY-MM-DD) 408 | - `endDate`: 结束日期 (YYYY-MM-DD) 409 | 410 | #### 4. 获取版本信息 411 | 412 | ```http 413 | GET /api/version 414 | ``` 415 | 416 | **响应示例:** 417 | ```json 418 | { 419 | "version": "1.1.7" 420 | } 421 | ``` 422 | 423 | #### 5. 缓存管理 424 | 425 | ```http 426 | # 获取缓存统计 427 | GET /api/cache/stats 428 | 429 | # 清空缓存 430 | POST /api/cache/clear 431 | ``` 432 | 433 | #### 6. 系统监控 434 | 435 | ```http 436 | # 获取内存使用 437 | GET /api/system/memory 438 | 439 | # 获取服务器状态 440 | GET /api/system/status 441 | 442 | # 测试vnstat 443 | GET /api/test/vnstat 444 | ``` 445 | 446 | ### 错误处理 447 | 448 | 所有API都遵循统一的错误响应格式: 449 | 450 | ```json 451 | { 452 | "error": "错误描述", 453 | "timestamp": "2024-01-01T00:00:00.000Z", 454 | "requestId": "abc123def" 455 | } 456 | ``` 457 | 458 | **常见HTTP状态码:** 459 | - `200`: 请求成功 460 | - `400`: 请求参数错误 461 | - `500`: 服务器内部错误 462 | - `503`: 服务暂时不可用 463 | - `504`: 请求超时 464 | 465 | ## 🚀 高级功能 466 | 467 | ### 智能缓存系统 468 | 469 | FlowMaster 实现了高效的缓存管理系统: 470 | 471 | - **LRU算法**: 最近最少使用策略 472 | - **内存监控**: 实时监控缓存内存使用 473 | - **自动清理**: 定期清理过期缓存 474 | - **统计信息**: 缓存命中率、条目数量等 475 | 476 | ### 性能监控 477 | 478 | 系统内置性能监控功能: 479 | 480 | - **响应时间**: 实时监控API响应时间 481 | - **请求统计**: 请求次数、平均响应时间 482 | - **内存使用**: 详细的内存使用情况 483 | - **缓存性能**: 缓存命中率和效率 484 | 485 | ### 诊断系统 486 | 487 | 自动诊断功能帮助快速定位问题: 488 | 489 | - **连接诊断**: 检查服务器连接状态 490 | - **vnstat测试**: 验证vnstat命令可用性 491 | - **配置检查**: 检查系统配置是否正确 492 | - **建议生成**: 根据诊断结果提供解决建议 493 | 494 | ### 图表优化 495 | 496 | 前端图表系统经过深度优化: 497 | 498 | - **实例池管理**: Chart.js实例复用 499 | - **防抖节流**: 避免频繁更新 500 | - **批量更新**: 批量处理图表更新 501 | - **内存清理**: 自动清理无用实例 502 | 503 | ## 🔍 故障排除 504 | 505 | ### 常见问题 506 | 507 | #### 1. vnstat命令不可用 508 | 509 | **症状**: 页面显示"vnstat命令不可用"错误 510 | 511 | **解决方案**: 512 | ```bash 513 | # 检查vnstat是否安装 514 | which vnstat 515 | 516 | # 安装vnstat 517 | sudo apt-get install vnstat # Ubuntu/Debian 518 | sudo yum install vnstat # CentOS/RHEL 519 | 520 | # 启动vnstat服务 521 | sudo systemctl enable vnstat 522 | sudo systemctl start vnstat 523 | ``` 524 | 525 | #### 2. 端口被占用 526 | 527 | **症状**: 启动时显示"端口已被占用" 528 | 529 | **解决方案**: 530 | ```bash 531 | # 查看端口占用 532 | netstat -tlnp | grep 10089 533 | 534 | # 修改端口 535 | export PORT=8080 536 | pm2 restart flowmaster 537 | ``` 538 | 539 | #### 3. 缓存问题 540 | 541 | **症状**: 数据更新缓慢或显示异常 542 | 543 | **解决方案**: 544 | ```bash 545 | # 清空缓存 546 | curl -X POST http://localhost:10089/api/cache/clear 547 | 548 | # 重启服务 549 | pm2 restart flowmaster 550 | ``` 551 | 552 | #### 4. 内存使用过高 553 | 554 | **症状**: 系统内存使用持续增长 555 | 556 | **解决方案**: 557 | ```bash 558 | # 调整缓存配置 559 | export CACHE_MAX_MEMORY_MB=25 560 | export CACHE_MAX_SIZE=50 561 | pm2 restart flowmaster 562 | ``` 563 | 564 | ### 日志分析 565 | 566 | 查看详细日志信息: 567 | 568 | ```bash 569 | # 查看PM2日志 570 | pm2 logs flowmaster 571 | 572 | # 查看实时日志 573 | pm2 logs flowmaster --lines 100 574 | 575 | # 查看错误日志 576 | pm2 logs flowmaster --err 577 | ``` 578 | 579 | ### 性能诊断 580 | 581 | 使用内置诊断功能: 582 | 583 | 1. 访问系统页面 584 | 2. 点击"诊断问题"按钮 585 | 3. 查看诊断结果和建议 586 | 587 | ## ⚡ 性能优化 588 | 589 | ### 系统优化建议 590 | 591 | #### 1. 缓存配置优化 592 | 593 | ```bash 594 | # 生产环境推荐配置 595 | CACHE_MAX_SIZE=200 # 增加缓存条目 596 | CACHE_MAX_MEMORY_MB=100 # 增加缓存内存 597 | CACHE_CLEANUP_INTERVAL=300000 # 5分钟清理一次 598 | ``` 599 | 600 | #### 2. 网络接口优化 601 | 602 | - 优先监控主要网络接口 603 | - 避免监控虚拟接口(如docker、veth) 604 | - 定期检查接口状态 605 | 606 | #### 3. 系统资源优化 607 | 608 | ```bash 609 | # 增加文件描述符限制 610 | echo "* soft nofile 65536" >> /etc/security/limits.conf 611 | echo "* hard nofile 65536" >> /etc/security/limits.conf 612 | 613 | # 优化内核参数 614 | echo "net.core.somaxconn = 65535" >> /etc/sysctl.conf 615 | sysctl -p 616 | ``` 617 | 618 | ### 监控指标 619 | 620 | 关键性能指标: 621 | 622 | - **响应时间**: < 100ms (正常), < 500ms (警告) 623 | - **缓存命中率**: > 80% (优秀), > 60% (良好) 624 | - **内存使用**: < 100MB (正常), < 200MB (警告) 625 | - **CPU使用率**: < 10% (正常), < 30% (警告) 626 | 627 | ## 👨💻 开发指南 628 | 629 | ### 本地开发环境 630 | 631 | #### 1. 克隆项目 632 | 633 | ```bash 634 | git clone https://github.com/vbskycn/FlowMaster.git 635 | cd FlowMaster 636 | ``` 637 | 638 | #### 2. 安装依赖 639 | 640 | ```bash 641 | npm install 642 | ``` 643 | 644 | #### 3. 启动开发服务器 645 | 646 | ```bash 647 | # 开发模式启动 648 | npm run dev 649 | 650 | # 或直接启动 651 | node server.js 652 | ``` 653 | 654 | #### 4. 访问开发环境 655 | 656 | 打开浏览器访问:`http://localhost:10089` 657 | 658 | ### 项目结构 659 | 660 | ``` 661 | FlowMaster/ 662 | ├── server.js # 主服务器文件 663 | ├── package.json # 项目配置 664 | ├── README.md # 项目文档 665 | ├── .env.example # 环境变量示例 666 | ├── install.sh # 安装脚本 667 | ├── public/ # 静态资源 668 | │ ├── index.html # 主页面 669 | │ ├── css/ # 样式文件 670 | │ │ ├── main.css # 主样式 671 | │ │ └── dark-theme.css # 深色主题 672 | │ └── favicon.png # 网站图标 673 | └── assets/ # 资源文件 674 | └── FlowMaster1.1.3.jpg # 项目截图 675 | ``` 676 | 677 | ### 代码规范 678 | 679 | - 使用ES6+语法 680 | - 遵循JavaScript标准规范 681 | - 添加适当的注释 682 | - 使用有意义的变量名 683 | 684 | ### 测试 685 | 686 | ```bash 687 | # 运行API测试 688 | curl http://localhost:10089/api/version 689 | 690 | # 测试vnstat 691 | curl http://localhost:10089/api/test/vnstat 692 | 693 | # 检查系统状态 694 | curl http://localhost:10089/api/system/status 695 | ``` 696 | 697 | ## 🤝 贡献指南 698 | 699 | 我们欢迎所有形式的贡献! 700 | 701 | ### 贡献方式 702 | 703 | 1. **报告问题**: 提交Issue报告bug或建议新功能 704 | 2. **代码贡献**: 提交Pull Request改进代码 705 | 3. **文档改进**: 完善文档和翻译 706 | 4. **测试反馈**: 测试新功能并提供反馈 707 | 708 | ### 开发流程 709 | 710 | 1. Fork 本项目 711 | 2. 创建功能分支:`git checkout -b feature/AmazingFeature` 712 | 3. 提交更改:`git commit -m 'Add some AmazingFeature'` 713 | 4. 推送分支:`git push origin feature/AmazingFeature` 714 | 5. 提交 Pull Request 715 | 716 | ### 提交规范 717 | 718 | 提交信息格式: 719 | ``` 720 | type(scope): description 721 | 722 | [optional body] 723 | 724 | [optional footer] 725 | ``` 726 | 727 | **类型说明**: 728 | - `feat`: 新功能 729 | - `fix`: 修复bug 730 | - `docs`: 文档更新 731 | - `style`: 代码格式调整 732 | - `refactor`: 代码重构 733 | - `test`: 测试相关 734 | - `chore`: 构建过程或辅助工具的变动 735 | 736 | ### 代码审查 737 | 738 | 所有Pull Request都需要通过代码审查: 739 | 740 | - 代码质量检查 741 | - 功能测试验证 742 | - 文档更新确认 743 | - 性能影响评估 744 | 745 | ## 📄 开源协议 746 | 747 | 本项目采用 [MIT 协议](LICENSE) 开源。 748 | 749 | ### 协议要点 750 | 751 | - ✅ 允许商业使用 752 | - ✅ 允许修改源码 753 | - ✅ 允许分发 754 | - ✅ 允许私人使用 755 | - ❌ 不提供担保 756 | 757 | ## 📞 联系方式 758 | 759 | ### 项目维护者 760 | 761 | **vbskycn** 762 | 763 | - GitHub: [@vbskycn](https://github.com/vbskycn) 764 | - 项目主页: [FlowMaster](https://github.com/vbskycn/FlowMaster) 765 | 766 | ### 获取帮助 767 | 768 | - 📧 **提交Issue**: [GitHub Issues](https://github.com/vbskycn/FlowMaster/issues) 769 | - 📖 **查看文档**: [项目Wiki](https://github.com/vbskycn/FlowMaster/wiki) 770 | - 💬 **讨论交流**: [GitHub Discussions](https://github.com/vbskycn/FlowMaster/discussions) 771 | 772 | ### 支持项目 773 | 774 | 如果这个项目对你有帮助,欢迎: 775 | 776 | - ⭐ **Star**: 给项目点个星 777 | 778 | - 🍴 **Fork**: 复制项目到你的仓库 779 | 780 | - 💡 **贡献**: 提交代码或建议 781 | 782 | - 📢 **分享**: 推荐给其他开发者 783 | 784 | - 🍴直接请作者喝咖啡 785 | 786 |  787 | 788 | --- 789 | 790 | ## 🙏 致谢 791 | 792 | - [vnstat](https://github.com/vergoh/vnstat) - 强大的网络流量监控工具 793 | - [Vue.js](https://vuejs.org/) - 渐进式 JavaScript 框架 794 | - [Bootstrap](https://getbootstrap.com/) - 流行的前端组件库 795 | - [Chart.js](https://www.chartjs.org/) - 交互式图表库 796 | - [Express.js](https://expressjs.com/) - 快速、开放、极简的 Node.js Web 应用框架 797 | - [PM2](https://pm2.keymetrics.io/) - 生产环境进程管理器 798 | 799 | **感谢使用 FlowMaster!** 800 | 801 | *让网络流量监控变得简单而强大* 802 | 803 | 804 | 805 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { exec } = require('child_process'); 3 | const cors = require('cors'); 4 | const app = express(); 5 | const packageJson = require('./package.json'); 6 | 7 | // 从环境变量获取端口,如果没有则使用10089 8 | const port = process.env.PORT || 10089; 9 | 10 | // 缓存配置 11 | const cacheConfig = { 12 | maxSize: parseInt(process.env.CACHE_MAX_SIZE) || 100, 13 | maxMemoryMB: parseInt(process.env.CACHE_MAX_MEMORY_MB) || 50, 14 | cleanupInterval: parseInt(process.env.CACHE_CLEANUP_INTERVAL) || 60000, // 1分钟 15 | memoryMonitorInterval: parseInt(process.env.MEMORY_MONITOR_INTERVAL) || 300000 // 5分钟 16 | }; 17 | 18 | // 缓存管理器类 19 | class CacheManager { 20 | constructor(maxSize = 100, maxMemoryMB = 50) { 21 | this.cache = new Map(); 22 | this.maxSize = maxSize; 23 | this.maxMemoryBytes = maxMemoryMB * 1024 * 1024; 24 | this.stats = { 25 | hits: 0, 26 | misses: 0, 27 | sets: 0, 28 | deletes: 0 29 | }; 30 | 31 | // 定期清理过期缓存 32 | setInterval(() => this.cleanup(), cacheConfig.cleanupInterval); 33 | } 34 | 35 | // 生成缓存键 36 | generateKey(prefix, ...params) { 37 | return `${prefix}:${params.join(':')}`; 38 | } 39 | 40 | // 获取缓存 41 | get(key) { 42 | const item = this.cache.get(key); 43 | if (!item) { 44 | this.stats.misses++; 45 | return null; 46 | } 47 | 48 | // 检查是否过期 49 | if (Date.now() > item.expiresAt) { 50 | this.cache.delete(key); 51 | this.stats.misses++; 52 | return null; 53 | } 54 | 55 | // 更新访问时间(LRU) 56 | item.lastAccessed = Date.now(); 57 | this.stats.hits++; 58 | return item.data; 59 | } 60 | 61 | // 设置缓存 62 | set(key, data, ttlMs = 60000) { 63 | // 检查内存使用 64 | if (this.shouldEvict()) { 65 | this.evictLRU(); 66 | } 67 | 68 | const item = { 69 | data, 70 | expiresAt: Date.now() + ttlMs, 71 | lastAccessed: Date.now(), 72 | size: this.estimateSize(data) 73 | }; 74 | 75 | this.cache.set(key, item); 76 | this.stats.sets++; 77 | } 78 | 79 | // 删除缓存 80 | delete(key) { 81 | const deleted = this.cache.delete(key); 82 | if (deleted) { 83 | this.stats.deletes++; 84 | } 85 | return deleted; 86 | } 87 | 88 | // 清理过期缓存 89 | cleanup() { 90 | const now = Date.now(); 91 | for (const [key, item] of this.cache.entries()) { 92 | if (now > item.expiresAt) { 93 | this.cache.delete(key); 94 | } 95 | } 96 | } 97 | 98 | // 检查是否需要清理 99 | shouldEvict() { 100 | return this.cache.size >= this.maxSize || this.getCurrentMemoryUsage() > this.maxMemoryBytes; 101 | } 102 | 103 | // 清理LRU项目 104 | evictLRU() { 105 | let oldestKey = null; 106 | let oldestTime = Date.now(); 107 | 108 | for (const [key, item] of this.cache.entries()) { 109 | if (item.lastAccessed < oldestTime) { 110 | oldestTime = item.lastAccessed; 111 | oldestKey = key; 112 | } 113 | } 114 | 115 | if (oldestKey) { 116 | this.cache.delete(oldestKey); 117 | } 118 | } 119 | 120 | // 估算数据大小(字节) 121 | estimateSize(data) { 122 | if (typeof data === 'string') { 123 | return Buffer.byteLength(data, 'utf8'); 124 | } 125 | if (typeof data === 'object') { 126 | return Buffer.byteLength(JSON.stringify(data), 'utf8'); 127 | } 128 | return 8; // 基本类型估算 129 | } 130 | 131 | // 获取当前内存使用 132 | getCurrentMemoryUsage() { 133 | let totalSize = 0; 134 | for (const item of this.cache.values()) { 135 | totalSize += item.size; 136 | } 137 | return totalSize; 138 | } 139 | 140 | // 获取缓存统计 141 | getStats() { 142 | const hitRate = this.stats.hits + this.stats.misses > 0 143 | ? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2) 144 | : 0; 145 | 146 | return { 147 | ...this.stats, 148 | hitRate: `${hitRate}%`, 149 | size: this.cache.size, 150 | maxSize: this.maxSize, 151 | memoryUsage: `${(this.getCurrentMemoryUsage() / 1024 / 1024).toFixed(2)}MB`, 152 | maxMemory: `${(this.maxMemoryBytes / 1024 / 1024).toFixed(2)}MB` 153 | }; 154 | } 155 | 156 | // 清空所有缓存 157 | clear() { 158 | this.cache.clear(); 159 | } 160 | } 161 | 162 | // 创建全局缓存实例 163 | const cacheManager = new CacheManager(cacheConfig.maxSize, cacheConfig.maxMemoryMB); 164 | 165 | // 翻译映射 166 | const translations = { 167 | 'month': '月份', 168 | 'day': '日期', 169 | 'hour': '小时', 170 | 'rx': '接收', 171 | 'tx': '发送', 172 | 'total': '总计', 173 | 'avg. rate': '平均速率', 174 | 'estimated': '预计', 175 | 'daily': '每日', 176 | 'monthly': '每月', 177 | 'hourly': '每小时', 178 | 'yearly': '每年', 179 | 'year': '年份', 180 | 'time': '时间', 181 | 'Available interfaces': '可用接口', 182 | 'received': '接收', 183 | 'transmitted': '发送', 184 | 'Sampling': '正在采样', 185 | 'seconds average': '秒平均值', 186 | 'packets sampled in': '个数据包采样于', 187 | 'seconds': '秒', 188 | 'Traffic average for': '流量平均值 -', 189 | 'current rate': '当前速率', 190 | 'bytes': '字节', 191 | 'packets': '数据包', 192 | 'packets/s': '包/秒', 193 | 'bits/s': 'b/秒', 194 | 'kbit/s': 'kb/秒', 195 | 'Mbit/s': 'Mb/秒', 196 | 'Gbit/s': 'Gb/秒', 197 | 'KiB/s': 'KB/秒', 198 | 'MiB/s': 'MB/秒', 199 | 'GiB/s': 'GB/秒', 200 | 'yesterday': '昨天', 201 | 'today': '今天', 202 | 'last 5 minutes': '最近5分钟', 203 | 'last hour': '最近1小时', 204 | 'last day': '最近24小时', 205 | 'last month': '最近30天' 206 | }; 207 | 208 | // 预编译正则表达式以提高性能 209 | const compiledTranslations = Object.entries(translations).map(([key, value]) => ({ 210 | regex: new RegExp(`\\b${key}\\b`, 'gi'), 211 | value 212 | })); 213 | 214 | // 预编译特殊处理正则表达式 215 | const samplingRegex = /Sampling ([^ ]+) \((\d+) seconds average\)/; 216 | const packetsSampledRegex = /(\d+) packets sampled in (\d+) seconds/; 217 | const trafficAverageRegex = /Traffic average for (.+)/; 218 | 219 | // 周期到单位的映射表 220 | const periodUnitMap = { 221 | '5': 'MiB', // 5分钟 222 | 'h': 'MiB', // 小时 223 | 'd': 'GiB', // 天 224 | 'm': 'GiB', // 月 225 | 'y': 'TiB' // 年 226 | }; 227 | 228 | // 翻译函数 229 | function translateOutput(text) { 230 | const lines = text.split('\n'); 231 | return lines.map(line => { 232 | // 特殊处理采样信息 233 | if (line.includes('Sampling')) { 234 | return line 235 | .replace(samplingRegex, '正在采样 $1 ($2秒平均值)') 236 | .replace(packetsSampledRegex, '$1 个数据包采样于 $2 秒'); 237 | } 238 | 239 | // 特殊处理流量平均值 240 | if (line.includes('Traffic average for')) { 241 | return line.replace(trafficAverageRegex, '流量平均值 - $1'); 242 | } 243 | 244 | // 使用预编译正则表达式替换其他常规文本 245 | for (const { regex, value } of compiledTranslations) { 246 | line = line.replace(regex, value); 247 | } 248 | 249 | return line; 250 | }).join('\n'); 251 | } 252 | 253 | // 修改时间处理函数 254 | function filterStatsByTime(lines, period) { 255 | let isHeader = true; // 用于标记表头部分 256 | const headers = []; // 存储表头行 257 | const currentTime = new Date(); 258 | 259 | return lines.filter(line => { 260 | // 保存表头信息 261 | if (isHeader) { 262 | if (line.includes('---')) { 263 | headers.push(line); 264 | isHeader = false; // 遇到分隔线后结束表头部分 265 | return true; 266 | } 267 | headers.push(line); 268 | return true; 269 | } 270 | 271 | // 空行保留 272 | if (!line.trim()) { 273 | return true; 274 | } 275 | 276 | let match; 277 | 278 | switch(period) { 279 | case 'minutes': 280 | // 匹配时间格式 HH:mm 281 | match = line.match(/(\d{2}):(\d{2})/); 282 | if (match) { 283 | const [hours, minutes] = match.slice(1).map(Number); 284 | const lineTime = new Date(); 285 | lineTime.setHours(hours, minutes, 0, 0); 286 | 287 | // 如果时间大于当前时间,说明是前一天的数据 288 | if (lineTime > currentTime) { 289 | lineTime.setDate(lineTime.getDate() - 1); 290 | } 291 | 292 | // 检查是否在最近60分钟内 293 | return (currentTime - lineTime) <= 60 * 60 * 1000; 294 | } 295 | return false; 296 | 297 | case 'hours': 298 | // 匹配时间格式 HH:mm 299 | match = line.match(/(\d{2}):(\d{2})/); 300 | if (match) { 301 | const [hours] = match.slice(1).map(Number); 302 | const lineTime = new Date(); 303 | lineTime.setHours(hours, 0, 0, 0); 304 | 305 | // 如果时间大于当前时间,说明是前一天的数据 306 | if (lineTime > currentTime) { 307 | lineTime.setDate(lineTime.getDate() - 1); 308 | } 309 | 310 | // 检查是否在最近12小时内 311 | return (currentTime - lineTime) <= 12 * 60 * 60 * 1000; 312 | } 313 | return false; 314 | 315 | case 'days': 316 | // 匹配日期格式 MM/DD/YY 或 YYYY-MM-DD 317 | match = line.match(/(\d{2})\/(\d{2})\/(\d{2})/) || line.match(/(\d{4})-(\d{2})-(\d{2})/); 318 | if (match) { 319 | let lineTime; 320 | if (match[0].includes('/')) { 321 | // MM/DD/YY 格式 322 | const [month, day, year] = match.slice(1).map(Number); 323 | lineTime = new Date(2000 + year, month - 1, day); 324 | } else { 325 | // YYYY-MM-DD 格式 326 | const [year, month, day] = match.slice(1).map(Number); 327 | lineTime = new Date(year, month - 1, day); 328 | } 329 | 330 | // 检查是否在最近12天内 331 | const diffTime = currentTime - lineTime; 332 | return diffTime <= 12 * 24 * 60 * 60 * 1000 && diffTime >= 0; 333 | } 334 | return false; 335 | } 336 | return false; 337 | }); 338 | } 339 | 340 | // ========== 主动定时采集和缓存vnstat数据 ========== // 341 | const REALTIME_CACHE_SIZE = 20; 342 | const REALTIME_INTERVAL = 5000; // 5秒 343 | const PERIODS = ['5', 'h', 'd', 'm', 'y']; 344 | const PERIOD_INTERVAL = 3 * 60 * 1000; // 3分钟 345 | 346 | // 记录已采集的接口,避免重复定时 347 | const startedRealtime = new Set(); 348 | const startedPeriod = new Set(); 349 | 350 | function scheduleRealtimeCollection(interfaceName) { 351 | if (startedRealtime.has(interfaceName)) return; 352 | startedRealtime.add(interfaceName); 353 | setInterval(() => { 354 | exec(`vnstat -tr 5 -i ${interfaceName}`, (error, stdout, stderr) => { 355 | if (!error && stdout) { 356 | const translatedOutput = translateOutput(stdout); 357 | let queue = cacheManager.get(`realtime:${interfaceName}`) || []; 358 | queue.push(translatedOutput.split('\n')); 359 | if (queue.length > REALTIME_CACHE_SIZE) queue.shift(); 360 | cacheManager.set(`realtime:${interfaceName}`, queue, REALTIME_CACHE_SIZE * REALTIME_INTERVAL); 361 | } 362 | }); 363 | }, REALTIME_INTERVAL); 364 | } 365 | 366 | function schedulePeriodCollection(interfaceName, period) { 367 | const key = `${interfaceName}:${period}`; 368 | if (startedPeriod.has(key)) return; 369 | startedPeriod.add(key); 370 | setInterval(() => { 371 | let cmd = period === '5' ? `vnstat -5 -i ${interfaceName}` : `vnstat -${period} -i ${interfaceName}`; 372 | exec(cmd, (error, stdout, stderr) => { 373 | if (!error && stdout) { 374 | let translatedOutput = translateOutput(stdout); 375 | let lines = translatedOutput.split('\n'); 376 | // 过滤和单位归一化处理(复用getStatsWithoutCache的逻辑) 377 | switch(period) { 378 | case '5': lines = filterStatsByTime(lines, 'minutes'); break; 379 | case 'h': lines = filterStatsByTime(lines, 'hours'); break; 380 | case 'd': lines = filterStatsByTime(lines, 'days'); break; 381 | } 382 | // 单位归一化 383 | const periodUnitMap = { '5': 'MiB', 'h': 'MiB', 'd': 'GiB', 'm': 'GiB', 'y': 'TiB' }; 384 | const targetUnit = periodUnitMap[period] || 'MiB'; 385 | function normalizeValue(val, targetUnit) { 386 | if (!val) return val; 387 | const match = val.match(/([\d.]+)\s*(MiB|GiB|TiB)?/i); 388 | if (!match) return val; 389 | const num = parseFloat(match[1]); 390 | const unit = (match[2] || 'MiB').toUpperCase(); 391 | let normalizedNum = num; 392 | if (unit === 'GIB') normalizedNum *= 1024; 393 | if (unit === 'TIB') normalizedNum *= 1024 * 1024; 394 | if (targetUnit === 'GiB') { 395 | normalizedNum = normalizedNum / 1024; 396 | return `${normalizedNum.toFixed(2)} GiB`; 397 | } else if (targetUnit === 'TiB') { 398 | normalizedNum = normalizedNum / (1024 * 1024); 399 | return `${normalizedNum.toFixed(2)} TiB`; 400 | } else { 401 | return `${normalizedNum.toFixed(2)} MiB`; 402 | } 403 | } 404 | lines = lines.map((line, idx) => { 405 | if (line.includes('---') || !line.trim()) return line; 406 | if ((period === 'm' || period === 'y') && line.includes('预计')) return null; 407 | // 强制修正表头 408 | if (line.includes('接收') && 409 | (line.includes('时间') || line.includes('小时') || line.includes('日期') || line.includes('月份') || line.includes('年份')) 410 | ) { 411 | if (line.includes('时间')) return `时间\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 412 | if (line.includes('小时')) return `小时\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 413 | if (line.includes('日期')) return `日期\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 414 | if (line.includes('月份')) return `月份\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 415 | if (line.includes('年份')) return `年份\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 416 | } 417 | // 分隔符 418 | line = line.replace(/^(\s*\d{2}(:\d{2})?)(\s+)/, '$1 |$3'); 419 | line = line.replace(/^(\s*\d{4}-\d{2}-\d{2})(\s+)/, '$1 |$2'); 420 | line = line.replace(/^(\s*\d{4}-\d{2})(\s+)/, '$1 |$2'); 421 | line = line.replace(/^(\s*\d{4})(\s+)/, '$1 |$2'); 422 | if (['5', 'h', 'd', 'm', 'y'].includes(period)) { 423 | let parts = line.split('|'); 424 | if (parts.length < 5) return line; 425 | let rx = parts[1].trim(); 426 | let tx = parts[2].trim(); 427 | let total = parts[3].trim(); 428 | parts[1] = ' ' + normalizeValue(rx, targetUnit); 429 | parts[2] = ' ' + normalizeValue(tx, targetUnit); 430 | parts[3] = ' ' + normalizeValue(total, targetUnit); 431 | return parts.join('|'); 432 | } 433 | return line; 434 | }).filter(Boolean); 435 | const result = { data: lines }; 436 | cacheManager.set(`stats:${interfaceName}:${period}`, result, PERIOD_INTERVAL * 2); 437 | } 438 | }); 439 | }, PERIOD_INTERVAL); 440 | } 441 | 442 | // 启动时获取所有接口并为每个接口启动定时采集 443 | function startAllScheduledCollections() { 444 | exec('vnstat --iflist', (error, stdout, stderr) => { 445 | let allInterfaces = []; 446 | if (!error && stdout) { 447 | allInterfaces = stdout 448 | .split('\n') 449 | .find(line => line.includes('Available interfaces:')) 450 | ?.replace('Available interfaces:', '') 451 | .trim() 452 | .split(' ') 453 | .filter(Boolean) || []; 454 | } 455 | if (allInterfaces.length === 0) allInterfaces = ['eth0']; 456 | allInterfaces.forEach(iface => { 457 | scheduleRealtimeCollection(iface); 458 | PERIODS.forEach(period => schedulePeriodCollection(iface, period)); 459 | }); 460 | }); 461 | } 462 | 463 | // 启动定时采集 464 | startAllScheduledCollections(); 465 | 466 | // 启用CORS和JSON解析 467 | app.use(cors()); 468 | app.use(express.json()); 469 | app.use(express.static('public')); 470 | 471 | // 获取网络接口列表 472 | app.get('/api/interfaces', async (req, res) => { 473 | try { 474 | // 检查缓存 475 | const cacheKey = cacheManager.generateKey('interfaces'); 476 | const cachedData = cacheManager.get(cacheKey); 477 | 478 | if (cachedData) { 479 | return res.json(cachedData); 480 | } 481 | 482 | // 获取所有接口列表 483 | const iflistResult = await new Promise((resolve, reject) => { 484 | exec('vnstat --iflist', (error, stdout, stderr) => { 485 | if (error) reject(error); 486 | else resolve(stdout); 487 | }); 488 | }); 489 | 490 | // 解析接口列表 491 | const allInterfaces = iflistResult 492 | .split('\n') 493 | .find(line => line.includes('Available interfaces:')) 494 | ?.replace('Available interfaces:', '') 495 | .trim() 496 | .split(' ') 497 | .filter(Boolean) || []; 498 | 499 | // 验证每个接口是否有效 500 | const validInterfaces = []; 501 | for (const interface of allInterfaces) { 502 | try { 503 | await new Promise((resolve, reject) => { 504 | exec(`vnstat -i ${interface} --oneline`, (error, stdout, stderr) => { 505 | if (!error && stdout.trim()) { 506 | validInterfaces.push(interface); 507 | } 508 | resolve(); 509 | }); 510 | }); 511 | } catch (error) { 512 | console.error(`检查接口 ${interface} 时出错:`, error); 513 | } 514 | } 515 | 516 | // 如果没有找到有效接口,默认返回 eth0 517 | if (validInterfaces.length === 0) { 518 | validInterfaces.push('eth0'); 519 | } 520 | 521 | const result = { interfaces: validInterfaces }; 522 | 523 | // 缓存结果(5分钟) 524 | cacheManager.set(cacheKey, result, 5 * 60 * 1000); 525 | 526 | res.json(result); 527 | } catch (error) { 528 | res.status(500).json({ error: `获取网络接口列表失败: ${error.message}` }); 529 | } 530 | }); 531 | 532 | // 获取统计数据 533 | app.get('/api/stats/:interface/:period', (req, res) => { 534 | const { interface: interfaceName, period } = req.params; 535 | const validPeriods = ['l', '5', 'h', 'd', 'm', 'y']; 536 | if (!interfaceName.match(/^[a-zA-Z0-9]+[a-zA-Z0-9:._-]*$/)) { 537 | return res.status(400).json({ error: '无效的接口名称' }); 538 | } 539 | if (!validPeriods.includes(period)) { 540 | return res.status(400).json({ error: '无效的时间周期' }); 541 | } 542 | if (period === 'l') { 543 | // 优先返回主动缓存的实时数据 544 | const cachedQueue = cacheManager.get(`realtime:${interfaceName}`); 545 | if (cachedQueue && cachedQueue.length > 0) { 546 | // 拍平为一维字符串数组,兼容前端 547 | const flat = cachedQueue.flat(); 548 | return res.json({ data: flat }); 549 | } 550 | // 否则降级为现查现算 551 | return getStatsWithoutCache(interfaceName, period, res); 552 | } 553 | // 其他周期优先返回主动缓存 554 | const cachedData = cacheManager.get(`stats:${interfaceName}:${period}`); 555 | if (cachedData) { 556 | return res.json(cachedData); 557 | } 558 | // 否则降级为现查现算 559 | getStatsWithoutCache(interfaceName, period, res, (result) => { 560 | // 缓存结果 561 | cacheManager.set(`stats:${interfaceName}:${period}`, result, PERIOD_INTERVAL * 2); 562 | }); 563 | }); 564 | 565 | // 获取缓存时间 566 | function getCacheTimeForPeriod(period) { 567 | const cacheTimes = { 568 | '5': 30 * 1000, // 30秒 569 | 'h': 60 * 1000, // 1分钟 570 | 'd': 2 * 60 * 1000, // 2分钟 571 | 'm': 5 * 60 * 1000, // 5分钟 572 | 'y': 10 * 60 * 1000 // 10分钟 573 | }; 574 | return cacheTimes[period] || 60 * 1000; 575 | } 576 | 577 | // 获取统计数据(无缓存) 578 | function getStatsWithoutCache(interface, period, res, callback) { 579 | let cmd; 580 | switch(period) { 581 | case 'l': 582 | cmd = `vnstat -tr 5 -i ${interface}`; 583 | break; 584 | case '5': 585 | cmd = `vnstat -5 -i ${interface}`; 586 | break; 587 | default: 588 | cmd = `vnstat -${period} -i ${interface}`; 589 | } 590 | 591 | exec(cmd, (error, stdout, stderr) => { 592 | if (error) { 593 | return res.status(500).json({ error: error.message }); 594 | } 595 | 596 | let translatedOutput = translateOutput(stdout); 597 | let lines = translatedOutput.split('\n'); 598 | 599 | // 根据不同时间周期过滤数据 600 | switch(period) { 601 | case '5': 602 | lines = filterStatsByTime(lines, 'minutes'); 603 | break; 604 | case 'h': 605 | lines = filterStatsByTime(lines, 'hours'); 606 | break; 607 | case 'd': 608 | lines = filterStatsByTime(lines, 'days'); 609 | break; 610 | } 611 | 612 | // 单位归一化辅助函数 613 | function normalizeValue(val, targetUnit) { 614 | if (!val) return val; 615 | const match = val.match(/([\d.]+)\s*(MiB|GiB|TiB)?/i); 616 | if (!match) return val; 617 | const num = parseFloat(match[1]); 618 | const unit = (match[2] || 'MiB').toUpperCase(); 619 | 620 | // 统一换算为MiB 621 | let normalizedNum = num; 622 | if (unit === 'GIB') normalizedNum *= 1024; 623 | if (unit === 'TIB') normalizedNum *= 1024 * 1024; 624 | 625 | // 目标单位 626 | if (targetUnit === 'GiB') { 627 | normalizedNum = normalizedNum / 1024; 628 | return `${normalizedNum.toFixed(2)} GiB`; 629 | } else if (targetUnit === 'TiB') { 630 | normalizedNum = normalizedNum / (1024 * 1024); 631 | return `${normalizedNum.toFixed(2)} TiB`; 632 | } else { 633 | return `${normalizedNum.toFixed(2)} MiB`; 634 | } 635 | } 636 | 637 | // 获取当前周期目标单位 638 | const targetUnit = periodUnitMap[period] || 'MiB'; 639 | 640 | // 强制生成标准表头(不依赖原始内容) 641 | function forceHeader(line) { 642 | // 判断表头类型 643 | if (line.includes('时间')) { 644 | return `时间\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 645 | } else if (line.includes('小时')) { 646 | return `小时\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 647 | } else if (line.includes('日期')) { 648 | return `日期\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 649 | } else if (line.includes('月份')) { 650 | return `月份\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 651 | } else if (line.includes('年份')) { 652 | return `年份\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 653 | } 654 | return line; 655 | } 656 | 657 | lines = lines.map((line, idx) => { 658 | // 跳过分隔线、空行 659 | if (line.includes('---') || !line.trim()) return line; 660 | // 月/年卡片去掉"预计"行 661 | if ((period === 'm' || period === 'y') && line.includes('预计')) return null; 662 | // 强制修正表头 663 | if (line.includes('接收') && 664 | (line.includes('时间') || line.includes('小时') || line.includes('日期') || line.includes('月份') || line.includes('年份')) 665 | ) { 666 | return forceHeader(line); 667 | } 668 | // 处理数据行分隔符 669 | // 时间(分钟/小时) 670 | line = line.replace(/^(\s*\d{2}(:\d{2})?)(\s+)/, '$1 |$3'); 671 | // 日期(YYYY-MM-DD) 672 | line = line.replace(/^(\s*\d{4}-\d{2}-\d{2})(\s+)/, '$1 |$2'); 673 | // 月份(YYYY-MM) 674 | line = line.replace(/^(\s*\d{4}-\d{2})(\s+)/, '$1 |$2'); 675 | // 年份(YYYY) 676 | line = line.replace(/^(\s*\d{4})(\s+)/, '$1 |$2'); 677 | 678 | // 单位归一化处理 679 | if (['5', 'h', 'd', 'm', 'y'].includes(period)) { 680 | // 用 | 分割,找到接收/发送/总计字段 681 | let parts = line.split('|'); 682 | if (parts.length < 5) return line; // 不处理异常行 683 | // 只处理数据部分(去除首尾空格) 684 | let rx = parts[1].trim(); 685 | let tx = parts[2].trim(); 686 | let total = parts[3].trim(); 687 | parts[1] = ' ' + normalizeValue(rx, targetUnit); 688 | parts[2] = ' ' + normalizeValue(tx, targetUnit); 689 | parts[3] = ' ' + normalizeValue(total, targetUnit); 690 | return parts.join('|'); 691 | } 692 | return line; 693 | }).filter(Boolean); 694 | 695 | const result = { data: lines }; 696 | 697 | // 如果有回调函数,执行回调 698 | if (callback) { 699 | callback(result); 700 | } 701 | 702 | res.json(result); 703 | }); 704 | } 705 | 706 | // 添加日期范围查询API 707 | app.get('/api/stats/:interface/range/:startDate/:endDate', (req, res) => { 708 | const { interface, startDate, endDate } = req.params; 709 | 710 | if (!interface.match(/^[a-zA-Z0-9]+[a-zA-Z0-9:._-]*$/)) { 711 | return res.status(400).json({ error: '无效的接口名称' }); 712 | } 713 | 714 | // 验证日期格式 (YYYY-MM-DD) 715 | const dateRegex = /^\d{4}-\d{2}-\d{2}$/; 716 | if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { 717 | return res.status(400).json({ error: '无效的日期格式' }); 718 | } 719 | 720 | // 检查缓存 721 | const cacheKey = cacheManager.generateKey('range', interface, startDate, endDate); 722 | const cachedData = cacheManager.get(cacheKey); 723 | 724 | if (cachedData) { 725 | return res.json(cachedData); 726 | } 727 | 728 | const cmd = `vnstat -i ${interface} --begin ${startDate} --end ${endDate} -d`; 729 | 730 | exec(cmd, (error, stdout, stderr) => { 731 | if (error) { 732 | return res.status(500).json({ error: error.message }); 733 | } 734 | 735 | let translatedOutput = translateOutput(stdout); 736 | let lines = translatedOutput.split('\n'); 737 | 738 | // 单位归一化辅助函数 739 | function normalizeValue(val, targetUnit) { 740 | if (!val) return val; 741 | const match = val.match(/([\d.]+)\s*(MiB|GiB|TiB)?/i); 742 | if (!match) return val; 743 | const num = parseFloat(match[1]); 744 | const unit = (match[2] || 'MiB').toUpperCase(); 745 | 746 | // 统一换算为MiB 747 | let normalizedNum = num; 748 | if (unit === 'GIB') normalizedNum *= 1024; 749 | if (unit === 'TIB') normalizedNum *= 1024 * 1024; 750 | 751 | // 目标单位 752 | if (targetUnit === 'GiB') { 753 | normalizedNum = normalizedNum / 1024; 754 | return `${normalizedNum.toFixed(2)} GiB`; 755 | } else if (targetUnit === 'TiB') { 756 | normalizedNum = normalizedNum / (1024 * 1024); 757 | return `${normalizedNum.toFixed(2)} TiB`; 758 | } else { 759 | return `${normalizedNum.toFixed(2)} MiB`; 760 | } 761 | } 762 | 763 | // 指定日期查询固定使用GiB单位 764 | const targetUnit = 'GiB'; 765 | 766 | // 强制生成标准表头(5列格式) 767 | function forceHeader(line) { 768 | if (line.includes('日期')) { 769 | return `日期\t| 接收(${targetUnit})\t| 发送(${targetUnit})\t| 总计(${targetUnit})\t| 平均速率`; 770 | } 771 | return line; 772 | } 773 | 774 | lines = lines.map((line, idx) => { 775 | // 跳过分隔线、空行 776 | if (line.includes('---') || !line.trim()) return line; 777 | // 强制修正表头 778 | if (line.includes('接收') && line.includes('日期')) { 779 | return forceHeader(line); 780 | } 781 | // 处理数据行分隔符 782 | // 日期(YYYY-MM-DD) 783 | line = line.replace(/^(\s*\d{4}-\d{2}-\d{2})(\s+)/, '$1 |$2'); 784 | 785 | // 单位归一化处理 786 | // 用 | 分割,找到接收/发送/总计字段 787 | let parts = line.split('|'); 788 | if (parts.length >= 4) { 789 | // 只处理数据部分(去除首尾空格) 790 | let rx = parts[1].trim(); 791 | let tx = parts[2].trim(); 792 | let total = parts[3].trim(); 793 | parts[1] = ' ' + normalizeValue(rx, targetUnit); 794 | parts[2] = ' ' + normalizeValue(tx, targetUnit); 795 | parts[3] = ' ' + normalizeValue(total, targetUnit); 796 | return parts.join('|'); 797 | } 798 | return line; 799 | }).filter(Boolean); 800 | 801 | const result = { data: lines }; 802 | 803 | // 缓存结果(10分钟) 804 | cacheManager.set(cacheKey, result, 10 * 60 * 1000); 805 | 806 | res.json(result); 807 | }); 808 | }); 809 | 810 | // 添加获取版本号的路由 811 | app.get('/api/version', (req, res) => { 812 | res.json({ version: packageJson.version }); 813 | }); 814 | 815 | // 添加缓存统计API 816 | app.get('/api/cache/stats', (req, res) => { 817 | res.json(cacheManager.getStats()); 818 | }); 819 | 820 | // 添加缓存清理API 821 | app.post('/api/cache/clear', (req, res) => { 822 | cacheManager.clear(); 823 | res.json({ message: '缓存已清空' }); 824 | }); 825 | 826 | // 添加内存使用监控API 827 | app.get('/api/system/memory', (req, res) => { 828 | const memUsage = process.memoryUsage(); 829 | res.json({ 830 | rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)}MB`, 831 | heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)}MB`, 832 | heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)}MB`, 833 | external: `${(memUsage.external / 1024 / 1024).toFixed(2)}MB`, 834 | cacheMemory: cacheManager.getStats().memoryUsage 835 | }); 836 | }); 837 | 838 | // 添加服务器状态检查API 839 | app.get('/api/system/status', async (req, res) => { 840 | try { 841 | const status = { 842 | server: { 843 | uptime: process.uptime(), 844 | memory: process.memoryUsage(), 845 | version: process.version, 846 | platform: process.platform, 847 | arch: process.arch 848 | }, 849 | vnstat: { 850 | available: false, 851 | version: null, 852 | error: null 853 | }, 854 | cache: cacheManager.getStats(), 855 | timestamp: new Date().toISOString() 856 | }; 857 | 858 | // 检查vnstat命令是否可用 859 | try { 860 | const vnstatResult = await new Promise((resolve, reject) => { 861 | exec('vnstat --version', { timeout: 5000 }, (error, stdout, stderr) => { 862 | if (error) reject(error); 863 | else resolve(stdout); 864 | }); 865 | }); 866 | status.vnstat.available = true; 867 | status.vnstat.version = vnstatResult.trim(); 868 | } catch (error) { 869 | status.vnstat.error = error.message; 870 | } 871 | 872 | res.json(status); 873 | } catch (error) { 874 | res.status(500).json({ 875 | error: '服务器状态检查失败', 876 | details: error.message 877 | }); 878 | } 879 | }); 880 | 881 | // 添加vnstat命令测试API 882 | app.get('/api/test/vnstat', async (req, res) => { 883 | try { 884 | const testCommands = [ 885 | { name: 'version', cmd: 'vnstat --version' }, 886 | { name: 'iflist', cmd: 'vnstat --iflist' }, 887 | { name: 'help', cmd: 'vnstat --help' } 888 | ]; 889 | 890 | const results = {}; 891 | 892 | for (const test of testCommands) { 893 | try { 894 | const result = await new Promise((resolve, reject) => { 895 | exec(test.cmd, { timeout: 10000 }, (error, stdout, stderr) => { 896 | if (error) reject(error); 897 | else resolve(stdout); 898 | }); 899 | }); 900 | results[test.name] = { 901 | success: true, 902 | output: result.trim() 903 | }; 904 | } catch (error) { 905 | results[test.name] = { 906 | success: false, 907 | error: error.message 908 | }; 909 | } 910 | } 911 | 912 | res.json({ 913 | timestamp: new Date().toISOString(), 914 | results 915 | }); 916 | } catch (error) { 917 | res.status(500).json({ 918 | error: 'vnstat测试失败', 919 | details: error.message 920 | }); 921 | } 922 | }); 923 | 924 | // 错误处理中间件 925 | app.use((err, req, res, next) => { 926 | console.error('服务器错误:', err.stack); 927 | 928 | // 记录详细的错误信息 929 | const errorInfo = { 930 | timestamp: new Date().toISOString(), 931 | url: req.url, 932 | method: req.method, 933 | userAgent: req.get('User-Agent'), 934 | ip: req.ip, 935 | error: { 936 | message: err.message, 937 | stack: err.stack, 938 | name: err.name 939 | } 940 | }; 941 | 942 | // 如果是缓存相关错误,记录详细信息 943 | if (err.message && err.message.includes('cache')) { 944 | console.error('缓存错误详情:', { 945 | ...errorInfo, 946 | cacheStats: cacheManager.getStats() 947 | }); 948 | } 949 | 950 | // 如果是vnstat相关错误,记录详细信息 951 | if (err.message && (err.message.includes('vnstat') || err.message.includes('command'))) { 952 | console.error('vnstat命令错误详情:', errorInfo); 953 | } 954 | 955 | // 根据错误类型返回不同的响应 956 | let statusCode = 500; 957 | let errorMessage = '服务器内部错误'; 958 | 959 | if (err.code === 'ENOENT') { 960 | statusCode = 503; 961 | errorMessage = '服务暂时不可用,请检查vnstat命令是否正确安装'; 962 | } else if (err.code === 'ETIMEDOUT') { 963 | statusCode = 504; 964 | errorMessage = '请求超时,请稍后重试'; 965 | } else if (err.message && err.message.includes('vnstat')) { 966 | statusCode = 503; 967 | errorMessage = 'vnstat命令执行失败,请检查系统配置'; 968 | } 969 | 970 | res.status(statusCode).json({ 971 | error: errorMessage, 972 | timestamp: errorInfo.timestamp, 973 | requestId: Math.random().toString(36).substr(2, 9) 974 | }); 975 | }); 976 | 977 | // 启动服务器 978 | const server = app.listen(port, () => { 979 | console.log(`服务器运行在 http://localhost:${port}`); 980 | console.log(`缓存配置: 最大条目=${cacheConfig.maxSize}, 最大内存=${cacheConfig.maxMemoryMB}MB`); 981 | 982 | // 检查vnstat命令可用性 983 | exec('vnstat --version', (error, stdout, stderr) => { 984 | if (error) { 985 | console.error('⚠️ vnstat命令不可用:', error.message); 986 | console.error('请确保已安装vnstat:'); 987 | console.error(' Ubuntu/Debian: sudo apt-get install vnstat'); 988 | console.error(' CentOS/RHEL: sudo yum install vnstat'); 989 | console.error(' Windows: 请安装WSL或使用其他网络监控工具'); 990 | } else { 991 | console.log('✅ vnstat命令可用:', stdout.trim()); 992 | } 993 | }); 994 | 995 | // 启动内存监控 996 | setInterval(() => { 997 | const memUsage = process.memoryUsage(); 998 | const cacheStats = cacheManager.getStats(); 999 | console.log(`内存使用: RSS=${(memUsage.rss / 1024 / 1024).toFixed(2)}MB, 缓存=${cacheStats.memoryUsage}, 命中率=${cacheStats.hitRate}`); 1000 | }, cacheConfig.memoryMonitorInterval); 1001 | 1002 | }).on('error', (err) => { 1003 | if (err.code === 'EADDRINUSE') { 1004 | console.error(`端口 ${port} 已被占用,请尝试使用其他端口`); 1005 | console.error('你可以通过设置环境变量 PORT 来指定其他端口,例如:'); 1006 | console.error('PORT=8080 npm start'); 1007 | } else { 1008 | console.error('启动服务器时发生错误:', err); 1009 | } 1010 | process.exit(1); 1011 | }); 1012 | 1013 | // 优雅关闭 1014 | process.on('SIGTERM', () => { 1015 | console.log('收到 SIGTERM 信号,正在关闭服务器...'); 1016 | server.close(() => { 1017 | console.log('服务器已关闭'); 1018 | process.exit(0); 1019 | }); 1020 | }); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |158 | 当前选择的网络接口: {{selectedInterface}} 159 |
160 |