Files
mesh-drop/internal/discovery/service.go
2026-02-04 02:21:23 +08:00

270 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package discovery
import (
"encoding/json"
"fmt"
"log/slog"
"net"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/google/uuid"
"github.com/wailsapp/wails/v3/pkg/application"
)
const (
DiscoveryPort = 9988
HeartbeatRate = 3 * time.Second
PeerTimeout = 10 * time.Second
)
type Service struct {
app *application.App
ID string
Name string
FileServerPort int
// key 使用 peer.id 和 peer.ip 组合而成的 hash
peers map[string]*Peer
peersMutex sync.RWMutex
}
func init() {
application.RegisterEvent[[]Peer]("peers:update")
}
// getOrInitDeviceID 获取或初始化设备 ID
func getOrInitDeviceID() string {
configDir, err := os.UserConfigDir()
if err != nil {
return uuid.New().String()
}
appDir := filepath.Join(configDir, "mesh-drop")
if err := os.MkdirAll(appDir, 0755); err != nil {
return uuid.New().String()
}
idFile := filepath.Join(appDir, "device_id")
if data, err := os.ReadFile(idFile); err == nil {
return string(data)
}
id := uuid.New().String()
_ = os.WriteFile(idFile, []byte(id), 0644)
return id
}
func NewService(app *application.App, name string, port int) *Service {
return &Service{
app: app,
ID: getOrInitDeviceID(),
Name: name,
FileServerPort: port,
peers: make(map[string]*Peer),
}
}
func (s *Service) startBroadcasting() {
ticker := time.NewTicker(HeartbeatRate)
for range ticker.C {
interfaces, err := net.Interfaces()
if err != nil {
slog.Error("Failed to get network interfaces", "error", err, "component", "discovery")
continue
}
packet := PresencePacket{
ID: s.ID,
Name: s.Name,
Port: s.FileServerPort,
OS: OS(runtime.GOOS),
}
data, _ := json.Marshal(packet)
for _, iface := range interfaces {
// 过滤掉 Down 的接口和 Loopback 接口
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
// 获取该接口的地址
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ip, ipNet, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if ip.To4() == nil {
continue
}
// 计算该网段的广播地址
// 例如 IP: 192.168.1.5/24 -> 广播地址: 192.168.1.255
broadcastIPV4 := make(net.IP, len(ip.To4()))
copy(broadcastIPV4, ip.To4())
for i, b := range ipNet.Mask {
broadcastIPV4[i] |= ^b
}
slog.Debug("Broadcast IP", "ip", broadcastIPV4.String(), "component", "discovery")
s.sendPacketTo(broadcastIPV4.String(), DiscoveryPort, data)
}
}
}
}
func (s *Service) sendPacketTo(ip string, port int, data []byte) {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ip, port))
if err != nil {
return
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return
}
defer conn.Close()
_, err = conn.Write(data)
if err != nil {
slog.Error("Failed to send packet", "error", err, "component", "discovery")
return
}
}
func (s *Service) startListening() {
addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", DiscoveryPort))
conn, err := net.ListenUDP("udp", addr)
if err != nil {
slog.Error("Failed to start listening", "error", err, "component", "discovery")
return
}
defer conn.Close()
buf := make([]byte, 1024)
for {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}
var packet PresencePacket
if err := json.Unmarshal(buf[:n], &packet); err != nil {
continue
}
// 忽略自己发出的包
if packet.ID == s.ID {
continue
}
s.handleHeartbeat(packet, remoteAddr.IP.String())
}
}
// 处理心跳包
func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
s.peersMutex.Lock()
peer, exists := s.peers[pkt.ID]
if !exists {
// 发现新节点
peer = &Peer{
ID: pkt.ID,
Name: pkt.Name,
Routes: map[string]*RouteState{
ip: {
IP: ip,
LastSeen: time.Now(),
},
},
Port: pkt.Port,
OS: pkt.OS,
}
s.peers[peer.ID] = peer
slog.Info("New device found", "name", pkt.Name, "ip", ip, "component", "discovery")
} else {
// 更新节点
peer.Routes[ip] = &RouteState{
IP: ip,
LastSeen: time.Now(),
}
}
peer.IsOnline = true
s.peersMutex.Unlock()
// 触发前端更新 (防抖逻辑可以之后加,这里每次变动都推)
s.app.Event.Emit("peers:update", s.GetPeers())
}
// 3. 掉线清理协程
func (s *Service) startCleanup() {
ticker := time.NewTicker(2 * time.Second)
for range ticker.C {
s.peersMutex.Lock()
changed := false
now := time.Now()
for id, peer := range s.peers {
for ip, route := range peer.Routes {
// 超过10秒没心跳认为下线
if now.Sub(route.LastSeen) > PeerTimeout {
delete(peer.Routes, ip)
changed = true
slog.Info("Device offline", "name", peer.Name, "component", "discovery")
}
}
if len(peer.Routes) == 0 {
delete(s.peers, id)
changed = true
slog.Info("Device offline", "name", peer.Name, "component", "discovery")
}
}
s.peersMutex.Unlock()
if changed {
s.app.Event.Emit("peers:update", s.GetPeers())
}
}
}
func (s *Service) Start() {
go s.startBroadcasting()
go s.startListening()
go s.startCleanup()
}
func (s *Service) GetPeerByIP(ip string) *Peer {
s.peersMutex.RLock()
defer s.peersMutex.RUnlock()
for _, p := range s.peers {
if p.Routes[ip] != nil {
return p
}
}
return nil
}
func (s *Service) GetPeers() []Peer {
s.peersMutex.RLock()
defer s.peersMutex.RUnlock()
list := make([]Peer, 0)
for _, p := range s.peers {
list = append(list, *p)
}
return list
}
func (s *Service) GetName() string {
return s.Name
}
func (s *Service) GetID() string {
return s.ID
}