init: 1.0.0

This commit is contained in:
Chang lue Tsen
2025-04-25 12:08:29 +09:00
commit 8addcc584b
1031 changed files with 76472 additions and 0 deletions
+359
View File
@@ -0,0 +1,359 @@
package device
import (
"context"
"fmt"
"net/http"
"sync"
"sync/atomic"
"time"
"go.uber.org/zap"
"github.com/gorilla/websocket"
)
type Operator int
const (
MaxDevices Operator = iota
Admin
SubscribeUpdate = "subscribe_update"
)
// Device represents a device structure
type Device struct {
Session string
DeviceID string
Conn *websocket.Conn
CreatedAt time.Time
LastPingTime time.Time
}
// WebSocket upgrader
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// DeviceManager manages devices
type DeviceManager struct {
userDevices sync.Map // userID -> []*Device
totalOnline int32 // total online devices
userMutexes sync.Map // userID level locks
heartbeatTimeout int // heartbeat timeout (seconds)
checkInterval int // heartbeat check interval (seconds)
// event callbacks
OnDeviceOnline func(userID int64, deviceID, session string)
OnDeviceOffline func(userID int64, deviceID, session string, createAt time.Time)
OnDeviceKicked func(userID int64, deviceID, session string, operator Operator)
OnMessage func(userID int64, deviceID, session string, message string)
}
// Get user-level mutex
func (dm *DeviceManager) getUserMutex(userID int64) *sync.Mutex {
mu, _ := dm.userMutexes.LoadOrStore(userID, &sync.Mutex{})
return mu.(*sync.Mutex)
}
// Listen to WebSocket data
func (dm *DeviceManager) listenToDevice(userID int64, device *Device) {
defer func() {
dm.removeDevice(userID, device.DeviceID) // remove device when disconnected
}()
for {
_, msg, err := device.Conn.ReadMessage()
if err != nil {
zap.S().Infof("Device %s (User %d) disconnected: %v", device.DeviceID, userID, err)
break
}
message := string(msg)
if message == "ping" || message == "heartbeat" {
dm.UpdateHeartbeat(userID, device.DeviceID)
continue
}
// Trigger message callback
if dm.OnMessage != nil {
go dm.OnMessage(userID, device.DeviceID, device.Session, message)
}
}
}
// UpdateHeartbeat updates device heartbeat
func (dm *DeviceManager) UpdateHeartbeat(userID int64, deviceID string) {
mu := dm.getUserMutex(userID)
mu.Lock()
defer mu.Unlock()
if val, ok := dm.userDevices.Load(userID); ok {
devices := val.([]*Device)
for _, d := range devices {
if d.DeviceID == deviceID {
d.LastPingTime = time.Now()
if err := d.Conn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
zap.S().Infof("✅ Heartbeat updated: Device %s (User %d) err: %s", deviceID, userID, err.Error())
}
break
}
}
}
}
// AddDevice **Add: Device connects WebSocket and is added to the manager**
func (dm *DeviceManager) AddDevice(w http.ResponseWriter, r *http.Request, session string, userID int64, deviceID string, maxDevices int) {
// **Upgrade WebSocket connection**
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
zap.S().Infof("WebSocket upgrade failed: %v", err)
return
}
mu := dm.getUserMutex(userID)
mu.Lock()
defer mu.Unlock()
newDevice := &Device{
Session: session,
DeviceID: deviceID,
Conn: conn,
CreatedAt: time.Now(),
LastPingTime: time.Now(),
}
//不限制设备数量
if maxDevices < 1 {
maxDevices = 99
}
// Get user's device list
var restConnection bool
var devices []*Device
if val, ok := dm.userDevices.Load(userID); ok {
devices = val.([]*Device)
var tempDevice []*Device
for _, d := range devices {
if d.DeviceID == deviceID {
restConnection = true
} else {
tempDevice = append(tempDevice, d)
}
}
devices = tempDevice
}
// **If exceeding the limit, kick out the earliest device**
if !restConnection && len(devices) >= maxDevices {
oldestDevice := devices[0]
devices = devices[1:]
if dm.OnDeviceKicked != nil {
done := make(chan struct{})
go func() {
defer close(done)
dm.OnDeviceKicked(userID, oldestDevice.DeviceID, oldestDevice.Session, MaxDevices)
}()
<-done // block and wait for callback to complete
}
oldestDevice.Conn.Close()
atomic.AddInt32(&dm.totalOnline, -1)
}
// Add new device
devices = append(devices, newDevice)
dm.userDevices.Store(userID, devices)
atomic.AddInt32(&dm.totalOnline, 1)
// Trigger online event
if dm.OnDeviceOnline != nil {
go dm.OnDeviceOnline(userID, deviceID, session)
}
// Start listening
go dm.listenToDevice(userID, newDevice)
}
// removeDevice removes a device
func (dm *DeviceManager) removeDevice(userID int64, deviceID string) {
mu := dm.getUserMutex(userID)
mu.Lock()
defer mu.Unlock()
if val, ok := dm.userDevices.Load(userID); ok {
devices := val.([]*Device)
for i, d := range devices {
if d.DeviceID == deviceID {
devices = append(devices[:i], devices[i+1:]...)
d.Conn.Close()
atomic.AddInt32(&dm.totalOnline, -1)
if dm.OnDeviceOffline != nil {
go dm.OnDeviceOffline(userID, deviceID, d.Session, d.CreatedAt)
}
break
}
}
if len(devices) == 0 {
dm.userDevices.Delete(userID)
} else {
dm.userDevices.Store(userID, devices)
}
}
}
// KickDevice kicks a device (supports individual device or entire user)
func (dm *DeviceManager) KickDevice(userID int64, deviceID string) {
mu := dm.getUserMutex(userID)
mu.Lock()
defer mu.Unlock()
// Get user's device list
val, ok := dm.userDevices.Load(userID)
if !ok {
zap.S().Infof("⚠️ User %d has no online devices, unable to kick out", userID)
return
}
devices := val.([]*Device)
var activeDevices []*Device
for _, d := range devices {
if deviceID == "" || d.DeviceID == deviceID {
// Trigger kick event callback
if dm.OnDeviceKicked != nil {
done := make(chan struct{})
go func() {
defer close(done)
dm.OnDeviceKicked(userID, d.DeviceID, d.Session, Admin)
}()
<-done // block and wait for callback to complete
}
// Close WebSocket connection
d.Conn.Close()
atomic.AddInt32(&dm.totalOnline, -1)
zap.S().Infof("❌ Device %s (User %d) kicked out", d.DeviceID, userID)
} else {
activeDevices = append(activeDevices, d)
}
}
// Update user's device mapping
if len(activeDevices) == 0 {
dm.userDevices.Delete(userID)
} else {
dm.userDevices.Store(userID, activeDevices)
}
}
// StartHeartbeatCheck periodically checks for heartbeat timeout devices
func (dm *DeviceManager) StartHeartbeatCheck() {
ticker := time.NewTicker(time.Duration(dm.checkInterval) * time.Second)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
dm.userDevices.Range(func(userID, val interface{}) bool {
uid := userID.(int64)
devices := val.([]*Device)
mu := dm.getUserMutex(uid)
mu.Lock()
defer mu.Unlock()
var activeDevices []*Device
for _, d := range devices {
if now.Sub(d.LastPingTime) > time.Duration(dm.heartbeatTimeout)*time.Second {
zap.S().Infof("⚠️ Device %s (User %d) heartbeat timeout, removed", d.DeviceID, uid)
d.Conn.Close()
atomic.AddInt32(&dm.totalOnline, -1)
if dm.OnDeviceOffline != nil {
go dm.OnDeviceOffline(uid, d.DeviceID, d.Session, d.CreatedAt)
}
} else {
activeDevices = append(activeDevices, d)
}
}
if len(activeDevices) == 0 {
dm.userDevices.Delete(uid)
} else {
dm.userDevices.Store(uid, activeDevices)
}
return true
})
//zap.S().Infof("Total online devices: %d\n", dm.totalOnline)
}
}
// NewDeviceManager creates a new device manager
func NewDeviceManager(heartbeatTimeout, checkInterval int) *DeviceManager {
dm := &DeviceManager{
heartbeatTimeout: heartbeatTimeout,
checkInterval: checkInterval,
}
go dm.StartHeartbeatCheck()
return dm
}
// SendToDevice sends a message to a specific device
func (dm *DeviceManager) SendToDevice(userID int64, deviceID string, message string) error {
if val, ok := dm.userDevices.Load(userID); ok {
devices := val.([]*Device)
if deviceID == "" {
for _, d := range devices {
err := d.Conn.WriteMessage(websocket.TextMessage, []byte(message))
if err != nil {
return err
}
continue
}
} else {
for _, d := range devices {
if d.DeviceID == deviceID {
return d.Conn.WriteMessage(websocket.TextMessage, []byte(message))
}
}
}
}
return fmt.Errorf("device %s (User %d) is offline", deviceID, userID)
}
// Broadcast sends a message to all devices
func (dm *DeviceManager) Broadcast(message string) {
go func(message string) {
dm.userDevices.Range(func(_, val interface{}) bool {
devices := val.([]*Device)
for _, d := range devices {
_ = d.Conn.WriteMessage(websocket.TextMessage, []byte(message))
}
return true
})
}(message)
}
// Gracefully shut down all WebSocket connections
func (dm *DeviceManager) Shutdown(ctx context.Context) {
<-ctx.Done()
zap.S().Infof("🔴 Shutting down all WebSocket connections...")
dm.userDevices.Range(func(userID, val interface{}) bool {
uid := userID.(int64)
devices := val.([]*Device)
for _, d := range devices {
d.Conn.Close()
zap.S().Infof("✅ Closed device %s (User %d)", d.DeviceID, uid)
}
dm.userDevices.Delete(uid)
return true
})
}
+123
View File
@@ -0,0 +1,123 @@
package device
import (
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/gorilla/websocket"
)
func TestDevice(t *testing.T) {
t.Skip("skip test")
/* deviceManager := NewDeviceManager(10, 3)
deviceManager.OnDeviceOnline = func(userID int64, deviceID, session string) {
fmt.Printf("✅ 设备 %s (用户 %d) 上线\n", deviceID, userID)
}
deviceManager.OnDeviceOffline = func(userID int64, deviceID, session string) {
fmt.Printf("❌ 设备 %s (用户 %d) 下线\n", deviceID, userID)
}
deviceManager.OnDeviceKicked = func(userID int64, deviceID, session string, operator Operator) {
fmt.Printf("⚠️ 设备 %s (用户 %d) 被踢下线\n", deviceID, userID)
}
deviceManager.OnMessage = func(userID int64, deviceID, session string, message string) {
log.Printf("✅收到消息: 设备 %s (用户 %d) 内容: %s,sesion: %s\n", deviceID, userID, message, session)
}
engine := gin.Default()
engine.GET("/ws/:userid/:device_number", func(c *gin.Context) {
//根据Authorization获取session
authorization := c.GetHeader("Authorization")
userid, err := strconv.ParseInt(c.Param("userid"), 10, 64)
if err != nil {
t.Errorf("get user id err:%v", err)
return
}
deviceNumber := c.Param("device_number")
deviceManager.AddDevice(c, authorization, userid, deviceNumber, 3)
return
})
go func() {
err := http.ListenAndServe(":8081", engine)
if err != nil {
t.Fatalf("engine start failed: %v", err)
}
}()
*/
h := http.Header{}
h.Add("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTZXNzaW9uSWQiOiIwMTk0Y2ZiNy1hYjY0LTdjYjMtODUzYi03ZGU5YTAzNWRlZTgiLCJVc2VySWQiOjI5LCJleHAiOjE3MzkyNTY1MDgsImlhdCI6MTczODY1MTcwOH0.BGKT5-hongJPZrA_yAb6cf6go5iDR8T9uu1ZxUg8HDw")
mutex := sync.Mutex{}
serverURL := fmt.Sprintf("ws://localhost:8080/v1/app/ws/%d/%s", 29, "15502502051") // 假设 userID 为 1001,设备ID 为 deviceA
// 建立 WebSocket 连接
conn, resp, err := websocket.DefaultDialer.Dial(serverURL, h)
if err != nil {
all, err := io.ReadAll(resp.Body)
t.Fatalf("websocket dial failed: %v:%s", err, string(all))
}
// 启动一个 goroutine 来读取服务器消息
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), "use of closed network connection") {
log.Println("连接已关闭")
return
}
log.Printf("接收消息失败: %v", err)
return
}
fmt.Printf("收到来自服务器的消息: %s\n", msg)
}
}()
//发送心跳
go func() {
ticker := time.NewTicker(time.Second * 5)
defer ticker.Stop()
for range ticker.C {
mutex.Lock()
err := conn.WriteMessage(websocket.TextMessage, []byte("ping"))
mutex.Unlock()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
log.Println("连接已关闭")
return
}
t.Errorf("websocket 写入失败: %v", err)
return
}
}
}()
updateSubscribe, _ := json.Marshal(map[string]interface{}{
"method": "test_method",
})
//发送一条消息
mutex.Lock()
err = conn.WriteMessage(websocket.TextMessage, updateSubscribe)
mutex.Unlock()
if err != nil {
t.Errorf("websocket write failed: %v", err)
}
time.Sleep(time.Second * 20)
conn.Close()
time.Sleep(time.Second * 5)
}