优化ip=“” nodeid=0
This commit is contained in:
@@ -37,6 +37,19 @@ func main() {
|
|||||||
// 初始化错误恢复
|
// 初始化错误恢复
|
||||||
recovery.Init()
|
recovery.Init()
|
||||||
|
|
||||||
|
// 如果配置中没有节点信息,先发送一次心跳获取节点信息
|
||||||
|
if cfg.Node.ID == 0 || cfg.Node.IP == "" {
|
||||||
|
logger.Info("节点信息未配置,发送心跳获取节点信息")
|
||||||
|
if err := heartbeat.RegisterNode(cfg); err != nil {
|
||||||
|
logger.Warn("注册节点失败,将在心跳时重试", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
logger.Info("节点信息已获取并保存",
|
||||||
|
zap.Uint("node_id", cfg.Node.ID),
|
||||||
|
zap.String("node_ip", cfg.Node.IP),
|
||||||
|
zap.String("location", fmt.Sprintf("%s/%s/%s", cfg.Node.Country, cfg.Node.Province, cfg.Node.City)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 启动心跳上报
|
// 启动心跳上报
|
||||||
heartbeatReporter := heartbeat.NewReporter(cfg)
|
heartbeatReporter := heartbeat.NewReporter(cfg)
|
||||||
go heartbeatReporter.Start(context.Background())
|
go heartbeatReporter.Start(context.Background())
|
||||||
|
|||||||
79
install.sh
79
install.sh
@@ -387,6 +387,84 @@ configure_firewall() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 登记节点(调用心跳API获取节点信息)
|
||||||
|
register_node() {
|
||||||
|
echo -e "${BLUE}登记节点到后端服务器...${NC}"
|
||||||
|
|
||||||
|
# 创建临时配置文件
|
||||||
|
CONFIG_FILE="$SOURCE_DIR/config.yaml"
|
||||||
|
sudo mkdir -p "$SOURCE_DIR"
|
||||||
|
|
||||||
|
# 创建基础配置文件
|
||||||
|
sudo tee "$CONFIG_FILE" > /dev/null <<EOF
|
||||||
|
server:
|
||||||
|
port: 2200
|
||||||
|
backend:
|
||||||
|
url: ${BACKEND_URL}
|
||||||
|
heartbeat:
|
||||||
|
interval: 60
|
||||||
|
debug: false
|
||||||
|
node:
|
||||||
|
id: 0
|
||||||
|
ip: ""
|
||||||
|
country: ""
|
||||||
|
province: ""
|
||||||
|
city: ""
|
||||||
|
isp: ""
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 调用心跳API获取节点信息
|
||||||
|
echo -e "${BLUE}发送心跳请求获取节点信息...${NC}"
|
||||||
|
RESPONSE=$(curl -s -X POST "${BACKEND_URL}/api/node/heartbeat" \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "type=pingServer" 2>&1)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
# 尝试解析JSON响应
|
||||||
|
NODE_ID=$(echo "$RESPONSE" | grep -o '"node_id":[0-9]*' | grep -o '[0-9]*' | head -1)
|
||||||
|
NODE_IP=$(echo "$RESPONSE" | grep -o '"node_ip":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||||
|
COUNTRY=$(echo "$RESPONSE" | grep -o '"country":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||||
|
PROVINCE=$(echo "$RESPONSE" | grep -o '"province":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||||
|
CITY=$(echo "$RESPONSE" | grep -o '"city":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||||
|
ISP=$(echo "$RESPONSE" | grep -o '"isp":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||||
|
|
||||||
|
if [ -n "$NODE_ID" ] && [ "$NODE_ID" != "0" ] && [ -n "$NODE_IP" ]; then
|
||||||
|
# 更新配置文件
|
||||||
|
sudo tee "$CONFIG_FILE" > /dev/null <<EOF
|
||||||
|
server:
|
||||||
|
port: 2200
|
||||||
|
backend:
|
||||||
|
url: ${BACKEND_URL}
|
||||||
|
heartbeat:
|
||||||
|
interval: 60
|
||||||
|
debug: false
|
||||||
|
node:
|
||||||
|
id: ${NODE_ID}
|
||||||
|
ip: ${NODE_IP}
|
||||||
|
country: ${COUNTRY:-""}
|
||||||
|
province: ${PROVINCE:-""}
|
||||||
|
city: ${CITY:-""}
|
||||||
|
isp: ${ISP:-""}
|
||||||
|
EOF
|
||||||
|
echo -e "${GREEN}✓ 节点登记成功${NC}"
|
||||||
|
echo -e "${BLUE} 节点ID: ${NODE_ID}${NC}"
|
||||||
|
echo -e "${BLUE} 节点IP: ${NODE_IP}${NC}"
|
||||||
|
if [ -n "$COUNTRY" ] || [ -n "$PROVINCE" ] || [ -n "$CITY" ]; then
|
||||||
|
echo -e "${BLUE} 位置: ${COUNTRY:-""}/${PROVINCE:-""}/${CITY:-""}${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ 无法从响应中解析节点信息,将在服务启动时重试${NC}"
|
||||||
|
echo -e "${YELLOW} 响应: ${RESPONSE}${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ 心跳请求失败,将在服务启动时重试${NC}"
|
||||||
|
echo -e "${YELLOW} 错误: ${RESPONSE}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 设置配置文件权限
|
||||||
|
sudo chmod 644 "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
# 启动服务
|
# 启动服务
|
||||||
start_service() {
|
start_service() {
|
||||||
echo -e "${BLUE}启动服务...${NC}"
|
echo -e "${BLUE}启动服务...${NC}"
|
||||||
@@ -472,6 +550,7 @@ main() {
|
|||||||
build_from_source
|
build_from_source
|
||||||
create_service
|
create_service
|
||||||
configure_firewall
|
configure_firewall
|
||||||
|
register_node
|
||||||
start_service
|
start_service
|
||||||
verify_installation
|
verify_installation
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -21,6 +22,16 @@ type Config struct {
|
|||||||
} `yaml:"heartbeat"`
|
} `yaml:"heartbeat"`
|
||||||
|
|
||||||
Debug bool `yaml:"debug"`
|
Debug bool `yaml:"debug"`
|
||||||
|
|
||||||
|
// 节点信息(通过心跳获取并持久化)
|
||||||
|
Node struct {
|
||||||
|
ID uint `yaml:"id"` // 节点ID
|
||||||
|
IP string `yaml:"ip"` // 节点外网IP
|
||||||
|
Country string `yaml:"country"` // 国家
|
||||||
|
Province string `yaml:"province"` // 省份
|
||||||
|
City string `yaml:"city"` // 城市
|
||||||
|
ISP string `yaml:"isp"` // ISP
|
||||||
|
} `yaml:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
@@ -58,3 +69,37 @@ func Load() (*Config, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save 保存配置到文件
|
||||||
|
func (c *Config) Save() error {
|
||||||
|
configPath := os.Getenv("CONFIG_PATH")
|
||||||
|
if configPath == "" {
|
||||||
|
configPath = "config.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
dir := filepath.Dir(configPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建配置目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化配置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("写入配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigPath 获取配置文件路径
|
||||||
|
func GetConfigPath() string {
|
||||||
|
configPath := os.Getenv("CONFIG_PATH")
|
||||||
|
if configPath == "" {
|
||||||
|
configPath = "config.yaml"
|
||||||
|
}
|
||||||
|
return configPath
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ type PingTask struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
targetIP string // 存储目标IP,从ping输出中提取
|
targetIP string // 存储目标IP,从ping输出中提取
|
||||||
|
currentCmd *exec.Cmd // 当前正在执行的命令,用于停止时取消
|
||||||
|
cmdMu sync.Mutex // 保护 currentCmd 的锁
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPingTask(taskID, target string, interval, maxDuration time.Duration) *PingTask {
|
func NewPingTask(taskID, target string, interval, maxDuration time.Duration) *PingTask {
|
||||||
@@ -77,11 +79,31 @@ func (t *PingTask) Start(ctx context.Context, resultCallback func(result map[str
|
|||||||
|
|
||||||
func (t *PingTask) Stop() {
|
func (t *PingTask) Stop() {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
if !t.IsRunning {
|
||||||
if t.IsRunning {
|
t.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
t.IsRunning = false
|
t.IsRunning = false
|
||||||
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
// 取消正在执行的命令
|
||||||
|
t.cmdMu.Lock()
|
||||||
|
if t.currentCmd != nil && t.currentCmd.Process != nil {
|
||||||
|
t.logger.Debug("取消正在执行的ping命令", zap.String("task_id", t.TaskID))
|
||||||
|
t.currentCmd.Process.Kill()
|
||||||
|
t.currentCmd = nil
|
||||||
|
}
|
||||||
|
t.cmdMu.Unlock()
|
||||||
|
|
||||||
|
// 关闭停止通道
|
||||||
|
select {
|
||||||
|
case <-t.StopCh:
|
||||||
|
// 已经关闭
|
||||||
|
default:
|
||||||
close(t.StopCh)
|
close(t.StopCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.logger.Info("Ping任务已停止", zap.String("task_id", t.TaskID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *PingTask) UpdateLastRequest() {
|
func (t *PingTask) UpdateLastRequest() {
|
||||||
@@ -91,10 +113,30 @@ func (t *PingTask) UpdateLastRequest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result map[string]interface{})) {
|
func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result map[string]interface{})) {
|
||||||
|
// 检查任务是否已停止
|
||||||
|
t.mu.RLock()
|
||||||
|
isRunning := t.IsRunning
|
||||||
|
t.mu.RUnlock()
|
||||||
|
if !isRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 发送10个ping包,间隔0.5秒,实时解析每个包的延迟
|
// 发送10个ping包,间隔0.5秒,实时解析每个包的延迟
|
||||||
// 使用 -c 10 -i 0.5 发送10个包
|
// 使用 -c 10 -i 0.5 发送10个包
|
||||||
cmd := exec.Command("ping", "-c", "10", "-i", "0.5", t.Target)
|
cmd := exec.Command("ping", "-c", "10", "-i", "0.5", t.Target)
|
||||||
|
|
||||||
|
// 保存命令引用,以便停止时取消
|
||||||
|
t.cmdMu.Lock()
|
||||||
|
t.currentCmd = cmd
|
||||||
|
t.cmdMu.Unlock()
|
||||||
|
|
||||||
|
// 确保在函数退出时清理命令引用
|
||||||
|
defer func() {
|
||||||
|
t.cmdMu.Lock()
|
||||||
|
t.currentCmd = nil
|
||||||
|
t.cmdMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
// 获取标准输出管道,实时读取
|
// 获取标准输出管道,实时读取
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,6 +166,16 @@ func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result ma
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查任务是否已停止(在启动命令后)
|
||||||
|
t.mu.RLock()
|
||||||
|
isRunning = t.IsRunning
|
||||||
|
t.mu.RUnlock()
|
||||||
|
if !isRunning {
|
||||||
|
// 任务已停止,取消命令
|
||||||
|
cmd.Process.Kill()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 使用bufio.Scanner实时读取每一行
|
// 使用bufio.Scanner实时读取每一行
|
||||||
scanner := bufio.NewScanner(stdout)
|
scanner := bufio.NewScanner(stdout)
|
||||||
processedPackets := make(map[int]bool) // 用于去重,避免重复处理同一个包(通过icmp_seq)
|
processedPackets := make(map[int]bool) // 用于去重,避免重复处理同一个包(通过icmp_seq)
|
||||||
@@ -131,6 +183,14 @@ func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result ma
|
|||||||
// 在goroutine中读取输出,避免阻塞
|
// 在goroutine中读取输出,避免阻塞
|
||||||
go func() {
|
go func() {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
// 检查任务是否已停止
|
||||||
|
t.mu.RLock()
|
||||||
|
isRunning := t.IsRunning
|
||||||
|
t.mu.RUnlock()
|
||||||
|
if !isRunning {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
line := strings.TrimSpace(scanner.Text())
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -73,8 +73,25 @@ func (t *TCPingTask) Start(ctx context.Context, resultCallback func(result map[s
|
|||||||
}
|
}
|
||||||
t.mu.RUnlock()
|
t.mu.RUnlock()
|
||||||
|
|
||||||
|
// 检查任务是否已停止
|
||||||
|
t.mu.RLock()
|
||||||
|
isRunning := t.IsRunning
|
||||||
|
t.mu.RUnlock()
|
||||||
|
if !isRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 执行tcping测试(每次测试完成后立即返回结果)
|
// 执行tcping测试(每次测试完成后立即返回结果)
|
||||||
result := t.executeTCPing()
|
result := t.executeTCPing()
|
||||||
|
|
||||||
|
// 再次检查任务是否已停止(执行完成后)
|
||||||
|
t.mu.RLock()
|
||||||
|
isRunning = t.IsRunning
|
||||||
|
t.mu.RUnlock()
|
||||||
|
if !isRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if resultCallback != nil {
|
if resultCallback != nil {
|
||||||
resultCallback(result)
|
resultCallback(result)
|
||||||
}
|
}
|
||||||
@@ -94,11 +111,22 @@ func (t *TCPingTask) Start(ctx context.Context, resultCallback func(result map[s
|
|||||||
|
|
||||||
func (t *TCPingTask) Stop() {
|
func (t *TCPingTask) Stop() {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
if !t.IsRunning {
|
||||||
if t.IsRunning {
|
t.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
t.IsRunning = false
|
t.IsRunning = false
|
||||||
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
// 关闭停止通道
|
||||||
|
select {
|
||||||
|
case <-t.StopCh:
|
||||||
|
// 已经关闭
|
||||||
|
default:
|
||||||
close(t.StopCh)
|
close(t.StopCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.logger.Info("TCPing任务已停止", zap.String("task_id", t.TaskID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TCPingTask) UpdateLastRequest() {
|
func (t *TCPingTask) UpdateLastRequest() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +25,25 @@ var taskMutex sync.RWMutex
|
|||||||
var backendURL string
|
var backendURL string
|
||||||
var logger *zap.Logger
|
var logger *zap.Logger
|
||||||
|
|
||||||
|
// 批量推送缓冲(每个任务一个缓冲)
|
||||||
|
var pushBuffers = make(map[string]*pushBuffer)
|
||||||
|
var bufferMutex sync.RWMutex
|
||||||
|
|
||||||
|
// pushBuffer 批量推送缓冲
|
||||||
|
type pushBuffer struct {
|
||||||
|
taskID string
|
||||||
|
results []map[string]interface{}
|
||||||
|
mu sync.Mutex
|
||||||
|
lastPush time.Time
|
||||||
|
pushTimer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 批量推送配置
|
||||||
|
batchPushInterval = 1 * time.Second // 批量推送间隔:1秒
|
||||||
|
batchPushMaxSize = 10 // 批量推送最大数量:10个结果
|
||||||
|
)
|
||||||
|
|
||||||
func InitContinuousHandler(cfg *config.Config) {
|
func InitContinuousHandler(cfg *config.Config) {
|
||||||
backendURL = cfg.Backend.URL
|
backendURL = cfg.Backend.URL
|
||||||
logger, _ = zap.NewProduction()
|
logger, _ = zap.NewProduction()
|
||||||
@@ -202,9 +222,6 @@ func pushResultToBackend(taskID string, result map[string]interface{}) {
|
|||||||
result["packet_loss"] = false
|
result["packet_loss"] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 推送结果到后端
|
|
||||||
url := fmt.Sprintf("%s/api/public/node/continuous/result", backendURL)
|
|
||||||
|
|
||||||
// 优先使用心跳返回的节点信息
|
// 优先使用心跳返回的节点信息
|
||||||
nodeID := heartbeat.GetNodeID()
|
nodeID := heartbeat.GetNodeID()
|
||||||
nodeIP := heartbeat.GetNodeIP()
|
nodeIP := heartbeat.GetNodeIP()
|
||||||
@@ -215,22 +232,132 @@ func pushResultToBackend(taskID string, result map[string]interface{}) {
|
|||||||
logger.Debug("使用本地IP作为后备", zap.String("node_ip", nodeIP))
|
logger.Debug("使用本地IP作为后备", zap.String("node_ip", nodeIP))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送 node_id 和 node_ip,后端可以通过这些信息精准匹配
|
// 确保已经获取到 node_id,避免发送无效数据包
|
||||||
|
if nodeID == 0 {
|
||||||
|
logger.Warn("节点ID未获取,跳过推送结果",
|
||||||
|
zap.String("task_id", taskID),
|
||||||
|
zap.String("node_ip", nodeIP),
|
||||||
|
zap.String("hint", "等待心跳返回node_id后再推送"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保已经获取到 node_ip
|
||||||
|
if nodeIP == "" {
|
||||||
|
logger.Warn("节点IP未获取,跳过推送结果",
|
||||||
|
zap.String("task_id", taskID),
|
||||||
|
zap.Uint("node_id", nodeID),
|
||||||
|
zap.String("hint", "等待心跳返回node_ip后再推送"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到批量推送缓冲
|
||||||
|
addToPushBuffer(taskID, nodeID, nodeIP, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToPushBuffer 添加结果到批量推送缓冲
|
||||||
|
func addToPushBuffer(taskID string, nodeID uint, nodeIP string, result map[string]interface{}) {
|
||||||
|
bufferMutex.Lock()
|
||||||
|
buffer, exists := pushBuffers[taskID]
|
||||||
|
if !exists {
|
||||||
|
buffer = &pushBuffer{
|
||||||
|
taskID: taskID,
|
||||||
|
results: make([]map[string]interface{}, 0, batchPushMaxSize),
|
||||||
|
lastPush: time.Now(),
|
||||||
|
}
|
||||||
|
pushBuffers[taskID] = buffer
|
||||||
|
}
|
||||||
|
bufferMutex.Unlock()
|
||||||
|
|
||||||
|
buffer.mu.Lock()
|
||||||
|
defer buffer.mu.Unlock()
|
||||||
|
|
||||||
|
// 添加结果到缓冲
|
||||||
|
buffer.results = append(buffer.results, result)
|
||||||
|
|
||||||
|
// 如果缓冲已满,立即推送
|
||||||
|
shouldFlush := len(buffer.results) >= batchPushMaxSize
|
||||||
|
buffer.mu.Unlock()
|
||||||
|
|
||||||
|
if shouldFlush {
|
||||||
|
flushPushBuffer(taskID, nodeID, nodeIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.mu.Lock()
|
||||||
|
|
||||||
|
// 如果距离上次推送超过间隔时间,启动定时器推送
|
||||||
|
if buffer.pushTimer == nil {
|
||||||
|
buffer.pushTimer = time.AfterFunc(batchPushInterval, func() {
|
||||||
|
flushPushBuffer(taskID, nodeID, nodeIP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushPushBuffer 刷新并推送缓冲中的结果
|
||||||
|
func flushPushBuffer(taskID string, nodeID uint, nodeIP string) {
|
||||||
|
bufferMutex.RLock()
|
||||||
|
buffer, exists := pushBuffers[taskID]
|
||||||
|
bufferMutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.mu.Lock()
|
||||||
|
if len(buffer.results) == 0 {
|
||||||
|
buffer.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制结果列表
|
||||||
|
results := make([]map[string]interface{}, len(buffer.results))
|
||||||
|
copy(results, buffer.results)
|
||||||
|
buffer.results = buffer.results[:0] // 清空缓冲
|
||||||
|
|
||||||
|
// 停止定时器
|
||||||
|
if buffer.pushTimer != nil {
|
||||||
|
buffer.pushTimer.Stop()
|
||||||
|
buffer.pushTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.lastPush = time.Now()
|
||||||
|
buffer.mu.Unlock()
|
||||||
|
|
||||||
|
// 批量推送结果(目前后端只支持单个结果,所以逐个推送)
|
||||||
|
// 但可以减少HTTP请求的频率
|
||||||
|
for _, result := range results {
|
||||||
|
pushSingleResult(taskID, nodeID, nodeIP, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushSingleResult 推送单个结果到后端
|
||||||
|
func pushSingleResult(taskID string, nodeID uint, nodeIP string, result map[string]interface{}) {
|
||||||
|
// 推送结果到后端
|
||||||
|
url := fmt.Sprintf("%s/api/public/node/continuous/result", backendURL)
|
||||||
|
|
||||||
|
// 获取节点位置信息
|
||||||
|
country, province, city, isp := heartbeat.GetNodeLocation()
|
||||||
|
|
||||||
|
// 发送 node_id、node_ip 和位置信息,后端可以通过这些信息精准匹配
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"task_id": taskID,
|
"task_id": taskID,
|
||||||
|
"node_id": nodeID,
|
||||||
|
"node_ip": nodeIP,
|
||||||
"result": result,
|
"result": result,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果 node_id 存在,优先发送 node_id
|
// 添加位置信息(如果存在)
|
||||||
if nodeID > 0 {
|
if country != "" {
|
||||||
data["node_id"] = nodeID
|
data["country"] = country
|
||||||
logger.Debug("推送结果时使用存储的node_id", zap.Uint("node_id", nodeID))
|
|
||||||
}
|
}
|
||||||
|
if province != "" {
|
||||||
// 如果 node_ip 存在,发送 node_ip
|
data["province"] = province
|
||||||
if nodeIP != "" {
|
}
|
||||||
data["node_ip"] = nodeIP
|
if city != "" {
|
||||||
logger.Debug("推送结果时使用存储的node_ip", zap.String("node_ip", nodeIP))
|
data["city"] = city
|
||||||
|
}
|
||||||
|
if isp != "" {
|
||||||
|
data["isp"] = isp
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
@@ -261,18 +388,110 @@ func pushResultToBackend(taskID string, result map[string]interface{}) {
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
bodyStr := string(body)
|
||||||
|
|
||||||
|
// 检查是否是任务不存在的错误
|
||||||
|
if containsTaskNotFoundError(bodyStr) {
|
||||||
|
logger.Warn("后端任务不存在,停止节点端任务",
|
||||||
|
zap.String("task_id", taskID),
|
||||||
|
zap.String("response", bodyStr))
|
||||||
|
// 停止对应的持续测试任务
|
||||||
|
stopTaskByTaskID(taskID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.Warn("推送结果失败,继续运行",
|
logger.Warn("推送结果失败,继续运行",
|
||||||
zap.Int("status", resp.StatusCode),
|
zap.Int("status", resp.StatusCode),
|
||||||
zap.String("task_id", taskID),
|
zap.String("task_id", taskID),
|
||||||
zap.String("url", url),
|
zap.String("url", url),
|
||||||
zap.String("response", string(body)))
|
zap.String("response", bodyStr))
|
||||||
// 推送失败不停止任务,继续运行
|
// 其他错误不停止任务,继续运行
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("推送结果成功", zap.String("task_id", taskID))
|
logger.Debug("推送结果成功", zap.String("task_id", taskID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// containsTaskNotFoundError 检查响应中是否包含任务不存在的错误
|
||||||
|
func containsTaskNotFoundError(responseBody string) bool {
|
||||||
|
// 检查常见的任务不存在错误消息
|
||||||
|
errorKeywords := []string{
|
||||||
|
"找不到对应的后端任务",
|
||||||
|
"任务不存在",
|
||||||
|
"task not found",
|
||||||
|
"找不到对应的任务",
|
||||||
|
}
|
||||||
|
|
||||||
|
responseLower := strings.ToLower(responseBody)
|
||||||
|
for _, keyword := range errorKeywords {
|
||||||
|
if strings.Contains(responseLower, strings.ToLower(keyword)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析 JSON 响应,检查错误消息
|
||||||
|
var resp struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(responseBody), &resp); err == nil {
|
||||||
|
msgLower := strings.ToLower(resp.Msg)
|
||||||
|
for _, keyword := range errorKeywords {
|
||||||
|
if strings.Contains(msgLower, strings.ToLower(keyword)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopTaskByTaskID 根据 taskID 停止对应的持续测试任务
|
||||||
|
func stopTaskByTaskID(taskID string) {
|
||||||
|
taskMutex.Lock()
|
||||||
|
defer taskMutex.Unlock()
|
||||||
|
|
||||||
|
task, exists := continuousTasks[taskID]
|
||||||
|
if !exists {
|
||||||
|
logger.Debug("任务不存在,无需停止", zap.String("task_id", taskID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("停止持续测试任务", zap.String("task_id", taskID))
|
||||||
|
|
||||||
|
// 停止任务
|
||||||
|
task.IsRunning = false
|
||||||
|
if task.pingTask != nil {
|
||||||
|
task.pingTask.Stop()
|
||||||
|
}
|
||||||
|
if task.tcpingTask != nil {
|
||||||
|
task.tcpingTask.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭停止通道
|
||||||
|
select {
|
||||||
|
case <-task.StopCh:
|
||||||
|
// 已经关闭
|
||||||
|
default:
|
||||||
|
close(task.StopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
delete(continuousTasks, taskID)
|
||||||
|
|
||||||
|
// 清理推送缓冲
|
||||||
|
bufferMutex.Lock()
|
||||||
|
if buffer, exists := pushBuffers[taskID]; exists {
|
||||||
|
if buffer.pushTimer != nil {
|
||||||
|
buffer.pushTimer.Stop()
|
||||||
|
}
|
||||||
|
delete(pushBuffers, taskID)
|
||||||
|
}
|
||||||
|
bufferMutex.Unlock()
|
||||||
|
|
||||||
|
logger.Info("持续测试任务已停止", zap.String("task_id", taskID))
|
||||||
|
}
|
||||||
|
|
||||||
func getLocalIP() string {
|
func getLocalIP() string {
|
||||||
// 简化实现:返回第一个非回环IP
|
// 简化实现:返回第一个非回环IP
|
||||||
// 实际应该获取外网IP
|
// 实际应该获取外网IP
|
||||||
|
|||||||
@@ -15,11 +15,32 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 节点信息存储(通过心跳更新)
|
// 节点信息存储(通过心跳更新,优先从配置文件读取)
|
||||||
var nodeInfo struct {
|
var nodeInfo struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
nodeID uint
|
nodeID uint
|
||||||
nodeIP string
|
nodeIP string
|
||||||
|
country string
|
||||||
|
province string
|
||||||
|
city string
|
||||||
|
isp string
|
||||||
|
cfg *config.Config
|
||||||
|
initialized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitNodeInfo 初始化节点信息(从配置文件读取)
|
||||||
|
func InitNodeInfo(cfg *config.Config) {
|
||||||
|
nodeInfo.Lock()
|
||||||
|
defer nodeInfo.Unlock()
|
||||||
|
|
||||||
|
nodeInfo.cfg = cfg
|
||||||
|
nodeInfo.nodeID = cfg.Node.ID
|
||||||
|
nodeInfo.nodeIP = cfg.Node.IP
|
||||||
|
nodeInfo.country = cfg.Node.Country
|
||||||
|
nodeInfo.province = cfg.Node.Province
|
||||||
|
nodeInfo.city = cfg.Node.City
|
||||||
|
nodeInfo.isp = cfg.Node.ISP
|
||||||
|
nodeInfo.initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNodeID 获取节点ID
|
// GetNodeID 获取节点ID
|
||||||
@@ -36,6 +57,13 @@ func GetNodeIP() string {
|
|||||||
return nodeInfo.nodeIP
|
return nodeInfo.nodeIP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNodeLocation 获取节点位置信息
|
||||||
|
func GetNodeLocation() (country, province, city, isp string) {
|
||||||
|
nodeInfo.RLock()
|
||||||
|
defer nodeInfo.RUnlock()
|
||||||
|
return nodeInfo.country, nodeInfo.province, nodeInfo.city, nodeInfo.isp
|
||||||
|
}
|
||||||
|
|
||||||
type Reporter struct {
|
type Reporter struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
client *http.Client
|
client *http.Client
|
||||||
@@ -45,6 +73,10 @@ type Reporter struct {
|
|||||||
|
|
||||||
func NewReporter(cfg *config.Config) *Reporter {
|
func NewReporter(cfg *config.Config) *Reporter {
|
||||||
logger, _ := zap.NewProduction()
|
logger, _ := zap.NewProduction()
|
||||||
|
|
||||||
|
// 初始化节点信息(从配置文件读取)
|
||||||
|
InitNodeInfo(cfg)
|
||||||
|
|
||||||
return &Reporter{
|
return &Reporter{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
@@ -78,8 +110,76 @@ func (r *Reporter) Stop() {
|
|||||||
close(r.stopCh)
|
close(r.stopCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterNode 注册节点(安装时或首次启动时调用)
|
||||||
|
func RegisterNode(cfg *config.Config) error {
|
||||||
|
url := fmt.Sprintf("%s/api/node/heartbeat", cfg.Backend.URL)
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBufferString("type=pingServer"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建心跳请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("发送心跳失败: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析 JSON 响应
|
||||||
|
var result struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
NodeID uint `json:"node_id"`
|
||||||
|
NodeIP string `json:"node_ip"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
City string `json:"city"`
|
||||||
|
ISP string `json:"isp"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &result); err == nil {
|
||||||
|
// 成功解析 JSON,更新配置文件和内存
|
||||||
|
if result.NodeID > 0 && result.NodeIP != "" {
|
||||||
|
cfg.Node.ID = result.NodeID
|
||||||
|
cfg.Node.IP = result.NodeIP
|
||||||
|
cfg.Node.Country = result.Country
|
||||||
|
cfg.Node.Province = result.Province
|
||||||
|
cfg.Node.City = result.City
|
||||||
|
cfg.Node.ISP = result.ISP
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
|
if err := cfg.Save(); err != nil {
|
||||||
|
return fmt.Errorf("保存配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新内存中的节点信息
|
||||||
|
nodeInfo.Lock()
|
||||||
|
nodeInfo.nodeID = result.NodeID
|
||||||
|
nodeInfo.nodeIP = result.NodeIP
|
||||||
|
nodeInfo.country = result.Country
|
||||||
|
nodeInfo.province = result.Province
|
||||||
|
nodeInfo.city = result.City
|
||||||
|
nodeInfo.isp = result.ISP
|
||||||
|
nodeInfo.cfg = cfg
|
||||||
|
nodeInfo.initialized = true
|
||||||
|
nodeInfo.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("心跳响应格式无效或节点信息不完整")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("心跳请求失败,状态码: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) sendHeartbeat() {
|
func (r *Reporter) sendHeartbeat() {
|
||||||
// 新节点不发送IP,让后端服务器从请求中获取
|
|
||||||
// 发送心跳(使用Form格式,兼容旧接口)
|
// 发送心跳(使用Form格式,兼容旧接口)
|
||||||
url := fmt.Sprintf("%s/api/node/heartbeat", r.cfg.Backend.URL)
|
url := fmt.Sprintf("%s/api/node/heartbeat", r.cfg.Backend.URL)
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBufferString("type=pingServer"))
|
req, err := http.NewRequest("POST", url, bytes.NewBufferString("type=pingServer"))
|
||||||
@@ -106,17 +206,52 @@ func (r *Reporter) sendHeartbeat() {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
NodeID uint `json:"node_id"`
|
NodeID uint `json:"node_id"`
|
||||||
NodeIP string `json:"node_ip"`
|
NodeIP string `json:"node_ip"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
City string `json:"city"`
|
||||||
|
ISP string `json:"isp"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &result); err == nil {
|
if err := json.Unmarshal(body, &result); err == nil {
|
||||||
// 成功解析 JSON,更新节点信息
|
// 成功解析 JSON,检查是否有更新
|
||||||
if result.NodeID > 0 && result.NodeIP != "" {
|
if result.NodeID > 0 && result.NodeIP != "" {
|
||||||
nodeInfo.Lock()
|
nodeInfo.Lock()
|
||||||
|
needUpdate := false
|
||||||
|
if nodeInfo.nodeID != result.NodeID || nodeInfo.nodeIP != result.NodeIP ||
|
||||||
|
nodeInfo.country != result.Country || nodeInfo.province != result.Province ||
|
||||||
|
nodeInfo.city != result.City || nodeInfo.isp != result.ISP {
|
||||||
|
needUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needUpdate {
|
||||||
|
// 更新内存
|
||||||
nodeInfo.nodeID = result.NodeID
|
nodeInfo.nodeID = result.NodeID
|
||||||
nodeInfo.nodeIP = result.NodeIP
|
nodeInfo.nodeIP = result.NodeIP
|
||||||
|
nodeInfo.country = result.Country
|
||||||
|
nodeInfo.province = result.Province
|
||||||
|
nodeInfo.city = result.City
|
||||||
|
nodeInfo.isp = result.ISP
|
||||||
|
|
||||||
|
// 更新配置文件
|
||||||
|
if nodeInfo.cfg != nil {
|
||||||
|
nodeInfo.cfg.Node.ID = result.NodeID
|
||||||
|
nodeInfo.cfg.Node.IP = result.NodeIP
|
||||||
|
nodeInfo.cfg.Node.Country = result.Country
|
||||||
|
nodeInfo.cfg.Node.Province = result.Province
|
||||||
|
nodeInfo.cfg.Node.City = result.City
|
||||||
|
nodeInfo.cfg.Node.ISP = result.ISP
|
||||||
|
if err := nodeInfo.cfg.Save(); err != nil {
|
||||||
|
r.logger.Warn("保存节点信息到配置文件失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
nodeInfo.Unlock()
|
nodeInfo.Unlock()
|
||||||
r.logger.Debug("心跳响应解析成功,已更新节点信息",
|
|
||||||
|
r.logger.Info("节点信息已更新",
|
||||||
zap.Uint("node_id", result.NodeID),
|
zap.Uint("node_id", result.NodeID),
|
||||||
zap.String("node_ip", result.NodeIP))
|
zap.String("node_ip", result.NodeIP),
|
||||||
|
zap.String("location", fmt.Sprintf("%s/%s/%s", result.Country, result.Province, result.City)))
|
||||||
|
} else {
|
||||||
|
nodeInfo.Unlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 不是 JSON 格式,可能是旧格式的 "done",忽略
|
// 不是 JSON 格式,可能是旧格式的 "done",忽略
|
||||||
|
|||||||
Reference in New Issue
Block a user