commit 1ccff88c56f14f630b595618cba554047d038e5c Author: lirui Date: Sun Jan 4 21:51:40 2026 +0800 项目初始化 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c01124d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)" + ] + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d5c98b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,163 @@ +# 项目信息 +BINARY_NAME=nodeprobe +MODULE_NAME=github.com/meowrain/nodeprobe +MAIN_PATH=./cmd/nodeprobe + +# Go 相关命令 +GO=go +GOFLAGS=-v +GOBUILD=$(GO) build $(GOFLAGS) +GOCLEAN=$(GO) clean +GOTEST=$(GO) test +GOGET=$(GO) get +GOMOD=$(GO) mod + +# 输出目录 +BUILD_DIR=build +DIST_DIR=dist + +# 版本信息 +VERSION?=dev +BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S') +GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)" + +# 默认目标 +.PHONY: all +all: clean build + +# 构建 +.PHONY: build +build: + @echo "Building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)$(shell go env GOEXE) $(MAIN_PATH) + @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)" + +# 快速构建(不清理) +.PHONY: quick +quick: + @echo "Quick building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)$(shell go env GOEXE) $(MAIN_PATH) + +# 运行 +.PHONY: run +run: build + @echo "Running $(BINARY_NAME)..." + @$(BUILD_DIR)/$(BINARY_NAME)$(shell go env GOEXE) + +# 直接运行(不构建) +.PHONY: dev +dev: + @echo "Running $(BINARY_NAME) in development mode..." + $(GO) run $(MAIN_PATH) + +# 测试 +.PHONY: test +test: + @echo "Running tests..." + $(GOTEST) -v -cover ./... + +# 测试覆盖率 +.PHONY: coverage +coverage: + @echo "Running tests with coverage..." + $(GOTEST) -v -coverprofile=coverage.out ./... + $(GO) tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +# 清理 +.PHONY: clean +clean: + @echo "Cleaning..." + $(GOCLEAN) + @rm -rf $(BUILD_DIR) + @rm -rf $(DIST_DIR) + @rm -f coverage.out coverage.html + @echo "Clean complete" + +# 依赖管理 +.PHONY: deps +deps: + @echo "Downloading dependencies..." + $(GOMOD) download + $(GOMOD) tidy + +# 更新依赖 +.PHONY: deps-update +deps-update: + @echo "Updating dependencies..." + $(GOGET) -u ./... + $(GOMOD) tidy + +# 代码检查 +.PHONY: lint +lint: + @echo "Running linter..." + @which golangci-lint > /dev/null || (echo "golangci-lint not found. Install it from https://golangci-lint.run/usage/install/" && exit 1) + golangci-lint run ./... + +# 代码格式化 +.PHONY: fmt +fmt: + @echo "Formatting code..." + $(GO) fmt ./... + +# 代码审查 +.PHONY: vet +vet: + @echo "Running go vet..." + $(GO) vet ./... + +# 交叉编译(多平台) +.PHONY: build-all +build-all: clean + @echo "Building for multiple platforms..." + @mkdir -p $(DIST_DIR) + @echo "Building for Windows amd64..." + @GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH) + @echo "Building for Linux amd64..." + @GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PATH) + @echo "Building for Linux arm64..." + @GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PATH) + @echo "Building for macOS amd64..." + @GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PATH) + @echo "Building for macOS arm64..." + @GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PATH) + @echo "All builds complete in $(DIST_DIR)/" + +# 安装到系统 +.PHONY: install +install: build + @echo "Installing $(BINARY_NAME)..." + $(GO) install $(LDFLAGS) $(MAIN_PATH) + @echo "Installed to $(shell go env GOPATH)/bin/$(BINARY_NAME)" + +# 卸载 +.PHONY: uninstall +uninstall: + @echo "Uninstalling $(BINARY_NAME)..." + @rm -f $(shell go env GOPATH)/bin/$(BINARY_NAME)$(shell go env GOEXE) + @echo "Uninstall complete" + +# 帮助信息 +.PHONY: help +help: + @echo "Available targets:" + @echo " make build - Build the binary" + @echo " make quick - Quick build without cleaning" + @echo " make run - Build and run the binary" + @echo " make dev - Run without building (using go run)" + @echo " make test - Run tests" + @echo " make coverage - Run tests with coverage report" + @echo " make clean - Clean build artifacts" + @echo " make deps - Download and tidy dependencies" + @echo " make deps-update - Update dependencies" + @echo " make lint - Run linter (requires golangci-lint)" + @echo " make fmt - Format code" + @echo " make vet - Run go vet" + @echo " make build-all - Build for multiple platforms" + @echo " make install - Install binary to GOPATH/bin" + @echo " make uninstall - Uninstall binary from GOPATH/bin" + @echo " make help - Show this help message" diff --git a/cmd/nodeprobe/main.go b/cmd/nodeprobe/main.go new file mode 100644 index 0000000..2d27ad1 --- /dev/null +++ b/cmd/nodeprobe/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/meowrain/nodeprobe/internal/netstat" +) + +func main() { + // 1. 获取初始数据 + prevStats, err := netstat.GetInterfaceStats() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if len(prevStats) == 0 { + fmt.Println("No network interfaces found") + os.Exit(1) + } + + // 2. 设置刷新间隔(默认 1 秒) + interval := 1 * time.Second + + // 3. 打印表头 + printHeader() + + // 4. 创建 ticker 定时器 + ticker := time.NewTicker(interval) + defer ticker.Stop() + + // 5. 监听 Ctrl+C 退出 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // 6. 主循环 + for { + select { + case <-ticker.C: + // 获取当前统计数据 + currStats, err := netstat.GetInterfaceStats() + if err != nil { + continue + } + + // 计算速率 + rates := netstat.CalculateRate(prevStats, currStats, interval) + + // 打印一行统计 + printStats(rates, currStats) + + // 更新上一次的数据 + prevStats = currStats + + case <-sigChan: + fmt.Println("\nExiting...") + return + } + } +} + +func printHeader() { + fmt.Println("Network Interface Traffic Monitor") + fmt.Println("----------------------------------") + fmt.Printf("%-10s %15s %15s %15s %15s\n", + "Interface", "RX Rate", "TX Rate", "RX Total", "TX Total") + fmt.Println("----------------------------------") +} + +func printStats(rates []netstat.TrafficRate, stats []netstat.InterfaceStats) { + // 创建统计 map + statsMap := make(map[string]netstat.InterfaceStats) + for _, s := range stats { + statsMap[s.Name] = s + } + + for _, r := range rates { + s := statsMap[r.Name] + fmt.Printf("%-10s %15s %15s %15s %15s\n", + r.Name, + netstat.FormatBps(r.RxKbps*1000), + netstat.FormatBps(r.TxKbps*1000), + netstat.FormatBytes(float64(s.RxBytes)), + netstat.FormatBytes(float64(s.TxBytes)), + ) + } + fmt.Println() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4c1723 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/meowrain/nodeprobe + +go 1.25.5 diff --git a/internal/netstat/netstat.go b/internal/netstat/netstat.go new file mode 100644 index 0000000..2792fbb --- /dev/null +++ b/internal/netstat/netstat.go @@ -0,0 +1,134 @@ +package netstat + +import ( + "bufio" + "fmt" + "os" + "strings" + "time" +) + +// InterfaceStats 网卡统计信息 + +type InterfaceStats struct { + Name string //网卡名称 + RxBytes uint64 //接收字节数 + TxBytes uint64 //发送字节数 + RxPackets uint64 //接收数据包数 + TxPackets uint64 //发送数据包数 + RxErrors uint64 //接收错误数 + TxErrors uint64 //发送错误数 +} +type TrafficRate struct { + Name string // 网卡名称 + RxBytesRate float64 // 接收字节速率 + TxBytesRate float64 // 发送字节速率 + RxKbps float64 // 接收比特速率 + TxKbps float64 // 发送比特速率 +} + +// GetInterfaceStats 获取所有网卡的统计信息 +func GetInterfaceStats() ([]InterfaceStats, error) { + // 打开/proc/net/dev文件 + file, err := os.Open("/proc/net/dev") + if err != nil { + return nil, err + } + defer file.Close() + + // 创建scanner逐行读取文件内容 + scanner := bufio.NewScanner(file) + + // 跳过前两行 头部信息 + scanner.Scan() + scanner.Scan() + + var stats []InterfaceStats + + for scanner.Scan() { + line := scanner.Text() + // 按空格分割 + fields := strings.Fields(line) + if len(fields) < 17 { + continue + } + // 提取网卡名 + ifaceName := strings.TrimSuffix(fields[0], ":") + if ifaceName == "lo" { + continue //跳过回环接口 + } + + stat := InterfaceStats{ + Name: ifaceName, + RxBytes: parseUint64(fields[1]), + RxPackets: parseUint64(fields[2]), + RxErrors: parseUint64(fields[3]), + TxBytes: parseUint64(fields[9]), + TxPackets: parseUint64(fields[10]), + TxErrors: parseUint64(fields[11]), + } + stats = append(stats, stat) + } + return stats, scanner.Err() +} + +func parseUint64(s string) uint64 { + var val uint64 + fmt.Sscanf(s, "%d", &val) + return val +} + +func CalculateRate(prev, curr []InterfaceStats, duration time.Duration) []TrafficRate { + prevMap := make(map[string]InterfaceStats) + for _, stat := range prev { + prevMap[stat.Name] = stat + } + var rates []TrafficRate + seconds := duration.Seconds() + for _, c := range curr { + p, exists := prevMap[c.Name] + if !exists { + continue + } + rxBytesDiff := c.RxBytes - p.RxBytes + txBytesDiff := c.TxBytes - p.TxBytes + rate := TrafficRate{ + Name: c.Name, + RxBytesRate: float64(rxBytesDiff) / seconds, + TxBytesRate: float64(txBytesDiff) / seconds, + RxKbps: (float64(rxBytesDiff) * 8) / (seconds * 1024), + TxKbps: (float64(txBytesDiff) * 8) / (seconds * 1024), + } + rates = append(rates, rate) + } + return rates +} + +func FormatBytes(bytes float64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%.2f B", bytes) + } + div, exp := uint64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.2f %cB", bytes/float64(div), "KMGTPE"[exp]) +} + +// FormatBps 格式化比特率为 1.23 Kbps、4.56 Mbps 等 +func FormatBps(bps float64) string { + const unit = 1000 + if bps < unit { + return fmt.Sprintf("%.2f bps", bps) + } + + div, exp := float64(unit), 0 + for n := bps / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + + return fmt.Sprintf("%.2f %cps", bps/div, "KMGTPE"[exp]) +} diff --git a/main.exe b/main.exe new file mode 100644 index 0000000..c870020 Binary files /dev/null and b/main.exe differ