package main import ( "fmt" "io" "net" "regexp" "strconv" "strings" "sync" "time" ) // 客户端信息结构 type ClientInfo struct { IP string // IP地址 Port string // 端口 LastSeen time.Time // 最后活跃时间 } // 客户端列表(使用互斥锁保护的映射) var ( clientsMutex sync.Mutex clients = make(map[string]*ClientInfo) ) // StartTCPServer 启动TCP服务器 func StartTCPServer(address string) error { listener, err := net.Listen("tcp", address) if err != nil { return err } startClientCleanup() Logger.Printf("TCP服务器已启动,正在监听 %s\n", address) for { conn, err := listener.Accept() if err != nil { Logger.Printf("接受连接失败: %v", err) continue } go handleConnection(conn) } } // handleConnection 处理客户端连接 func handleConnection(conn net.Conn) { defer conn.Close() remoteAddr := conn.RemoteAddr().String() Logger.Printf("新的客户端连接: %s", remoteAddr) addClient(remoteAddr) // 启动定时发送指令的goroutine go sendPeriodicCommand(conn, remoteAddr) buffer := make([]byte, 1024) for { n, err := conn.Read(buffer) if err != nil { if err != io.EOF { Logger.Printf("从客户端读取失败 %s: %v", remoteAddr, err) } else { Logger.Printf("客户端断开连接 %s", remoteAddr) } removeClient(remoteAddr) break } rawData := string(buffer[:n]) TCPDataLogger.Printf("从客户端 %s 接收到原始数据: %s", remoteAddr, rawData) sensorID, x, y, z, temperature, err := parseData(rawData) if err == nil { TCPDataLogger.Printf("解析成功 - 客户端: %s, 传感器ID: %d, 值: X=%.3f, Y=%.3f, Z=%.3f, 温度=%.1f°C", remoteAddr, sensorID, x, y, z, temperature) if err := SaveSensorData(sensorID, x, y, z, temperature); err != nil { Logger.Printf("保存传感器数据失败: %v", err) } } else { TCPDataLogger.Printf("无法解析从客户端 %s 接收到的数据: %s, 错误: %v", remoteAddr, rawData, err) } resp := "OK\n" if _, err := conn.Write([]byte(resp)); err != nil { Logger.Printf("发送响应到客户端 %s 失败: %v", remoteAddr, err) removeClient(remoteAddr) break } updateClientLastSeen(remoteAddr) } } // sendPeriodicCommand 每10秒发送一次查询指令 func sendPeriodicCommand(conn net.Conn, remoteAddr string) { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: command := "@1602301014A*0!\n" if _, err := conn.Write([]byte(command)); err != nil { Logger.Printf("发送定时指令到客户端 %s 失败: %v", remoteAddr, err) return // 连接断开,退出goroutine } TCPDataLogger.Printf("发送定时指令到客户端 %s: %s", remoteAddr, strings.TrimSpace(command)) } } } // parseData 使用正则表达式解析传感器数据,支持新格式 #{1602301014-01,1,1,28.4,-6.884,1.540}! func parseData(data string) (int, float64, float64, float64, float64, error) { // 尝试解析新格式: #{1602301014-01,1,1,28.4,-6.884,1.540}! newPattern := regexp.MustCompile(`#\{[^,]+-(\d+),\d+,(\d+),([-]?\d+\.\d+),([-]?\d+\.\d+),([-]?\d+\.\d+)\}!`) matches := newPattern.FindStringSubmatch(data) if len(matches) == 6 { // 新格式解析 sensorID, err := strconv.Atoi(matches[2]) // 使用传感器地址编号 if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析传感器ID失败: %v", err) } temperature, err := strconv.ParseFloat(strings.TrimSpace(matches[3]), 64) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析温度值失败: %v", err) } x, err := strconv.ParseFloat(strings.TrimSpace(matches[4]), 64) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析X值失败: %v", err) } y, err := strconv.ParseFloat(strings.TrimSpace(matches[5]), 64) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析Y值失败: %v", err) } z := 0.0 // 新格式没有Z值,设为0 return sensorID, x, y, z, temperature, nil } // 尝试解析旧格式: 1:1.000, 2.000, 3.000 oldPattern := regexp.MustCompile(`(\d+):([-]?\d+\.\d+),\s*([-]?\d+\.\d+),\s*([-]?\d+\.\d+)`) matches = oldPattern.FindStringSubmatch(data) if len(matches) == 5 { // 旧格式解析 sensorID, err := strconv.Atoi(matches[1]) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析传感器ID失败: %v", err) } x, err := strconv.ParseFloat(strings.TrimSpace(matches[2]), 64) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析X值失败: %v", err) } y, err := strconv.ParseFloat(strings.TrimSpace(matches[3]), 64) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析Y值失败: %v", err) } z, err := strconv.ParseFloat(strings.TrimSpace(matches[4]), 64) if err != nil { return 0, 0, 0, 0, 0, fmt.Errorf("解析Z值失败: %v", err) } temperature := 0.0 // 旧格式没有温度值,设为0 return sensorID, x, y, z, temperature, nil } return 0, 0, 0, 0, 0, fmt.Errorf("数据格式不正确: %s", data) } // addClient 添加客户端 func addClient(addr string) { clientsMutex.Lock() defer clientsMutex.Unlock() host, port, err := net.SplitHostPort(addr) if err != nil { Logger.Printf("解析客户端地址失败 %s: %v", addr, err) host = addr port = "unknown" } clients[addr] = &ClientInfo{ IP: host, Port: port, LastSeen: time.Now(), } Logger.Printf("添加新客户端: %s", addr) } // updateClientLastSeen 更新客户端最后活跃时间 func updateClientLastSeen(addr string) { clientsMutex.Lock() defer clientsMutex.Unlock() if client, exists := clients[addr]; exists { client.LastSeen = time.Now() } } // removeClient 移除客户端 func removeClient(addr string) { clientsMutex.Lock() defer clientsMutex.Unlock() if client, exists := clients[addr]; exists { client.LastSeen = time.Now() Logger.Printf("客户端标记为断开连接: %s", addr) } } // getAllClients 获取所有客户端信息 func getAllClients() []map[string]interface{} { clientsMutex.Lock() defer clientsMutex.Unlock() now := time.Now() result := make([]map[string]interface{}, 0, len(clients)) for addr, client := range clients { lastSeenDuration := now.Sub(client.LastSeen) if lastSeenDuration > 24*time.Hour { delete(clients, addr) continue } isOnline := lastSeenDuration < 10*time.Minute result = append(result, map[string]interface{}{ "address": addr, "ip": client.IP, "port": client.Port, "lastSeen": client.LastSeen, "isOnline": isOnline, "lastSeenFormatted": formatDuration(lastSeenDuration), }) } return result } // formatDuration 格式化持续时间为友好的字符串 func formatDuration(d time.Duration) string { if d < time.Minute { return "刚刚" } else if d < time.Hour { return fmt.Sprintf("%d分钟前", int(d.Minutes())) } else if d < 24*time.Hour { return fmt.Sprintf("%d小时前", int(d.Hours())) } else { return fmt.Sprintf("%d天前", int(d.Hours()/24)) } } // startClientCleanup 启动清理过期客户端的goroutine func startClientCleanup() { go func() { for { time.Sleep(1 * time.Hour) // 每小时检查一次 clientsMutex.Lock() now := time.Now() for addr, client := range clients { if now.Sub(client.LastSeen) > 24*time.Hour { delete(clients, addr) Logger.Printf("移除过期客户端: %s", addr) } } clientsMutex.Unlock() } }() }