init
This commit is contained in:
269
internal/discovery/service.go
Normal file
269
internal/discovery/service.go
Normal file
@@ -0,0 +1,269 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user